✠OpenGL-12-曲面细分

Posted itzyjr

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了✠OpenGL-12-曲面细分相关的知识,希望对你有一定的参考价值。


术语 Tessellation(镶嵌)是指一大类设计活动,通常是指在平坦的表面上,用各种几何形状的瓷砖相邻排列以形成图案。
曲面细分指的是生成并且操控大量三角形以渲染复杂的形状和表面,尤其是使用硬件进行渲染。

OpenGL中的曲面细分

OpenGL 对硬件曲面细分的支持,通过 3 个管线阶段提供:
(1)曲面细分控制着色器;
(2)曲面细分器(此阶段不可编程);
(3)曲面细分评估着色器。

曲面细分器(其全名是曲面细分图元生成器,或 TPG)是硬件支持的引擎,可以生成固定的三角形网格。
控制着色器允许我们配置曲面细分器要构建什么样的三角形网格。 然后,
评估着色器允许我们以各种方式操控网格。然后,被操控过的三角形网格,会作为通过管线前进的顶点的源数据。

Tessellation Control Processor

曲面细分控制处理器是一个可编程单元,它对传入顶点及其相关数据的面片进行操作,并发出新的输出面片。用OpenGL着色语言编写并在此处理器上运行的编译单元称为细分控制着色器。编译并链接一整套细分控制着色器后,它们将生成在细分控制处理器上运行的细分控制着色器可执行文件。
将为输出面片的每个顶点调用细分控制着色器。每次调用都可以读取输入或输出面片中任何顶点的属性,但只能写入相应输出面片顶点的逐顶点属性。着色器调用共同为输出面片生成一组每面片属性。完成所有细分控制着色器调用后,输出顶点和每个面片属性将被组合以形成一个面片,供后续管线阶段使用。
细分控制着色器的调用基本上是独立运行的,它的相对执行顺序未定义。
但是,内置函数barrier()可以通过同步调用来控制执行顺序,从而有效地将细分控制着色器执行划分为一组阶段。如果一个调用在同一阶段的任意点读取由另一个调用写入的逐顶点或逐面片属性,或者如果两个调用尝试在单个阶段将不同的值写入相同的逐面片输出,则细分控制着色器将获得未定义的结果。

Tessellation Evaluation Processor

曲面细分评估处理器是一个可编程单元,它使用传入顶点的面片及其关联数据评估由镶嵌基本体生成器生成的顶点的位置和其他属性。用OpenGL着色语言编写并在此处理器上运行的编译单元称为细分求值着色器。编译并链接一整套细分计算着色器后,它们将生成在细分计算处理器上运行的细分计算着色器可执行文件。
每次调用细分评估可执行文件都会计算由细分基本体生成器生成的单个顶点的位置和属性。可执行文件可以读取输入面片中任何顶点的属性,以及细分坐标,该坐标是要细分的基本体中顶点的相对位置。可执行文件写入顶点的位置和其他属性。

示例:
我们从一个简单的应用程序开始,该应用程序只使用曲面细分器创建顶点的三角形网格,然后在不进行任何操作的情况下显示它。为此,我们需要以下模块:

  1. C++/OpenGL 应用程序:
    创建一个摄像机和相关的 MVP 矩阵,视图(v)和投影(p)矩阵确定摄像机朝向,模型(m)矩阵可用于修改网格的位置和方向。
  2. 顶点着色器:
    在这个例子中基本上什么都不做,顶点将在曲面细分器中生成。
  3. 曲面细分控制着色器:
    指定曲面细分器要构建的网格。
  4. 曲面细分评估着色器:
    将 MVP 矩阵应用于网格中的顶点。
  5. 片段着色器:
    只需为每个像素输出固定颜色。

为此我们需要实现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);
}
// 顶点着色器
#version 430
void main() {}

