几何着色器(Geometry Shader)
- 在顶点和片元着色器之间存在一个可选的着色器阶段称为几何着色器(geometry shader)。一个几何着色器接收组成一个基元的顶点集合作为输入,并在将其发送到下一阶段的着色器前进行合适的变换。
- 下面展示一个几何着色器的例子。
#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;
void main()
{
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
- 几何着色器首先通过在
in
关键字之前声明一个layout标识符,来声明从顶点着色器接收那种类型的基元作为输入。layout限定符可以接受如下基元的类型值:-
points
:绘制GL_POINTS
基元。 -
lines
:绘制GL_LINES
或GL_LINE_STRIP
。 -
lines_adjacency
:GL_LINES_ADJACENCY
或GL_LINE_STRIP_ADJACENCY
-
triangles
:GL_TRIANGLES
,GL_TRIANGLE_STRIP
或GL_TRIANGLE_FAN
。 -
triangles_adjacency
:GL_TRIANGLES_ADJACENCY
或GL_TRIANGLES_STRIP_ADJACENCY
-
- 几何着色器通过在
out
关键字之前声明layout标识符来指定输出的基元类型,layout限定符可以接受如下基元的类型值:points
line_strip
triangle_strip
- 要输出有意义的结果我们需要以某种方式获取上一着色器阶段的输出,GLSL提供了一个内置变量
gl_in
,其结构如下所示:
in gl_Vertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
- 注意:上面的结构定义为数组,因为基元一般包含不止一个顶点,几何着色器需要接收基元的所有顶点作为输入。
- 在几何着色器中,我们可以使用两个函数
EmitVertex
和EndPrimitive
,利用从顶点着色器接收的顶点数据来生成新的数据。几何着色器需要你至少生成/输出一个你指定的基元作为输出。
#version 330 core
layout (points) in;
layout (line_strip, max_vertices = 2) out;
void main()
{
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.0, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.1, 0.0, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
1. 每次调用EmitVertex,当前设置的gl_Position的矢量会被添加到输出的基元中。
2. 无论何时调用EndPrimitive,所有为基元生成的顶点被组合到指定的输出基元。重复调用EndPrimitive可以持续生成多个基元。
- 使用上面的几何着色器,输入四方形的四个顶点位置数据进行渲染。
glDrawArrays(GL_POINTS, 0, 4);
-
渲染效果。
1. 使用几何着色器
- 要展示如何使用几何着色器,我们来渲染一个简单场景,在z平面上以标准设备坐标绘制四个点,顶点数据如下:
float points[] = {
-0.5f, 0.5f, // 左上角
0.5f, 0.5f,
0.5f, -0.5f,
-0.5f, -0.5f, // 左下角
}
- 顶点着色器如下:
#version 330 core
layout (location = 0) in vec2 aPos;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
}
- 片元着色器如下:
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(0.0f, 1.0f, 0.0f, 1.0f);
}
- 出于学习目的,我们创建一个称为直通几何着色器,接收一个点基元并直接传递到下一个着色器。
#version 330 core
layout (points) in;
layout (points, max_vertices = 1) out;
void main()
{
gl_Position = gl_in[0].gl_Position;
EmitVertex();
EndPrimitive();
}
- 完善我们的着色器类,添加一个包含几何着色器的构造函数,并在实现中将其附加到着色器程序中进行链接。
Shader(const char* vertexPath, const char* geometryPath, const char* fragmentPath);
// 几何着色器对象创建、附加和连接关键步骤:
geometry = glCreateShader(GL_GEOMETRY_SHADER);
glShaderSource(geometry, 1, &gShaderCode, NULL);
glCompileShader(geometry);
glGetShaderiv(geometry, GL_COMPILE_STATUS, &success);
if (!success)
{
glGetShaderInfoLog(geometry, 512, NULL, infoLog);
std::cout << "ERROR::SHADER::GEOMETRY::COMPILATION_FAILED\n" << infoLog << std::endl;
}
// 3. link shader program
ID = glCreateProgram();
glAttachShader(ID, vertex);
glAttachShader(ID, fragment);
glAttachShader(ID, geometry);
glLinkProgram(ID);
- 渲染代码。
shader.use();
glBindVertexArray(VAO);
glDrawArrays(GL_POINTS, 0, 4);
-
渲染效果。
2. 构建房子图形
- 要输出一个房子图形,我们可以将几何着色器的输出设置为
triangle_strip
并绘制三个三角形,两个构成四方形的房子,一个构造房顶。 - 在OpenGL中,三角形带是一种更高效地使用更少顶点绘制三角形的方式。在第一个三角形绘制后,每下一个顶点会生成另外一个与第一个三角形相邻的三角形,每三个相邻的顶点都构成一个三角形。如下图所示:(图片取自书中)
- 使用三角形带作为几何着色器的输出,通过以正确的顺序生成三个相邻的三角形,我们可以很容易的创建一个房子图形。下面的图显示,以蓝色点作为输入,我们要创建房子图形所需要的顺序:(图片取自书中)
- 上面的房子造型对应的几何着色器如下:
#version 330 core
layput (points) in;
layout (triangle_strip, max_vertices = 5) out;
void build_house(vec4 position)
{
gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0);
EmitVertex();
gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0);
EmitVertex();
gl_Position = position + vec4(-0.2, 0.2, 0.0, 0.0);
EmitVertex();
gl_Position = position + vec4( 0.2, 0.2, 0.0, 0.0);
EmitVertex();
gl_Position = position + vec4( 0.0, 0.4, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
void main()
{
build_house(gl_in[0].gl_Position);
}
- 下面我们使用几何着色器在场景渲染四个不同颜色的房子图形,顶点数据设置如下:
float vertices[] = {
0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
-0.5f, 0.5f, 1.0f, 1.0f, 0.0f
};
- 在顶点着色器中我们使用接口模块来将顶点颜色属性传递到几何着色器。
#version 330 core
layout (location = 0) in vec2 aPos;
layout (location = 1) in vec3 aColor;
out VS_OUT {
vec3 color;
} vs_out;
void main()
{
gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0);
vs_out.color = aColor;
}
- 这里使用接口模块来传递数据,是因为实际中几何着色器的输入可能变得很大(记住:几何着色器是对基元的顶点集合进行处理),使用接口模块可以更好进行组织。
- 根据顶点着色器的接口模块声明,我们对前面的几何着色器进行调整,并设置一个输出颜色,传递到片元着色器。
#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;
in VS_OUT {
vec3 color;
} gs_in[];
out vec3 fColor;
void main()
{
fColor = gs_in[0].color;
gl_Position = gl_in[0].gl_Position + vec4(-0.2, -0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.2, -0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4(-0.2, 0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.2, 0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.0, 0.4, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
-
渲染效果。
- 对于几何着色器,我们可以将最后生成顶点的颜色修改为白色,让房子看起来像有点积雪的样子。
void main()
{
fColor = gs_in[0].color;
gl_Position = gl_in[0].gl_Position + vec4(-0.2, -0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.2, -0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4(-0.2, 0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.2, 0.2, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4( 0.0, 0.4, 0.0, 0.0);
fColor = vec3(1.0, 1.0, 1.0);
EmitVertex();
EndPrimitive();
}
-
渲染效果。
3. 物体爆炸
- 我们这里说的物体爆炸是指在很短时间内将每个三角形面片朝它们的法向量移动,产生整个物体的三角形面片像是爆炸了一样。
- 因为我们要将三角形的顶点朝三角形法向量进行移动,因此我们需要使用三角形的三个顶点来计算三角形的法向量。从变换章节我们知道,我们可以使用叉积(cross product)来计算两个矢量的垂直矢量。下面的方法就是对法向量进行计算。
vec3 GetNormal()
{
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
return normalize(cross(a, b));
}
- 注意:如果上面的计算,我们转换a和b的位置,得到的是反方向的法向量,这里顺序很重要。
- 计算出法向量后,我们就可以设计一个“爆炸”函数,接受顶点位置矢量和法向量参数,输出位置矢量沿法向量移动的新的位置矢量。
vec4 explode(vec4 position, vec3 normal)
{
float magnitude = 2.0;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
- 上面爆炸函数的计算中,我们不希望物体“向内爆炸”,所以我们将函数的结果转换到[0, 1]之间,然后缩放法向量并保证将移动方向矢量增加到位置矢量上。其中time是uniform变量,我们可以在渲染循环中进行设置。
objectShader.setFloat("time", glfwGetTime());
- 完整的几何着色器如下:
#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;
in VS_OUT {
vec2 texCoords;
} gs_in[];
out vec2 TexCoords;
uniform float time;
vec4 explode(vec4 position, vec3 normal)
{
float magnitude = 2.0;
vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude;
return position + vec4(direction, 0.0);
}
vec3 GetNormal()
{
vec3 a = vec3(gl_in[0].gl_Position) - vec3(gl_in[1].gl_Position);
vec3 b = vec3(gl_in[2].gl_Position) - vec3(gl_in[1].gl_Position);
return normalize(cross(a, b));
}
void main()
{
vec3 normal = GetNormal();
gl_Position = explode(gl_in[0].gl_Position, normal);
TexCoords = gs_in[0].texCoords;
EmitVertex();
gl_Position = explode(gl_in[1].gl_Position, normal);
TexCoords = gs_in[1].texCoords;
EmitVertex();
gl_Position = explode(gl_in[2].gl_Position, normal);
TexCoords = gs_in[2].texCoords;
EmitVertex();
EndPrimitive();
}
-
渲染效果。
4. 法向量可视化
- 下面我们讨论几何着色器一个实际的使用场景:物体法向量可视化。当我们编写光照着色程序,很容易出现奇怪的渲染效果,而常见原因就可能是错误的法向量造成的。要确定我们提供的法向量是否正确,一种很好的方式就是使用几何着色器可视化法向量。
- 法向量可视化的想法很简单:我首先我们不使用几何着色器绘制常规场景,然后我们使用几何着色器只绘制物体的法向量。几何着色器接收三角形基元作为输入,为每个顶点生成一个法向量。伪代码如下:
shader.use();
DrawScene();
normalDisplayShader.use();
DrawScene();
- 这次我们不再计算法向量,而是直接使用模型提供的法向量数据。因为几何着色器接收的是视空间的位置矢量,我们需要将法向量转换到相同的空间,因此我们在顶点着色器中先对法向量进行转换并通过接口模块将其传递到几何着色器。
#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec3 aNormal;
out VS_OUT {
vec3 normal;
} vs_out;
uniform mat4 view;
uniform mat4 model;
void main()
{
gl_Position = view * model * vec4(aPos, 1.0);
mat3 normalMatrix = mat3(transpose(inverse(view * model)));
vs_out.normal = normalize(vec3(vec4(normalMatrix * aNormal, 0.0)));
}
- 几何着色器则对每个接收到的顶点绘制法向量。
#version 330 core
layout (triangles) in;
layout (line_strip, max_vertices = 6) out;
in VS_OUT {
vec3 normal;
} gs_in[];
const float MAGNITUDE = 0.4;
uniform mat4 projection;
void GenerateLine(int index)
{
gl_Position = projection * gl_in[index].gl_Position;
EmitVertex();
gl_Position = projection * (gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE);
EmitVertex();
EndPrimitive();
}
void main()
{
GenerateLine(0);
GenerateLine(1);
GenerateLine(2);
}
- 因为可视化法向量一般用于程序调试,因此我们使用单色线显示法向量。片元着色器如下:
#version 330 core
out vec4 FragColor;
void main()
{
FragColor = vec4(1.0, 1.0, 0.0, 1.0);
}
-
最后在渲染循环中,我们使用常规着色器绘制模型,使用上述法向量着色器绘制法向量线。效果如下:(马桶刷即视感!!!)