✠OpenGL-12-曲面细分


术语 Tessellation(镶嵌)是指一大类设计活动,通常是指在平坦的表面上,用各种几何形状的瓷砖相邻排列以形成图案。
曲面细分指的是生成并且操控大量三角形以渲染复杂的形状和表面,尤其是使用硬件进行渲染。

OpenGL中的曲面细分

OpenGL 对硬件曲面细分的支持,通过 3 个管线阶段提供:
(1)曲面细分控制着色器;
(2)曲面细分器(此阶段不可编程);
(3)曲面细分评估着色器。

曲面细分器其全名是曲面细分图元生成器(Tessellation Primitive Generator),TPG,是硬件支持的引擎,可以生成固定的三角形网格。
曲面细控制着色器允许我们配置曲面细分器要构建什么样的三角形网格。
曲面细评估着色器允许我们以各种方式操控网格。然后,被操控过的三角形网格,会作为通过管线前进的顶点的源数据。

细分控制处理器(Tessellation Control Processor)

细分控制处理器是一个可编程单元,它对输入顶点及其相关数据的面片(patch)进行操作,并发出新的输出面片。用OpenGL着色语言编写并在该处理器上运行的编译单元称为细分控制着色器(tessellation control shaders)。成功编译并链接一组细分控制着色器后,它们将生成在细分控制处理器上运行的细分控制着色器可执行文件。

将为输出面片的每个顶点调用细分控制着色器。每次调用都可以读取输入或输出面片中任何顶点的属性,但只能写入相应输出面片顶点的逐顶点属性(per-vertex attributes)。着色器调用为输出面片共同生成一组逐面片属性(per-patch attributes)。

在所有细分控制着色器调用完成后,输出顶点和逐面片属性将被组合,以形成一个面片,供后续管道阶段使用。

细分控制着色器调用主要独立运行,相对执行顺序未定义。但是,内置函数 barrier() 可以通过同步调用来控制执行顺序,从而有效地将细分控制着色器执行划分为一组阶段。如果一个调用在同一阶段的任何时间点读取另一个调用写入的逐顶点或逐面片属性,或者如果两个调用试图在单个阶段向同一个逐面片输出32位组件(32-bit component)写入不同的值,细分控制着色器将获得未定义的结果。

细分评估处理器(Tessellation Evaluation Processor)

细分评估处理器是一个可编程单元,它使用传入顶点的面片及其相关数据,评估由细分图元生成器(tessellation primitive generator)生成的顶点的位置和其他属性。用OpenGL着色语言编写并在该处理器上运行的编译单元称为细分评估着色器(tessellation evaluation shaders)。成功编译并链接一组细分评估着色器后,它们将生成在细分评估处理器上运行的细分评估着色器可执行文件。

每次调用细分评估可执行文件都会计算由细分图元生成器生成的单个顶点的位置和属性。可执行文件可以读取输入面片中任何顶点的属性,以及细分坐标,该坐标是要细分的图元中顶点的相对位置。可执行文件写入顶点的位置和其他属性。

面片(patch):最基本的面片(也叫“补丁”)是平面三角形区域,我们通常说的面片也就是三角形面片。(如果一个多边形在同一平面,则多边形区域也是一个面片。)

图元(primitive):OpenGL认为,所有物体都是由点、线、多边形构成的;点、线、多边形就被称为图元。

示例

我们从一个简单的应用程序开始,该应用程序只使用曲面细分器创建顶点的三角形网格,然后在不进行任何操作的情况下显示它。为此,我们需要以下模块:

  1. C++/OpenGL 应用程序:
    创建一个摄像机和相关的 MVP 矩阵,视图(v)和投影(p)矩阵确定摄像机朝向,模型(m)矩阵可用于修改网格的位置和方向。
  2. 顶点着色器:
    在这个例子中基本上什么都不做,顶点将在曲面细分器中生成。
  3. 曲面细分控制着色器:
    指定曲面细分器要构建的网格。
  4. 曲面细分评估着色器:
    将 MVP 矩阵应用于网格中的顶点。
  5. 片段着色器:
    只需为每个像素输出固定颜色。

为此我们需要实现createShaderProgram()的4参数重载版本:

GLuint createShaderProgram(const char *vp, const char *tCS, const char *tES, const char *fp) {
	string vertShaderStr = readShaderSource(vp);
	string tcShaderStr = readShaderSource(tCS);// New!
	string teShaderStr = readShaderSource(tES);// New!
	string fragShaderStr = readShaderSource(fp);

	const char *vertShaderSrc = vertShaderStr.c_str();
	const char *tcShaderSrc = tcShaderStr.c_str();// New!
	const char *teShaderSrc = teShaderStr.c_str();// New!
	const char *fragShaderSrc = fragShaderStr.c_str();

	GLuint vShader = glCreateShader(GL_VERTEX_SHADER);
	GLuint tcShader = glCreateShader(GL_TESS_CONTROL_SHADER);// New!
	GLuint teShader = glCreateShader(GL_TESS_EVALUATION_SHADER);// New!
	GLuint fShader = glCreateShader(GL_FRAGMENT_SHADER);

	glShaderSource(vShader, 1, &vertShaderSrc, NULL);
	glShaderSource(tcShader, 1, &tcShaderSrc, NULL);// New!
	glShaderSource(teShader, 1, &teShaderSrc, NULL);// New!
	glShaderSource(fShader, 1, &fragShaderSrc, NULL);

	glCompileShader(vShader);
	glCompileShader(tcShader);// New!
	glCompileShader(teShader);// New!
	glCompileShader(fShader);

	GLuint vtfprogram = createProgram();
	glAttachShader(vtfprogram, vShader);
	glAttachShader(vtfprogram, tcShader);// New!
	glAttachShader(vtfprogram, teShader);// New!
	glAttachShader(vtfprogram, fShader);

	glLinkProgram(vtfprogram);
	return vtfprogram;
}

void init(GLFWwindow* window) {
	renderingProgram = 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;// 网格位置
	...
}

void display(GLFWwindow* window, double currentTime) {
	...
	glUseProgram(renderingProgram);
	...
	mMat = glm::rotate(mMat, toRadians(35.0f), glm::vec3(1.0f, 0.0f, 0.0f));
	...
	glPatchParameter(GL_PATCH_VERTICES, 1);
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);
	glDrawArrays(GL_PATCHES, 0, 1);
}

void glPatchParameteri(GLenum pname, GLint value)
指定面片图元的参数。
pname - 指定要设置的参数的名称。
    接受符号常量:GL_PATCH_VERTICES, GL_PATCH_DEFAULT_OUTER_LEVEL, and GL_PATCH_DEFAULT_INNER_LEVEL
