几何着色器

在顶点和片段着色器之间有一个可选的几何着色器(Geometry Shader),几何着色器的输入是一个图元(如点或三角形)的一组顶点。几何着色器可以在顶点发送到下一着色器阶段之前对它们随意变换。然而,几何着色器最有趣的地方在于,它能够将(这一组)顶点变换为完全不同的图元,并且还能生成比原来更多的顶点。

几何着色器

    #version 330 core
    //对应glDrawArrays一种绘制方式: points、lines、lines_adjacency、triangles、triangles_adjacency
    layout (points) in;

    //线的方式输出,并限制最大顶点数为2
    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 Qualifier)。这个输入布局修饰符可以从顶点着色器接收下列任何一个图元值, 对应了不同的绘制方式下的图元值: points:绘制GL_POINTS图元时(1)。 lines:绘制GL_LINES或GL_LINE_STRIP时(2) lines_adjacency:GL_LINES_ADJACENCY或GL_LINE_STRIP_ADJACENCY(4) triangles:GL_TRIANGLES、GL_TRIANGLE_STRIP或GL_TRIANGLE_FAN(3) triangles_adjacency:GL_TRIANGLES_ADJACENCY或GL_TRIANGLE_STRIP_ADJACENCY(6)

以上是能提供给glDrawArrays渲染函数的几乎所有图元了。如果我们想要将顶点绘制为GL_TRIANGLES,我们就要将输入修饰符设置为triangles。括号内的数字表示的是一个图元所包含的最小顶点数。

输出: 接下来,我们还需要指定几何着色器输出的图元类型,这需要在out关键字前面加一个布局修饰符。和输入布局修饰符一样,输出布局修饰符也可以接受几个图元值:点线三角形 points line_strip triangle_strip 几何着色器同时希望我们设置一个它最大能够输出的顶点数量(如果你超过了这个值,OpenGL将不会绘制多出的顶点),这个也可以在out关键字的布局修饰符中设置

为了生成更有意义的结果,我们需要某种方式来获取前一着色器阶段的输出。GLSL提供给我们一个内建(Built-in)变量,在内部看起来(可能)是这样的:

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

要注意的是,它被声明为一个数组,因为大多数的渲染图元包含多于1个的顶点,而几何着色器的输入是一个图元的所有顶点

有了之前顶点着色器阶段的顶点数据,我们就可以使用2个几何着色器函数,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中的向量会被添加到图元中来。当EndPrimitive被调用时,所有发射出的(Emitted)顶点都会合成为指定的输出渲染图元。在一个或多个EmitVertex调用之后重复调用EndPrimitive能够生成多个图元。

实践一,创建更多顶点

  1. 顶点
    
    // 2d顶点 + 顶点颜色
    float points[] = {
        -0.5f,  0.5f, 1.0f, 0.0f, 0.0f, // top-left
         0.5f,  0.5f, 0.0f, 1.0f, 0.0f, // top-right
         0.5f, -0.5f, 0.0f, 0.0f, 1.0f, // bottom-right
        -0.5f, -0.5f, 1.0f, 1.0f, 0.0f  // bottom-left
    };

    unsigned int VBO, VAO;
    glGenBuffers(1, &VBO);
    glGenVertexArrays(1, &VAO);
    glBindVertexArray(VAO);
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(points), &points, GL_STATIC_DRAW);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), 0);
    glEnableVertexAttribArray(1);
    glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(2 * sizeof(float)));
    glBindVertexArray(0);
  1. 绘制
    // 使用 GL_POINTS 绘制
    shader.use();
    glBindVertexArray(VAO);
    glDrawArrays(GL_POINTS, 0, 4);
  1. 着色器
    1. 几何着色器
     #version 330 core
    
     //1. 对应glDrawArrays(GL_POINTS,
     layout (points) in;
     //2. triangle_strip输出, 限制最多5个顶点
     layout (triangle_strip, max_vertices = 5) out;
    
     //3. 接受顶点的颜色值
     in VS_OUT {
         vec3 color;
     } gs_in[];
    
     //4. 传递颜色值
     out vec3 fColor;
    
     //5. `EmitVertex`创建顶点,`EndPrimitive`创建图元。原本是gl_in[0].gl_Position, 输出创建的顶点
     void build_house(vec4 position)
     {    
         //5.1 接收的颜色值
         fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
         //1. 顶点1
         gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:bottom-left   
         EmitVertex();   
    
         //2. 顶点2
         gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:bottom-right
         EmitVertex();
    
         //3. 顶点3
         gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0); // 3:top-left
         EmitVertex();
    
         //4. 顶点4
         gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0); // 4:top-right
         EmitVertex();
    
         //5. 顶点5
         gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0); // 5:top
         //这里改变颜色,将会影响后面的顶点颜色。之前的颜色为上次的改变。
         //比如在第一个顶点结束, 第二个顶点开始设置颜色。则在下次改变之前都使用这个颜色,作为顶点颜色传递。
         //也可以这么理解: 一个 EmitVertex调用对应一次顶点着色器执行,虽然不准确,但是相对比较好理解。
         fColor = vec3(0.0, 1.0, 0.0);
         EmitVertex();
    
         EndPrimitive();
     }
    
     void main() {    
         build_house(gl_in[0].gl_Position);
     }
    1. 顶点着色器
     // 传递顶点和颜色,2d顶点z=0
     #version 330 core
     layout (location = 0) in vec2 aPos;
     layout (location = 1) in vec3 aColor;
     out VS_OUT {
         vec3 color;
     } vs_out;
     void main()
     {
         vs_out.color = aColor;
         gl_Position = vec4(aPos.x, aPos.y, 0.0, 1.0); 
     }
    1. 片元着色器
     #version 330 core
     out vec4 FragColor;
    
     in vec3 fColor;
    
     void main()
     {
         FragColor = vec4(fColor, 1.0);   
     }

