通过顶点着色器和片段着色器,可以将输入的顶点经过着色器的处理显示到屏幕上。OpenGL 3.2及更新的版本支持几何着色器,介于顶点着色器和片段着色器之间,几何着色器接收顶点着色器的输出作为输入,通过高效的几何运算,将数据输出到片段着色器。
假设我们想绘制如下图形:
通过一个简单的函数调用就可以绘制:
glDrawArrays(GL_POINTS, 0, 4);
此时几何着色器就派上用场了。虽然几何着色器做的事情也可以在CPU中实现,不过通过几何着色器,你只需要传递很少的数据到显卡,后面的事情就交给GPU做了,减少了CPU的负载、内存到显卡数据的传输压力,同时也提升了程序运行效率(GPU运算更快)。
首先在屏幕上绘制四个点,代码如下:
// Vertex shader
const char* vertexShaderSrc = GLSL(
in vec2 pos;
void main() {
gl_Position = vec4(pos, 0.0, 1.0);
}
);
// Fragment shader
const char* fragmentShaderSrc = GLSL(
out vec4 outColor;
void main() {
outColor = vec4(1.0, 0.0, 0.0, 1.0);
}
);
以上声明了两个简单的顶点着色器和片段着色器。顶点着色器简单的转发每个点的顶点属性,片段着色器输出固定的红色。
添加GLSL版本的宏:
#define GLSL(src) "#version 150 core\n" #src
创建和编译着色器:
GLuint createShader(GLenum type, const GLchar* src) {
GLuint shader = glCreateShader(type);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
return shader;
}
main函数中通过GLEW库创建一个窗口和OpenGL上下文,创建着色器程序,添加着色器源码,链接源代码,激活着色器程序。
然后创建一个保存了顶点坐标的缓存。
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);
创建四个点,每个点包含x和y坐标。此处坐标范围从-1到1,x坐标和y坐标分别为从左到右,从下到上,最终显示效果就是每个象限有一个点。
创建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);
效果图中出现四个红点:
为了理解几何着色器的工作原理,首先看一个几何着色器的简单例子:
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();
}
顶点着色器处理顶点,片段着色器处理每个小的片段,而几何着色器处理整个原语。第一行描述几何着色器处理的原语类型。
layout(points) in;
支持的原语类型如下,对应绘制命令类型:
输出类型
第二行描述着色的输出。通过处理,几何着色器能输出完全不同的几何体类型,生成的原语数也可以变化。
layout(line_strip, max_vertices = 2) out;
此行定义输出类型和最大输出顶点数。
支持的输出类型如下:
几何着色器中可通过gl_in数组访问顶点着色器中的变量gl_Position,gl_in为结构化的数组,具体如下:
in gl_PerVertex
{
vec4 gl_Position;
float gl_PointSize;
float gl_ClipDistance[];
} gl_in[];
顶点输出
几何着色器程序通过两个指定的函数产生原语: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,类似于顶点着色器中的赋值。
几何着色器的创建和激活方式和其他类型着色器类似。我们在只绘制了四个点的实例中添加几何着色器。
const char* geometryShaderSrc = GLSL(
layout(points) in;
layout(points, max_vertices = 1) out;
void main() {
gl_Position = gl_in[0].gl_Position;
EmitVertex();
EndPrimitive();
}
);
此处的几何着色器功能简单。对于每一个输入顶点,产生对应的一个输出顶点。我们使用最少的代码在屏幕上显示点。
使用已经封装好的代码,可简单的创建几何着色器:
GLuint geometryShader = createShader(GL_GEOMETRY_SHADER, geometryShaderSrc);
添加着色器代码到着色器程序:
glAttachShader(shaderProgram, geometryShader);
程序运行效果同样只显示四个点。但是移除几何着色器中的代码,程序却没有任何显示,这说明几何着色器已经发挥了作用。
修改几何着色器中的代码points为line_strip,并设置每次传输的最大顶点数为2,修改如下:
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绘制的不在是点,而是线段。
我们设置每一条线的颜色。增加颜色信息作为顶点着色器的输入变量,为每一个顶点指定一个颜色。
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;
}
传递颜色属性:
更新点的坐标数据,为每个点添加RGB颜色属性: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)));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)));
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变量作为输入参数。
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() {
...
片段着色器中处理输入数据的方式和以前相似,唯一的不同就是输入必须是数组,因为几何着色器能接收多个顶点的原语作为输入,可同是作为属性传递到片段着色器。
因为颜色属性最终要传递到片段着色器,我们添加fColor变量作为输出变量,同时在代码中赋值。
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()时,顶点属性附带颜色属性fColor将被传递到片段着色器,此时我们可以便可以在片段着色器访问顶点和颜色信息。
in vec3 fColor;
out vec4 outColor;
void main() {
outColor = vec4(fColor, 1.0);
}
因此,当定义了顶点的属性时,属性首先传递给顶点着色器,顶点着色器可选择将信息直接或者经过修改传给几何着色器,几何着色器通过处理再将数据传递到片段着色器。
几何着色器的核心功能在于能生成变化的原语数。
比如我们创建一个由圆形组成的游戏,你可以先绘制一个单独的模型,然后重复绘制此模型,这种方式很原始。当距离圆形很近时,圆形感觉不平滑,效果会比较差,当距离很远时,眼睛无法辨识很精细的绘制效果,则绘制太精细会浪费硬件资源,降低程序的显示性能。
此时几何着色器可以发挥作用了。几何着色器可根据运行环境(例如眼睛离物体的距离)产生适合分辨率的圆形。我们首先修改几何着色器围绕点绘制十条边的多边形,如果几何学的好,实现会比较容易。
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();
}
第一个点和最后一个点要相同,这样图形才能形成闭环,所以十条边需要十一各顶点。效果如下:
然后增加顶点属性控制边的数量,增加点的属性信息,并传递到着色器。
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)));
修改顶点着色器,传递属性到几何着色器。
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;
}
几何着色器中使用传递的边数,修改输出最大顶点数。
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应用请不要使用几何着色器。