术语Tessellation(镶嵌)是指一大类设计活动,通常是指在平坦的表面上,用各种几何形状的瓷砖相邻排列以形成图案。它的目的可以是艺术性的或实用性的,很多例子可以追溯到几千年前[TS16]。
在3D图形学中,Tessellation指的是有点不同的东西(曲面细分),但显然是由它的经典对应物(镶嵌)启发而成的。在这里,曲面细分指的是生成并且操控大量三角形以渲染复杂的形状和表面,尤其是使用硬件进行渲染。曲面细分是OpenGL核心近期才增加的新功能,在2010年的4.0版本中出现。
12.1 OpenGL中的曲面细分
OpenGL对硬件曲面细分的支持,通过3个管线阶段提供:
(1)曲面细分控制着色器;
(2)曲面细分器;
(3)曲面细分评估着色器。
第(1)和第(3)阶段是可编程的;而中间的第(2)阶段不是。为了使用曲面细分,程序员通常会提供控制着色器和评估着色器。
曲面细分器(其全名是曲面细分图元生成器,或TPG)是硬件支持的引擎,可以生成固定的三角形网格。控制着色器允许我们配置曲面细分器要构建什么样的三角形网格。然后,评估着色器允许我们以各种方式操控网格。然后,被操控过的三角形网格,会作为通过管线前进的顶点的源数据。回想一下图2.2,在管线上,曲面细分着色器位于顶点着色器和几何着色器阶段之间。
让我们从一个简单的应用程序开始,该应用程序只使用曲面细分器创建顶点的三角形网格,然后在不进行任何操作的情况下显示它。 为此,我们需要以下模块。
(1)C++/OpenGL应用程序:
创建一个摄像机和相关的MVP矩阵,视图(v)和投影§矩阵确定摄像机朝向,模型(m)矩阵可用于修改网格的位置和方向。
(2)顶点着色器:在这个例子中基本上什么都不做,顶点将在曲面细分器中生成。
(3)曲面细分控制着色器:指定曲面细分器要构建的网格。
(4)曲面细分评估着色器:将MVP矩阵应用于网格中的顶点。
(5)片段着色器:只需为每个像素输出固定颜色。
程序12.1显示了整个应用程序的代码。即使像这样的简单示例也相当复杂,因此许多代码元素都需要解释。请注意,这是我们第一次使用除顶点和片段着色器之外的组件构建GLSL渲染程序。因此,我们实现了createShaderProgram()的4参数重载版本。
程序12.1 基本曲面细分器网格
.
完整源代码
vertShader.glsl
#version 430
uniform mat4 mvp_matrix;
void main(void)
{
}
fragShader.glsl
#version 430
out vec4 color;
uniform mat4 mvp_matrix;
void main(void)
{
color = vec4(1.0, 1.0, 0.0, 1.0);
}
曲面细分控制着色器tessCShader.glsl
#version 430
uniform mat4 mvp_matrix;
layout (vertices = 1) out;
void main(void)
{ gl_TessLevelOuter[0] = 6;
gl_TessLevelOuter[2] = 6;
gl_TessLevelOuter[1] = 6;
gl_TessLevelOuter[3] = 6;
gl_TessLevelInner[0] = 12;
gl_TessLevelInner[1] = 12;
}
曲面细分评估着色器tessEShader.glsl
#version 430
layout (quads, equal_spacing, ccw) in;
uniform mat4 mvp_matrix;
void main (void)
{
float u = gl_TessCoord.x;
float v = gl_TessCoord.y;
gl_Position = mvp_matrix * vec4(u,0,v,1);
}
main.cpp
#include <GL\glew.h>
#include <GLFW\glfw3.h>
#include <SOIL2\soil2.h>
#include <string>
#include <iostream>
#include <fstream>
#include <glm\gtc\type_ptr.hpp> // glm::value_ptr
#include <glm\gtc\matrix_transform.hpp> // glm::translate, glm::rotate, glm::scale, glm::perspective
#include "Utils.h"
using namespace std;
float toRadians(float degrees) { return (degrees * 2.0f * 3.14159f) / 360.0f; }
#define numVAOs 1
//-------------------------------------------Utils util = Utils();
float cameraX, cameraY, cameraZ;
float terLocX, terLocY, terLocZ;
GLuint renderingProgram;
GLuint vao[numVAOs];
// variable allocation for display
GLuint mvpLoc;
int width, height;
float aspect;
glm::mat4 pMat, vMat, mMat, mvpMat;
float tessInner = 30.0f;
float tessOuter = 20.0f;
void init(GLFWwindow* window) {
renderingProgram = Utils::createShaderProgram("vertShader.glsl", "tessCShader.glsl", "tessEShader.glsl", "fragShader.glsl");
cameraX = 0.5f; cameraY = -0.5f; cameraZ = 2.0f;
terLocX = 0.0f; terLocY = 0.0f; terLocZ = 0.0f;
glfwGetFramebufferSize(window, &width, &height);
aspect = (float)width / (float)height;
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
glGenVertexArrays(numVAOs, vao);
glBindVertexArray(vao[0]);
}
void display(GLFWwindow* window, double currentTime) {
glClear(GL_DEPTH_BUFFER_BIT);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(renderingProgram);
vMat = glm::translate(glm::mat4(1.0f), glm::vec3(-cameraX, -cameraY, -cameraZ));
mMat = glm::translate(glm::mat4(1.0f), glm::vec3(terLocX, terLocY, terLocZ));
mMat = glm::rotate(mMat, toRadians(35.0f), glm::vec3(1.0f, 0.0f, 0.0f));
mvpMat = pMat * vMat * mMat;
mvpLoc = glGetUniformLocation(renderingProgram, "mvp_matrix");
glUniformMatrix4fv(mvpLoc, 1, GL_FALSE, glm::value_ptr(mvpMat));
glFrontFace(GL_CCW);
glPatchParameteri(GL_PATCH_VERTICES, 1);
glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // FILL or LINE
glDrawArrays(GL_PATCHES, 0, 1);
}
void window_size_callback(GLFWwindow* win, int newWidth, int newHeight) {
aspect = (float)newWidth / (float)newHeight;
glViewport(0, 0, newWidth, newHeight);
pMat = glm::perspective(1.0472f, aspect, 0.1f, 1000.0f);
}
int main(void) {
if (!glfwInit()) { exit(EXIT_FAILURE); }
glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
GLFWwindow* window = glfwCreateWindow(800, 800, "Chapter12 - program1", NULL, NULL);
glfwMakeContextCurrent(window);
if (glewInit() != GLEW_OK) { exit(EXIT_FAILURE); }
glfwSwapInterval(1);
glfwSetWindowSizeCallback(window, window_size_callback);
init(window);
while (!glfwWindowShouldClose(window)) {
display(window, glfwGetTime());
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwDestroyWindow(window);
glfwTerminate();
exit(EXIT_SUCCESS);
}
得到的输出网格如图12.1所示(见彩插)。
曲面细分器生成由两个参数定义的顶点网格:内层级别和外层级别。在这种情况下,内层级别为12,外层级别为6——网格的外边缘被分为6段,而跨越内部的线被分为12段。
程序12.1中的特别相关的新结构被高亮显示。让我们首先讨论第一部分——C++/ OpenGL代码。
编译这两个新着色器,跟顶点和片段着色器完全相同。然后将它们附加到同一个渲染程序,并且链接调用保持不变。唯一的新项目是用于指定要实例化的着色器类型的常量——新常量如下:
GL_TESS_CONTROL_SHADER
GL_TESS_EVALUATION_SHADER
请注意display()函数中的新项目。glDrawArrays()
调用现在指定 GL_PATCHES
。当使用曲面细分时,从C++/OpenGL应用程序发送到管线(即在VBO中)的顶点不会被渲染,但通常会被当作控制点,就像我们 在贝塞尔曲线中看到的那些一样。一组控制点被称作“补丁”,并且在使用曲面细分的代码段中,GL_PATCHES
是唯一允许的图元类型。 “补丁”中顶点的数量在glPatchParameteri()
的调用中指定。在这个特定示例中,没有任何控制点被发送,但我们仍然需要指定至少一个。类似地,在glDrawArrays()
调用中,我们指示起始值为0,顶点数量为1,即使我们实际上没有从C++程序发送任何顶点。
对glPolygonMode()
的调用指定了如何光栅化网格。默认值为 GL_FILL
。而我们的代码中显示的是GL_LINE
,如我们在图12.1中看到的那样,它只会导致连接线被光栅化(因此我们可以看到由曲面细分器生成的网格本身)。如果我们将该行代码更改为GL_FILL
(或将其注 释掉,从而使用默认行为GL_FILL
),我们将得到如图12.2所示的版本。
现在让我们来过一遍4个着色器。如前所述,顶点着色器几乎没什么可做的,因为C++/OpenGL应用程序没有提供任何顶点。它包含的是一个统一变量声明,以和其他着色器相匹配,以及一个空的main()。 在任何情况下,所有着色器程序都必须包含顶点着色器。
曲面细分控制着色器指定曲面细分器要生成的三角形网格的拓扑结构。通过将值分配给名为gl_TessLevelxxx
的保留字,设置6个“级别”参数——两个“内部”和4个“外部”级别。我们这里细分了一个 由三角形组成的大矩形网格,称为四边形。[3]级别参数告诉曲面细分器在形成三角形时如何细分网格,它们的排列如图12.3所示。
请注意控制着色器中的代码行:
layout (vertices=1) out;
这与之前的GL_PATCHES
讨论有关,用来指定从顶点着色器传递给控制着色器(以及“输出”给评估着色器)的每个“补丁”的顶点数。在我们现在这个程序中没有任何顶点,但我们仍然必须指定至少一个,因为它也会影响控制着色器被执行的次数。稍后这个值将反映控制点的数量,并且必须与C++/OpenGL应用程序中glPatchParameteri()
调用中的值匹配。
接下来让我们看一下曲面细分评估着色器。它以一行代码开头,形如:
layout (quads, equal_spacing, ccw) in;
乍一看这好像与控件着色器中的“out”布局语句有关,但实际上它们是无关的。相反,这行代码是我们指示曲面细分器去生成排列在一个大矩形(“四边形”)中顶点的位置。它还指定了细分线段(包括内部和外部)具有相等的长度(稍后我们将看到长度不等的细分的应用场景)。“ccw”参数指定生成曲面细分网格顶点的缠绕顺序(在当前情况下,是逆时针)。
然后,由曲面细分器生成的顶点被发送到评估着色器。因此,评估着色器既可以从控制着色器(通常作为控制点),又可以从曲面细分器(曲面细分网格)接收顶点。在程序12.1中,仅从曲面细分器接收顶点。
评估着色器对曲面细分器生成的每个顶点执行一次。可以使用内置变量gl_TessCoord
访问顶点位置。曲面细分网格的朝向使得它位于X-Z平面中,因此gl_TessCoord
的X和Y分量被应用于网格的X和Z坐标。网格坐标,以及 gl_TessCoord
的值,范围为0.0~1.0(这在计算纹理坐标时会很方便)。然后,评估着色器使用MVP矩阵定向每个顶点(这在前面章节的示例中,是由顶点着色器完成的)。
最后,片段着色器只为每个像素输出一个恒定的黄色。当然,我们也可以使用它来为我们的场景应用纹理或光照,就像我们在前面的章节中看到的那样。
12.2 贝塞尔曲面细分
现在让我们扩展我们的程序,使它将我们简单的矩形网格转换为贝塞尔曲面。细分网格应该为我们提供了足够的顶点来对曲面进行采样(如果我们想要更多的话,我们可以增加内部/外部细分级别)。我们现在需要的是通过管线发送控制点,然后使用这些控制点执行计算以将细分网格转换为我们所需的贝塞尔曲面。
假设我们希望建立一个立方体贝塞尔曲面,我们将需要16个控制点。我们可以通过VBO从C++端发送它们,或者我们可以在顶点着色器中硬编码写死它们。图12.4概述了来自C++端的控制点的过程。
现在是更准确地解释曲面细分控制着色器(TCS)如何工作的好时机。与顶点着色器类似,TCS对每个传入顶点执行一次。另外,回想一下第2章,OpenGL提供了一个名为·gl_VertexID·的内置变量,它保存一个计数器,指示顶点着色器当前正在执行哪次调用。曲面细分控制着色器中存在一个类似的内置变量gl_InvocationID
。
曲面细分的一个强大功能是TCS(以及TES)着色器可以同时访问数组中的所有控制点顶点。首先,当每个调用都可以访问所有顶点时,TCS对每个顶点执行一次可能会让人感到困惑。在每个TCS调用中,冗余地在赋值语句中指定曲面细分级别也是违反直觉的。尽管所 有这些看起来都很奇怪,但这样做是因为曲面细分的架构设计使得TCS 调用可以并行运行。
OpenGL提供了几个用于TCS和TES着色器的内置变量。我们已经提到过的是gl_InvocationID
,当然还有gl_TessLevelInner
和gl_TessLevelOuter
。以下是一些最有用的内置变量的更多细节和描述。
-
曲面细分控制着色器(TCS)内置变量。
gl_in[ ]
——包含每个传入的控制点顶点的数组——每个传入顶点是一个数组元素。可以使用“.”表示法将特定顶点属性作为字段进行访问。一个内置属性是gl_Position
——因此,输入顶点“i”的位置可以通过gl_in[i].gl_Position
访问。gl_out[ ]
——用于将输出控制点的顶点发送到TES的一个数组——每个输出顶点是一个数组元素。可以使用“.”表示法将特定 顶点属性作为字段进行访问。一个内置属性是gl_Position
——因此,输出顶点“i”的位置可以通过gl_out[i].gl_Position
访问。gl_InvocationID
——整型ID计数器,指示TCS当前正在执行哪个调用。一个常见的用途是用于传递顶点属性;例如,将当前调用 的顶点位置从TCS传递到TES可以用如下方式完成:gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position
。 -
曲面细分评估着色器(TES)内置变量。
gl_in[ ]
——包含每个传入的控制点顶点的数组——每个传入顶点是一个数组元素。可以使用“.”表示法将特定顶点属性作为字段进行访问。一个内置属性是gl_Position
——因此,输入顶点 “i”的位置可以通过gl_in[i].gl_Position
访问。gl_Position
——曲面细分网格顶点的输出位置,可能在TES中被修改。重要的是要注意gl_Position
和gl_in[xxx].gl_Position
是不同的——gl_Position
是起源于曲面细分器的输出顶点的位置,而gl_in[xxx].gl_Position
是一个从TCS进入TES的控制点顶点位置。
值得注意的是,TCS中的输入和输出控制点顶点属性是数组。不同的是,TES中的输入控制点顶点和顶点属性是数组,但输出顶点是标量。此外,很容易混淆哪些顶点来自于控制点,哪些顶点是细分建立的,然后移动以形成结果曲面。总而言之,TCS的所有顶点输入和输出都是控制点,而在TES中,gl_in[ ]
保存输入控制点,gl_TessCoord
保存输入的细分网格点,gl_Position
保存用于渲染的输出表面顶点。
我们的曲面细分控制着色器现在有两个任务:指定曲面细分级别并将控制点从顶点着色器传递到评估着色器。然后,评估着色器可以根据贝塞尔控制点修改网格点(gl_TessCoords
)的位置。
程序12.2显示了所有4个着色器——顶点、TCS、TES和片段——用于指定控制点补丁,生成平坦的曲面细分顶点网格,在控制点指定的曲面上重新定位这些顶点,并使用纹理图像绘制生成的曲面。它还显示了C++/OpenGL应用程序的相关部分,特别是在display()函数中。在 此示例中,控制点源自顶点着色器(它们在那里硬编码写死),而不是从C++/OpenGL应用程序进入OpenGL管线。代码后面会讲述其他详细信息。
程序12.2 贝塞尔曲面的曲面细分
顶点着色器现在指定代表特定贝塞尔曲面的16个控制点(“补丁”顶点)。在这个例子中,它们都被归一化到范围[−1…+1]。顶点着色器还使用控制点来确定适合细分网格的纹理坐标,其值在[0…1]范围内。很重要的是,要重申顶点着色器输出的顶点不是将要用来光栅化的顶点,而是贝塞尔控制点。使用曲面细分时,补丁顶点永远不会被光栅化——只有曲面细分顶点会被光栅化。
控制着色器仍然会指定内部和外部曲面细分级别。它现在还负责将控制点和纹理坐标发送到评估着色器。请注意,曲面细分级别只需要指定一次,因此该步骤仅在第0次调用期间完成(回想一下TCS每个顶点运行一次,因此在此示例中有16次调用)。为方便起见,我们为每个细分级别指定了32个细分。
接下来,评估着色器执行所有贝塞尔曲面计算。main()开头的大块赋值语句从每个传入gl_in
的gl_Position
中提取控制点(请注意,这些控制点对应于控制着色器的gl_out
变量)。然后使用来自曲面细分器的网格点计算混合函数的权重,从而生成一个新的outputPosition
,然后应用模型-视图-投影矩阵,为每个网格点生成输出gl_Position
并形成贝塞尔曲面。
另外,还需要创建纹理坐标。顶点着色器仅为每个控制点位置提供一个纹理坐标。但我们并不是要渲染控制点,我们最终需要更多的曲面细分网格点的纹理坐标。有很多方法可以做到这一点,在这里我们利用GLSL方便的混合功能对它们进行线性插值。mix()函数需要3个参数:
(a)起始点;
(b)结束点;
(c)内插值,范围为0~1。
它返 回与内插值对应的起点和终点之间的值。由于细分网格坐标的范围也是0~1,所以它们可以直接用于此目的。
这次在片段着色器中,不再是输出单一颜色,而是应用标准纹理。属性texCoord_TESou
t中的纹理坐标是在评估着色器中生成的纹理坐标。对C++程序的更改同样很简单——请注意,现在指定的补丁大小为16。结果输出如图12.5所示(应用了[LU16]的平铺纹理)。
12.3 地形、高度图的细分
回想一下,在顶点着色器中执行高度贴图可能会遇到顶点数量不足以用来渲染所需的细节的情况。现在我们有了生成大量顶点的方 法,让我们回到Hastings-Trew的月球表面纹理贴图[HT16]并将其用作高度贴图,提升曲面细分顶点来生成月球表面细节。正如我们将看到的,这具有一些优点,可以让顶点的几何形状更好地匹配月亮图像, 并且提升轮廓(边缘)细节。
我们的策略是修改程序12.1,在X-Z平面中放置细分网格,并使用高度贴图来设置每个细分网格点的Y坐标。要做到这一点,我们不需要补丁,因为可以硬编码细分网格的位置,因此我们将在glDrawArrays()
和glPatchParameteri()
中为每个补丁指定所需的最少的1个顶点,如程序12.1中所做的那样。Hastings-Trew的月亮纹理图像既用于颜色,也用作高度图。
我们通过将曲面细分网格的gl_TessCoord
值映射到顶点和纹理的适当范围,在评估着色器中生成顶点和纹理坐标。[4]评估着色器也通过添加月亮纹理的一小部分颜色分量到输出顶点的Y分量,来实现高度贴图。着色器的更改显示在程序12.3中。
程序12.3 简单的地形曲面细分
这里的片段着色器类似于程序12.2的,只是根据纹理图像输出颜色。C++/OpenGL应用程序基本上没有变化——它加载纹理(用作纹理 和高度图)并为其启用采样器。图12.6显示了纹理图像(左侧)和第一次尝试的最终输出,遗憾的是,它还没有实现正确的高度贴图。
第一次结果存在严重缺陷。虽然我们现在可以看到远处地平线上的轮廓细节,但是那里的凸起与纹理贴图中的实际细节不对应。回想一下,在高度图中,白色应该表示“高”,而黑色应该表示“低”。 特别是图像右上方的区域显示的大山丘与其中的浅色和深色无关。
导致此问题的原因是细分网格的分辨率。曲面细分器可以生成的最大顶点数取决于硬件。要符合OpenGL标准,唯一的要求是每个曲面细分级别的最大值至少为64。我们的程序指定了一个内部和外部曲面细分级别均为32的单一细分网格,因此我们生成了大约32×32或者说刚刚超过1 000个顶点,这不足以准确反映图像中的细节。这在图12.6右上方(图中放大)尤其明显——边缘细节仅在沿地平线的32个点处采样,这会产生巨大而看起来很随机的山丘。即使我们将曲面细分值增加到64,总共64×64或刚刚超过4 000个顶点仍然不足以满足使用月球图像进行高度贴图的需要。
增加顶点数量的一个好方法是使用我们在第4章中看到的实例化。 我们的策略是让曲面细分器生成网格,并使用实例化重复数次。在顶点着色器中,我们构建了一个由4个顶点定义的补丁,每个顶点用于细分网格的每个角。在我们的C++/OpenGL应用程序中,我们将glDrawArrays()
调用更改为glDrawArraysInstanced()
。如此,我们指定一个64×64个补丁的网格,每个补丁包含一个细分级别为32的网格。这将带给我们总共64×64×32×32个,或者说超过400万个顶点。
顶点着色器首先指定4个纹理坐标(0,0)、(0,1)、(1,0)和(1,1)。 使用实例化时,请回想一下,顶点着色器可以访问整数变量 gl_InstanceID
,它包含一个对应于当前正在处理的 glDrawArraysInstanced()
调用的计数器。我们使用此ID值来分配大网格中各个补丁的位置。补丁位于行和列中,第一个补丁位于(0,0),第 二个位于(1,0),下一个位于(2,0),依此类推,第一列中的最后一个 补丁在(63,0)。下一列的补丁位于(0,1)、(1,1),依此类推,直至 (63,1)。最后一列的补丁位于(0,63)、(1,63),依此类推,最后是(63,63)。给定补丁的X坐标是实例ID整除64,Y坐标是实例ID除以64(整数除法)。然后着色器将坐标向下缩放到范围[0…1]。
控制着色器没有更改,除了它将顶点和纹理坐标传递下去。
接下来,评估着色器获取传入的细分网格顶点(由gl_TessCoord
指定)并将它们移动到传入补丁指定的坐标范围内。它对纹理坐标也进行一样的处理,并且也会以与程序12.3中相同的方式应用高度贴图。片段着色器没有修改。
每个组件的更改显示在程序12.4中。结果如图12.7所示。请注意,高点和低点现在更接近于图像的亮部和暗部。
程序12.4 实例化细分地形
现在我们已经实现了高度贴图,我们可以着手改进它并整合光照。一个挑战是我们的顶点还没有与它们相关的法向量。另一个挑战是简单地使用纹理图像作为高度图产生了过度“锯齿状”的结果——在这种情况下是因为并非纹理图像中的所有灰度变化都是由高度引起的。对于这个特定的纹理贴图,Hastings-Trew已经生成了一个改进的高度贴图,我们可以使用[HT16]。如图12.8左图所示。我们可以通过生成相邻顶点(或高度图中的相邻纹素)的高度,构建连接它们的向量以及使用叉积来计算法向量,以动态计算和创建法向量。这需要一些细微的调整,具体取决于场景的精度(和/或高度图图像)。在这里,我们使用GIMP“normalmap”插件[GP16]来根据Hastings-Trew的高度图生成法线贴图,如图12.8右图所示。
我们对代码进行的大部分更改现在只是为了实现Phong着色的标准方法。
- C++/OpenGL应用程序。
我们加载并激活一个额外的纹理来保存法线贴图,还添加了代码来指定光照和材质,就像我们在以前的应用程序中所做的那样。
- 顶点着色器。
唯一的增补是光照统一变量的声明和法线贴图的采样器。通常在顶点着色器中完成的光照代码被移动到曲面细分评估着色器,因为直到曲面细分阶段才生成顶点。
- 曲面细分控制着色器。
唯一的增补是光照统一变量的声明和法线贴图的采样器。
- 曲面细分评估着色器。
Phong光照的准备代码现在放在评估着色器中:
varyingVertPos = (mv_matrix * position).xyz;
varyingLightDir = light.position - varyingVertPos;
- 片段着色器。
这里完成了用于计算Phong(或Blinn-Phong)照明的典型代码段,以及从法线贴图中提取法向量的代码。然后将光照结果与纹理图像用加权求和的方式结合起来。
带有高度和法线贴图以及Phong照明的最终结果如图12.9所示。地形现在会响应光照。在此示例中,位置光已放置在左侧图像中心的左侧,右侧图像中心的右侧。
尽管从静止图像很难判断出对光的移动的响应,但是读者应该能够辨别出漫射光的变化,并且山峰的镜面高光在两个图像中是非常不同的。当摄像机或光源移动时,这当然会更明显。结果仍然不完美,因为无论什么样的光照,输出中包含的原始纹理都包括了将出现在渲染结果上的阴影。
12.4 控制细节级别(LOD)
在程序12.4中,使用实例化来实时生成数百万个顶点,即使是装备精良的现代计算机也可能会感受到负担。幸运的是,将地形划分为单独的补丁的策略,正如我们为增加生成的网格顶点的数量所做的那样,也为我们提供了一种减少负担的好机制。
在生成的数百万个顶点中,许多顶点不是必需的。靠近摄像机的补丁中的顶点非常重要,因为我们希望能够识别附近物体的细节。但是,补丁越远离摄像机,甚至光栅化过程中有足够的像素来体现我们生成的顶点数量的可能性就越小!
根据距摄像机的距离更改补丁中的顶点数量是一种称为细节级别或LOD的技术。Sellers等人描述了一种通过修改控制着色器来控制实例化曲面细分中的LOD的方法[SW15]。程序12.5显示了Sellers等人的方法的简化版本。策略是使用补丁的感知大小来确定其曲面细分级别的值。由于补丁的细分网格最终将放置在由进入控制着色器的4个控制点定义的方格内,我们可以使用控制点相对于摄像机的位置来确定应该为补丁生成多少个顶点。其步骤如下。
(1)通过将MVP矩阵应用于4个控制点,计算它们的屏幕位置。
(2)计算由控制点(在屏幕上的空间中)定义的正方形边长(即宽度和高度)。请注意,即使4个控制点形成正方形,这些边长也可能不同,因为应用了透视矩阵。
(3)根据曲面细分级别所需的精度(基于高度图中的细节数量),将长度的值按可调整常数进行缩放。
(4)将缩放长度值加1,以避免将曲面细分级别指定为0(这将导致不生成顶点)。
(5)将曲面细分级别设置为相应的计算宽度和高度值。
回想一下,在我们的实例中,我们不是只创建一个网格,而是创建64×64个网格。因此,对每个补丁执行以上列表中的5个步骤,细节级别因补丁而异。所有更改都在控制着色器中,并显示在程序12.5中,生成的输出如图12.10所示。请注意,变量gl_InvocationID
指的是正在处理补丁中的哪个顶点(而不是正在处理哪个补丁)。因此,告诉曲面细分器在每个补丁中生成多少个顶点的LOD计算发生在每个补丁的第0个顶点期间
程序12.5 曲面细分细节级别(LOD)
将这些控制着色器的更改应用于图12.7中我们场景的实例化(但不带光照)版本,并将高度图替换为Hastings-Trew的更精细调整的版本(如图12.8所示),将会生成改善的场景,带有更逼真的地平线细节(如图12.10所示)。
在此示例中,更改评估着色器中的布局说明符也很有用:
layout (quads, equal_spacing) in;
更改为:
layout (quads, fractional_even_spacing) in;
在静止图像中难以说明这种修改的原因。在动画场景中,当曲面细分对象在3D空间中移动时,如果使用LOD,有时可以在对象表面上看到曲面细分级别的变化,看起来像一种叫作“弹出”的摆动伪影。从等间距变为分数间距,通过使相邻补丁实例的网格几何体更相似,达成了即使它们的细节级别不同,也可以减少此影响的目的。(参见习 题12.2和12.3。)
使用LOD可以显著降低系统负载。例如,在动画时,如果不控制 LOD,场景可能会出现不稳定或滞后的情况。
将这种简单的LOD技术应用于包含Phong着色的版本(程序12.4)有点棘手。这是因为相邻补丁实例之间的LOD变化反过来会导致相关法向量的突然变化,从而导致光照中的弹出伪影!与以往一样,在构建复杂的3D场景时需要权衡和妥协。
补充说明
将曲面细分与LOD组合在实时虚拟现实应用中特别有用,例如在计算机游戏中,其需要复杂的现实主义细节和频繁的物体移动和/或摄像机位置的变化。在本章中,我们已经说明了曲面细分和LOD用于实时地 形生成的应用场景,尽管它也可以应用于其他领域,例如3D模型的位移贴图(曲面细分顶点被添加到模型的表面,然后被移动以便添加细节)在计算机辅助设计应用程序中也很有用。
Sellers等人通过消除摄像机后方的补丁中的顶点(他们通过将内 部和外部级别设置为零来实现这一点)[SW15],进一步扩展了LOD技术(在程序12.5中显示)。这是一个剔除技术的示例,是一项非常有用 的技术,因为实例化细分的负载仍然可以在系统上正常运行。文章来源:https://www.toymoban.com/news/detail-481998.html
程序12.1中描述的createShaderProgram()的4参数版本被添加到Utils.cpp文件中。稍后,我们将添加其他版本以适应几何着色器阶段。文章来源地址https://www.toymoban.com/news/detail-481998.html
到了这里,关于计算机图形学与opengl C++版 学习笔记 第12章 曲面细分的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!