value - 为pname给定的参数指定新值。
当pname为GL_PATCH_VERTICES时,value指定将用于组成单个面片图元的顶点数。细分控制着色器(如果存在)使用面片图元,然后用于细分。当使用glDrawArrays或类似函数指定图元时,每个面片将由参数控制点生成,每个控制点由从启用的顶点数组中获取的顶点表示。

void glPolygonMode(GLenum face, GLenum mode)
选择多边形光栅化模式。
face - 指定mode应用于的多边形。
mode - 指定多边形将如何栅格化。接受符号常量:GL_POINT, GL_LINE, GL_FILL。
默认值:face = GL_FRONT_AND_BACK,mode = GL_FILL。

glDrawArrays(GL_PATCHES, first, count)-从数组数据渲染图元。
patch仅仅是传入到OpenGL的一列顶点列表,该列表在处理期间保存它们的次序。当用细分曲面与patch进行渲染时,使用像glDrawArrays()这样的渲染命令,并指定从绑定的顶点缓存对象(VBO)将被读出的顶点的总数,然后为该绘制调用进行处理。当用其它的OpenGL图元进行渲染时,OpenGL基于在绘制调用中所指定的图元类型而隐式地知道要使用多少顶点,比如使用三个顶点来绘制一个三角形。然后,当使用一个patch时,需要告诉OpenGL顶点数组中要使用多少个顶点来组成一个patch,而这可以通过使用glPatchParameteri()进行指定。由同一个绘制调用所处理的patch,它们的尺寸(即每个patch的顶点个数)将是相同的。

// 顶点着色器
#version 430
void main() {}

// 曲面细分控制着色器
#version 430
layout(vertices = 1) out;
void main() {
	gl_TessLevelOuter[0] = 6;// 外层级别0
	gl_TessLevelOuter[2] = 6;// 外层级别2
	gl_TessLevelOuter[1] = 6;// 外层级别1
	gl_TessLevelOuter[3] = 6;// 外层级别3
	gl_TessLevelInner[0] = 12;// 内层级别0
	gl_TessLevelInner[1] = 12;// 内层级别1
}

// 曲面细分评估着色器
#version 430
uniform mat4 mvp_matrix;
layout(quads, equal_spacing, ccw) in;
void main() {
	float u = gl_TessCoord.x;// 细分网格点
	float v = gl_TessCoord.y;
	gl_Position = mvp_matrix * vec(u, 0, v, 1);// 在X-Z平面展示网络(输出要渲染的表面的点)
}

// 片段着色器
#version 430
out vec4 color;
void main() {
	color = vec4(1.0, 1.0, 0.0, 1.0);// 黄色
}


细分级别如下图:

正解细分级别设置:

// 曲面细分控制着色器
#version 410 core
layout (vertices = 4) out;
void main(void) {
    gl_TessLevelInner[0] = 3.0;     // 内部划分3条垂直区域,即内部新增2列顶点
    gl_TessLevelInner[1] = 4.0;     // 内部划分4条水平区域,即内部新增3行顶点
    
    gl_TessLevelOuter[0] = 2.0;     // 左边2条线段
    gl_TessLevelOuter[1] = 3.0;     // 下边3条线段
    gl_TessLevelOuter[2] = 4.0;     // 右边4条线段
    gl_TessLevelOuter[3] = 5.0;     // 上边5条线段
}


调整摄像机位置,并让网格分别绕XW轴旋转0°、35°和90°:

void init(GLFWwindow* window) {
	...
	// 原:cameraX = 0.5f; cameraY = -0.5f; cameraZ = 2.0f;
	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 2.0f;
}

void display(GLFWwindow* window, double currentTime) {
	...
	mMat = glm::rotate(mMat, toRadians(0.0f/35.0f/90.0f), glm::vec3(1.0f, 0.0f, 0.0f));
	...
}

则效果如下:

网格绕XW轴旋转35°,并画出XYZ轴,其中Z轴方向垂直屏幕向外。

由以上效果图,可知:
1)调整摄像机的位置,可以让视角变化,从而在视口呈现产生变化。从而可以让网格平面居屏幕中央。
2)rotate=0°时,看到front朝向覆盖了back朝向的点,从而说明网格是在XZ平面的。并且,默认构造出的网格的所有顶点都在[0…1]范围内。
3)网格的左上角与XYZ轴的(0,0,0)点重合。

示例代码分析:
输出变量gl_TessLevelOuter[]gl_TessLevelInner[]仅在曲面细分控制着色器语言中可用。写入这些变量的值被指定给输出面片(output patch)的相应外部和内部镶嵌级别。它们由曲面细分控制着色器用于控制基本体镶嵌,并可由曲面细分评估着色器读取。如果细分控制着色器处于活动状态,则这些变量将填充曲面细分控制着色器写入的相应输出。否则,将使用OpenGL图形系统规范中指定的默认细分级别来指定它们。

变量gl_TessCoord仅在曲面细分着色器语言中可用。它指定一个三分量(u, v, w)向量,标识着色器处理的顶点相对于要细分的图元的位置。其值将遵循属性,以帮助复制细分计算。

layout(vertices = 1) out;
曲面细分控制着色器允许只在接口限定符out上使用输出布局限定符,而不是在输出块、块成员或变量声明。
标识符 vertices 指定由曲面细分控制着色器生成的输出面片中的顶点数,它也是曲面细分着色器被调用的次数(本例中只调用一次)。如果一个输出顶点计数小于或等于零,或大于基于实现的最大面片大小时,会出错。
vertices 用来指定从顶点着色器传递给控制着色器(以及“输出”给评估着色器)的每个“补丁”的顶点数。在我们现在这个程序中没有任何顶点,但我们仍然必须指定至少一个,因为它也会影响控制着色器被执行的次数。 稍后这个值将反映控制点的数量, 并且必须与 C++/OpenGL 应用程序中 glPatchParameteri() 调用中的值匹配。

layout(quads, equal_spacing, ccw) in;
(a)图元模式:triangles(分割三角形为较小的三角形)、quads(分割四边形为三角形)、isolines(分割四边形为一组线)。
(b)顶点间隔:equal_spacing(边缘应该被分成一组大小相等的片段)、fractional_even_spacing(边缘应被分成偶数个等长段加上两个额外的较短的“分数”段)、fractional_odd_spacing(边缘应被分成奇数个等长段加上两个额外的较短的“分数”段)。
(c)绘制三角形的绕向:cw、ccw。
(d)点模式(本例未使用):曲面细分控制着色器应该为细分的图元中的每个唯一顶点生成一个点,而不是生成直线或三角形。
以上这些标识符中的任何或所有标识符可以在单个输入布局声明中指定一次或多次。

