[OpenGL]使用GeometryShader实现法线可视化

一、简介

本文介绍了 OpenGL 中 Geometry Shader 的基本用法。
包括对 Geometry Shader 的简介,并给出了如何使用 Geometry Shader 从线段生成多边形从三角形生成点从三角形生成四边形显示模型顶点法线 这几种情况的代码示例。
按照本文代码实现完成后,可以得到如下效果(显示模型顶点法线):

渲染结果

二、Geometry Shader

1. Geometry Shader 简介

Geometry Shader(几何着色器,GS) 是图形渲染管线中的一个可选阶段,它位于 顶点着色器(Vertex Shader, VS)光栅化阶段(Rasterization) 之间。主要作用是接收顶点着色器的输出数据,并生成新图元数据。

1.1 输入与输出

1). 输入数据类型

GS可以处理完整的 图元(如点、线段、三角形),GS 中需要在开始处使用以下代码定义输入的图元类型:

layout(input_primitive) in;

例如:

layout(triangles) in;

就表明该GS的输入(也即 VS 的输出)为GL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN这几种类型的图元之一。
GS 可接受的输入图元 input_primitive为以下几种[1]

input_primitiveOpenGL primitives
pointsGL_POINTS
linesGL_LINES, GL_LINE_STRIP, GL_LINE_LOOP
lines_adjacencyGL_LINES_ADJACENCY, GL_LINE_STRIP_ADJACENCY
trianglesGL_TRIANGLES, GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN
triangles_adjacencyGL_TRIANGLES_ADJACENCY, GL_TRIANGLE_STRIP_ADJACENCY

GS每次调用可以访问一个图元的所有顶点数据,GS 中内置一个输入结构体变量:

in gl_PerVertex
{
vec4 gl_Position;	// 当前顶点的输出位置
float gl_PointSize; // 光栅化点的像素宽度/高度
float gl_ClipDistance[]; // 用户定义的顶点到 clip plane 的距离
} gl_in[];

可以在GS中使用gl_in[id].gl_Position;类似的代码处理输入的图元的各个顶点。例如:

// Geometry Shader
...
vec4 vertex_pos = gl_in[0].gl_Position;
...

就是将输入图元第一个顶点的gl_Position属性赋值给变量vertex_pos

2). 输出数据类型

GS 可以输出与输入类型相同或不同的几何数据,如:

  • 输入线段,输出多边形;
  • 输入三角形,输出点;
  • 允许生成新的顶点或丢弃某些图元;
    2.示例中会对这几种情况提供相应的代码示例。
    与输入图元类似,GS 中也需要在开始处定义输出图元的类型,代码如下:
layout(output_primitive​, max_vertices = vert_count​) out;

其中,out_primitive为输入图元类型,vert_count为单次调用GS的输出最大顶点数,这两者都是必须定义的参数。GS 可接受的的 out_primitive类型如下:

output_primitiveOpenGL Primitive
pointsGL_POINTS
line_stripGL_LINESTRIP
triangle_stripGL_TRIANGLE_STRIP

1.2 Geometry Shader 用途

  • 粒子系统:通过扩展点图元生成粒子。
  • 动态细分:在场景中根据需求生成更多几何细节。
  • 几何变换:如对图元执行缩放、旋转等变换操作。
  • 轮廓生成:生成图元的轮廓线或阴影体。
  • 法线可视化:用线段从顶点发射法线。

2. 使用 Geometry Shader 示例

2.1 从线段生成多边形

  1. Vertex Shader 代码

    #version 330 core
    layout (location = 0) in vec3 aPos;
    void main()
    {
    gl_Position = vec4(aPos.x, aPos.y, aPos.z, 1.0);
    }
    
  2. Geometry Shader 代码

    #version 330 core
    layout (lines) in;
    layout (triangle_strip, max_vertices=3) out;
    void main(){
        // 线段的 第一个 点
        vec4 p0 = gl_in[0].gl_Position;
        // 线段的 第二个 点
        vec4 p1 = gl_in[1].gl_Position;
        // 新增加的点
        vec4 p2 = vec4(0, 0, 0, 1);
        // 添加一个顶点到输出图元
        gl_Position = p0;
        EmitVertex();
        // 添加一个顶点到输出图元
        gl_Position = p1;
        EmitVertex();
        // 添加一个顶点到输出图元
        gl_Position = p2;
        EmitVertex();
        // 结束添加新图元
        EndPrimitive();
    }
    
  3. Fragment Shader 代码

    #version 330 core
    out vec4 FragColor;
    void main()
    {
    FragColor = vec4(1.0f, 0.5f, 0.2f, 1.0f);
    }
    
  4. 顶点数据及渲染代码

    ...
    float vertices[] = {
        0.0f,  0.5f,  0.0,  // 上
        -0.5f, -0.5f, 0.0f, // 左下
        0.5f,  -0.5f, 0.0f  // 右下
    };
    unsigned int indices[] = {
        0, 1, 2 // first Triangle
    };
    ...
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 轮廓
    glDrawElements(GL_LINE_STRIP, 3, GL_UNSIGNED_INT, 0);
    ...
    
  5. 渲染结果
    未使用 Geometry Shader 的渲染结果:
    在这里插入图片描述

    使用 Geometry Shader 的渲染结果:

