当前位置: 首页 > news >正文

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

1. 曲面细分

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

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

  1. 细分控制着色器(Tessellation Control Shader, TCS)
    决定每个patch(补丁)的细分程度,可以动态调整细分级别,实现自适应细分。

  2. 细分评估着色器(Tessellation Evaluation Shader, TES)
    根据细分后的顶点坐标,计算每个新生成顶点的具体位置,实现曲面插值和变形。

曲面细分着色器的优点是可以在不增加原始模型数据的情况下,动态生成高精度的曲面,提高渲染质量,广泛应用于角色建模、地形渲染等领域。

从顶点着色器中获取顶点位置后,曲面细分着色器会根据细分控制着色器输出的细分因子,对每个顶点进行细分,生成新的顶点。这些新生成的顶点会传递给细分评估着色器,由细分评估着色器计算每个顶点的位置,实现曲面插值和变形。最后,曲面细分着色器会将新的顶点传递给片段着色器,进行最后的渲染。

注意:顶点着色器中输出的顶点不会用于最后渲染,其只是做为细分控制着色器输入的顶点,用于计算细分因子。

6

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

TCS(细分控制着色器)中的主要任务就是设置外层细分等级(gl_TessLevelOuter)和内层细分等级(gl_TessLevelInner)。这是曲面细分着色器中用于控制patch(补丁)细分精度的参数。

  • 外层细分等级(gl_TessLevelOuter)
    控制patch边界(如四边形的四条边)被细分成多少段。每个外层等级对应patch的一条边,数值越大,边被细分得越细,生成的网格越密集。

  • 内层细分等级(gl_TessLevelInner)
    控制patch内部的细分程度。对于四边形patch,有两个内层等级,分别控制u和v方向内部的细分密度。数值越大,patch内部被细分得越细,曲面越平滑。

简单理解:

  • 外层细分等级决定边界的分段数,影响轮廓的精细度。
  • 内层细分等级决定内部网格的密度,影响曲面内部的平滑度。

5

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 中进行法线、纹理坐标等属性的插值计算。

示例代码:

#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

1.3.1. 思路

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

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

1.4.1. 思路

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

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. 参考

  1. 学习笔记完整代码下载

相关文章:

  • MySQL初阶:sql事务和索引
  • 深入解析Spring Boot与Redis集成:高效缓存实践
  • 如何彻底清空docker里面不使用的容器?
  • 面向对象详解和JVM底层内存分析
  • Windows运维工具批处理版
  • 使用Python和`python-docx`库复制Word文档样式
  • mysql中4种扫描方式和聚簇索引非聚簇索引【爽文一篇】
  • BG开发者日志517:demo数据分析与修改方向
  • muduo库TcpConnection模块详解——C++
  • MySQL只操作同一条记录也会死锁吗?
  • Linux面试题集合(5)
  • 通俗版解释CPU、核心、进程、线程、协程的定义及关系
  • AGI大模型(21):混合检索之混合搜索
  • CSS 浮动与定位以及定位中z-index的堆叠问题
  • 管理前端项目依赖版本冲突导致启动失败的问题的解决办法
  • 深度学习---知识蒸馏(Knowledge Distillation, KD)
  • 代码随想录算法训练营第60期第三十九天打卡
  • C# 深入理解类(静态函数成员)
  • UDP三种通信方式
  • Axure元件动作四:设置选中
  • 六省会共建交通枢纽集群,中部离经济“第五极”有多远?
  • 国寿资产获批参与第三批保险资金长期投资改革试点
  • 曾犯强奸罪教师出狱后办教培机构?柳州鱼峰区教育局:正核实
  • 经常口干口渴的人,要当心这些病
  • 从能源装备向应急装备蓝海拓展,川润股份发布智能综合防灾应急仓
  • 王征、解宁元、牛恺任西安市副市长