gl_Position = mvp_matrix * vec(u, 0, v, 1);
曲面细分网格的朝向使得它位于 X-Z 平面中,因此 gl_TessCoord 的 X 和 Y 分量被应用于网格的 X 和 Z 坐标。本例中网格坐标和gl_TessCoord的值的范围都在0.0~1.0(这在计算纹理坐标时会很方便),并且gl_TessCoord的xyz值和为1(三个分量应该就是三个顶点分别占的比重)。然后,评估着色器使用 MVP 矩阵定向每个顶点(这在之前的示例中,是由顶点着色器完成的)。

glDrawArrays(GL_PATCHES, first, count);
当使用曲面细分时,从C++/OpenGL 应用程序发送到管线(即在 VBO 中)的顶点不会被渲染,但通常会被当作控制点,就像我们在贝塞尔曲线中看到的那些一样。一组控制点被称作“补丁”,并且在使用曲面细分的代码段中,GL_PATCHES 是唯一允许的图元类型。 “补丁”中顶点的数量在 glPatchParameteri()的调用中指定。
在这个特定示例中,没有任何【控制点】被发送,但我们仍然需要指定至少一个。类似地,在 glDrawArrays()调用中,我们指示起始值为0,顶点数量为 1,即使我们实际上没有从 C++程序发送任何顶点。

glPolygonMode(face, mode)的调用指定了如何光栅化网格。我们的代码中 mode 用的是 GL_LINE,如在上图中看到的那样,它只会导致连接线被光栅化(因此我们可以看到由曲面细分器生成的网格本身)。如果我们将该行代码更改为 GL_FILL(或将其注释掉),我们将得到如下图所示的版本。

贝塞尔曲面细分

假设我们希望建立一个三次方贝塞尔曲面, 我们将需要 16 个控制点。 我们可以通过 VBO从 C++端发送它们, 或者我们可以在顶点着色器中硬编码写死它们。

细分网格应该为我们提供了足够的顶点来对曲面进行采样(如果我们想要更多的话,我们可以增加内部/外部细分级别)。

我们知道OpenGL 提供了一个名为 gl_VertexID 的内置变量,它保存一个计数器,指示顶点着色器当前正在执行哪次调用。曲面细分控制着色器中存在一个类似的内置变量 gl_InvocationID

曲面细分的一个强大功能是 TCS(以及 TES)着色器可以同时访问数组中的所有[控制点]顶点

当每个调用都可以访问所有顶点时,TCS 仍对每个顶点都会执行一次,这可能会让人感到困惑。
在每个 TCS 调用中,冗余地在赋值语句中指定曲面细分级别,这也是违反直觉的。
尽管所有这些看起来都很奇怪,但这样做是因为曲面细分的架构设计使得 TCS 调用可以并行运行

OpenGL 提供了几个用于 TCS 和 TES 着色器的内置变量。我们已经提到过的是 gl_InvocationID,当然还有 gl_TessLevelInner 和 gl_TessLevelOuter。以下是一些最有用的内置变量的更多细节和描述。

在曲面细分控制着色器(TCS)中,内置变量原始定义为:

in gl_PerVertex {// PerVertex=Per+Vertex,即每网格顶点
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
} gl_in[gl_MaxPatchVertices];

in int gl_PatchVerticesIn;
in int gl_PrimitiveID;
in int gl_InvocationID;

out gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
} gl_out[];

patch out float gl_TessLevelOuter[4];
patch out float gl_TessLevelInner[2];

在曲面细分评估着色器(TES)中,内置变量原始定义为:

in gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
} gl_in[gl_MaxPatchVertices];

in int gl_PatchVerticesIn;
in int gl_PrimitiveID;
in vec3 gl_TessCoord;

patch in float gl_TessLevelOuter[4];
patch in float gl_TessLevelInner[2];

out gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
};

曲面细分控制着色器( TCS)内置变量:

  • gl_in[]——包含每个传入的[控制点]顶点的数组——每个传入顶点是一个数组元素。
  • gl_out[]——用于将输出[控制点]的顶点发送到TES的一个数组——每个输出顶点是一个数组元素。
  • gl_InvacationID——整形ID计数器,指示TCS当前正在执行哪个调用。

曲面细分评估着色器( TES)内置变量:

  • gl_in[]——包含每个传入的[控制点]顶点的数组——每个传入顶点是一个数组元素。
  • gl_Position——曲面细分网格顶点的输出位置,可能在 TES 中被修改。

要注意 gl_Position 和 gl_in[N].gl_Position 是不同的——gl_Position 是起源于[曲面细分器]的输出顶点的位置, 而 gl_in[N].gl_Position 是一个从 TCS 进入 TES 的控制点顶点位置。

值得注意的是, TCS 中的输入和输出控制点顶点属性是数组。不同的是, TES 中的输入控制点顶点和顶点属性是数组,但输出顶点是标量。此外,很容易混淆哪些顶点来自于控制点,哪些顶点是细分建立的,然后移动以形成结果曲面。总而言之, TCS 中的所有顶点输入和输出都是控制点;TES 中,gl_in[]保存输入的控制点,gl_TessCoord 保存输入的细分网格点,gl_Position 保存用于渲染的输出表面顶点。

生成三次贝塞尔曲面:
下面程序显示了所有 4 个着色器——顶点、 TCS、 TES 和片段——用于指定控制点补丁,生成平坦的曲面细分顶点网格,在控制点指定的曲面上重新定位这些顶点,并使用纹理图像绘制生成的曲面。

我们的曲面细分控制着色器现在有两个任务:指定曲面细分级别并将控制点从顶点着色器传递到评估着色器。 然后, 评估着色器可以根据贝塞尔控制点修改网格点( gl_TessCoords)的位置。

回顾一下坐标点与uv轴的相对关系(二次贝塞尔曲面):


u轴方向是p30、p31、p32…下标的个位数递增的方向;v轴方向是p30、p20、p10…下标的十位数递减的方向。

回顾一下“三次贝塞尔曲面”公式:
P ( u , v ) = ∑ i = 0 3 ∑ j = 0 3 p i j B i ( u ) B j ( v ) 其 中 , B 0 ( u ) = ( 1 − u ) 3 B 1 ( u ) = − 3 u 3 − 6 u 2 + 3 u B 2 ( u ) = − 3 u 3 + 3 u 2 B 3 ( u ) = u 3 B 0 ( v ) = ( 1 − v ) 3 B 1 ( v ) = − 3 v 3 − 6 v 2 + 3 v B 2 ( v ) = − 3 v 3 + 3 v 2 B 3 ( v ) = v 3 P(u,v)=\sum_{i=0}^3\sum_{j=0}^3{p_{ij}B_i(u)B_j(v)}\\ 其中,\\ B_0(u)=(1-u)^3\\ B_1(u)=-3u^3-6u^2+3u\\ B_2(u)=-3u^3+3u^2\\ B_3(u)=u^3\\ B_0(v)=(1-v)^3\\ B_1(v)=-3v^3-6v^2+3v\\ B_2(v)=-3v^3+3v^2\\ B_3(v)=v^3\\ P(u,v)=i=03j=03pijBi(u)Bj(v)B0(u)=(1u)3B1(u)=3u36u2+3uB2(u)=3u3+3u2B3(u)=u3B0(v)=(1v)3B1(v)=3v36v2+3vB2(v)=3v3+3v2B3(v)=v3

