一、简介
本文介绍了 延迟着色(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 中下载。