几何着色器

通过顶点着色器和片段着色器,可以将输入的顶点经过着色器的处理显示到屏幕上。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;
支持的原语类型如下,对应绘制命令类型:

  • points - GL_POINTS (1 vertex)
  • lines - GL_LINES, GL_LINE_STRIP, GL_LINE_LIST (2 vertices)
  • lines_adjacency - GL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY (4 vertices)
  • triangles - GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN (3 vertices)
  • triangles_adjacency - GL_TRIANGLES_ADJACENCY, GL_TRIANGLE_STRIP_ADJACENCY (6 vertices)
实例中绘制类型为GL_POINTS,此处使用points。

输出类型

第二行描述着色的输出。通过处理,几何着色器能输出完全不同的几何体类型,生成的原语数也可以变化。

layout(line_strip, max_vertices = 2) out;

此行定义输出类型和最大输出顶点数。

支持的输出类型如下:

  • points
  • line_strip
  • triangle_strip
类型虽然很少,但可以充分覆盖所有的原语类型。例如,三个顶点的triangle_strip相当于一个普通的三角形。

顶点输入

几何着色器中可通过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;
}

传递颜色属性:

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,
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)));
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变量作为输入参数。

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应用请不要使用几何着色器。

你可能感兴趣的:(图形,GPU)