关于 Learnopengl 中用 Geometry Shader 实现机器人爆炸【Opengl – 图形学 – C/C++ 系列】【寅虎】

前言

上一回合《Opengl – 图形学 – C/C++ 每天进步一点点【丑牛】》讲到了画三角形、四边形,但 3D 的世界肯定更加精彩,这里我来聊聊在 web 世界的 Webgl / Webgpu 中没有玩过的东西 – Geometry Shader,废话不多说,让各种 aigc 科普下。

一言说:Geometry Shader 是一种可选的着色器,它开始被引入在 Driect3D 10 和 OpenGL 3.2 中,然后在 OpenGL 2.0+ 中被作为扩展使用,在 OpenGL3.x 中它也成为了核心。Geometry Shader 主要的输入是点、线和三角形,输出是点、线带和三角形带。Geometry Shader 的处理阶段处于流水线的栅格化之前,也在视锥体裁剪和裁剪空间坐标归一化之前。另外,Geometry Shader 程序是在 Vertex Shader 程序执行完之后才执行的
接着 gpt 说:几何着色器(Geometry Shader)是图形渲染管线中的一个可编程着色器阶段,它是 DirectX 和 OpenGL 中的一项功能。几何着色器位于顶点着色器和片元着色器之间,负责处理输入的几何图元(如点、线、三角形等)并产生新的几何图元。

几何着色器的主要作用是在图形渲染过程中对几何图元进行操作和变换。它可以接收一组顶点作为输入,然后根据程序员定义的算法对其进行处理,并生成新的几何图元或修改现有图元的属性

几何着色器可以执行多种操作,包括几何图元的剪裁、放大、变换、复制和生成等。例如,它可以接收一个输入三角形,并根据一定规则生成多个子三角形,从而实现细分曲面的效果。

使用几何着色器可以实现一些高级的图形效果,比如粒子系统、几何细分、动态环境映射等。它可以在渲染管线中实时处理复杂的几何操作,为图形应用带来更多的自由度和创造力

需要注意的是,几何着色器的使用需要一定的硬件和驱动支持,因此在使用之前需要检查设备的兼容性。并且由于几何着色器的计算量较大,对性能的影响也比较大,因此在使用过程中要注意性能优化的问题。

先通过一个官方的最简单 2D 例子来了解 Geometry Shader

书上得来终觉浅,绝知此事要躬行。先把代码跑起来再慢慢理解更佳!!呵呵,实现思路就是:
一、我在 main.cpp 中输入 4 个位置点(x, y)与相应的颜色(rgb 的 0 ~ 1 值),通过 VAO(Vertex Array Object) 的方式给到 Vertex Shader,开始是让管线画点的(glDrawArrays(GL_POINTS, 0, 4);),然后 Vertex Shader 取出颜色通过 VS_OUT(Vertex Shader Output)给到 Geometry Shader
二、重头戏来了,在 Geometry Shader 中把前面的 4 个点与颜色数据,每个作为输入点分别生成 5 个新的点的数据(作为房子的 5 个顶点及颜色数据)更改到 gl_Position 并输出最终颜色 fColor 给 Fragment Shader;与此同时,再通知 Fragment Shader 我准备下一步要画三角面不画点了(layout (triangle_strip, max_vertices = 5) out;)。
三、最后 Fragment Shader 把 fColor 按上面 Geometry Shader 要求画出三角面,最终完成 4 个点变成 4 个房子的渲染

Geometry Shader Houses Colored
Geometry Shader Houses Colored

