计算机图形学编程(使用OpenGL和C++)(第2版)学习笔记 12.曲面细分

1. 曲面细分

曲面细分着色器(Tessellation Shader)是OpenGL 4.0及以上版本引入的一种可编程着色器阶段,用于在GPU上对几何体进行细分,将粗糙的多边形网格自动细分为更平滑、更精细的曲面。它主要用于实现高质量的曲面渲染,如贝塞尔曲面、NURBS曲面等。

曲面细分着色器由两个部分组成:

  1. 细分控制着色器(Tessellation Control Shader, TCS)

    决定每个patch(补丁)的细分程度,可以动态调整细分级别,实现自适应细分。

  2. 细分评估着色器(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_TessLevelOutergl_TessLevelInner 这两个数组的元素个数取决于 patch 的类型:

  • 对于 四边形 patch(quads)

    • gl_TessLevelOuter4 个元素(分别对应四条边)。
    • gl_TessLevelInner2 个元素(分别对应 u 和 v 两个方向的内部细分)。
  • 对于 三角形 patch(triangles)

    • gl_TessLevelOuter3 个元素(分别对应三条边)。
    • gl_TessLevelInner1 个元素(对应内部细分)。

1.0.2. 内部网格新生成的顶点数量

生成的顶点数主要由内层细分等级决定,与外层细分等级无关。

  • 内层细分等级(gl_TessLevelInner) 决定了 patch 内部网格的密度,也就是细分后生成的顶点数量。
  • 外层细分等级(gl_TessLevelOuter) 决定 patch 边界的分段数,影响边界的平滑度,但不会直接影响整个 patch 内部生成的顶点总数。
1.0.2.1. 三角形patch

对于三角形 patch(triangles):

  • gl_TessLevelOuter3 个元素,分别对应三角形的三条边,每个值决定对应边被细分成多少段。
  • gl_TessLevelInner1 个元素,决定三角形内部的细分密度。

生成的顶点数量

三角形 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_TessLevelOuter4 个元素,分别对应四条边,每个值决定对应边被细分成多少段。
  • gl_TessLevelInner2 个元素,分别对应 u 和 v 两个方向的内部细分密度。

生成的顶点数量

四边形 patch 细分后,生成的顶点数为:
(内层细分等级[0] + 1) × (内层细分等级[1] + 1)

例如,若 gl_TessLevelInner[0] = Mgl_TessLevelInner[1] = N,则顶点数为 (M+1) × (N+1)。

示例:

如果 gl_TessLevelInner[0] = 12gl_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 中进行法线、纹理坐标等属性的插值计算。

示例代码:

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);
}

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. 思路

  1. cpp 中指定曲面细分着色器,设置相关参数
  2. 顶点着色器中输出顶点位置,用于计算细分因子
  3. 细分控制着色器中计算细分因子
  4. 细分评估着色器中计算每个顶点的位置
  5. 片段着色器中渲染顶点

1.3.2. main.cpp: display()

cpp 复制代码
	glPatchParameteri(GL_PATCH_VERTICES, 1);
	glPolygonMode(GL_FRONT_AND_BACK, GL_LINE);  // FILL or LINE
	glDrawArrays(GL_PATCHES, 0, 1);

glPatchParameteri 是 OpenGL 4.0 及以上版本用于曲面细分(Tessellation)的一条函数指令。它的作用是设置 patch(补丁)的相关参数,最常用的是指定每个 patch 包含的顶点数量。

常用语法:

cpp 复制代码
glPatchParameteri(GLenum pname, GLint value);
  • pname:参数名称,常用的是 GL_PATCH_VERTICES,表示设置每个 patch 的顶点数。
  • value:具体的数值,比如 1、3、4、16 等,取决于你的 patch 结构(如贝塞尔曲面常用 16)。

示例:

cpp 复制代码
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. 顶点着色器

我们不用做任何事情

cpp 复制代码
#version 430
void main(void)
{
}

1.3.4. 细分控制着色器(TCS)

cpp 复制代码
#version 430

uniform 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)

cpp 复制代码
#version 430

layout (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. 思路

  1. 设置16个控制顶点 ,作为贝塞尔曲面的控制顶点
  2. 设置细分等级为32,决定patch边界被细分的段数
  3. 按照贝塞尔曲面公式计算每个细分点的位置

1.4.2. 顶点着色器

我们在顶点着色器中定义16个控制顶点,作为贝塞尔曲面的控制顶点。

cpp 复制代码
#version 430

uniform 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)

cpp 复制代码
#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)

cpp 复制代码
#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. 片段着色器

cpp 复制代码
// 片段着色器
#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. 参考

  1. 学习笔记完整代码下载
相关推荐
知识分享小能手1 小时前
React学习教程,从入门到精通, React 属性(Props)语法知识点与案例详解(14)
前端·javascript·vue.js·学习·react.js·vue·react
汇能感知3 小时前
摄像头模块在运动相机中的特殊应用
经验分享·笔记·科技
阿巴Jun3 小时前
【数学】线性代数知识点总结
笔记·线性代数·矩阵
茯苓gao4 小时前
STM32G4 速度环开环,电流环闭环 IF模式建模
笔记·stm32·单片机·嵌入式硬件·学习
是誰萆微了承諾4 小时前
【golang学习笔记 gin 】1.2 redis 的使用
笔记·学习·golang
利刃大大4 小时前
【高并发内存池】五、页缓存的设计
c++·缓存·项目·内存池
DKPT4 小时前
Java内存区域与内存溢出
java·开发语言·jvm·笔记·学习
aaaweiaaaaaa5 小时前
HTML和CSS学习
前端·css·学习·html
ST.J5 小时前
前端笔记2025
前端·javascript·css·vue.js·笔记
C语言小火车5 小时前
【C++八股文】基础知识篇
c++·tcp/ip·const·智能指针·多线程同步·static关键字·c++内存模型