延迟渲染

前向渲染

在VS中对每个物体的顶点做一系列的变换(主要是将顶点位置和法线变换到裁剪空间),然后在FS中对每个像素进行光照计算。由于每个物体上的每个像素只会调用一次FS,因此对于每个FS要传入所有的光源数据并计算每个光源的效果。

弊端:

(1)当一个场景非常复杂会有大量的物体以及复杂的深度,每个像素可能被多个物体重叠覆盖,这样会有大量的无效的GPU迭代计算。例如,如果某个像素深度复杂度为4,那么就会有3个像素计算是无效的,因为最后只有最顶部的像素会显示。

(2)在多光源情景下,例如大量点光源,光源很小照亮的区域也很小,只会照亮场景中的部分物体,但是FS还是会计算每个光源对每个像素的光照影响,即使光源离该像素很远。

延迟渲染

​ 主要是解决大量光照渲染的方案,延迟渲染的实质是先不要迭代三角形做光照计算,而是先找出来能看到的所有像素,再去迭代光照。直接迭代三角形的话,由于大量三角形是看不到的,无疑是极大的浪费。

(1)第一个阶段几何pass:正常执行VS但并不是将处理后的属性变量数据继续传递给FS进行光照计算,而是将结果保存到G buffer中。G buffer是一组按照设计格式存储的一组2D贴图,保存着顶点每个属性的数据。将数据分离并一次性写入到不同的贴图中,保存这些贴图的地方是借助于OpenGL的MRT特性支持。由于是在FS中写入属性值进行片段计算,因此G buffer中最终的值是光栅化器对顶点属性执行插值的结果。由于进行了深度测试,因此当几何pass阶段完成时,G缓冲区中的贴图最终保存的将是离相机最近的像素的插值属性值,留在G buffer中的像素都是之后必须要进行光照计算的。

(2)第二个阶段Lighting Pass:会依次遍历G buffer中的每个像素,从不同的贴图中读取每个像素对应的属性值然后进行光照计算。由于在创建G buffer的时候,经过深度测试除了离相机最近的像素其他的全部被抛弃了,所以现在每个像素对应只会进行一次光照计算。

在延迟渲染中对于点光源可以计算围绕光源的球体的尺寸(对于spot light聚光灯光源我们则计算光锥体的尺寸)。那个球体表示的是点光源影响的区域,而在球体之外的则会忽视这个光源的影响不进行该光源的光照计算。可以使用一个有少量多边形的粗糙的球体模型,并以光源为中心来进行渲染。VS阶段只需要将position数据变换到裁剪空间而不用干别的。FS阶段只在受光源影响的的相关像素上执行并进行光照计算。

几何pass

填充G buffer

​ 初始化G缓冲区。首先为顶点属性(位置和法线)和深度缓冲区创建FBO和纹理。然后在执行以下操作的循环中创建纹理的存储区域(不进行初始化),将纹理附加到FBO作为目标。为了进行MRT需要启用对所有两种纹理的写入。通过向glDrawBuffers()函数提供一个附件位置数组来实现这一点。位置纹理写入GL_COLOR_ATTACHMENT0;法线纹理写入GL_COLOR_ATTACHMENT1。(下面只展示了纹理附件的定义过程)

1
2
3
4
5
6
7
8
9
10
11
void Init()
{
glGenTextures(2, textures);
for (unsigned int i = 0; i < 2; i++) {
glBindTexture(GL_TEXTURE_2D, textures[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB32F, 1024, 1024, 0, GL_RGB, GL_FLOAT, NULL);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i, GL_TEXTURE_2D, textures[i], 0);
}
GLenum DrawBuffers[] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2, DrawBuffers);
}

绘制物体

绘制完成时,G buffer区将包含最近像素的属性。使用函数glDepthMask()允许写入深度缓冲区,几何 pass需要深度缓冲区,以便用最接近的像素填充G buffer,而光照pass没有可以写入深度缓冲区的。深度测试限制在几何pass上,在没有竞争的情况下,在光 pass上做深度测试毫无意义。禁用混合,在几何过程中,这是无关紧要的。

1
2
3
4
5
6
7
8
9
glBindFramebuffer(GL_DRAW_FRAMEBUFFER, fbo);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glDepthMask(GL_TRUE);
glEnable(GL_DEPTH_TEST);
glDisable(GL_BLEND);
/*绘制物体*/
glDepthMask(GL_FALSE);
glDisable(GL_DEPTH_TEST);//这两行顺序不能反
glBindFramebuffer(GL_FRAMEBUFFER, 0);

VS

简单地执行通常的转换并将需要的结果传递给FS

1
2
3
4
5
6
7
8
9
10
11
#version 330
layout (location = 0) in vec3 Position;
layout (location = 1) in vec3 Normal;
uniform mat4 gWVP;uniform mat4 gWorld;
out vec3 Normal0;out vec3 WorldPos0;
void main()
{
gl_Position = gWVP * vec4(Position, 1.0);
Normal0 = (gWorld * vec4(Normal, 0.0)).xyz;
WorldPos0 = (gWorld * vec4(Position, 1.0)).xyz;
}

FS

负责进行MRT,它不输出单个向量,而是输出多个向量。这些向量中的每一个都指向数组中先前由glDrawBuffers()设置的相应索引。因此,在每次FS调用中,都会写入G缓冲区的两个纹理。location = 0的WorldPosOut写入GL_COLOR_ATTACHMENT0;location = 1的NormalOut写入GL_COLOR_ATTACHMENT1。

1
2
3
4
5
6
7
8
9
10
#version 330
in vec3 Normal0;
in vec3 WorldPos0;
layout (location = 0) out vec3 WorldPosOut;
layout (location = 1) out vec3 NormalOut;
void main()
{
WorldPosOut = WorldPos0;
NormalOut = normalize(Normal0);
}

光照pass

基本设置

此处有点光源和平行光两种光源,混合这两种光源类型,因为每个光源都由自己的draw调用处理。在正向渲染中,我们积累了FS中所有光源的结果,但现在每个FS调用只处理一个光源。在我们的例子中,我们将混合方程设置为GL_FUNC_ADD。这意味着GPU只需添加源和目标。因为我们想要真正的加法,所以将源和目标的混合函数设置为gluOne。结果是:1src+1dst。哦,我们需要在做这件事之前启用混合。。。在处理好混合后,我们将G缓冲区设置为读取

1
2
3
4
5
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE);
glBindFramebuffer(GL_READ_FRAMEBUFFER, fbo);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

点光源

Donate
  • Copyright: Copyright is owned by the author. For commercial reprints, please contact the author for authorization. For non-commercial reprints, please indicate the source.

扫一扫,分享到微信

微信分享二维码

请我喝杯咖啡吧~

支付宝
微信