// 曲面细分控制着色器
#version 430
layout(vertices = 1) out;
void main() {
	gl_TessLevelOuter[0] = 6;// 外层级别0
	gl_TessLevelOuter[1] = 6;// 外层级别1
	gl_TessLevelOuter[2] = 6;// 外层级别2
	gl_TessLevelOuter[3] = 6;// 外层级别3
	gl_TessLevelInner[4] = 12;// 内层级别0
	gl_TessLevelInner[5] = 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);
}

// 片段着色器
#version 430
out vec4 color;
void main() {
	color = vec4(1.0, 1.0, 0.0, 1.0);// 黄色
}


调整摄像机位置,并让网格分别绕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)点重合。
4)网格的旋转是绕XW轴进行的,而不是自转(详见“✠OpenGL-7-光照-自转与绕轴旋转”)。

在曲面细分控制着色器(tessellation control language)中,内置变量本质上定义为:

in gl_PerVertex {// PerVertex=Per+Vertex,即每网格顶点
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
} gl_in[gl_MaxPatchVertices];
in int gl_InvocationID;
out gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
} gl_out[];
patch out float gl_TessLevelOuter[4];
patch out float gl_TessLevelInner[2];

在曲面细分评估着色器(tessellation evaluation language)中,内置变量本质上定义为:

in gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
} gl_in[gl_MaxPatchVertices];
in vec3 gl_TessCoord;
patch in float gl_TessLevelOuter[4];
patch in float gl_TessLevelInner[2];

输出变量gl_TessLevelOuter[]gl_TessLevelInner[]仅在曲面细分控制着色器语言中可用。写入这些变量的值被指定给输出面片(output patch)的相应外部和内部镶嵌级别。它们由曲面细分控制着色器用于控制基本体镶嵌,并可由曲面细分评估着色器读取。

变量gl_TessCoord仅在曲面细分着色器语言中可用。它指定一个三分量(u, v, w)向量,标识着色器处理的顶点相对于要细分的基本体的位置。其值将遵循属性,以帮助复制细分计算。

输入变量gl_TessLevelOuter[]和gl_TessLevelInner[]仅在细分求值着色器中可用。如果细分控制着色器处于活动状态,则这些变量将填充曲面细分控制着色器写入的相应输出。否则,将使用OpenGL图形系统规范中指定的默认细分级别来指定它们。
细分级别如下图:

layout(vertices = 1) out;
曲面细分控制着色器允许只在接口限定符out上使用输出布局限定符,而不是在输出块、块成员或变量声明。
标识符vertices指定由曲面细分控制着色器生成的输出面片中的顶点数,它也是曲面细分着色器被调用的次数。如果一个输出顶点计数小于或等于零,或大于基于实现的最大面片大小时,会出错。
vetices用来指定从顶点着色器传递给控制着色器(以及“输出”给评估着色器)的每个“补丁”的顶点数。在我们现在这个程序中没有任何顶点,但我们仍然必须指定至少一个,因为它也会影响控制着色器被执行的次数。 稍后这个值将反映控制点的数量, 并且必须与 C++/OpenGL 应用程序中glPatchParameteri()调用中的值匹配

layout(quads, equal_spacing, ccw) in;
图元模式:triangles(分割三角形为较小的三角形)、quads(分割四边形为三角形)、isolines(分割四边形为一组线)。
顶点间隔:equal_spacing(边缘应该被分成一组大小相等的片段)、fractional_even_spacing(边缘应被分成偶数个等长段加上两个额外的较短的“分数”段)、fractional_odd_spacing(边缘应被分成奇数个等长段加上两个额外的较短的“分数”段)。
绘制三角形的绕向:cw、ccw。
点模式(本例未使用):曲面细分控制着色器应该为细分的基本体中的每个唯一顶点生成一个点,而不是生成直线或三角形。
以上这些标识符中的任何或所有标识符可以在单个输入布局声明中指定一次或多次。

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。当使用曲面细分时,从C++/OpenGL 应用程序发送到管线(即在 VBO 中)的顶点不会被渲染,但通常会被当作控制点,就像我们在贝塞尔曲线中看到的那些一样。一组控制点被称作“补丁”,并且在使用曲面细分的代码段中, GL_PATCHES 是唯一允许的图元类型。 “补丁”中顶点的数量在 glPatchParameteri()的调用中指定。在这个特定示例中,没有任何【控制点】被发送,但我们仍然需要指定至少一个。
类似地,在 glDrawArrays()调用中,我们指示起始值为0,顶点数量为 1,即使我们实际上没有从 C++程序发送任何顶点。

