✠OpenGL-13-几何着色器


与曲面细分一样,几何着色器使程序员能够以顶点着色器中无法实现的方式操纵顶点组。在某些情况下,可以使用曲面细分着色器或者几何着色器完成同样的任务,因为它们的功能在某些方面重叠。

几何着色器(Geometry Processor)是一个可编程单元,它对输入顶点的数据进行操作在顶点处理后组装的基本体,并输出形成输出的顶点序列原语。使用OpenGL着色语言编写并在此处理器上运行的编译单元是称为几何着色器。成功编译和链接一组几何着色器后,它们将生成在几何处理器上运行的几何着色器可执行文件。

对几何处理器上的几何体着色器可执行文件的单个调用将在具有固定顶点数的声明输入基元。这个调用可以发出一个变量组装成声明的输出基本体类型和类型的基本体的顶点数传递到后续管道阶段。

OpenGL中的逐个图元处理

几何着色器阶段位于曲面细分和光栅化之间,位于用于图元处理的管线段内。顶点着色器允许一次操作一个顶点,而片段着色器一次可以操作一个片段(实际上是一个像素),但几何着色器却可以一次操作一个图元。

图元是 OpenGL 中绘制对象的基本元件。只有少数几种类型的图元;我们将主要关注操纵三角形图元的几何着色器。因此,当我们说几何着色器可以一次操作一个图元时,我们通常意味着着色器一次可以访问三角形的 3 个顶点。几何着色器允许一次性访问图元中的所有顶点,然后:

  • 输出相同的图元保持不变;
  • 输出修改了顶点位置的相同类型图元;
  • 输出不同类型的图元;
  • 输出更多的其他图元;
  • 删除图元(根本不输出)。

与曲面细分评估着色器类似,可以在几何着色器中将传入的顶点属性作为数组进行访问。但是,在几何着色器中,传入属性数组仅索引到图元尺寸那么大。例如,如果图元是三角形,则可用索引为 0、 1、 2。使用预先定义的数组 gl_in 访问顶点数据本身,如下所示。

gl_in[2].gl_Position;// 第三个顶点的位置

与曲面细分评估着色器类似,几何着色器输出的顶点属性都是标量。也就是说,输出是形成图元的各个顶点(它们的位置和其他属性变量,如果有的话)的流。

有一个布局修饰符用于设置图元输入/输出类型和输出大小。特殊的 GLSL 命令EmitVertex()指定了将要输出一个顶点。特殊的 GLSL 命令 EndPrimitive()表示一个特定的图元构建完成。
有一个内置变量 gl_PrimitiveIDIn,它保存当前图元的 ID。 ID 从 0 开始,并计数到图元总数减1。

我们将探讨四种常见的操作类型:

  • 修改图元;
  • 删除图元;
  • 添加图元;
  • 更改图元类型。

void EmitVertex()
向第一个顶点流发送顶点。
它将输出变量的当前值发送到第一个(可能只有一个)图元流上的当前输出图元。

void EndPrimitive()
在第一个顶点流上完成当前输出图元。
它在第一个(可能是唯一的)顶点流上完成当前输出图元,并启动一个新的输出图元。不发送顶点。

内建变量:

in gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
} gl_in[];

in int gl_PrimitiveIDIn;
in int gl_InvocationID;

out gl_PerVertex {
	vec4 gl_Position;
	float gl_PointSize;
	float gl_ClipDistance[];
	float gl_CullDistance[];
};

out int gl_PrimitiveID;
out int gl_Layer;
out int gl_ViewportIndex;
修改图元

当通过对图元(通常为三角形)的单独更改就可以影响对象形状的改变时,使用几何着色器就很方便。
C++/OpenGL 应用程序需要编译几何着色器并在链接之前将其附加到着色器程序。新着色器被指定为几何着色器,如下所示。

GLuint gShader = glCreateShader(GL_GEOMETRY_SHADER);
// 几何着色器
#version 430
in vec3 varyingNormal[];// 来自顶点着色器的输入
in vec3 varyingLightDir[];
in vec3 varyingHalfVector[];

out vec3 varyingNormalG;// 输出给光栅着色器然后到片段着色器
out vec3 varyingNormalDirG;
out vec3 varyingHalfVectorG;

