计算机图形学编程(使用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. 学习笔记完整代码下载
相关推荐
985小水博一枚呀30 分钟前
【AI大模型学习路线】第二阶段之RAG基础与架构——第七章(【项目实战】基于RAG的PDF文档助手)技术方案与架构设计?
人工智能·学习·语言模型·架构·大模型
pystraf30 分钟前
LG P9844 [ICPC 2021 Nanjing R] Paimon Segment Tree Solution
数据结构·c++·算法·线段树·洛谷
Funny-Boy1 小时前
菱形继承原理
c++
weixin_514548891 小时前
一种开源的高斯泼溅实现库——gsplat: An Open-Source Library for Gaussian Splatting
人工智能·计算机视觉·3d
FakeOccupational1 小时前
计算机科技笔记: 容错计算机设计05 n模冗余系统 TMR 三模冗余系统
笔记·科技
hello1114-2 小时前
Redis学习打卡-Day3-分布式ID生成策略、分布式锁
redis·分布式·学习
小Tomkk2 小时前
2025年PMP 学习二十 第13章 项目相关方管理
学习·pmp·项目pmp
Nobkins2 小时前
2021ICPC四川省赛个人补题ABDHKLM
开发语言·数据结构·c++·算法·图论
独行soc3 小时前
2025年渗透测试面试题总结-百度面经(题目+回答)
运维·开发语言·经验分享·学习·面试·渗透测试·php
海棠蚀omo3 小时前
C++笔记-红黑树
开发语言·c++·笔记