glPolygonMode()的调用指定了如何光栅化网格。默认值为 GL_FILL。而我们的代码中显示的是 GL_LINE,如在上图中看到的那样,它只会导致连接线被光栅化(因此我们可以看到由曲面细分器生成的网格本身)。如果我们将该行代码更改为 GL_FILL(或将其注释掉),我们将得到如下图所示的版本。

void glPolygonMode(GLenum face, GLenum mode);
face - 指定应用mode的网格朝向。对于front和back朝向的网格必须为GL_FRONT_AND_BACK。
mode - 指定网络如何被栅格化。对于front或back朝向的网络,初始默认值为GL_FILL。可能的值为GL_POINT、GL_LINE、GL_FILL。

贝塞尔曲面细分

假设我们希望建立一个三次方贝塞尔曲面, 我们将需要 16 个控制点。 我们可以通过 VBO从 C++端发送它们, 或者我们可以在顶点着色器中硬编码写死它们。

细分网格应该为我们提供了足够的顶点来对曲面进行采样(如果我们想要更多的话,我们可以增加内部/外部细分级别)。

我们知道OpenGL 提供了一个名为 gl_VertexID 的内置变量,它保存一个计数器,指示顶点着色器当前正在执行哪次调用。曲面细分控制着色器中存在一个类似的内置变量 gl_InvocationID

曲面细分的一个强大功能是 TCS(以及 TES)着色器可以同时访问数组中的所有控制点顶点。首先,当每个调用都可以访问所有顶点时, TCS 对每个顶点执行一次可能会让人感到困惑。在每个 TCS 调用中,冗余地在赋值语句中指定曲面细分级别也是违反直觉的。尽管所有这些看起来都很奇怪,但这样做是因为曲面细分的架构设计使得 TCS 调用可以并行运行。

OpenGL 提供了几个用于 TCS 和 TES 着色器的内置变量。我们已经提到过的是 gl_InvocationID,当然还有 gl_TessLevelInner 和 gl_TessLevelOuter。以下是一些最有用的内置变量的更多细节和描述。

曲面细分控制着色器( TCS)内置变量:

  • gl_in[]——包含每个传入的控制点顶点的数组——每个传入顶点是一个数组元素。它的本质定义如下:
in gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
} gl_in[gl_MaxPatchVertices];
  • gl_out[]——用于将输出控制点的顶点发送到TES的一个数组——每个输出顶点是一个数组元素。它的本质定义如下:
out gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
} gl_out[];
  • gl_InvacationID——整形ID计数器,指示TCS当前正在执行哪个调用。

曲面细分评估着色器( TES)内置变量:

  • gl_in[]——包含每个传入的控制点顶点的数组——每个传入顶点是一个数组元素。
  • gl_Position——曲面细分网格顶点的输出位置,可能在 TES 中被修改。要注意 gl_Position 和 gl_in[xxx].gl_Position 是不同的——gl_Position 是起源于曲面细分器的输出顶点的位置, 而 gl_in[xxx].gl_Position 是一个从 TCS 进入 TES 的控制点顶点位置。

值得注意的是, TCS 中的输入和输出控制点顶点属性是数组。不同的是, TES 中的输入控制点顶点和顶点属性是数组,但输出顶点是标量。此外,很容易混淆哪些顶点来自于控制点,哪些顶点是细分建立的,然后移动以形成结果曲面。总而言之, TCS 的所有顶点输入和输出都是控制点,而在 TES 中, gl_in[ ]保存输入控制点, gl_TessCoord 保存输入的细分网格点, gl_Position 保存用于渲染的输出表面顶点。

