计算机图形学编程(使用OpenGL和C++)(第2版)学习笔记 12.曲面细分
1. 曲面细分
曲面细分着色器(Tessellation Shader)是OpenGL 4.0及以上版本引入的一种可编程着色器阶段,用于在GPU上对几何体进行细分,将粗糙的多边形网格自动细分为更平滑、更精细的曲面。它主要用于实现高质量的曲面渲染,如贝塞尔曲面、NURBS曲面等。
曲面细分着色器由两个部分组成:
-
细分控制着色器(Tessellation Control Shader, TCS)
决定每个patch(补丁)的细分程度,可以动态调整细分级别,实现自适应细分。 -
细分评估着色器(Tessellation Evaluation Shader, TES)
根据细分后的顶点坐标,计算每个新生成顶点的具体位置,实现曲面插值和变形。
曲面细分着色器的优点是可以在不增加原始模型数据的情况下,动态生成高精度的曲面,提高渲染质量,广泛应用于角色建模、地形渲染等领域。
从顶点着色器中获取顶点位置后,曲面细分着色器会根据细分控制着色器输出的细分因子,对每个顶点进行细分,生成新的顶点。这些新生成的顶点会传递给细分评估着色器,由细分评估着色器计算每个顶点的位置,实现曲面插值和变形。最后,曲面细分着色器会将新的顶点传递给片段着色器,进行最后的渲染。
注意:顶点着色器中输出的顶点不会用于最后渲染,其只是做为细分控制着色器输入的顶点,用于计算细分因子。
1.0.1. 细分控制着色器(TCS)
TCS(细分控制着色器)中的主要任务就是设置外层细分等级(gl_TessLevelOuter)和内层细分等级(gl_TessLevelInner)。这是曲面细分着色器中用于控制patch(补丁)细分精度的参数。
-
外层细分等级(gl_TessLevelOuter)
控制patch边界(如四边形的四条边)被细分成多少段。每个外层等级对应patch的一条边,数值越大,边被细分得越细,生成的网格越密集。 -
内层细分等级(gl_TessLevelInner)
控制patch内部的细分程度。对于四边形patch,有两个内层等级,分别控制u和v方向内部的细分密度。数值越大,patch内部被细分得越细,曲面越平滑。
简单理解:
- 外层细分等级决定边界的分段数,影响轮廓的精细度。
- 内层细分等级决定内部网格的密度,影响曲面内部的平滑度。
gl_TessLevelOuter
和 gl_TessLevelInner
这两个数组的元素个数取决于 patch 的类型:
-
对于 四边形 patch(quads):
gl_TessLevelOuter
有 4 个元素(分别对应四条边)。gl_TessLevelInner
有 2 个元素(分别对应 u 和 v 两个方向的内部细分)。
-
对于 三角形 patch(triangles):
gl_TessLevelOuter
有 3 个元素(分别对应三条边)。gl_TessLevelInner
有 1 个元素(对应内部细分)。
1.0.2. 内部网格新生成的顶点数量
生成的顶点数主要由内层细分等级决定,与外层细分等级无关。
- 内层细分等级(gl_TessLevelInner) 决定了 patch 内部网格的密度,也就是细分后生成的顶点数量。
- 外层细分等级(gl_TessLevelOuter) 决定 patch 边界的分段数,影响边界的平滑度,但不会直接影响整个 patch 内部生成的顶点总数。
1.0.2.1. 三角形patch
对于三角形 patch(triangles):
gl_TessLevelOuter
有 3 个元素,分别对应三角形的三条边,每个值决定对应边被细分成多少段。gl_TessLevelInner
有 1 个元素,决定三角形内部的细分密度。
生成的顶点数量
三角形 patch 细分后,生成的顶点数为:
(内层细分等级 + 1) × (内层细分等级 + 2) / 2
例如,内层细分等级为 N,则顶点数为 (N+1) × (N+2) / 2。
示例:
如果 gl_TessLevelInner[0] = 4
,则生成的顶点数为 (4+1) × (4+2) / 2 = 5 × 6 / 2 = 15 个顶点。
总结:
- 三角形 patch:
gl_TessLevelOuter[3]
,gl_TessLevelInner[1]
- 顶点数 = (内层细分等级 + 1) × (内层细分等级 + 2) / 2
1.0.2.2. 四边形patch
对于四边形 patch(quads):
gl_TessLevelOuter
有 4 个元素,分别对应四条边,每个值决定对应边被细分成多少段。gl_TessLevelInner
有 2 个元素,分别对应 u 和 v 两个方向的内部细分密度。
生成的顶点数量
四边形 patch 细分后,生成的顶点数为:
(内层细分等级[0] + 1) × (内层细分等级[1] + 1)
例如,若 gl_TessLevelInner[0] = M
,gl_TessLevelInner[1] = N
,则顶点数为 (M+1) × (N+1)。
示例:
如果 gl_TessLevelInner[0] = 12
,gl_TessLevelInner[1] = 12
,则生成的顶点数为 (12+1) × (12+1) = 169 个顶点。
总结:
- 四边形 patch:
gl_TessLevelOuter[4]
,gl_TessLevelInner[2]
- 顶点数 = (内层细分等级[0] + 1) × (内层细分等级[1] + 1)
1.1. 细分评估着色器(TES)
细分评估着色器(Tessellation Evaluation Shader,简称 TES)是 OpenGL 曲面细分管线中的一个阶段。它的主要作用是:
在细分控制着色器(TCS)设置好细分等级并生成了新的细分点后,TES 会根据每个细分点的参数(如 u、v 坐标),结合 patch 的控制点,计算出每个新顶点的具体位置,实现曲面的插值和变形。
主要特点:
- TES 的输入是细分后的参数坐标(如 gl_TessCoord),以及 patch 的控制点数据。
- TES 负责根据细分参数和控制点,计算每个新生成顶点的最终位置(如贝塞尔曲面插值)。
- TES 的输出会传递给后续的几何着色器或片段着色器,参与最终渲染。
常见用法:
- 在 TES 中可以实现贝塞尔曲面、NURBS 曲面等高质量曲面插值。
- 也可以在 TES 中进行法线、纹理坐标等属性的插值计算。
示例代码:
#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);
}
TES 是实现高质量曲面细分和渲染的关键阶段。
1.2. 常用变量
TCS(细分控制着色器)和 TES(细分评估着色器)的常用全局变量如下:
1.2.1. TCS(Tessellation Control Shader)常用全局变量
-
gl_in[]
输入顶点数组,包含从顶点着色器传递过来的每个控制点的数据(如位置、属性等)。 -
gl_out[]
输出顶点数组,传递给 TES 的每个控制点的数据。 -
gl_InvocationID
当前 TCS 实例的索引(0 ~ patch顶点数-1),用于区分每个控制点。 -
gl_PatchVerticesIn
当前 patch 包含的控制点数量。 -
gl_TessLevelOuter[]
外层细分等级数组,用于设置 patch 边界的细分密度。 -
gl_TessLevelInner[]
内层细分等级数组,用于设置 patch 内部的细分密度。
1.2.2. TES(Tessellation Evaluation Shader)常用全局变量
-
gl_in[]
输入 patch 的控制点数据(由 TCS 输出)。 -
gl_TessCoord
当前细分点在 patch 内的参数坐标(如(u, v)或(barycentric)),范围通常为[0,1]。 -
gl_PatchVerticesIn
当前 patch 包含的控制点数量。 -
gl_PrimitiveID
当前 patch 的索引(用于实例化渲染时区分不同 patch)。
曲面细分控制着色器中的输入和输出控制点顶点和顶点属性是数组。不同的是,曲面细分评估着色器中的输入控制点顶点和顶点属性是数组,但输出顶点是标量
1.3. 基本曲面细分器网格
运行结果
1.3.1. 思路
- cpp 中指定曲面细分着色器,设置相关参数
- 顶点着色器中输出顶点位置,用于计算细分因子
- 细分控制着色器中计算细分因子
- 细分评估着色器中计算每个顶点的位置
- 片段着色器中渲染顶点
1.3.2. main.cpp: display()
glPatchParameteri(GL_PATCH_VERTICES, 1);glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // FILL or LINEglDrawArrays(GL_PATCHES, 0, 1);
glPatchParameteri
是 OpenGL 4.0 及以上版本用于曲面细分(Tessellation)的一条函数指令。它的作用是设置 patch(补丁)的相关参数,最常用的是指定每个 patch 包含的顶点数量。
常用语法:
glPatchParameteri(GLenum pname, GLint value);
pname
:参数名称,常用的是GL_PATCH_VERTICES
,表示设置每个 patch 的顶点数。value
:具体的数值,比如 1、3、4、16 等,取决于你的 patch 结构(如贝塞尔曲面常用 16)。
示例:
glPatchParameteri(GL_PATCH_VERTICES, 16); // 每个patch包含16个顶点
作用:
告诉 OpenGL 后续的 glDrawArrays(GL_PATCHES, ...)
或 glDrawElements(GL_PATCHES, ...)
绘制时,每多少个顶点为一组,作为一个 patch 送入细分着色器阶段。
注意:我们此处要调用glDrawArrays(GL_PATCHES, 0, 1);
,而不是glDrawArrays(GL_TRIANGLES, 0, 1);
,因为我们要绘制的是一个 patch,而不是一个三角形。
1.3.3. 顶点着色器
我们不用做任何事情
#version 430
void main(void)
{
}
1.3.4. 细分控制着色器(TCS)
#version 430uniform mat4 mvp_matrix;
layout (vertices = 1) out; // 每个patch包含1个顶点输出到下一个阶段void main(void)
{// 设置外层细分等级,决定patch边界被细分的段数gl_TessLevelOuter[0] = 6; // 第一条边细分为6段gl_TessLevelOuter[1] = 6; // 第二条边细分为6段gl_TessLevelOuter[2] = 6; // 第三条边细分为6段gl_TessLevelOuter[3] = 6; // 第四条边细分为6段// 设置内层细分等级,决定patch内部被细分的程度gl_TessLevelInner[0] = 12; // 第一组内层细分为12段gl_TessLevelInner[1] = 12; // 第二组内层细分为12段
}
1.3.5. 细分评估着色器(TES)
#version 430layout (quads, equal_spacing, ccw) in; // 使用四边形patch,均匀间隔,逆时针顺序uniform mat4 mvp_matrix; // 传入的MVP变换矩阵void main (void)
{float u = gl_TessCoord.x; // 获取当前细分点的u坐标float v = gl_TessCoord.y; // 获取当前细分点的v坐标gl_Position = mvp_matrix * vec4(u, 0, v, 1); // 计算变换后的位置,y为0表示在xz平面
}
1.4. 贝塞尔曲面细分
1.4.1. 思路
- 设置16个控制顶点 ,作为贝塞尔曲面的控制顶点
- 设置细分等级为32,决定patch边界被细分的段数
- 按照贝塞尔曲面公式计算每个细分点的位置
1.4.2. 顶点着色器
我们在顶点着色器中定义16个控制顶点,作为贝塞尔曲面的控制顶点。
#version 430uniform mat4 mvp_matrix;
out vec2 texCoord; // 纹理坐标输出void main(void)
{// 定义16个控制点,作为贝塞尔曲面的控制顶点const vec4 vertices[ ] = 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];
}
1.4.3. 细分控制着色器(TCS)
#version 430
in vec2 texCoord[ ];
out vec2 texCoord_TCSout[ ]; // 以标量形式从顶点着色器传来的纹理坐标输出,以数组形式被接收,然后被发送给曲面细分评估着色器
uniform mat4 mvp_matrix;
layout (binding = 0) uniform sampler2D tex_color;
layout (vertices = 16) out; // 每个补丁有 16 个控制点
void main(void)
{ int TL = 32; // 曲面细分级别都被设置为 32 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; } // 将纹理和控制点传递给曲面细分评估着色器texCoord_TCSout[gl_InvocationID] = texCoord[gl_InvocationID]; gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}
1.4.4. 曲面细分评估着色器(TES)
#version 430
layout (quads, equal_spacing,ccw) in;
uniform mat4 mvp_matrix;
layout (binding = 0) uniform sampler2D tex_color;
in vec2 texCoord_TCSout[ ];
out vec2 texCoord_TESout; // 以标量形式传来的纹理坐标数组被一个个传出
void main (void)
{ 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; 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); // 输出插值过的纹理坐标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;
}
1.4.5. 片段着色器
// 片段着色器
#version 430
in vec2 texCoord_TESout;
out vec4 color;
uniform mat4 mvp_matrix;
layout (binding = 0) uniform sampler2D tex_color;
void main(void)
{ color = texture(tex_color, texCoord_TESout);
}
1.5. 参考
- 学习笔记完整代码下载