在这里插入图片描述

2.2 从三角形生成点

  1. Vertex Shader 代码
    同 2.1。

  2. Geometry Shader 代码

    #version 330 core
    layout (triangles) in;
    layout (points, max_vertices=3) out;
    void main(){
        // 三角形的 第一个 点
        vec4 p0 = gl_in[0].gl_Position;
        // 三角形的 第二个 点
        vec4 p1 = gl_in[1].gl_Position;
        
        // 三角形的 第三个 点
        vec4 p2 = gl_in[2].gl_Position;
    
        // 添加一个顶点到输出图元
        gl_Position = p0;
        EmitVertex();
        // 添加一个顶点到输出图元
        gl_Position = p1;
        EmitVertex();
        // 添加一个顶点到输出图元
        gl_Position = p2;
        EmitVertex();
        // 结束添加新图元
        EndPrimitive();
    }
    
  3. Fragment Shader 代码
    同 2.1。

  4. 顶点数据及渲染代码

    ...
    float vertices[] = {
        0.0f,  0.5f,  0.0,  // 上
        -0.5f, -0.5f, 0.0f, // 左下
        0.5f,  -0.5f, 0.0f  // 右下
    };
    unsigned int indices[] = {
        0, 1, 2 // first Triangle
    };
    ...
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 轮廓
    glPointSize(10.0f); // 设置渲染 POINT 的大小以便于观察
    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
    ...
    
  5. 渲染结果
    未使用 Geometry Shader 的渲染结果:
    在这里插入图片描述

    使用 Geometry Shader 的渲染结果:
    在这里插入图片描述

2.3 从三角形生成四边形

  1. Vertex Shader 代码
    同 2.1。

  2. Geometry Shader 代码

    #version 330 core
    layout (triangles) in;
    layout (triangle_strip, max_vertices=4) out;
    void main(){
        // 三角形的 第一个 点
        vec4 p0 = gl_in[0].gl_Position;
        // 三角形的 第二个 点
        vec4 p1 = gl_in[1].gl_Position;
        // 三角形的 第三个 点
        vec4 p2 = gl_in[2].gl_Position;
        // 根据p0, p1, p2计算四边形的第四个点位置
        vec4 p3 = p1 + (p0-p1) + (p2-p1);
        
        // 需要特别注意 Emit 顶点的顺序为 1,0,2,3
        // 因为一个四边形是由 三角形 1,0,2 和三角形 0,2,3 组成的
        // 添加一个顶点到输出图元
        gl_Position = p1;
        EmitVertex();
        // 添加一个顶点到输出图元
        gl_Position = p0;
        EmitVertex();
        // 添加一个顶点到输出图元
        gl_Position = p2;
        EmitVertex();
        // 添加一个顶点到输出图元
        gl_Position = p3;
        EmitVertex();
        // 结束添加新图元
        EndPrimitive();
    }
    
  3. Fragment Shader 代码
    同 2.1。

  4. 顶点数据及渲染代码

    ...
    float vertices[] = {
        0.0f,  0.5f,  0.0,  // 上
        -0.5f, -0.5f, 0.0f, // 左下
        0.5f,  -0.5f, 0.0f  // 右下
    };
    unsigned int indices[] = {
        0, 1, 2 // first Triangle
    };
    ...
    glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 轮廓
    glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
    ...
    
  5. 渲染结果
    未使用 Geometry Shader 的渲染结果:
    在这里插入图片描述

    使用 Geometry Shader 的渲染结果:
    在这里插入图片描述