生成三次贝塞尔曲面:
下面程序显示了所有 4 个着色器——顶点、 TCS、 TES 和片段——用于指定控制点补丁,生成平坦的曲面细分顶点网格,在控制点指定的曲面上重新定位这些顶点,并使用纹理图像绘制生成的曲面。

我们的曲面细分控制着色器现在有两个任务:指定曲面细分级别并将控制点从顶点着色器传递到评估着色器。 然后, 评估着色器可以根据贝塞尔控制点修改网格点( gl_TessCoords)的位置。

回顾一下坐标点与uv轴的相对关系(二次贝塞尔曲面):


即:u轴方向是p00、p10、p20…下标的十位数递增的方向;v轴方向是p00、p01、p02…下标的个位数递增的方向。
还有一种更通用的表示法,如下:

即:u轴方向是p30、p31、p32…下标的个位数递增的方向;v轴方向是p30、p20、p10…下标的十位数递减的方向。
本例中就是使用的第二种这种更加通用的表示法。

再来回顾一下三次贝塞尔曲面:

void init(GLFWwindow* window) {
	renderingProgram = Util::createShaderProgram("vertShader.glsl", "tessCShader.glsl", "tessEShader.glsl", "fragShader.glsl");
	cameraX = 0.0f; cameraY = 0.0f; cameraZ = 4.0f;
	terLocX = 0.0f; terLocY = 0.0f; terLocZ = 0.0f;
	...
}

void display(GLFWwindow* window, double currentTime) {
	...
	// 模型绕Xw轴旋转30°,再绕Yw轴旋转100°
	mMat = glm::rotate(mMat, toRadians(30.0f), glm::vec3(1.0f, 0.0f, 0.0f));
	mMat = glm::rotate(mMat, toRadians(100.0f), glm::vec3(0.0f, 1.0f, 0.0f));
	...
	// 传入一个纹理以用来绘制表面
	glActiveTexture(GL_TEXTURE0);
	glBindTexture(GL_TEXTURE_2D, textureID);
	
	glFrontFace(GL_CCW);

	glPatchParameteri(GL_PATCH_VERTICES, 16);// 每个补丁的顶点数量=16
	glPolyonMode(GL_FRONT_AND_BACK, GL_FILL);
	glDrawArrays(GL_PATCHES, 0, 16);// 补丁顶点总数量:16×1个补丁=16
// 顶点着色器
#version 430
out vec2 texCoord;
void main() {
	// 由顶点着色器指定和发送控制点(这里硬编码写死)
	const vec4 vectices[] = 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];
}

// 曲面细分控制着色器
#version 430
in vec2 texCoord[];
out vec2 texCoord_TCSout[];// 以标量形式从顶点着色器传来的纹理坐标输出,以数组形式被接收,然后被发送给评估着色器
layout(vertices = 16) out;// 每个补丁有16个控制点
void main() {
	int TL = 32;// 曲面细分级别都被设置为这个值
	if (gl_InvocationID == 0) {
		gl_TessLevelOuter[0] = TL;
		gl_TessLevelOuter[1] = TL;
		gl_TessLevelOuter[2] = TL;
		gl_TessLevelOuter[3] = TL;
		gl_TessLevelInner[0] = TL;
		gl_TessLevelInner[1] = TL;
	}
	// 将纹理和控制点传递给TES
	texCoord_TCSout[gl_InvocationID] = texCoord[gl_InvocationID];
	gl_out[gl_InvocationID].gl_Position = gl_in[gl_InvocationID].gl_Position;
}

// 曲面细分评估着色器
#version 430
layout(quads, equal_spacing, ccw) in;
uniform mat4 mvp_matrix;
in vec2 texCoord_TCSoutTessellation (曲面细分) Displacement Mapping (贴图置换)

细分网格建模

渐进插值的LOOP 曲面细分

渲染管道几何阶段三“曲面细分着色器”

渲染管道几何阶段三“曲面细分着色器”

渲染管道几何阶段三“曲面细分着色器”子阶段