现在来理解关键代码:
main.cpp 代码

    // ... 其他代码
    Shader shader("../src/geometry_shader.vs", "../src/geometry_shader.fs", "../src/geometry_shader.gs"); // 三个 shader 文件,管线的顺续其实是 VS -> GS -> FS

    // set up vertex data (and buffer(s)) and configure vertex attributes
    // 搞 5 个点与相应的颜色,通过 VAO 方式送管线
    float points[] = {
        // x y r g b
        -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);

    // render loop,渲染入口
    while (!glfwWindowShouldClose(window))
    {
        // render
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // draw points
        shader.use();
        glBindVertexArray(VAO);
        glDrawArrays(GL_POINTS, 0, 4); // 让你画点,其实是骗你的,后面我在 Geometry Shader 中会改成画三角面

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        glfwSwapBuffers(window);
        glfwPollEvents();
    }
   // ... 其他代码

Vertex Shader 处理: geometry_shader.vs 文件

#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); 
}

Geometry Shader 处理: geometry_shader.gs 文件

#version 330 core
layout (points) in;
layout (triangle_strip, max_vertices = 5) out;

in VS_OUT {
    vec3 color;
} gs_in[];

out vec3 fColor;

void build_house(vec4 position)
{    
    // EmitVertex 一次就生成一个点,EndPrimitive 就生成一个房子啦,每个点的颜色 fColor 都可以更改哦
    fColor = gs_in[0].color; // gs_in[0] since there's only one input vertex
    gl_Position = position + vec4(-0.2, -0.2, 0.0, 0.0); // 1:bottom-left   
    EmitVertex();   
    gl_Position = position + vec4( 0.2, -0.2, 0.0, 0.0); // 2:bottom-right
    EmitVertex();
    gl_Position = position + vec4(-0.2,  0.2, 0.0, 0.0); // 3:top-left
    EmitVertex();
    gl_Position = position + vec4( 0.2,  0.2, 0.0, 0.0); // 4:top-right
    EmitVertex();
    gl_Position = position + vec4( 0.0,  0.4, 0.0, 0.0); // 5:top
    fColor = vec3(1.0, 1.0, 1.0); // use white color for the top vertice
    EmitVertex();
    EndPrimitive();
}

void main() {
    // 针对 vs 过来的 4 个点,分别生成 4 个房子(每个房子里面也有 5 个顶点哈)
    build_house(gl_in[0].gl_Position);
}

Fragment Shader 处理: geometry_shader.fs 文件

#version 330 core
out vec4 FragColor;

in vec3 fColor;

void main()
{
    FragColor = vec4(fColor, 1.0);  // 简单的输出插值后的 final color
}
用 Geometry Shader 实现机器人爆炸

先把代码跑起来再慢慢理解更佳!!

实现思路:
一、在 main.cpp 用 assimp 加载模型(整个模形的加载与渲染逻辑已经封装在 head only 类 model.hmesh.h 类里面),把 position、normal 和 texCoords 等数据通过 VAO 加载到 Vertice Shader。
二、重头戏来了,在 Geometry Shader 中把前面的 triangle (3 个点组成)与 texCoords 数据作为入参,动态计算出新的 Normal(vec3 GetNormal()) 在 Normal 方向上前进一定的距离(vec4 explode(vec4 position, vec3 normal)),生成新的 3 个更远一点的点,然后作为新的三角面输出(layout (triangle_strip, max_vertices = 3) out;)。
三、最后 Fragment Shader 按常规方式画出上面的新的三角面,最终完成 3 个点向外扩张形成爆炸效果
现在来理解关键代码:
main.cpp 代码

    // 其他代码 ...

    // build and compile our shader program
    Shader shader("../src/geometry_shader.vs", "../src/geometry_shader.fs", "../src/geometry_shader.gs");

     // load models
    Model nanosuit("../../resources/objects/nanosuit/nanosuit.obj"); 

    // render loop
    while (!glfwWindowShouldClose(window))
    {
        // per-frame time logic
        float currentFrame = static_cast<float>(glfwGetTime());
        deltaTime = currentFrame - lastFrame;
        lastFrame = currentFrame;

        // input
        processInput(window);

        // render
        glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // configure transformation matrices
        glm::mat4 projection = glm::perspective(glm::radians(45.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 1.0f, 100.0f);
        glm::mat4 view = camera.GetViewMatrix();;
        glm::mat4 model = glm::mat4(1.0f);
        shader.use();
        shader.setMat4("projection", projection);
        shader.setMat4("view", view);
        shader.setMat4("model", model);

        // add time component to geometry shader in the form of a uniform
        shader.setFloat("time", static_cast<float>(glfwGetTime()));

        // draw model
        nanosuit.Draw(shader);

        // glfw: swap buffers and poll IO events (keys pressed/released, mouse moved etc.)
        glfwSwapBuffers(window);
        glfwPollEvents();
    }

    // 其他代码 ...

Vertex Shader 处理: geometry_shader.vs 文件

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 2) in vec2 aTexCoords;

