问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

LearnOpenGL——延迟渲染学习笔记

创作时间:
作者:
@小白创作中心

LearnOpenGL——延迟渲染学习笔记

引用
CSDN
1.
https://m.blog.csdn.net/weixin_70073176/article/details/141391044

延迟渲染是一种高效的图形渲染技术,通过将几何处理和光照计算分离,可以显著提高复杂场景的渲染效率。本文详细介绍了延迟渲染的基本概念、实现方法以及与前向渲染的结合使用,适合有一定OpenGL基础的开发者阅读。

基本概念

延迟渲染(Deferred Rendering)包含两个Pass:

  1. 几何处理Pass:先渲染场景一次,获取对象的各种几何信息(如顶点位置、颜色、法线、高光信息等),并存储在G-Buffer中。
  2. 光照计算Pass:渲染一个满屏的方片,根据G-Buffer中的几何数据信息为每个片元进行光照计算。

与前向渲染不同,延迟渲染将片元着色器移动到后期处理阶段,确保每个像素只处理一次,从而节省大量无用的渲染调用。但延迟渲染也存在一些缺陷,如显存消耗较大、不能使用MSAA等。

G-Buffer

G-Buffer是一个用来存储光照计算所需数据的纹理集合,通常包括:

  • 3D位置向量
  • 3D法向量
  • RGB漫反射颜色向量
  • 镜面强度
  • 光源的位置向量、颜色向量
  • 观察者的位置向量

在前向渲染中,每个物体的光照计算都是基于实时数据的。而在延迟渲染中,G-Buffer已经将几何数据渲染到一张2D纹理中,每个片元都有正确的几何数据供光照计算使用。

MRT(多重渲染目标)

在几何处理阶段,我们需要渲染场景中所有物体,并将这些几何数据存储在G-Buffer中。可以使用MRT在一个Pass中渲染多个颜色缓冲。

以下是初始化G-Buffer的代码示例:

GLuint gBuffer;
glGenFramebuffer(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
GLuint gPosition, gNormal, gAlbedoSpec;

// 位置颜色缓冲
glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 
    0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, 
    GL_TEXTURE_2D, gPosition, 0);

// 法线颜色缓冲
glGenTextures(1, &gNormal);
glBindTexture(GL_TEXTURE_2D, gNormal);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 
    0, GL_RGB, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, 
    GL_TEXTURE_2D, gNormal, 0);

// 颜色+镜面颜色缓冲
glGenTextures(1, &gAlbedoSpec);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SCR_WIDTH, SCR_HEIGHT, 
    0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, 
    GL_TEXTURE_2D, gAlbedoSpec, 0);

// 告诉OpenGL我们将用哪个颜色附件来渲染
GLuint attachments[3] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, 
    GL_COLOR_ATTACHMENT2};
glDrawBuffers(3, attachments);

接下来是将数据渲染到G-Buffer中的片元着色器代码:

#version 330 core
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gAlbedoSpec;
in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_specular1;
void main()
{    
    // 存储第一个G缓冲纹理中的片段位置向量
    gPosition = FragPos;
    // 同样存储对每个逐片段法线到G缓冲中
    gNormal = normalize(Normal);
    // 和漫反射对每个逐片段颜色
    gAlbedoSpec.rgb = texture(texture_diffuse1, TexCoords).rgb;
    // 存储镜面强度到gAlbedoSpec的alpha分量
    gAlbedoSpec.a = texture(texture_specular1, TexCoords).r;
}

Lighting Pass

在光照计算阶段,我们需要遍历G-Buffer中的每个像素,将其数据作为光照计算的输入。以下是相关代码示例:

glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
shaderLightingPass();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gPosition);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, gNormal);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
// 发送光照相关的uniform
SendAllLightUniformsToShader(shaderLightingPass);
glUniform3fv(glGetUniformLocation(shaderLightingPass.Program, "viewPos"), 1, &camera.Position[0]);
RenderQuad();

在片元着色器中,我们直接从G-Buffer中采样数据进行光照计算:

#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gAlbedoSpec;
struct Light {
    vec3 Position;
    vec3 Color;
};
const int NR_LIGHTS = 32;
uniform Light lights[NR_LIGHTS];
uniform vec3 viewPos;
void main()
{             
    // 从G缓冲中获取数据
    vec3 FragPos = texture(gPosition, TexCoords).rgb;
    vec3 Normal = texture(gNormal, TexCoords).rgb;
    vec3 Albedo = texture(gAlbedoSpec, TexCoords).rgb;
    float Specular = texture(gAlbedoSpec, TexCoords).a;
    // 然后和往常一样地计算光照
    vec3 lighting = Albedo * 0.1; // 硬编码环境光照分量
    vec3 viewDir = normalize(viewPos - FragPos);
    for(int i = 0; i < NR_LIGHTS; ++i)
    {
        // 漫反射
        vec3 lightDir = normalize(lights[i].Position - FragPos);
        vec3 diffuse = max(dot(Normal, lightDir), 0.0) * Albedo * lights[i].Color;
        lighting += diffuse;
    }
    FragColor = vec4(lighting, 1.0);
}

结合延迟渲染和前向渲染

在延迟渲染中,光源通常被视为无形的点或方向,而不是具有材质和颜色的物体。如果我们想将光源渲染为一个带有光照颜色的立方体,就需要额外的几何处理,这超出了延迟渲染的范畴,因此需要结合前向渲染来处理透明物体、镜面反射和光源模型等。

前向渲染的部分会在延迟渲染操作之后进行:

// 延迟渲染光照渲染阶段
[...]
RenderQuad();
// 现在像正常情况一样正向渲染所有光立方体
shaderLightBox.Use();
glUniformMatrix4fv(locProjection, 1, GL_FALSE, glm::value_ptr(projection));
glUniformMatrix4fv(locView, 1, GL_FALSE, glm::value_ptr(view));
for (GLuint i = 0; i < lightPositions.size(); i++)
{
    model = glm::mat4();
    model = glm::translate(model, lightPositions[i]);
    model = glm::scale(model, glm::vec3(0.25f));
    glUniformMatrix4fv(locModel, 1, GL_FALSE, glm::value_ptr(model));
    glUniform3fv(locLightcolor, 1, &lightColors[i][0]);
    RenderCube();
}

但是现在的深度结果并不正确,因为除了光源立方体的深度信息都在延迟渲染过程中。我们需要将延迟渲染中的深度信息提取出来,然后再渲染光立方体。可以使用glBlitFramebuffer复制一个帧缓冲的内容到另一个帧缓冲中:

glBindFramebuffer(GL_READ_FRAMEBUFFER, gBuffer);
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // 写入到默认帧缓冲
glBlitFramebuffer(
  0, 0, SCR_WIDTH, SCR_HEIGHT, 0, 0, SCR_WIDTH, SCR_HEIGHT, GL_DEPTH_BUFFER_BIT, GL_NEAREST
);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 现在像之前一样渲染光立方体
[...]

更多光源

延迟渲染本身并不能支持非常大量的光源,但是我们可以为其引入一个优化:光体积(Light Volumes)。通过计算光源的衰减值,可以确定光的影响范围,从而只对在光影响范围内的片段进行光照计算。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号