P(u,v) = B0(u)*(B0(v)*p00 + B1(v)*p01 + B2(v)*p02 + B3(v)*p03
   + B1(u)*(B0(v)*p10 + B1(v)*p11 + B2(v)*p12 + B3(v)*p13
   + B2(u)*(B0(v)*p20 + B1(v)*p21 + B2(v)*p22 + B3(v)*p23
   + B3(u)*(B0(v)*p30 + B1(v)*p31 + B2(v)*p32 + B3(v)*p33

void init(GLFWwindow* window) {
	renderingProgram = Util::createShaderProgram("vertShader.glsl", "tessCShader.glsl", "tessEShader.glsl", "fragShader.glsl");
	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 4.0f;
	terLocX = 0.0f; terLocY = 0.0f; terLocZ = 0.0f;
	...
}

void display(GLFWwindow* window, double currentTime) {
	...
	// 模型绕Xw轴旋转30°,再绕Yw轴旋转100°
	mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));
	mMat = glm::rotate(mMat, toRadians(100.0f), glm::vec3(0.0f, 1.0f, 0.0f));
	...
	// 传入一个纹理以用来绘制表面
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, textureID);
	
	glFrontFace(GL_CCW);

	glPatchParameteri(GL_PATCH_VERTICES, 16);// 每个补丁的[控制点]数量=16
	glPolyonMode(GL_FRONT_AND_BACK, GL_FILL);
	glDrawArrays(GL_PATCHES, 0, 16);// (一组控制点被称作“补丁”)“补丁”中顶点数量=16
// 顶点着色器
#version 430
out vec2 texCoord;
void main() {
	// 由顶点着色器指定和发送控制点(这里硬编码写死)
	const vec4 vectices[] = vec4[] {
		vec4(-1.0, 0.5, -1.0, 1.0), 
		vec4(-0.5, 0.5, -1.0, 1.0),
		vec4( 0.5, 0.5, -1.0, 1.0), 
		vec4( 1.0, 0.5, -1.0, 1.0),
		
		vec4(-1.0, 0.0, -0.5, 1.0), 
		vec4(-0.5, 0.0, -0.5, 1.0),
		vec4( 0.5, 0.0, -0.5, 1.0), 
		vec4( 1.0, 0.0, -0.5, 1.0),
		
		vec4(-1.0, 0.0, 0.5, 1.0), 
		vec4(-0.5, 0.0, 0.5, 1.0),
		vec4( 0.5, 0.0, 0.5, 1.0), 
		vec4( 1.0, 0.0, 0.5, 1.0),
		
		vec4(-1.0, -0.5, 1.0, 1.0), 
		vec4(-0.5,  0.3, 1.0, 1.0),
		vec4( 0.5,  0.3, 1.0, 1.0), 
		vec4( 1.0,  0.3, 1.0, 1.0)
	}
	// 为当前顶点计算合适的纹理坐标,从[-1...+1]转换到[0...1]
	texCoord = vec2((vertices[gl_VertexID].x + 1.0) / 2.0,
					(vertices[gl_VertexID].z + 1.0) / 2.0);
	gl_Position = vertices[gl_VertexID];
}

// 曲面细分控制着色器
#version 430
in vec2 texCoord[];
out vec2 texCoord_TCSout[];// 以标量形式从顶点着色器传来的纹理坐标输出,以数组形式被接收,然后被发送给评估着色器
layout(vertices = 16) out;// 每个补丁有16个控制点,本着色器共执行16次
void main() {
	int TL = 32;// 曲面细分级别都被设置为这个值
	if (gl_InvocationID == 0) {
		gl_TessLevelOuter[0] = TL;
		gl_TessLevelOuter[1] = TL;
		gl_TessLevelOuter[2] = TL;
		gl_TessLevelOuter[3] = TL;
		gl_TessLevelInner[0] = TL;
		gl_TessLevelInner[1] = TL;
	}
	// 将纹理和控制点传递给TES
	texCoord_TCSout[gl_InvocationID] = texCoord[gl_InvocationID];
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

// 曲面细分评估着色器
#version 430
layout(quads, equal_spacing, ccw) in;
uniform mat4 mvp_matrix;
in vec2 texCoord_TCSout[];// 控制点纹理坐标以数组形式传进来
out vec2 texCoord_TESout;// 每个网格顶点纹理坐标以标量形式传出
void main() {
	vec3 p00 = (gl_in[0].gl_Position).xyz;
	vec3 p10 = (gl_in[1].gl_Position).xyz;
	vec3 p20 = (gl_in[2].gl_Position).xyz;
	vec3 p30 = (gl_in[3].gl_Position).xyz;
	vec3 p01 = (gl_in[4].gl_Position).xyz;
	vec3 p11 = (gl_in[5].gl_Position).xyz;
	vec3 p21 = (gl_in[6].gl_Position).xyz;
	vec3 p31 = (gl_in[7].gl_Position).xyz;
	vec3 p02 = (gl_in[8].gl_Position).xyz;
	vec3 p12 = (gl_in[9].gl_Position).xyz;
	vec3 p22 = (gl_in[10].gl_Position).xyz;
	vec3 p32 = (gl_in[11].gl_Position).xyz;
	vec3 p03 = (gl_in[12].gl_Position).xyz;
	vec3 p13 = (gl_in[13].gl_Position).xyz;
	vec3 p23 = (gl_in[14].gl_Position).xyz;
	vec3 p33 = (gl_in[15].gl_Position).xyz;

	// 曲面细分网格的朝向使得它位于 X-Z 平面中,
	// 因此 gl_TessCoord 的 X 和 Y 分量被应用于网格的 X 和 Z 坐标。
	float u = gl_TessCoord.x;
	float v = gl_TessCoord.y;

	// 三次贝塞尔基础函数
	float bu0 = (1.0-u)*(1.0-u)*(1.0-u);// (1-u)^3
	float bu1 = 3.0*u*(1.0-u)*(1.0-u);// 3u(1-u)^2
	float bu2 = 3.0*u*u*(1.0-u);// 3u^2(1-u)
	float bu3 = u*u*u;// u^3
	float bv0 = (1.0-v)*(1.0-v)*(1.0-v);// (1-v)^3
	float bv1 = 3.0*v*(1.0-v)*(1.0-v);// 3v(1-v)^2
	float bv2 = 3.0*v*v*(1.0-v);// 3v^2(1-v)
	float bv3 = v*v*v;// v^3

	// 输出曲面细分补丁中的顶点位置
	vec3 outputPosition = 
		bu0 * (bv0*p00 + bv1*p01 + bv2*p02 + bv3*p03)
	  + bu1 * (bv0*p10 + bv1*p11 + bv2*p12 + bv3*p13)
	  + bu2 * (bv0*p20 + bv1*p21 + bv2*p22 + bv3*p23)
	  + bu3 * (bv0*p30 + bv1*p31 + bv2*p32 + bv3*p33);
	gl_Position = mvp_matrix * vec4(outputPosition, 1.0f);

	// 输出插值过的纹理坐标
	// 注意:gl_TessCoord是曲面细分网格顶点的位置而非控制点的位置
	vec2 tc1 = mix(texCoord_TCSout[0], texCoord_TCSout[3], gl_TessCoord.x);
	vec2 tc2 = mix(texCoord_TCSout[12], texCoord_TCSout[15], gl_TessCoord.x);
	vec2 tc = mix(tc2, tc1, gl_TessCoord.y);
	texCoord_TESout = tc;
}

// 片段着色器
#version 430
in vec2 texCoord_TESout;
out vec4 color;
layout(binding = 0) uniform sampler2D tex_color;
void main() {
	color = texture(tex_color, texCoord_TESout);
}

控制点直观图如下:(注意:曲面细分网格的朝向使得它位于 X-Z 平面中)


我们使用的纹理图如下:

如果在display()函数中去掉两个旋转函数rotate(),则效果如下:

如果指定为线模式,即设置glPolyonMode(GL_FRONT_AND_BACK, GL_LINE); 并设置fragColor为黄色,则效果如下图:

程序最终运行效果如下图:

genType mix(genType A, genType B, float a)
返回A和B的线性混合,例如,A(1-a) + Ba,其中a的范围是:0~1
令A=(x0, y0),B=(x1, y1),则
C=A(1-a)+Ba
=((1-a)x0+ax1, (1-a)y0+ay1)
=(x0+a(x1-x0), y0+a(y1-y0))
而AB的直线方程为:(y-y0)/(x-x0)=(y1-y0)/(x1-x0)
上述点满足直线方程,所以点C在点A、B连线的直线上。
假设点B在点A的右上方,则x1>x0,y1>y0;又0<a<1,所以Cx∈(x0, x1),Cy∈(y0, y1),即点C在AB线段上。
a=0时C=(x0,y0),a=1时C=(x1,y1),所以a值的大小控制离起始点A的距离
推而广之,对于点A、点B的其他相对位置,结论是一样的。

tc1和tc2的mix()函数第三个参数都为同样的gl_TessCoord.x,这样才能保证在水平方向按gl_TessCoord.x的值线性取值了。同样,要保证在垂直方向按gl_TessCoord.y的值线性取值,因为在最终运行效果上,相对于上图的纹理图像从上到下是世界坐标系y值增大的方向,所以第3个mix()函数的第一个参数就得是第2个mix()函数的返回值、第二个参数就得是第1个mix()函数的返回值(别搞反了),第三个参数就得是gl_TessCoord.y。这样才能保证,每个细分小格子的坐标点都正确的取到纹理坐标。

这次在片段着色器中, 不再是输出单一颜色,而是应用标准纹理。属性 texCoord_TESout 中的纹理坐标是在评估着色器中生成的纹理坐标。对 C++程序的更改同样很简单——请注意,现在指定的补丁大小为16。

顶点着色器现在指定代表特定贝塞尔曲面的 16 个控制点(“补丁”顶点)。在这个例子中,它们都被归一化到范围[−1…+1]。顶点着色器还使用控制点来确定适合细分网格的纹理坐标,其值在[0…1]范围内。很重要的是,要重申顶点着色器输出的顶点不是将要用来光栅化的顶点,而是贝塞尔曲面控制点。使用曲面细分时,补丁顶点永远不会被光栅化——只有曲面细分顶点会被光栅化

控制着色器仍然会指定内部和外部曲面细分级别。它现在还负责将控制点和纹理坐标发送到评估着色器。请注意,曲面细分级别只需要指定一次,因此该步骤仅在第 0 次调用期间完成(回想一下 TCS 每个顶点运行一次,因此在此示例中有 16 次调用)。为方便起见,我们为每个细分级别指定了 32 个细分。

接下来,评估着色器执行所有贝塞尔曲面计算。 main()开头的大块赋值语句从每个传入 gl_in 的 gl_Position 中提取控制点(请注意,这些控制点对应于控制着色器的 gl_out 变量)。然后使用来自曲面细分器的网格点计算混合函数的权重,从而生成一个新的 outputPosition,然后应用模型-视图-投影矩阵,为每个网格点生成输出 gl_Position 并形成贝塞尔曲面。

另外,还需要创建纹理坐标。顶点着色器仅为每个控制点位置提供一个纹理坐标。但我们并不是要渲染控制点,我们最终需要更多的曲面细分网格点的纹理坐标。有很多方法可以做到这一点,在这里我们利用 GLSL 方便的混合功能对它们进行线性插值。 mix()函数需要 3 个参数:(a)起始点;(b)结束点;©内插值,范围为 0~1。它返回与内插值对应的起点和终点之间的值。由于细分网格坐标的范围也是 0~1,所以它们可以直接用于此目的。

地形、高度图的细分

在顶点着色器中执行高度贴图可能会遇到顶点数量不足以用来渲染所需的细节的情况。现在我们有了生成大量顶点的方法,让我们回到月球表面纹理贴图并将其用作高度贴图,提升曲面细分顶点来生成月球表面细节。

我们的策略是,在 X-Z 平面中放置细分网格,并使用高度贴图来设置每个细分网格点的 Y 坐标。要做到这一点,我们不需要补丁,因为可以硬编码细分网格的位置,因此我们将在 glDrawArrays()和 glPatchParameteri()中为每个补丁指定所需的最少的 1 个顶点,如上面程序中所做的那样。月亮纹理图像既用于颜色,也用作高度图。

我们通过将曲面细分网格的 gl_TessCoord 值映射到顶点和纹理的适当范围,在评估着色器中生成顶点和纹理坐标(当然,在某些应用程序中,纹理坐标是在外部生成的)。评估着色器也通过添加月亮纹理的一小部分颜色分量到输出顶点的 Y 分量,来实现高度贴图。

// 顶点着色器
#version 430
void main() {}

// 曲面细分控制着色器
#version 430
layout(vertices = 1) out;// 这个应用程序中不需要控制点
void main() {
	int TL = 32;
	if (gl_InvocationID == 0) {
		gl_TessLevelOuter[0] = TL;
		gl_TessLevelOuter[1] = TL;
		gl_TessLevelOuter[2] = TL;
		gl_TessLevelOuter[3] = TL;
		gl_TessLevelInner[0] = TL;
		gl_TessLevelInner[1] = TL;
	}
}

// 曲面细分评估着色器
#version 430
out vec2 tes_out;
uniform mat mvp_matrix;
layout(binding = 0) uniform sampler2D tex_color;
void main() {
	// 将曲面细分网格顶点从[0...1]映射到想要的顶点[-0.5...+0.5](为了居中视口显示),这对纹理无任何影响。
	vec4 tessellatedPoint = vec4(gl_TessCoord.x-0.5, 0.0, gl_TessCoord.y-0.5, 1.0);
	// 垂直“翻转”Y值,以将曲面细分网格顶点映射到纹理坐标
	// 因为:左上顶点坐标是(0,0),左下纹理坐标是(0,0)
	vec2 tc = vec2(gl_TessCoord.x, 1.0 - gl_TessCoord.y);
	// 图像是灰度图,所以任何一个颜色分量(R/G/B)都可作为高度偏移量
	// 将颜色值等比例缩小应用于Y值
	tessellatedPoint.y += texture(tex_color, tc).r / 40.0;
	// 将高度贴图提升的点转换到视觉空间
	gl_Position = mvp_matrix * tessellatedPoint;
	tes_out = tc;
}

// 片段着色器
#version 430
in vec2 tes_out;
out vec4 color;
layout(binding = 0) uniform sampler2D tex_color;
void main() {
	color = texture(tex_color, tes_out);
}

下图显示了纹理图像(左侧)和第一次尝试的最终输出。
遗憾的是,它还没有实现正确的高度贴图。

第一次结果存在严重缺陷。虽然我们现在可以看到远处地平线上的轮廓细节,但是那里的凸起与纹理贴图中的实际细节不对应。回想一下,在高度图中,白色应该表示“高”,而黑色应该表示“低”。特别是图像右上方的区域显示的大山丘与其中的浅色和深色无关。

导致此问题的原因是细分网格的分辨率。曲面细分器可以生成的最大顶点数取决于硬件。要符合 OpenGL 标准,唯一的要求是每个曲面细分级别的最大值至少为 64。我们的程序指定了一个内部和外部曲面细分级别均为 32 的单一细分网格,因此我们生成了大约 32×32 或者说刚刚超过 1 000 个顶点, 这不足以准确反映图像中的细节。 这在上图右上方(图中放大)尤其明显——边缘细节仅在沿地平线的 32 个点处采样,这会产生巨大而看起来很随机的山丘。即使我们将曲面细分值增加到 64,总共 64× 64 或刚刚超过 4 000 个顶点仍然不足以满足使用月球图像进行高度贴图的需要。

增加顶点数量的一个好方法是使用我们在前面“✠OpenGL-4-管理3D图形数据-实例化”中看到的实例化。我们的策略是让曲面细分器生成网格,并使用实例化重复数次。在顶点着色器中,我们构建了一个由 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)、(2,1)、…、(63,1);最后一列补丁位于(0,63)、…、(63,63)。给定补丁的 X 坐标是实例 ID整除 64(取模运算), Y 坐标是实例 ID 除以 64(整数除法)。然后着色器将坐标向下缩放到范围[0…1]。

控制着色器没有更改,除了它将顶点和纹理坐标传递下去。

接下来,评估着色器获取传入的细分网格顶点(由 gl_TessCoord 指定)并将它们移动到传入补丁指定的坐标范围内。它对纹理坐标也进行一样的处理,并且也会以与上面程序示例中相同的方式应用高度贴图。

片段着色器没有修改。

先看看程序最终运行效果图:

请注意,高点和低点现在更接近于图像的亮部和暗部。

// 和贝塞尔曲面示例相同,并做如下修改
glPatchParameteri(GL_PATCH_VERTICES, 4);// 补丁包含4个控制点
glDrawArraysInstanced(GL_PATCHES, 0, 4, 64 * 64);// 每4个补丁顶点一组,实例化数量64*64个
//glDrawArraysInstanced(mode, first, vertexCount, instancecount);
// 顶点着色器
#version 430
out vec2 tc;
void main() {// 实例化数量为64*64,每次实例化都对应4个补丁顶点
	vec2 patchTexCoords[] = vec2[](vec2(0,0), vec2(1,0), vec2(0,1), vec2(1,1));
	// 基于当前是哪个实例计算出坐标偏移量,从0开始计数
	int dx = gl_InstanceID % 64;// dx={[0...63],[0...63],...,[0...63]} 共64组
	int dy = gl_InstanceID / 64;// dy={[0...0],[1...1],...,[63...63]} 共64组
	// 纹理坐标分布在64个补丁上,并归一化到[0...1];翻转Y轴坐标
	tc = vec2((dx + patchTexCoords[gl_VertexID].x) / 64.0,
			  (63 - dy + patchTexCoords[gl_VertexID].y) / 64.0);
	// 顶点位置和纹理坐标相同,只是它的取值范围从-0.5到+0.5
	// 并且将 Y 轴坐标翻转回来
	gl_Position = vec4(tc.x-0.5, 0.0, (1.0-tc.y)-0.5, 1.0);
}

// 曲面细分控制着色器
#version 430
layout(vertices = 4) out;// 使用了4个补丁顶点
in vec2 tc[];
out vec2 tcs_out[];
void main() {// 一组控制点被称作“补丁”
	int TL = 32;// 每个补丁包含一个细分级别为 32 的网格
	if (gl_InvocationID == 0) { 
		gl_TessLevelOuter[0] = TL;
		gl_TessLevelOuter[1] = TL;
		gl_TessLevelOuter[2] = TL;
		gl_TessLevelOuter[3] = TL;
		gl_TessLevelInner[0] = TL;
		gl_TessLevelInner[1] = TL;
	}
	tcs_out[gl_InvocationID] = tc[gl_InvocationID];
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

// 曲面细分评估着色器
#version 430
layout(quads, equal_spacing,ccw) in;
uniform mat4 mvp_matrix;
in vec2 tcs_out[];
out vec2 tes_out;
layout (binding = 0) uniform sampler2D tex_color;
void main() {
	// 将纹理坐标映射到传入的控制点指定的子网格上
	vec2 tc = vec2(tcs_out[0].x + gl_TessCoord.x / 64.0,
				   tcs_out[0].y + (1.0 - gl_TessCoord.y) / 64.0);
	// 将细分网格映射到传入的控制点指定的子网格上
	// gl_in[N].gl_Position 是一个从 TCS 进入 TES 的 [控制点] 顶点位置。
	// gl_TessCoord.x是u分量,此处对应x分量;gl_TessCoord.y是v分量,此处对应z分量
	vec4 tessellatedPoint = vec4(gl_in[0].gl_Position.x + gl_TessCoord.x / 64.0,
								 0.0,
								 gl_in[0].gl_Position.z + gl_TessCoord.y / 64.0,
								 1.0);
	// 将高度图的高度增加给顶点
	tessellatedPoint.y += texture(tex_height, tc).r / 40.0;
	gl_Position = mvp_matrix * tessellatedPoint;
	tes_out = tc;
}

顶点着色器算法直观图:

曲面细分评估着色器算法直观图:

整合光照

现在我们已经实现了高度贴图,我们可以着手改进它并整合光照。一个挑战是我们的顶点还没有与它们相关的法向量。另一个挑战是简单地使用纹理图像作为高度图产生了过度“锯齿状”的结果——在这种情况下是因为并非纹理图像中的所有灰度变化都是由高度引起的。对于这个特定的纹理贴图, 下图(左)已经生成了一个改进的高度贴图,我们可以使用。

我们可以通过生成相邻顶点(或高度图中的相邻纹素)的高度,构建连接它们的向量以及使用叉积来计算法向量,以动态计算和创建法向量。这需要一些细微的调整,具体取决于场景的精度(和/或高度图图像)。在这里,我们使用GIMP插件来根据高度图生成法线贴图,如下图(右)所示。

我们对代码进行的大部分更改现在只是为了实现 Phong 着色的标准方法。

  • C++/OpenGL 应用程序。
    我们加载并激活一个额外的纹理来保存法线贴图,还添加了代码来指定光照和材质,就像我们在以前的应用程序中所做的那样。
  • 顶点着色器
    唯一的增补是光照统一变量的声明和法线贴图的采样器。通常在顶点着色器中完成的光照代码被移动到曲面细分评估着色器,因为直到曲面细分阶段才生成顶点。
  • 曲面细分控制着色器
    唯一的增补是光照统一变量的声明和法线贴图的采样器。
  • 曲面细分评估着色器
    Phong 光照的准备代码现在放在评估着色器中。
  • 片段着色器
    这里完成了用于计算 Phong(或 Blinn-Phong)照明的典型代码段,以及从法线贴图中提取法向量的代码。然后将光照结果与纹理图像用加权求和的方式结合起来。

顶点着色器:

#version 430
out vec2 tc;
void main(void) {	
	vec2 patchTexCoords[] = vec2[] (vec2(0,0), vec2(1,0), vec2(0,1), vec2(1,1));
	// compute an offset for coordinates based on which instance this is
	int x = gl_InstanceID % 64;
	int y = gl_InstanceID / 64;
	// texture coordinates are distributed across 64 patches	
	tc = vec2( (x+patchTexCoords[gl_VertexID].x)/64.0,
			   (63-y+patchTexCoords[gl_VertexID].y)/64.0 );
	// vertex locations range from -0.5 to +0.5
	gl_Position = vec4(tc.x-0.5, 0.0, (1.0-tc.y)-0.5, 1.0);
}

曲面细分控制着色器:

#version 430
layout (vertices = 4) out;
in vec2 tc[];
out vec2 tcs_out[];
void main(void) {
	int TL = 32;// TL:Tessellation Level
	if (gl_InvocationID == 0) {
	  	gl_TessLevelOuter[0] = TL;
	  	gl_TessLevelOuter[2] = TL;
	  	gl_TessLevelOuter[1] = TL;
	  	gl_TessLevelOuter[3] = TL;
	  	gl_TessLevelInner[0] = TL;
	  	gl_TessLevelInner[1] = TL;
	}
	tcs_out[gl_InvocationID] = tc[gl_InvocationID];
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

曲面细分评估着色器:

#version 430
layout (quads, equal_spacing,ccw) in;
layout (binding = 1) uniform sampler2D tex_height;
struct PositionalLight{	vec4 ambient; vec4 diffuse; vec4 specular; vec3 position; };
uniform PositionalLight light;
uniform mat4 mvp_matrix;
uniform mat4 mv_matrix;
out vec3 varyingVertPos;
out vec3 varyingLightDir; 

in vec2 tcs_out[];
out vec2 tes_out;
void main (void) {
	vec2 tc = vec2(tcs_out[0].x + (gl_TessCoord.x)/64.0,
				   tcs_out[0].y + (1.0-gl_TessCoord.y)/64.0);
	// map the tessellated grid onto the texture rectangle:
	vec4 tessellatedPoint = vec4(gl_in[0].gl_Position.x + gl_TessCoord.x/64.0, 0.0,
								gl_in[0].gl_Position.z + gl_TessCoord.y/64.0, 1.0);
	// add the height from the height map to the vertex:
	tessellatedPoint.y += (texture(tex_height, tc).r) / 60.0;
	
	varyingVertPos = (mv_matrix * tessellatedPoint).xyz;
	varyingLightDir = light.position - varyingVertPos;
	
	gl_Position = mvp_matrix * tessellatedPoint;
	tes_out = tc;
}

片段着色器:

#version 430
in vec2 tes_out;
out vec4 color;
layout (binding = 0) uniform sampler2D tex_color;
in vec3 varyingVertPos;
in vec3 varyingLightDir; 
struct PositionalLight{	vec4 ambient; vec4 diffuse; vec4 specular; vec3 position; };
struct Material{ vec4 ambient; vec4 diffuse; vec4 specular; float shininess; };  
uniform PositionalLight light;
uniform Material material;
uniform vec4 globalAmbient;

vec3 calcNewNormal() {
	vec3 normal = vec3(0,1,0);
	vec3 tangent = vec3(1,0,0);
	vec3 bitangent = cross(tangent, normal);
	mat3 tbn = mat3(tangent, bitangent, normal);
	vec3 retrievedNormal = texture(tex_normal,tes_out).xyz;
	retrievedNormal = retrievedNormal * 2.0 - 1.0;
	vec3 newNormal = tbn * retrievedNormal;
	newNormal = normalize(newNormal);
	return newNormal;
}
void main(void) {
	vec3 L = normalize(varyingLightDir);
	vec3 V = normalize(-varyingVertPos);
	vec3 N = calcNewNormal();
	vec3 R = normalize(reflect(-L, N));
	float cosTheta = dot(L,N);
	float cosPhi = dot(V,R);
	color = 0.5 * 
				( globalAmbient * material.ambient  +  light.ambient * material.ambient
				+ light.diffuse * material.diffuse * max(cosTheta,0.0)
				+ light.specular * material.specular * pow(max(cosPhi,0.0), material.shininess)
				) +
			0.5 * texture(tex_color, tes_out);
}

带有高度和法线贴图以及 Phong 照明的最终结果如下图所示。地形现在会响应光照。
在此示例中,位置光已放置在左侧图像中心的左侧,右侧图像中心的右侧。

尽管从静止图像很难判断出对光的移动的响应,但是读者应该能够辨别出漫射光的变化,并且山峰的镜面高光在两个图像中是非常不同的。当摄像机或光源移动时,这当然会更明显。结果仍然不完美,因为无论什么样的光照,输出中包含的原始纹理都包括了将出现在渲染结果上的阴影。

控制细节级别(LOD)

使用实例化来实时生成数百万个顶点,即使是装备精良的现代计算机也可能会感受到负担。幸运的是,将地形划分为单独的补丁的策略,正如我们为增加生成的网格顶点的数量所做的那样,也为我们提供了一种减少负担的好机制

在生成的数百万个顶点中, 许多顶点不是必需的。 靠近摄像机的补丁中的顶点非常重要,因为我们希望能够识别附近物体的细节。但是,补丁越远离摄像机,甚至光栅化过程中有足够的像素来体现我们生成的顶点数量的可能性就越小!

根据距摄像机的距离更改补丁中的顶点数量是一种称为细节级别或LOD的技术(LOD: Level Of Detail)。Sellers等人描述了一种通过修改控制着色器来控制实例化曲面细分中的 LOD 的方法。下面程序显示了 Sellers 等人的方法的简化版本。策略是使用补丁的感知大小来确定其曲面细分级别的值。由于补丁的细分网格最终将放置在由进入控制着色器的 4 个控制点定义的方格内,我们可以使用控制点相对于摄像机的位置来确定应该为补丁生成多少个顶点。
步骤如下:
(1)通过将 MVP 矩阵应用于 4 个控制点,计算它们的屏幕位置
(2)计算由控制点(在屏幕上的空间中)定义的正方形边长(即宽度和高度)。请注意,即使 4 个控制点形成正方形,这些边长也可能不同,因为应用了透视矩阵。
(3)根据曲面细分级别所需的精度(基于高度图中的细节数量),将长度的值按可调整常数进行缩放。
(4)将缩放长度值加 1,以避免将曲面细分级别指定为 0(这将导致不生成顶点)。
(5)将曲面细分级别设置为相应的计算宽度和高度值。

回想一下,在我们的实例中,我们不是只创建一个网格,而是创建 64×64 个网格。因此,对每个补丁执行以上列表中的 5 个步骤,细节级别因补丁而异。

所有更改都在控制着色器中。

变量 gl_InvocationID 指的是正在处理补丁中的哪个顶点(而不是正在处理哪个补丁)。因此,告诉曲面细分器在每个补丁中生成多少个顶点的 LOD 计算发生在每个补丁的第 0 个顶点期间。

对于远处的点,通过消除摄像机后方的补丁中的顶点(通过将内部和外部级别设置为零来实现这一点)。

// 曲面细分控制着色器
#version 430
layout (vertices = 4) out;
in vec2 tc[];
out vec2 tcs_out[];
uniform mat4 mvp_matrix;
void main(void) {
	float subdivisions = 16.0;
	if (gl_InvocationID == 0) {// 注:对每个[实例化],这个if块都执行一次
		// 透视除法(一般在管线的最后阶段自动进行),这里直接/w分量,得到在屏幕上的位置,用于比较计算操作。
	 	vec4 p0 = mvp_matrix * gl_in[0].gl_Position;  p0 = p0 / p0.w;
		vec4 p1 = mvp_matrix * gl_in[1].gl_Position;  p1 = p1 / p1.w;
		vec4 p2 = mvp_matrix * gl_in[2].gl_Position;  p2 = p2 / p2.w;
		// 加1,以避免将曲面细分级别指定为0而导致不生成顶点。
		float width  = length(p1-p0) * subdivisions + 1.0;
		float height = length(p2-p0) * subdivisions + 1.0;
		gl_TessLevelOuter[0] = height;
		gl_TessLevelOuter[2] = height;
		gl_TessLevelOuter[1] = width;
		gl_TessLevelOuter[3] = width;
		gl_TessLevelInner[0] = width;
		gl_TessLevelInner[1] = height;
	}
	tcs_out[gl_InvocationID] = tc[gl_InvocationID];
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}
// 像以前一样将纹理坐标和控制点发送给 TES

float length(genType x)
指定要计算其长度的向量。

将这些控制着色器的更改应用于之前我们场景的实例化(但不带光照)版本,并将高度图替换为的更精细调整的版本,将会生成改善的场景,带有更逼真的地平线细节。运行效果图如下:

在此示例中,更改评估着色器中的布局说明符也很有用:

layout(quads, equal_spacing) in;
更改为:
layout(quads, fractional_even_spacing) in;

在静止图像中难以说明这种修改的原因。在动画场景中,当曲面细分对象在 3D 空间中移动时,如果使用 LOD,有时可以在对象表面上看到曲面细分级别的变化,看起来像一种叫作“弹出”的弹出伪影也叫“摆动伪影”。从等间距变为分数间距,通过使相邻补丁实例的网格几何体更相似,达成了即使它们的细节级别不同,也可以减少此影响的目的。

使用 LOD 可以显著降低系统负载。例如,在动画时,如果不控制 LOD,场景可能会出现不稳定或滞后的情况。

将这种简单的 LOD 技术应用于包含 Phong 着色的版本有点棘手。这是因为相邻补丁实例之间的 LOD 变化反过来会导致相关法向量的突然变化,从而导致光照中的弹出伪影!与以往一样,在构建复杂的 3D 场景时需要权衡和妥协。

补充说明

将曲面细分与 LOD 组合在实时虚拟现实应用中特别有用,例如在计算机游戏中,其需要复杂的现实主义细节和频繁的物体移动和/或摄像机位置的变化。在本章中,我们已经说明了曲面细分和 LOD 用于实时地形生成的应用场景,尽管它也可以应用于其他领域,例如 3D 模型的位移贴图(曲面细分顶点被添加到模型的表面,然后被移动以便添加细节)在计算机辅助设计应用程序中也很有用。

Sellers 等人通过消除摄像机后方的补丁中的顶点(他们通过将内部和外部级别设置为零来实现这一点),进一步扩展了 LOD 技术(如上面例子所示)。这是一个剔除技术的示例,是一项非常有用的技术,因为实例化细分的负载仍然可以在系统上正常运行。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值