2.4 显示模型顶点法线

  1. 渲染流程
    渲染分为两趟:首先使用 showModelShader渲染模型。showModelShader由顶点着色器showModel.vert和片段着色器showModel.frag编译链接得到;然后使用 visualNormalShader渲染法线。visualNormalShader由顶点着色器visualNormal.vert、几何着色器visualNormal.geom和片段着色器visualNormal.frag编译链接得到;

  2. showModelShader 代码
    顶点着色器 showModel.vert

    #version 330 core
    layout (location = 0) in vec3 aPos;
    layout (location = 1) in vec3 aNor;
    layout (location = 2) in vec2 aTexCoord;
    uniform mat4 model;
    uniform mat4 view;
    uniform mat4 projection;
    
    out vec3 vertexPos;
    out vec3 vertexNor;
    out vec2 textureCoord;
    out vec3 vertexSpaceNor;
    void main()
    {
        textureCoord = aTexCoord;
        // 裁剪空间坐标系 (clip space) 中 点的位置
        gl_Position = projection * view * model * vec4(aPos, 1.0f);
        // 世界坐标系 (world space) 中 点的位置
        vertexPos = (model * vec4(aPos,1.0f)).xyz;
        // 世界坐标系 (world space) 中 点的法向
        vertexNor = mat3(transpose(inverse(model))) * aNor;
        // 观察坐标系中 (view space) 中 点的法向
        vertexSpaceNor = mat3(transpose(inverse(view * model))) * aNor;
    }
    

    片段着色器 showModel.frag

    #version 330 core
    out vec4 FragColor;
    
    in vec3 vertexPos;
    in vec3 vertexNor;
    in vec2 textureCoord;
    uniform vec3 cameraPos;
    uniform vec3 lightPos;
    uniform vec3 k;
    uniform sampler2D texture1;
    void main() {
    
      vec3 lightColor = vec3(1.0f, 1.0f, 1.0f);
    
      // Ambient
      // Ia = ka * La
      float ambientStrenth = k[0];
      vec3 ambient = ambientStrenth * lightColor;
    
      // Diffuse
      // Id = kd * max(0, normal dot light) * Ld
      float diffuseStrenth = k[1];
      vec3 normalDir = normalize(vertexNor);
      vec3 lightDir = normalize(lightPos - vertexPos);
      vec3 diffuse =
          diffuseStrenth * max(dot(normalDir, lightDir), 0.0) * lightColor;
    
      // Specular (Phong)
      // Is = ks * (view dot reflect)^s * Ls
    
      float specularStrenth = k[2];
      vec3 viewDir = normalize(cameraPos - vertexPos);
      vec3 reflectDir = reflect(-lightDir, normalDir);
      vec3 specular = specularStrenth *
                      pow(max(dot(viewDir, reflectDir), 0.0f), 2) * lightColor;
        
      // Specular (Blinn-Phong)
      // Is = ks * (normal dot halfway)^s Ls
    
      //   float specularStrenth = k[2];
      //   vec3 viewDir = normalize(cameraPos - vertexPos);
      //   vec3 halfwayDir = normalize(lightDir + viewDir);
      //   vec3 specular = specularStrenth *
      //                   pow(max(dot(normalDir, halfwayDir), 0.0f), 2) *
      //                   lightColor;
    
      // Obejct color
      vec3 objectColor = texture(texture1, textureCoord).xyz;
    
      // Color = Ambient + Diffuse + Specular
      // I = Ia + Id + Is
      FragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f);
    }
    
  3. visualNormalShader 代码
    顶点着色器 visualNormal.vert

    #version 330 core
    layout(location = 0) in vec3 aPos;
    layout(location = 1) in vec3 aNor;
    layout(location = 2) in vec2 aTexCoord;
    uniform mat4 model;
    uniform mat4 view;
    
    out vec3 vertexSpaceNor;
    void main() {
      // 观察空间坐标系 ( space) 中 点的位置
      gl_Position = view * model * vec4(aPos, 1.0f);
      // 观察坐标系中 (view space) 中 点的法向
      vertexSpaceNor = mat3(transpose(inverse(view * model))) * aNor;
    }
    

    几何着色器 visualNormal.geom

    #version 330 core
    layout(triangles) in;
    layout(line_strip, max_vertices = 6) out;
    
    in vec3 vertexSpaceNor[];
    
    uniform mat4 projection;
    
    void main() {
      vec4 p0 = gl_in[0].gl_Position;
      vec4 p1 = gl_in[1].gl_Position;
      vec4 p2 = gl_in[2].gl_Position;
      vec3 n0 = vertexSpaceNor[0];
      vec3 n1 = vertexSpaceNor[1];
      vec3 n2 = vertexSpaceNor[2];
    
      gl_Position = projection * p0;
      EmitVertex();
      gl_Position = projection * (p0 + vec4(n0, 0.0) * 0.03);
      EmitVertex();
      EndPrimitive();
    
      gl_Position = projection * p1;
      EmitVertex();
      gl_Position = projection * (p1 + vec4(n1, 0.0) * 0.03);
      EmitVertex();
      EndPrimitive();
    
      gl_Position = projection * p2;
      EmitVertex();
      gl_Position = projection * (p2 + vec4(n2, 0.0) * 0.03);
      EmitVertex();
      EndPrimitive();
    }
    

    片段着色器 visualNormal.frag

    #version 330 core
    out vec4 FragColor;
    void main() { FragColor = vec4(1.0, 1.0, 0.0, 1.0); }
    
  4. 渲染代码
    main.cpp

    ...
        /****** 5.开始绘制 ******/
        float rotate = 0.0f;
        while (!glfwWindowShouldClose(window))
        {
            rotate += 0.05f;
            // input
            // -----
            processInput(window);
    
            // render
            // ------
            glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
            // 清除颜色缓冲区 并且 清楚深度缓冲区
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    
            // 5.1 使用 showModelShader 渲染模型
            showModelShader.use();
    
            // model 矩阵
            glm::mat4 model = glm::mat4(1.0f);
            model = glm::translate(model, glm::vec3(0.0f, 0.0f, 0.0f));
            model = glm::rotate(model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f));
            model = glm::rotate(model, glm::radians(rotate), glm::vec3(0.0f, 1.0f, 0.0f));
            model = glm::rotate(model, glm::radians(0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
            model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f));
    
            // view 矩阵
            glm::mat4 view = glm::mat4(1.0f);
            view = glm::lookAt(camera_pos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
    
            // projection 矩阵
            glm::mat4 projection = glm::mat4(1.0f);
            projection = glm::perspective(glm::radians(60.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);
    
            showModelShader.setMat4("model", model);
            showModelShader.setMat4("view", view);
            showModelShader.setMat4("projection", projection);
            showModelShader.setVec3("k", k[0], k[1], k[2]);
            showModelShader.setVec3("cameraPos", camera_pos);
            showModelShader.setVec3("lightPos", light_pos);
    
            ourModel.Draw(showModelShader);
    
            // 5.2 使用 visualNormalShader 渲染法向
            visualNormalShader.use();
            visualNormalShader.setMat4("model", model);
            visualNormalShader.setMat4("view", view);
            visualNormalShader.setMat4("projection", projection);
            ourModel.Draw(visualNormalShader);
    
            glfwSwapBuffers(window); // 在gfw中启用双缓冲,确保绘制的平滑和无缝切换
            glfwPollEvents(); // 用于处理所有挂起的事件,例如键盘输入、鼠标移动、窗口大小变化等事件
        }
    ...
    
  5. 渲染结果

渲染结果

三、全部代码

1. 环境需要

  • Linux,或者 windos下使用wsl2。
  • 安装GLFW和GLAD。请参考[OpenGL] wsl2上安装使用cmake+OpenGL教程
  • 安装glmglm是个可以只使用头文件的库,因此可以直接下载release的压缩文件,然后解压到include目录下。例如,假设下载的release版本的压缩文件为glm-1.0.1-light.zip。将glm-1.0.1-light.zip复制include目录下,然后执行以下命令即可解压glm源代码:
    unzip glm-1.0.1-light.zip
    
  • 需要使用Assimp库加载obj模型,在 ubuntu 下可以使用以下命令安装 Assimp
    sudo apt-get update
    sudo apt-get install libassimp-dev
    
  • 需要下载 stb_image.h 作为加载.png图像的库。将 stb_image.h 下载后放入include/目录下。

2. 资源代码

用于显示模型顶点法线的全部代码以及模型文件可以在[OpenGL]使用GeometryShader实现显示模型法向中下载。

3. 编译运行及结果

编译运行:

cd ./build
cmake ..
make
./OpenGL_GeometryShader_VisualNormal

四、参考

[1.]OpenGL wiki-Geometry Shader
[2.]LearnOpenGL-CN-高级OpenGL-几何着色器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值