目录
术语 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认为,所有物体都是由点、线、多边形构成的;点、线、多边形就被称为图元。
示例
我们从一个简单的应用程序开始,该应用程序只使用曲面细分器创建顶点的三角形网格,然后在不进行任何操作的情况下显示它。为此,我们需要以下模块:
- C++/OpenGL 应用程序:
创建一个摄像机和相关的 MVP 矩阵,视图(v)和投影(p)矩阵确定摄像机朝向,模型(m)矩阵可用于修改网格的位置和方向。 - 顶点着色器:
在这个例子中基本上什么都不做,顶点将在曲面细分器中生成。 - 曲面细分控制着色器:
指定曲面细分器要构建的网格。 - 曲面细分评估着色器:
将 MVP 矩阵应用于网格中的顶点。 - 片段着色器:
只需为每个像素输出固定颜色。
为此我们需要实现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 调用中,冗余地在赋值语句中
OpenGL曲面细分详解

本文详细介绍了OpenGL中的曲面细分技术,包括曲面细分控制处理器和评估处理器的工作原理,以及如何通过实例化生成大量顶点以实现地形和高度图的渲染。文章还探讨了贝塞尔曲面细分和光照整合,并介绍了细节级别控制技术。
最低0.47元/天 解锁文章
690

被折叠的 条评论
为什么被折叠?