out VS_OUT {
    vec2 texCoords;
} vs_out;

uniform mat4 projection;
uniform mat4 view;
uniform mat4 model;

void main()
{
    vs_out.texCoords = aTexCoords;
    gl_Position = projection * view * model * vec4(aPos, 1.0); 
}

Geometry Shader 处理: geometry_shader.gs 文件

#version 330 core
layout (triangles) in;
layout (triangle_strip, max_vertices = 3) out;

in VS_OUT {
    vec2 texCoords;
} gs_in[];

out vec2 TexCoords; 

uniform float time;

vec4 explode(vec4 position, vec3 normal) // 玩过 shadertoy 的不陌生, 用时间控制向外扩张的新位置,原位置 + normal 方向的向外位移
{
    float magnitude = 2.0;
    vec3 direction = normal * ((sin(time) + 1.0) / 2.0) * magnitude; 
    return position + vec4(direction, 0.0);
}

vec3 GetNormal()  // 玩过 shadertoy 的不陌生,直接用两向量叉乘求 Normal
{
    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);
    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();
}

Fragment Shader 处理: geometry_shader.fs 文件

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture_diffuse1;

void main()
{
    FragColor = texture(texture_diffuse1, TexCoords); // 暂时只用漫反射贴图显示
}

注意事项:
assimp 的引用,本项目直接使用 cmake 本地构建出部分文件再结合拷贝原码下来使用。

本项目 CMakeLists.txt 如下:

cmake_minimum_required(VERSION 3.0.0)                                 # 版本号
project(helloworld)                                                   # 项目名
# link_libraries("${PROJECT_SOURCE_DIR}/lib/libglfw3.a")              # 链接之前生成的静态库文件
get_filename_component(DIR_ONE_ABOVE ../ ABSOLUTE)
include_directories(include ${DIR_ONE_ABOVE}/third-party/includes)    # 把 include ../third-party/includes 纳入包含目录中
link_libraries(${DIR_ONE_ABOVE}/third-party/libs/libglfw3.a ${DIR_ONE_ABOVE}/third-party/libs/libassimpd.dll.a)          # 链接之前生成的静态库文件
# aux_source_directory(./src DIR_ALL_SRC)				              # src目录下所有文件取个 DIR_ALL_SRC 名字
set(DIR_ALL_SRC
        ./src/main.cpp
        ${DIR_ONE_ABOVE}/third-party/src/glad.c
        )                                                            # src目录下所有文件取个 DIR_ALL_SRC 名字

add_executable(helloworld ${DIR_ALL_SRC})                             # 生成可执行文件
target_link_libraries(helloworld  ${DIR_ONE_ABOVE}/third-party/libs/libassimp-5d.dll)         # 为生成目标添加一个库 可以是多个 比如 target_link_libraries(main lib1 lib2)

引用第三方目录结构如下:

第三方引用库目录结构
第三方引用库目录结构

参考引用:https://learnopengl.com/Advanced-OpenGL/Geometry-Shader

【待续…】

作者: 博主

Talk is cheap, show me the code!

发表评论

邮箱地址不会被公开。

Captcha Code