layout(triangles) in;// 指定[输入]图元类型
layout(triangle_strip, max_vertices = 3) out;// 指定[输出]图元类型
// 矩阵和光照统一变量和以前一样
...
void main() {
	// 沿着法向量移动顶点,并将其他顶点属性原样传递
	for (int i = 0; i < 3; i++) {
		gl_Position = proj_matrix * gl_in[i].gl_Position 
		            + normalize(vec4(varyingNormal[i], 1.0)) * 0.4;
		varyingNormalG = varyingNormal[i];
		varyingLightDirG = varyingLightDir[i];
		varyingHalfVectorG = varyingHalfVector[i];
		EmitVertex();// 指定[输出]一个顶点:gl_Position、varying....标量
	}
	EndPrimitive();// 完成一个图元的构建
}

与顶点着色器的输出变量对应的输入变量被声明为数组。这为程序员提供了一种机制,可以使用索引 0、1 和 2 访问三角形图元中的每个顶点及其属性。

我们希望沿着它们的表面法向量向外移动这些顶点。在顶点着色器中,顶点和法向量都已经被转换到视图空间。我们为每个传入的顶点位置( gl_in[i].gl_Position)添加法向量的一小部分,然后将投影矩阵应用于结果,生成每个输出 gl_Position。

几何着色器输出的是标量, 使用 GLSL 调用EmitVertex()来指定我们何时完成了计算输出 gl_Position 及其相关的顶点属性并准备输出顶点。 EndPrimitive()调用指定我们已经完成了组成图元(在本例中为三角形)的一组顶点的定义。结果如下图所示。

几何着色器包括两个布局限定符:➀指定输入图元类型,并且必须与 C++端glDrawArrays()或 glDrawElements()调用中的图元类型兼容。选项如下表所示。

➁指定输出图元类型,必须是 points、 line_strip 或 triangle_strip。请注意,输出布局限定符也会指定着色器在每次调用中输出的最大顶点数。

假设不是沿着自己的表面法向量向外移动每个顶点,而是希望将每个三角形沿其表面法向量向外移动,实际上是将环面的组成三角形向外“爆炸”。顶点着色器做不到这一点,因为计算三角形的法向量需要对 3 个三角形顶点的顶点法向量进行平均,并且顶点着色器一次只能访问三角形中一个顶点的顶点属性。但是,我们可以在几何着色器中执行此操作,因为几何着色器可以访问每个三角形中的所有 3 个顶点。我们平均它们的法向量来计算三角形的曲面法向量,然后将该平均法向量加给三角形图元中的每个顶点。

曲面法向量的平均值:

修改后的几何着色器:

void main() {
	// 对三角形3个顶点法向量取平均值,得到三角形的曲面法向量
	vec4 triangleNormal = 
		vec4((varyingNormal[0]+varyingNormal[1]+varyingNormal[2])/3.0, 1.0);
	// 将三个点都沿所得法向量移动
	for (i = 0; i < 3; i++) {
		gl_Position = proj_matrix * (gl_in[i].gl_Position + normalize(triangleNormal)*0.4);
		varyingNormalG = varyingNormal[i];
		varyingLightDirG = varyingLightDir[i];
		varyingHalfVectorG = varyingHalfVector[i];
		EmitVertex();
	}
	EndPrimitive();
}

运行效果:

为改善“爆炸”环面的外观,可以让环面的内部也可见(通常这些三角形会被 OpenGL 剔除,因为它们是“背面”)。一种解决方式是使环面被渲染两次,一次以正常方式进行,一次使缠绕顺序反转(使缠绕顺序反转实际上相当于切换哪些面朝向前方,哪些面朝向后方)。我们还向着色器(通过统一变量)发送一个标志,以禁用背向三角形上的漫反射和镜面光,以使它们不那么突出。代码的更改如下。

void display() {
	...
	// 绘制前向三角形——启用光照
	glUniform1i(lLoc, 1);// 用来启用、禁用漫反射、镜面光组件的统一变量的位置
	glFrontFace(GL_CCW);
	glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
	// 绘制后向三角形——禁用光照
	glUniform1i(lLoc, 0);
	glFrontFace(GL_CW);
	glDrawElements(GL_TRIANGLES, numTorusIndices, GL_UNSIGNED_INT, 0);
}
// 片段着色器
#version 430
...
uniform int enableLighting;
if (enableLighting == 1) {
	fragColor = ...;// 当前渲染前向表面时,使用正常的光照计算
} else {// 当渲染后向表面时,只启用环境光组件
	fragColor = globalAmbient*material.ambient + light.ambient*material.ambient;
}

由此产生的“爆炸”环面,包括背面,如下图所示。

如果进行两次glDrawElements(),都为CCW,则效果如下图(左);如果都为CW,则效果如下图(右):

通过两次glDrawElements(),把外面和里面就都绘制了。并且对外面有正常光照模型,对里面就只有环境光。这样非常合适。