实践二,爆破物体

当我们说爆破一个物体时,我们并不是指要将宝贵的顶点集给炸掉,我们是要将每个三角形沿着法向量的方向移动一小段时间。效果就是,整个物体看起来像是沿着每个三角形的法线向量爆炸一样。爆炸三角形的效果在纳米装模型上看起来像是这样的:

  1. 重要点
    1. 法向量, 这里使用垂直于三顶点组成的平面(顶点不在一条线)
     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);
        // cross 作为法向量, 所以顺序很重要
        return normalize(cross(a, b));
     }
    1. 移动的爆破函数
     vec4 explode(vec4 position, vec3 normal)
     {
         float magnitude = 2.0;
         // time 作为变量,使用shader传递参数
         // 法向量 * magnitude 的 0-1 的sin值,作为移动向量
         vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; 
         return position + vec4(direction, 0.0);
     }
  2. 模型和参数准备
    Shader shader("./res/shaders/09geometry/02geometry_shader.vs", "./res/shaders/09geometry/02geometry_shader.gs", "./res/shaders/09geometry/02geometry_shader.fs");
    Model nanosuit("./res/models/nanosuit/nanosuit.obj");
  1. 绘制
    //。。。
    
    // 设置shader 的time变量
    shader.setFloat("time", glfwGetTime());

    //。。。
    glBindVertexArray(VAO);
    glDrawElements(GL_TRIANGLES, indices.size(), GL_UNSIGNED_INT, 0);
  1. 几何着色器, 顶点着色器使用mvp和uv的顶点着色器, 片元着色器使用贴图 uv的片元着色器
    #version 330 core
    //1. 对应glDrawElements(GL_TRIANGLES
    layout (triangles) in;

    //2. 限制为最大3个顶点
    layout (triangle_strip, max_vertices = 3) out;

    //3. 接受传递uv
    in VS_OUT {
        vec2 texCoords;
    } gs_in[];

    //4. 传递uv
    out vec2 TexCoords; 

    //接受 time 输入
    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()
    {
        //GL_TRIANGLES 模式下, gl_in 为三个大小的数据, 三个顶点
        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);
        //对应顶点的uv 不变
        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();
    }

实践三,法线可视化

  1. 几何着色器
#version 330 core
//1. 对应glDrawElements(GL_TRIANGLES
layout (triangles) in;
//2. 使用line_strip, 因为这里只绘制法线的颜色线段
//max_vertices 限制为6, 是因为需要另外多创建一组。
layout (line_strip, max_vertices = 6) out;

//3. 接收normal 输入
in VS_OUT {
    vec3 normal;
} gs_in[];

const float MAGNITUDE = 0.4;

//4. 为法线生成对应线段的另一个顶点, index 为三角形的顶点index,储存于gl_in[index]中
void GenerateLine(int index)
{
    //4.1 第一个顶点不变
    gl_Position = gl_in[index].gl_Position;
    EmitVertex();

    //2. 第二个顶点 偏移法线 *MAGNITUDE 的距离。
    gl_Position = gl_in[index].gl_Position + vec4(gs_in[index].normal, 0.0) * MAGNITUDE;
    EmitVertex();
    EndPrimitive();
}

void main()
{
    GenerateLine(0); // 第一个顶点法线
    GenerateLine(1); // 第二个顶点法线
    GenerateLine(2); // 第三个顶点法线
}

文章作者: Yonggang Long
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Yonggang Long !
 上一篇
2022-08-10 Yonggang Long
下一篇 
2022-08-10 Yonggang Long
  目录