目录
一、说明
二、几何着色器
2.1 设置
2.2 基本几何着色器
2.2.1 输入类型
2.2.2 输出类型
2.2.3 顶点输入
2.2.4 顶点输出
2.3 创建几何着色器
2.4 几何着色器和顶点属性
三、动态生成几何体
四、结论
练习
一、说明
几何着色器的应用比较高级,关于几何着色器使用究竟需要什么样的步骤,几何着色器能解决什么样的问题。需要示范案例,本文将演示这个过程。
二、几何着色器
到目前为止,我们已经使用顶点和片段着色器将输入顶点转换为屏幕上的像素。从 OpenGL 3.2 开始,顶点和片段着色器之间还有第三种可选着色器,称为几何着色 器。此着色器具有独特的能力,可以使用顶点着色器的输出作为输入,动态创建新的几何体。
由于我们忽视前几章中的小猫太久了,它跑到了新家。这给了我们一个重新开始的好机会。在本章的最后,我们将有以下演示:
这看起来并不那么令人兴奋......直到您认为上面的结果是通过一次绘制调用生成的:
glDrawArrays(GL_POINTS, 0, 4);
请注意,几何着色器可以做的所有事情都可以通过其他方式完成,但它们从少量输入数据生成几何体的能力允许您减少 CPU -> GPU 带宽使用。
2.1 设置
让我们首先编写一些简单的代码,仅在屏幕上绘制 4 个红点。
// Vertex shader
const char* vertexShaderSrc = R"glsl(
#version 150 core
in vec2 pos;
void main()
{
gl_Position = vec4(pos, 0.0, 1.0);
}
)glsl";
// Fragment shader
const char* fragmentShaderSrc = R"glsl(
#version 150 core
out vec4 outColor;
void main()
{
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
)glsl";
我们首先在文件顶部声明两个非常简单的顶点和片段着色器。顶点着色器只是转发每个点的位置属性,而片段着色器始终输出红色。那里没什么特别的。
我们还添加一个辅助函数来创建和编译着色器:
GLuint createShader(GLenum type, const GLchar* src) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
return shader;
}
在该main
函数中,使用选择的库创建一个窗口和 OpenGL 上下文并初始化 GLEW。着色器并编译和激活:
GLuint vertexShader = createShader(GL_VERTEX_SHADER, vertexShaderSrc);
GLuint fragmentShader = createShader(GL_FRAGMENT_SHADER, fragmentShaderSrc);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
之后,创建一个保存点坐标的缓冲区:
GLuint vbo;
glGenBuffers(1, &vbo);
float points[] = {
-0.45f, 0.45f,
0.45f, 0.45f,
0.45f, -0.45f,
-0.45f, -0.45f,
};
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
我们这里有 4 个点,每个点都有 x 和 y 设备坐标。请记住,设备坐标从左到右、从下到上的屏幕坐标范围为 -1 到 1,因此每个角都有一个点。
然后创建一个VAO并设置顶点格式规范:
// Create VAO
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// Specify layout of point data
GLint posAttrib = glGetAttribLocation(shaderProgram, "pos");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 0, 0);
最后是渲染循环:
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glDrawArrays(GL_POINTS, 0, 4);
使用此代码,您现在应该在黑色背景上看到 4 个红点,如下所示:
如果您遇到问题,请查看参考源代码。
2.2 基本几何着色器
要了解几何着色器的工作原理,让我们看一个示例:
#version 150 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();
}
2.2.1 输入类型
顶点着色器处理顶点,片段着色器处理片段,而几何着色器处理整个图元。第一行描述了我们的着色器应该处理什么样的基元。
layout(points) in;
下面列出了可用的类型及其等效的绘图命令类型:
- 点- GL_POINTS(1 个顶点)
- 线- GL_LINES、GL_LINE_STRIP、GL_LINE_LIST(2 个顶点)
- lines_adjacency - GL_LINES_ADJACENCY、GL_LINE_STRIP_ADJACENCY(4 个顶点)
- 三角形- GL_TRIANGLES、GL_TRIANGLE_STRIP、GL_TRIANGLE_FAN(3 个顶点)
- triangles_adjacency - GL_TRIANGLES_ADJACENCY、GL_TRIANGLE_STRIP_ADJACENCY(6 个顶点)
由于我们正在绘图GL_POINTS
,因此points
类型是合适的。
2.2.2 输出类型
下一行描述着色器的输出。几何着色器的有趣之处在于它们可以输出完全不同类型的几何形状,并且生成的图元数量甚至可以变化!
layout(line_strip, max_vertices = 2) out;
第二行指定输出类型及其可以传递的最大顶点数。这是着色器调用的最大数量,而不是单个图元(line_strip
在本例中)的最大数量。
可以使用以下输出类型:
- 点
- 线带
- 三角形带
这些类型看起来有些受限,但如果你仔细想想,这些类型足以涵盖所有可能的图元类型。例如,只有 3 个顶点的三角形条带相当于一个正三角形。
2.2.3 顶点输入
gl_Position
可以使用几何着色器中的数组来访问在顶点着色器中设置 的gl_in
。它是一个结构数组,如下所示:
in gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
请注意,诸如pos
和 之类的顶点属性color
不包括在内,我们稍后将考虑访问这些属性。
2.2.4 顶点输出
几何着色器程序可以调用两个特殊函数来生成图元,EmitVertex
和EndPrimitive
。每次程序调用 时 EmitVertex
,都会将一个顶点添加到当前图元中。添加完所有顶点后,程序将调用EndPrimitive
生成图元。
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();
}
在调用之前EmitVertex
,应将顶点的属性分配给诸如 之类的变量gl_Position
,就像在顶点着色器中一样。稍后我们将讨论如何设置color
片段着色器等属性。
现在您知道了每条线的含义,您能解释一下这个几何着色器的作用吗?
它为传递给它的每个点坐标创建一条水平线。
2.3 创建几何着色器
没有太多需要解释的,几何着色器的创建和激活方式与其他类型的着色器完全相同。让我们向 4 点示例添加一个尚未执行任何操作的几何着色器。
const char* geometryShaderSrc = R"glsl(
#version 150 core
layout(points) in;
layout(points, max_vertices = 1) out;
void main()
{
gl_Position = gl_in[0].gl_Position;
EmitVertex();
EndPrimitive();
}
)glsl";
这个几何着色器应该相当简单。对于每个输入点,它生成一个等效的输出点。这是在屏幕上显示点所需的最少代码量。
使用辅助函数,创建几何着色器很容易:
GLuint geometryShader = createShader(GL_GEOMETRY_SHADER, geometryShaderSrc);
将其附加到着色器程序也没有什么特别的:
glAttachShader(shaderProgram, geometryShader);
当您现在运行该程序时,它应该仍然像以前一样显示点。您可以通过从其函数中删除代码来验证几何着色器现在是否正在执行其工作main
。您将看到不再绘制任何点,因为没有生成任何点!
现在,尝试用上一节中的线带生成代码替换几何着色器代码:
#version 150 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();
}
即使我们没有对绘制调用进行任何更改,GPU 也会突然绘制细线而不是点!
尝试做一些实验来感受一下。例如,尝试使用输出矩形triangle_strip
。
2.4 几何着色器和顶点属性
让我们为正在绘制的线条添加一些变化,让每条线条都具有独特的颜色。通过向顶点着色器添加颜色输入变量,我们可以指定每个顶点的颜色,从而指定每个生成线的颜色。
#version 150 core
in vec2 pos;
in vec3 color;
out vec3 vColor; // Output to geometry (or fragment) shader
void main()
{
gl_Position = vec4(pos, 0.0, 1.0);
vColor = color;
}
更新程序代码中的顶点规范:
GLint posAttrib = glGetAttribLocation(shaderProgram, "pos");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 0);
GLint colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE,
5 * sizeof(float), (void*) (2 * sizeof(float)));
并更新点数据以包含每个点的 RGB 颜色:
float points[] = {
-0.45f, 0.45f, 1.0f, 0.0f, 0.0f, // Red point
0.45f, 0.45f, 0.0f, 1.0f, 0.0f, // Green point
0.45f, -0.45f, 0.0f, 0.0f, 1.0f, // Blue point
-0.45f, -0.45f, 1.0f, 1.0f, 0.0f, // Yellow point
};
因为顶点着色器现在后面不是片段着色器,而是几何着色器,所以我们必须将变量vColor
作为输入处理。
#version 150 core
layout(points) in;
layout(line_strip, max_vertices = 2) out;
in vec3 vColor[]; // Output from vertex shader for each vertex
out vec3 fColor; // Output to fragment shader
void main()
{
...
您可以看到它与片段着色器中处理输入的方式非常相似。唯一的区别是输入现在必须是数组,因为几何着色器可以接收具有多个顶点的图元作为输入,每个顶点都有自己的属性值。
由于颜色需要进一步向下传递到片段着色器,因此我们将其添加为几何着色器的输出。我们现在可以为其赋值,就像我们之前对gl_Position
.
void main()
{
fColor = vColor[0]; // Point has only one vertex
gl_Position = gl_in[0].gl_Position + vec4(-0.1, 0.1, 0.0, 0.0);
EmitVertex();
gl_Position = gl_in[0].gl_Position + vec4(0.1, 0.1, 0.0, 0.0);
EmitVertex();
EndPrimitive();
}
每当EmitVertex
调用 now 时,都会发出一个fColor
带有颜色属性当前值的顶点。我们现在可以在片段着色器中访问该属性:
#version 150 core
in vec3 fColor;
out vec4 outColor;
void main()
{
outColor = vec4(fColor, 1.0);
}
因此,当您为顶点指定属性时,它首先会作为输入传递给顶点着色器。然后顶点着色器可以选择将其输出到几何着色器。然后几何着色器可以选择进一步输出到片段着色器。
然而,这个演示并不是很有趣。我们可以通过使用单行创建顶点缓冲区并发出几个使用统一变量设置的不同颜色和位置的绘制调用来轻松复制此行为。
三、动态生成几何体
几何着色器的真正威力在于能够生成不同数量的图元,因此让我们创建一个正确滥用此功能的演示。
假设您正在制作一款世界由圆圈组成的游戏。您可以绘制一个圆形模型并重复绘制它,但这种方法并不理想。如果距离太近,这些“圆圈”看起来会像丑陋的多边形,如果距离太远,显卡就会在渲染复杂性上浪费性能,而您甚至看不到。
我们可以使用几何着色器做得更好!我们可以编写一个着色器,根据运行时条件生成适当的分辨率圆。我们首先修改几何着色器以在每个点绘制一个 10 边形多边形。如果你还记得你的三角函数,那么它应该是小菜一碟:
#version 150 core
layout(points) in;
layout(line_strip, max_vertices = 11) out;
in vec3 vColor[];
out vec3 fColor;
const float PI = 3.1415926;
void main()
{
fColor = vColor[0];
for (int i = 0; i <= 10; i++) {
// Angle between each side in radians
float ang = PI * 2.0 / 10.0 * i;
// Offset from center of point (0.3 to accomodate for aspect ratio)
vec4 offset = vec4(cos(ang) * 0.3, -sin(ang) * 0.4, 0.0, 0.0);
gl_Position = gl_in[0].gl_Position + offset;
EmitVertex();
}
EndPrimitive();
}
重复第一个点以闭合线循环,这就是绘制 11 个顶点的原因。结果如预期:
现在添加顶点属性来控制边数很简单。将新属性添加到数据和规范中:
float points[] = {
// Coordinates Color Sides
-0.45f, 0.45f, 1.0f, 0.0f, 0.0f, 4.0f,
0.45f, 0.45f, 0.0f, 1.0f, 0.0f, 8.0f,
0.45f, -0.45f, 0.0f, 0.0f, 1.0f, 16.0f,
-0.45f, -0.45f, 1.0f, 1.0f, 0.0f, 32.0f
};
...
// Specify layout of point data
GLint posAttrib = glGetAttribLocation(shaderProgram, "pos");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE,
6 * sizeof(float), 0);
GLint colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE,
6 * sizeof(float), (void*) (2 * sizeof(float)));
GLint sidesAttrib = glGetAttribLocation(shaderProgram, "sides");
glEnableVertexAttribArray(sidesAttrib);
glVertexAttribPointer(sidesAttrib, 1, GL_FLOAT, GL_FALSE,
6 * sizeof(float), (void*) (5 * sizeof(float)));
更改顶点着色器以将值传递给几何着色器:
#version 150 core
in vec2 pos;
in vec3 color;
in float sides;
out vec3 vColor;
out float vSides;
void main()
{
gl_Position = vec4(pos, 0.0, 1.0);
vColor = color;
vSides = sides;
}
并使用几何着色器中的变量而不是幻数边数 10.0
。还需要max_vertices
为我们的输入设置适当的值,否则具有更多顶点的圆将被切断。
layout(line_strip, max_vertices = 64) out;
...
in float vSides[];
...
// Safe, floats can represent small integers exactly
for (int i = 0; i <= vSides[0]; i++) {
// Angle between each side in radians
float ang = PI * 2.0 / vSides[0] * i;
...
现在,您只需添加更多点即可创建具有任意边数的圆!
如果没有几何着色器,每当这些圆中的任何一个需要更改时,我们就必须重建整个顶点缓冲区,现在我们可以简单地更改顶点属性的值。在游戏设置中,该属性可以根据玩家距离而改变,如上所述。您可以在此处找到完整的代码。
四、结论
诚然,几何着色器可能没有像帧缓冲区和纹理等那样多的现实世界用例,但它们绝对可以帮助在 GPU 上创建内容,如此处所示。
如果您需要多次重复单个网格(例如体素游戏中的立方体),您可以创建一个几何着色器,以类似的方式从点生成立方体。然而,对于每个生成的网格完全相同的情况,有更有效的方法,例如实例化代码(实例化代码)。
最后,关于可移植性,最新的 WebGL 和 OpenGL ES 标准尚不支持几何着色器,因此如果您正在考虑开发移动或 Web 应用程序,请记住这一点。
实例化代码:
// Link statically with GLEW
#define GLEW_STATIC
// Headers
#include <GL/glew.h>
#include <SFML/Window.hpp>
// Vertex shader
const GLchar* vertexShaderSrc = R"glsl(
#version 150 core
in vec2 pos;
in vec3 color;
in float sides;
out vec3 vColor;
out float vSides;
void main()
{
gl_Position = vec4(pos, 0.0, 1.0);
vColor = color;
vSides = sides;
}
)glsl";
// Geometry shader
const GLchar* geometryShaderSrc = R"glsl(
#version 150 core
layout(points) in;
layout(line_strip, max_vertices = 64) out;
in vec3 vColor[];
in float vSides[];
out vec3 fColor;
const float PI = 3.1415926;
void main()
{
fColor = vColor[0];
// Safe, GLfloats can represent small integers exactly
for (int i = 0; i <= vSides[0]; i++) {
// Angle between each side in radians
float ang = PI * 2.0 / vSides[0] * i;
// Offset from center of point (0.3 to accomodate for aspect ratio)
vec4 offset = vec4(cos(ang) * 0.3, -sin(ang) * 0.4, 0.0, 0.0);
gl_Position = gl_in[0].gl_Position + offset;
EmitVertex();
}
EndPrimitive();
}
)glsl";
// Fragment shader
const GLchar* fragmentShaderSrc = R"glsl(
#version 150 core
in vec3 fColor;
out vec4 outColor;
void main()
{
outColor = vec4(fColor, 1.0);
}
)glsl";
// Shader creation helper
GLuint createShader(GLenum type, const GLchar* src) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
return shader;
}
int main()
{
sf::ContextSettings settings;
settings.depthBits = 24;
settings.stencilBits = 8;
settings.majorVersion = 3;
settings.minorVersion = 2;
sf::Window window(sf::VideoMode(800, 600, 32), "Circles", sf::Style::Titlebar | sf::Style::Close, settings);
// Initialize GLEW
glewExperimental = GL_TRUE;
glewInit();
// Compile and activate shaders
GLuint vertexShader = createShader(GL_VERTEX_SHADER, vertexShaderSrc);
GLuint geometryShader = createShader(GL_GEOMETRY_SHADER, geometryShaderSrc);
GLuint fragmentShader = createShader(GL_FRAGMENT_SHADER, fragmentShaderSrc);
GLuint shaderProgram = glCreateProgram();
glAttachShader(shaderProgram, vertexShader);
glAttachShader(shaderProgram, geometryShader);
glAttachShader(shaderProgram, fragmentShader);
glLinkProgram(shaderProgram);
glUseProgram(shaderProgram);
// Create VBO with point coordinates
GLuint vbo;
glGenBuffers(1, &vbo);
GLfloat points[] = {
// Coordinates Color Sides
-0.45f, 0.45f, 1.0f, 0.0f, 0.0f, 4.0f,
0.45f, 0.45f, 0.0f, 1.0f, 0.0f, 8.0f,
0.45f, -0.45f, 0.0f, 0.0f, 1.0f, 16.0f,
-0.45f, -0.45f, 1.0f, 1.0f, 0.0f, 32.0f
};
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(points), points, GL_STATIC_DRAW);
// Create VAO
GLuint vao;
glGenVertexArrays(1, &vao);
glBindVertexArray(vao);
// Specify the layout of the vertex data
GLint posAttrib = glGetAttribLocation(shaderProgram, "pos");
glEnableVertexAttribArray(posAttrib);
glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), 0);
GLint colAttrib = glGetAttribLocation(shaderProgram, "color");
glEnableVertexAttribArray(colAttrib);
glVertexAttribPointer(colAttrib, 3, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (void*) (2 * sizeof(GLfloat)));
GLint sidesAttrib = glGetAttribLocation(shaderProgram, "sides");
glEnableVertexAttribArray(sidesAttrib);
glVertexAttribPointer(sidesAttrib, 1, GL_FLOAT, GL_FALSE, 6 * sizeof(GLfloat), (void*) (5 * sizeof(GLfloat)));
bool running = true;
while (running)
{
sf::Event windowEvent;
while (window.pollEvent(windowEvent))
{
switch (windowEvent.type)
{
case sf::Event::Closed:
running = false;
break;
}
}
// Clear the screen to black
glClearColor(0.0f, 0.0f, 0.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
// Render frame
glDrawArrays(GL_POINTS, 0, 4);
// Swap buffers
window.display();
}
glDeleteProgram(shaderProgram);
glDeleteShader(fragmentShader);
glDeleteShader(geometryShader);
glDeleteShader(vertexShader);
glDeleteBuffers(1, &vbo);
glDeleteVertexArrays(1, &vao);
window.close();
return 0;
}
练习
- 尝试在 3D 场景中使用几何着色器来创建更复杂的网格,例如基于点的立方体。