删除图元

几何着色器的一个常见用途是通过合理地删除一些图元来从简单的对象构建丰富的装饰对象。例如,从我们的环面中移除一些三角形可以将其变成一种复杂的格子结构,而从零开始建模这个结构是更加困难的。

#version 430
...
void main() {
	if (mod(gl_PrimitiveIDIn, 3) != 0) {
		for (int i = 0; i < 3; i++) {
			gl_Position = proj_matrix * gl_in[i].gl_Position;
			varyingNormalG = varyingNormal[i];
			varyingLightDirG = varyingLightDir[i];
			varyingHalfVectorG = varyingHalfVector[i];
			EmitVertex();
		}
	}
	EndPrimitive();
}

请注意这里使用了 mod 函数——所有顶点,除了每 3 个图元中的第一个图元的顶点被忽略之外,都被传递。

添加图元

也许几何着色器最有趣和最有用的用途是为正在渲染的模型添加额外的顶点和/或图元。这使得可以进行诸如增加对象中的细节以改善高度贴图,或者完全改变对象的形状之类的事情。

考虑以下示例,我们将环面中的每个三角形更改为一个微小的三角形金字塔。
我们的策略类似于我们之前的“爆炸”环面示例,如下图所示。传入三角形图元的顶点用于定义金字塔的基座。金字塔的壁由那些顶点和通过平均原始顶点的法向量计算的新点(称为“尖峰点”)构成。然后通过从尖峰点到基座的两个向量的叉积计算金字塔的 3 个“边”中的每一个的新法向量。

几何着色器为环面中的每个三角形图元执行此操作。对于每个输入三角形,它输出 3 个三角形图元,总共 9 个顶点。每个新三角形都在函数 makeNewTriangle()中构建,该函数被调用 3 次。它计算指定三角形的法向量,然后调用函数 setOutputValues()为发出的每个顶点分配适当的输出顶点属性。在发出所有 3 个顶点之后, 它调用 EndPrimitive()。为了确保准确地执行光照,为每个新创建的顶点计算光照方向向量的新值。

// 顶点着色器
#version 430
out vec3 varyingOriginalNormal;
...
void main(void) {
	varyingOriginalNormal = (norm_matrix * vertNormal).xyz;
	gl_Position = mv_matrix * vertPos;
}

// 几何着色器
#version 430
layout (triangles) in;
layout (triangle_strip, max_vertices = 9) out;

in vec3 varyingOriginalNormal[];
out vec3 varyingNormal;
out vec3 varyingLightDir;
out vec3 varyingVertPos;

vec3 newPoints[], lightDir[];
float sLen = 0.01;// sLen是“尖峰长度”,小金字塔的高度
...
// 为发出的每个顶点分配适当的输出顶点属性
void setOutputValues(int i, vec3 norm) {
	varyingNormal = norm;
	varyingLightDir = lightDir[i];
	varyingVertPos = newPoints[i];
	gl_Position = proj_matrix * vec4(newPoints[i], 1.0);
}

// 指定三角形的法向量
void makeNewTriangle(int i, int j) {
	// 为这个三角形生成表面法向量
	vec3 c1 = normalize(newPoints[i] - newPoints[3]);
	vec3 c2 = normalize(newPoints[j] - newPoints[3]);
	vec3 norm = cross(c1, c2);
	// 生成并发出3个顶点
	setOutputValues(i, norm); EmitVertex();
	setOutputValues(j, norm); EmitVertex();
	setOutputValues(3, norm); EmitVertex();
	EndPrimitive();
}

void main() {// 注:每个图元(三角形)执行一次
	// 给三角形3个顶点加上原始表面法向量
	vec3 sp0 = gl_in[0].gl_Position.xyz + varyingOriginalNormal[0]*sLen;
	vec3 sp1 = gl_in[1].gl_Position.xyz + varyingOriginalNormal[1]*sLen;
	vec3 sp2 = gl_in[2].gl_Position.xyz + varyingOriginalNormal[2]*sLen;
	// 计算组成小金字塔的新点
	newPoints[0] = gl_in[0].gl_Position.xyz;
	newPoints[1] = gl_in[1].gl_Position.xyz;
	newPoints[2] = gl_in[2].gl_Position.xyz;
	newPoints[3] = (sp0 + sp1 + sp2) / 3.0;// 尖峰点
	// 计算从顶点到光照的方向
	lightDir[0] = light.position - newPoints[0];
	lightDir[1] = light.position - newPoints[1];
	lightDir[2] = light.position - newPoints[2];
	lightDir[3] = light.position - newPoints[3];
	// 构建3个三角形,以组成小金字塔的表面
	makeNewTriangle(0, 1);// 第三个点永远是尖峰点
	makeNewTriangle(1, 2);
	makeNewTriangle(2, 0);
}


