LearnOpenGL——延迟渲染学习笔记

延迟渲染学习笔记

我们之前使用的一直是 前向渲染(正向渲染 Forward Rendering) ,指的是在场景中根据所有光源照亮一个物体,之后再渲染下一个物体。对程序性能影响很大,因为对于每一个需要渲染的物体,程序都要对每一个光源每一个需要渲染的片段进行迭代。 而且还有一部分被渲染的片段会被覆盖遮挡,造成浪费

一、基本概念

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

  • 第一个是几何处理Pass:先渲染场景一次,之后获取对象的各种几何信息(比如顶点位置、颜色、法线、高光信息等),并存储在G-Buffer中
  • 然后第二个Pass是用于计算光照信息------Lighting Pass:我们将会渲染一个满屏的方片并根据G-Buffer中的几何数据信息来为每个片元进行光照计算(在G-Buffer中每个像素进行迭代)。

与前向渲染不同,延迟渲染并不是将每个对象进行顶点着色器到片元着色器的计算,而是将片元着色器移动到后期处理。这样保证了对于在光照处理阶段中处理的每一个像素都只处理一次,所以我们能够省下很多无用的渲染调用。

缺陷:

  • 显存消耗会较大:G-Buffer需要我们存储较大的几何数据
  • 不能使用MSAA:因为我们通过只有几何数据的G-Buffer来进行着色

二、G-Buffer

G-Buffer是一个用来存储光照计算所需数据的纹理的总称。

  • 3D位置向量,来计算片段位置变量 lightDir,viewDir
  • 3D法向量,normal
  • RGB漫反射颜色向量,Albedo
  • 镜面强度(高光反射强度)
  • 光源的位置向量、颜色向量
  • 观察者的位置向量

在前向渲染中,我们每个物体的光照计算都是根据特定实时数据的,所以如何给Lighting Pass传递正确的光照数据是很重要的。延迟渲染G-Buffer已经把几何数据渲染到一张2D纹理中,纹理允许我们存储各种各样的数据类型,所以纹理上每个片元都有正确的几何数据供光照计算

cpp 复制代码
while(...) // render loop
{
    // 1. geometry pass: render all geometric/color data to g-buffer 
    glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
    glClearColor(0.0, 0.0, 0.0, 1.0); // keep it black so it doesn't leak into g-buffer
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
    gBufferShader.use();
    for(Object obj : Objects)
    {
        ConfigureShaderTransformsAndUniforms();
        obj.Draw();
    }  
    // 2. lighting pass: use g-buffer to calculate the scene's lighting
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    lightingPassShader.use();
    BindAllGBufferTextures();
    SetLightingUniforms();
    RenderQuad();
}

MRT

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

我们需要初始化一个帧缓冲gBuffer(这个gBuffer会有多个颜色缓冲附件,以及一个深度渲染缓冲对象)。对于位置和法向量的纹理,我们希望使用高精度的纹理(每分量16或32位的浮点数),而对于反照率和镜面值,使用默认的纹理(每分量8位浮点数)就够了。

cpp 复制代码
GLuint gBuffer;
glGenFramebuffer(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
GLuint gPosition, gNormal, gColorSpec;
//位置颜色缓冲
glGenTextures(1, &gPosition);
glBindTexture(GL_TEXTURE_2D, gPosition);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCRHEIGHT, 
	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 attatchments[3] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, 
	GL_COLOR_ATTACHMENT2};
glDrawBuffers(3, attachments);

// 之后同样添加渲染缓冲对象(Render Buffer Object)为深度缓冲(Depth Buffer),并检查完整性
[...]

接下来就是将数据渲染到G-Buffer中,我们将使用一下片元着色器

cpp 复制代码
#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进行逐像素的遍历,将其数据作为光照计算的输入,来计算场景最终的光照颜色。

cpp 复制代码
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中直接采样

cpp 复制代码
#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);
}  

四、结合延迟渲染和前向渲染

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

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

cpp 复制代码
// 延迟渲染光照渲染阶段
[...]
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复制一个帧缓冲的内容到另一个帧缓冲中。我们需要指定一个帧缓冲为读帧缓冲(Read Framebuffer),并且类似地指定一个帧缓冲为写帧缓冲(Write Framebuffer)

cpp 复制代码
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)。因为对于场景中的物体,有的光是影响非常非常小的,所以我们就可以计算光的影响半径。

我们可以通过光源的衰减值,来计算光的影响范围,我们只需要对在光影响范围内的片段进行光照计算就可以了。

相关推荐
我要吐泡泡了哦8 分钟前
GAMES104:15 游戏引擎的玩法系统基础-学习笔记
笔记·学习·游戏引擎
骑鱼过海的猫1239 分钟前
【tomcat】tomcat学习笔记
笔记·学习·tomcat
贾saisai2 小时前
Xilinx系FPGA学习笔记(九)DDR3学习
笔记·学习·fpga开发
北岛寒沫2 小时前
JavaScript(JS)学习笔记 1(简单介绍 注释和输入输出语句 变量 数据类型 运算符 流程控制 数组)
javascript·笔记·学习
烟雨666_java2 小时前
JDBC笔记
笔记
GEEKVIP2 小时前
Android 恢复挑战和解决方案:如何从 Android 设备恢复删除的文件
android·笔记·安全·macos·智能手机·电脑·笔记本电脑
铁匠匠匠4 小时前
从零开始学数据结构系列之第六章《排序简介》
c语言·数据结构·经验分享·笔记·学习·开源·课程设计
Moliay5 小时前
【资料分析】刷题日记2
笔记·公考·行测·常识·资料分析
架构文摘JGWZ5 小时前
Java 23 的12 个新特性!!
java·开发语言·学习
小齿轮lsl5 小时前
PFC理论基础与Matlab仿真模型学习笔记(1)--PFC电路概述
笔记·学习·matlab