[OpenGL]使用OpenGL实现延迟着色Deferred Shading

一、简介

本文介绍了 延迟着色(Deferred Shading) 的基本概念,实现流程和简单的代码实现。
按照本文代码实现后,可以实现以下效果:
渲染结果

二、延迟着色 Defered Shading

1. 延迟着色的优势

在正向渲染(Forward Rendering)或者正向着色法(Forward Shading)中,我们根据场景中的所有光源计算各个片段的颜色(例如使用Blinn-Phong模型计算模型表面的着色值)。它非常容易理解,也很容易实现,但是同时它对程序性能的影响也很大。因为对于每一个需要渲染的模型,程序都要遍历每个光源,在该光源下对每一个需要渲染的片段进行着色计算。
在光源、物体很多的时候,计算量是非常大的!因为大部分片段着色器的输出都会被之后的输出覆盖。一个像素中可能存在多个片段(每个片段来自不同的模型,但是都处于同一个像素内),而只有距离相机最近的片段才能真正能在屏幕上显示出来,较远处的片段都会被距离相机最近的片段遮挡。尽管如此,在 Fragment shader 实际上也会对这些会被遮挡的片段计算其 着色值!

有没有一种方法可以降低着色计算的计算量?可以想到的降低计算量的思路是:

  • 先进行“裁剪”,只计算被裁剪后的模型表面的着色值(实际上GPU渲染管线在 Fragment shader 之前就做了“裁剪”操作);
  • 先进行“深度测试”,只计算通过深度测试的模型表面的着色值;

延迟着色法(Deferred Shading),或者说是延迟渲染(Deferred Rendering),就是基于上面两条的优化思路,解决多光源、模型场景中着色计算量过大的问题。它改变了正向渲染物体的方式,在 延迟着色中,将几何处理和光照处理(着色计算)分开:

  • 几何阶段:只处理模型的几何信息,并将其写入 G-buffer。在G-buffer中存储最终会被看到的片段的 texture_color, position, normal 等信息。
  • 光照阶段:基于屏幕空间的 G-buffer 数据 和场景中的 lights 信息,计算各个像素(片段)的着色值(此时,一个像素中只有一个片段);
    这样,就避免了对被遮挡或不可见片段的着色计算。

2. 延迟着色的实现

如上面所讲的,延迟着色通过两个 pass 实现:

实现流程图如下所示:
延迟着色流程

3. 延迟着色的代码(部分)

3.1 geometryPassShader

顶点着色器geometryPassShader.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;

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

片段着色器geometryPassShader.frag

#version 330 core
layout(location = 0) out vec4 FragColor; // diffuse color
layout(location = 1) out vec3 FragPos;   // position in world space
layout(location = 2) out vec3 FragNor;   // normal in world space

in vec3 vertexPos;
in vec3 vertexNor;
in vec2 textureCoord;

uniform sampler2D texture0;

void main() {
  FragPos = vertexPos;
  FragNor = vertexNor;
  FragColor = texture(texture0, textureCoord);
}

3.2 lightingPassShader

顶点着色器lightingPassShader.vert

#version 330 core
layout(location = 0) in vec3 aPos;
layout(location = 1) in vec3 aNor;
layout(location = 2) in vec2 aTexCoord;
out vec2 textureCoord;
void main() {
  gl_Position = vec4(aPos, 1.0f);
  textureCoord = aTexCoord;
}

片段着色器lightingPassShader.frag

#version 330 core
out vec4 FragColor;

in vec2 textureCoord;

const int NR_LIGHTS = 32;
uniform int light_n;
uniform vec3 lightsPos[NR_LIGHTS];

uniform vec3 cameraPos;
uniform vec3 k;

uniform sampler2D textureColor; // color
uniform sampler2D texturePos;   // position (in world space)
uniform sampler2D textureNor;   // normal (in world space)