如果尖峰长度(sLen)变量增加,则添加的表面“金字塔”将更高。然而,在没有阴影的情况下,它们可能看起来并不真实。

即,原来是[0]、[1]、[2]三个顶点,现在已经没有表面012了,只有表面031、132、230了,3个三角形,共要发送9个顶点。顶点[3]被使用3次参与构建3个三角形,在每个三角形中它对应3个不同的法向量。由于构建圆环时,是以逆时间CCW方向绘制的三角形的,所以在原来的三个顶点在顺序是如上图的,如果顺序不对就会造成法向量方向计算错误。

关于“同一顶点有多个法向量的光照问题”,见Blog:“✠OpenGL-7-光照——一个顶点多个法向量的情况”。

更改图元类型

OpenGL 允许在几何着色器中更改图元类型。此功能的一个常见用途是将输入三角形转换为一个或多个输出线段,来模拟毛发或头发。虽然生成令人信服的头发仍然是更难的现实世界项目之一,但几何着色器可以在许多情况下帮助实现实时渲染。

下面程序显示了一个几何着色器, 它将每个输入的 3 个顶点的三角形转换为一个向外的两个顶点的线段。它首先通过平均三角形顶点位置生成三角形的质心,来计算头发束的起点。然后它使用和上述程序中相同的“尖峰点”作为头发的终点。输出图元被指定为具有两个顶点的线段,第一个顶点是起点,第二个顶点是终点。结果显示在下图中,用于实例化维数为 72 个切片的环面。

当然,这仅仅是产生完全逼真头发的起点。使头发弯曲或移动将需要若干修改,例如为线条生成更多顶点并沿曲线计算它们的位置和/或结合随机性。由于线段没有明显的表面法向量,光照会很复杂;在这个例子中,我们简单地指定法向量与原始三角形的表面法向量相同。

#version 430
layout (triangles) in;// 指定输入图元类型——三角形
layout(line_strip, max_vertices = 2) out;// 指定输出图元类型——线
...
void main() {
	// 原始三角形顶点
	vec3 op0 = gl_in[0].gl_Position.xyz;
	vec3 op1 = gl_in[1].gl_Position.xyz;
	vec3 op2 = gl_in[2].gl_Position.xyz;
	// 偏移三角形顶点
	vec3 ep0 = gl_in[0].gl_Position.xyz + varyingNormal[0]*sLen;
	vec3 ep1 = gl_in[1].gl_Position.xyz + varyingNormal[1]*sLen;
	vec3 ep2 = gl_in[2].gl_Position.xyz + varyingNormal[2]*sLen;
	// 计算[组成小线段]的新点
	vec3 newPoint1 = (op0 + op1 + op2) / 3.0;// 起始点
	vec3 newPoint2 = (ep0 + ep1 + ep2) / 3.0;// 结束点

	gl_Position = proj_matrix * vec4(newPoint1, 1.0);
	varyingVertPosG = newPoint1;
	varyingLightDirG = light.position - newPoint1;
	varyingNormalG = varyingNormal[0];
	EmitVertex();

	gl_Position = proj_matrix * vec4(newPoint2, 1.0);// 直接第二次使用gl_Position out
	varyingVertPosG = newPoint2;
	varyingLightDirG = light.position - newPoint2;
	varyingNormalG = varyingNormal[1];
	EmitVertex();// 直接第二次发送顶点
	
	EndPrimitive();// 由于输出图元类型是“线”,所以完成两个顶点即完成一个图元
}
补充说明

几何着色器吸引人的一点在于它们相对容易使用。虽然几何着色器的许多应用可以使用曲面细分来实现,但几何着色器的机制通常使它们更容易实现和调试。当然,几何与曲面细分的相对适用范围取决于特定的应用。

生成令人信服的真实头发或毛发具有挑战性,并且根据应用场景需要采用多种技术。在某些情况下,简单的纹理就足够了,或者可以使用曲面细分或几何着色器,例如本章所示的基本技术。当需要更真实的效果时,移动(动画)和光照变得棘手。头发和毛发生成的两个专用工具是 HairWorks 和 TressFX。 HairWorks 是 NVIDIA GameWorks 套件的一部分,而 TressFX 是由 AMD 开发的。前者适用于 OpenGL 和 DirectX,而后者仅适用于DirectX。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值