void main() {
  vec3 vertexPos = texture(texturePos, textureCoord).xyz;
  vec3 vertexNor = texture(textureNor, textureCoord).xyz;
  vec3 lightColor = vec3(1.0f, 1.0f, 1.0f);

  // Ambient
  // Ia = ka * La
  float ambientStrenth = k[0];
  vec3 ambient = ambientStrenth * lightColor;

  vec3 diffuse = vec3(0, 0, 0);
  vec3 specular = vec3(0, 0, 0);
  for (int i = 0; i < light_n; i++) {
    // Diffuse
    // Id = kd * max(0, normal dot light) * Ld
    float diffuseStrenth = k[1];
    vec3 normalDir = normalize(vertexNor);
    vec3 lightPos = lightsPos[i];
    vec3 lightDir = normalize(lightPos - vertexPos);
    vec3 temp_diffuse =
        diffuseStrenth * max(dot(normalDir, lightDir), 0.0) * lightColor;
    diffuse += temp_diffuse;

    // Specular (Phong)
    // Is = ks * (view dot reflect)^s * Ls

    float specularStrenth = k[2];
    vec3 viewDir = normalize(cameraPos - vertexPos);
    vec3 reflectDir = reflect(-lightDir, normalDir);
    vec3 temp_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 temp_specular = specularStrenth *
    //                      pow(max(dot(normalDir, halfwayDir), 0.0f), 2) *
    //                      lightColor;
    specular += temp_specular;
  }

  diffuse = clamp(diffuse, 0.0, 1.0);
  specular = clamp(specular, 0.0, 1.0);

  // Obejct color
  vec3 objectColor = texture(textureColor, textureCoord).xyz;

  // Color = Ambient + Diffuse + Specular
  // I = Ia + Id + Is
  FragColor = vec4((ambient + diffuse + specular) * objectColor, 1.0f);
}

3.3 main.cpp

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include "Skybox.hpp"
#include "Shader.hpp"
#include "Mesh.hpp"
#include "Model.hpp"

#include "glm/ext.hpp"
#include "glm/mat4x4.hpp"

#include <random>
#include <iostream>
// 用于处理窗口大小改变的回调函数
void framebuffer_size_callback(GLFWwindow *window, int width, int height);
// 用于处理用户输入的函数
void processInput(GLFWwindow *window);

// 指定窗口默认width和height像素大小
unsigned int SCR_WIDTH = 800;
unsigned int SCR_HEIGHT = 600;

float getRandom(float min = -1.0f, float max = 1.0f)
{
    std::random_device rd;  // 获取随机种子
    std::mt19937 gen(rd()); // 使用 Mersenne Twister 引擎
    std::uniform_real_distribution<float> dist(min, max);

    // 生成随机数
    return dist(gen);
}

int main()
{

    /****** 1.初始化glfw, glad, 窗口 *******/
    // glfw 初始化 + 配置 glfw 参数
    glfwInit();
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 3);
    glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

    // 在创建窗口之前
    glfwWindowHint(GLFW_SAMPLES, 4); // 设置多重采样级别为4
    // glfw 生成窗口
    GLFWwindow *window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    if (window == NULL)
    {
        // 检查是否成功生成窗口,如果没有成功打印出错信息并且退出
        std::cout << "Failed to create GLFW window" << std::endl;
        glfwTerminate();
        return -1;
    }

    // 设置窗口window的上下文
    glfwMakeContextCurrent(window);
    // 配置window变化时的回调函数
    glfwSetFramebufferSizeCallback(window, framebuffer_size_callback);

    // 使用 glad 加载 OpenGL 中的各种函数
    if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress))
    {
        std::cout << "Failed to initialize GLAD" << std::endl;
        return -1;
    }
    // 启用 深度测试
    glEnable(GL_DEPTH_TEST);
    // 启用 多重采样抗锯齿
    glEnable(GL_MULTISAMPLE);

    // glPolygonMode(GL_FRONT_AND_BACK, GL_LINE); // 使用线框模式,绘制时只绘制 三角形 的轮廓
    glPolygonMode(GL_FRONT_AND_BACK, GL_FILL); // 使用填充模式,绘制时对 三角形 内部进行填充
    /************************************/

    /****** 2.编译 shader 程序 ******/

    // Geometry Pass 使用的 shader
    // 使用Blinn-Phong模型渲染场景, 将屏幕上各像素内片元的 texture_color, position, normal
    // 存储到 G-buffer 中(colorTexture, posTexture 和 norTexture)
    Shader geometryPassShader("../resources/geometryPassShader.vert", "../resources/geometryPassShader.frag");

    // Lighting Pass 使用的 shader
    // 根据 G-buffer 和 场景中的光照信息 计算各个像素内片段的着色值
    Shader lightingPassShader("../resources/lightingPassShader.vert", "../resources/lightingPassShader.frag");
    /************************************/

    /****** 3.加载场景模型、quad模型 ******/

    // scene mesh
    // Model ourModel("../resources/models/nanosuit/nanosuit.obj");
    // 场景模型
    Model ourModel("../resources/models/spot/spot.obj");

    int models_n = 100;                           // 场景中模型的 个数
    vector<glm::vec3> models_translate(models_n); // 各模型的 平移变换
    vector<float> models_scale(models_n);         // 各模型的 缩放变换
    vector<float> models_rotate(models_n);        // 各模型的 旋转变换
    for (unsigned int i = 0; i < models_n; i++)
    {
        models_translate[i] = glm::vec3(getRandom(), getRandom(), getRandom());
        models_scale[i] = std::abs(getRandom() * 0.2f);
        models_rotate[i] = getRandom() * 360.0f;
    }

    // quad 模型
    // 需要注意的是 使用 geometryPassShader 渲染到 G-buffer 时,以 窗口(G-buffer) 的左上角为坐标(0,0),
    // 窗口(G-buffer)的右下角为坐标(1,1)
    // 因此,在 lightingPassShader 中使用 colorTexture, posTexture 和 norTextaure 时,
    // 需要将 quad 左上角的顶点 texture coord 设为 (0,0), 右下角的顶点 texture coord 设为 (1,1), 如下所示:
    /*
      id, (texture_coord.x, texture_coord.y)
      0(0,0)---3(1,0)
        |  \     |
        |    \   |
        |     \  |
      1(0,1)---2(1,1)
    */
    Model quadModel("../resources/models/quad/quad.obj");

    /************************************/

    /****** 4.设置光源和相机位置,Blinn-Phong 模型参数 ******/
    // I = Ia + Id + Is
    // Ia = ka * La
    // Id = kd * (normal dot light) * Ld
    // Is = ks * (reflect dot view)^s * Ls
    // 模型参数 ka, kd, ks
    int lights_n = 10; // 必须小于等于 lightingPassShader.frag 中的 const NR_LIGHTS
    float k[] = {0.1f / lights_n, 0.7f / lights_n, 0.2f / lights_n}; // ka, kd, ks
    // 光源位置
    vector<glm::vec3> lights_pos(lights_n);
    for (int i = 0; i < lights_n; i++)
    {
        lights_pos[i] = glm::vec3(getRandom() * 2, getRandom() * 2, getRandom() * 2);
    }
    // 相机位置
    glm::vec3 camera_pos = glm::vec3(0.0f, 0.0f, 1.5f);

    /************************************/

    /****** 5.配置 G-buffer ******/

    GLuint GBuffer; // G-buffer 是一个 frame buffer object
    glGenFramebuffers(1, &GBuffer);
    glBindFramebuffer(GL_FRAMEBUFFER, GBuffer);

    // 用于接收 geometryPassShader 渲染结果 的 texture
    GLuint colorTexture, posTexture, norTexture;
    glGenTextures(1, &colorTexture);
    glGenTextures(1, &posTexture);
    glGenTextures(1, &norTexture);
    // 存储 texture color
    glBindTexture(GL_TEXTURE_2D, colorTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA32F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    // 存储 postion
    glBindTexture(GL_TEXTURE_2D, posTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    // 存储 normal
    glBindTexture(GL_TEXTURE_2D, norTexture);
    glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, nullptr);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);

    glBindTexture(GL_TEXTURE_2D, 0);

    // 指定 colorTexture, posTexture, norTexture 分别用来接收 GL_COLOR_ATTACHMENTS0/1/2
    glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, colorTexture, 0);
    glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, posTexture, 0);
    glFramebufferTexture(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, norTexture, 0);

    // 需要在 G-buffer 中设置一个 depth buffer, 因为自定义的 Frame Buffer Object 中没有 depth buffer
    // 用于正确地渲染结果(主要根据渲染场景的深度信息确定哪些部分需要渲染,哪些部分可以丢弃,跟正常渲染流程一样)
    GLuint depthrenderbuffer;
    glGenRenderbuffers(1, &depthrenderbuffer);
    // 绑定渲染缓冲对象,指定后续的 操作(设置) 目标为 depthrederbuffer
    glBindRenderbuffer(GL_RENDERBUFFER, depthrenderbuffer);
    // 指定渲染缓冲的内部格式为深度格式,意味着这个缓冲区将用于存储深度信息
    glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH_COMPONENT, SCR_WIDTH, SCR_HEIGHT);
    // 将渲染缓冲对象附加到当前绑定的帧缓冲对象
    glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_RENDERBUFFER, depthrenderbuffer);

    GLenum DrawBuffers[3] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2};
    // 设置决定片段着色器的输出会写入哪些颜色缓冲(此时写入 GL_COLOR_ATTACHMENT0/1/2 缓冲)
    glDrawBuffers(3, DrawBuffers); // "3" is the size of DrawBuffers

    // Always check that our framebuffer is ok
    if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
    {
        std::cout << "Error\n";
        return 0;
    }
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    /************************************/

    /****** 6.开始渲染 ******/

    float rotate = 0.0f;
    while (!glfwWindowShouldClose(window))
    {
        rotate += 0.5f;
        // render
        // ------
        glEnable(GL_DEPTH_TEST);

        // 6.1. 先使用 geometryPassShader 讲场景的 diffuse_color, postion, normal 渲染到 G-buffer 中
        glBindFramebuffer(GL_FRAMEBUFFER, GBuffer);
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        // 清除颜色缓冲区 并且 清除深度缓冲区
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        geometryPassShader.use();

        // 设置每个模型的 MVP 矩阵, 假设以 camera 为视角,渲染 camera 视角下的场景深度图
        for (int i = 0; i < models_n; i++)
        {
            // model 矩阵
            glm::mat4 camera_model = glm::mat4(1.0f);
            camera_model = glm::translate(camera_model, models_translate[i]);
            camera_model = glm::rotate(camera_model, glm::radians(0.0f), glm::vec3(1.0f, 0.0f, 0.0f));
            camera_model =
                glm::rotate(camera_model, glm::radians(rotate + models_rotate[i]), glm::vec3(0.0f, 1.0f, 0.0f));
            camera_model = glm::rotate(camera_model, glm::radians(0.0f), glm::vec3(0.0f, 0.0f, 1.0f));
            camera_model = glm::scale(camera_model, glm::vec3(1.0f, 1.0f, 1.0f) * models_scale[i]);

            // view 矩阵
            glm::mat4 camera_view = glm::mat4(1.0f);
            camera_view = glm::lookAt(camera_pos, glm::vec3(0.0f, 0.0f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));

            // projection 矩阵
            glm::mat4 camera_projection = glm::mat4(1.0f);
            camera_projection =
                glm::perspective(glm::radians(60.0f), (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 2.50f);
            geometryPassShader.setMat4("model", camera_model);
            geometryPassShader.setMat4("view", camera_view);
            geometryPassShader.setMat4("projection", camera_projection);

            ourModel.Draw(geometryPassShader);
        }

        // 6.2 使用 lightingPassShader 渲染一个 quad
        // 根据 quad 各位置对应的 texture_color, postion, normal (还有光源的位置) 计算各个像素的 light shading 值
        glBindFramebuffer(GL_FRAMEBUFFER, 0);
        glClearColor(1.0f, 0.3f, 0.3f, 1.0f);
        // 清除颜色缓冲区 并且 清除深度缓冲区
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
        lightingPassShader.use();

        // 设置光源的位置信息
        for (int i = 0; i < lights_n; i++)
        {
            lightingPassShader.setVec3("lights_Pos[" + std::to_string(i) + "]", lights_pos[i]);
        }
        lightingPassShader.setVec3("k", k[0], k[1], k[2]);
        lightingPassShader.setVec3("cameraPos", camera_pos);
        lightingPassShader.setInt("light_n", lights_n);

        quadModel.Draw(lightingPassShader, {"textureColor", "texturePos", "textureNor"},
                       {colorTexture, posTexture, norTexture});

        glfwSwapBuffers(window); // 在gfw中启用双缓冲,确保绘制的平滑和无缝切换
        glfwPollEvents(); // 用于处理所有挂起的事件,例如键盘输入、鼠标移动、窗口大小变化等事件
    }

    /************************************/
    /****** 7.释放资源 ******/
    // glfw 释放 glfw使用的所有资源
    glDeleteTextures(1, &colorTexture);     // 删除纹理
    glDeleteTextures(1, &posTexture);       // 删除纹理
    glDeleteTextures(1, &norTexture);       // 删除纹理
    glDeleteBuffers(1, &depthrenderbuffer); // 删除缓冲区

    glfwTerminate();
    /************************************/
    return 0;
}

// 用于处理用户输入的函数
void processInput(GLFWwindow *window)
{
    // 当按下 Esc 按键时调用 glfwSetWindowShouldClose() 函数,关闭窗口
    if (glfwGetKey(window, GLFW_KEY_ESCAPE) == GLFW_PRESS)
    {
        glfwSetWindowShouldClose(window, true);
    }
}

// 在使用 OpenGL 和 GLFW 库时,处理窗口大小改变的回调函数
// 当窗口大小发生变化时,确保 OpenGL 渲染的内容能够适应新的窗口大小,避免图像被拉伸、压缩或出现其他比例失真的问题
void framebuffer_size_callback(GLFWwindow *window, int width, int height)
{
    SCR_WIDTH = width;
    SCR_HEIGHT = height;
    glViewport(0, 0, width, height);
}

4. 全部代码及模型文件

使用OpenGL实现延迟着色(Deferred Shading)的全部代码以及模型文件可以在 [OpenGL]使用OpenGL实现延迟着色Deferred Shading 中下载。

三、参考

[1]. LearnOpenGL-Deferred-Shading

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值