延迟渲染
引入延迟渲染
- 如果已经学习了一段时间的渲染,但还不知道什么是延迟渲染,那么之前用的渲染方式很有可能就是正向渲染(Forward Rendering)
- 之所以没有明确的提出正向渲染这个概念,可能是因为场景渲染情况比较简单,渲染过程没有用到很特殊的技巧,就是一套按部就班组成的顺理成章,所以没有必要再提出一个新的概念
- 但是,当场景变的复杂时,比如有一堆光源和物体,每个光源和每个物体都要进行光照计算,这时再来看看之前“按部就班”的渲染方式:
- 在渲染时如果场景中有多个光源,每个物体的每个顶点都要遍历所有的光源,并计算每个光源的光照;然后再计算下个顶点。这种顺序式的渲染看似理所当然,但引起了严重的效率问题:每个顶点都要进行多次光照计算,关键是大量被挡住的顶点进行了很多次无用的计算。
- 为了解决这种效率问题,产生了一种新的渲染方式,这种方式分两步完成。第一步:先将场景中的物体渲染到纹理;第二步:根据得到的纹理,逐像素的计算每个光源的光照。这样在保证相同光照效果的情况下,省去了很多无用的计算,从而提高效率。这种渲染方式叫做延迟渲染(Deferred Rendering)
引入GBuffer
- 延迟渲染是为了解决复杂场景的渲染效率,场景复杂的原因基本离不开光照,而光照的计算又离不开顶点位置、法线、颜色、光源位置、摄像机位置等一系列数据
- 为了提供以上数据,延迟渲染在第一个渲染过程中用了两个技巧
- 第一个技巧:将物体的顶点位置、法线、颜色分别存储到不同的纹理当中
- 因为这些纹理保存的都是物体的几何数据,所以就把他们叫做GBuffer(Geometry Buffer),这个渲染过程也叫做几何处理阶段
- 第二个技巧:运用多渲染目标,实现一次渲染生成多个纹理
两个阶段
几何着色阶段
- 在客户端创建GBuffer
- 注意最后两行代码与片段着色器中的输出变量对应,是实现多渲染目标的关键
GLuint gBuffer;
glGenFramebuffers(1, &gBuffer);
glBindFramebuffer(GL_FRAMEBUFFER, gBuffer);
GLuint gPosition, gNormal, gColor;
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);
//...
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, gPosition, 0);
glGenTextures(1, &gNormal);
//...
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT1, GL_TEXTURE_2D, gNormal, 0);
glGenTextures(1, &gColor);
//...
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT2, GL_TEXTURE_2D, gColor, 0);
//MRT
GLuint attachments[3] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1, GL_COLOR_ATTACHMENT2 }; glDrawBuffers(3, attachments);
- 顶点着色器
- 注意顶点位置坐标和法线坐标都转换到了世界空间中
void main()
{
vec4 worldPos = model * vec4(aPos, 1.0);
FragPos = worldPos.xyz;
TexCoords = aTexCoords;
mat3 normalMatrix = transpose(inverse(mat3(model)));
Normal = normalMatrix * aNormal;
gl_Position = projection * view * worldPos;
}
- 片段着色器
- 注意漫反射和镜面反射强度合并存储在一个纹理中
layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gColor;
in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;
uniform sampler2D texture_diffuse1;
uniform sampler2D texture_specular1;
void main()
{
gPosition = FragPos;
gNormal = normalize(Normal);
gColor.rgb = texture(texture_diffuse1, TexCoords).rgb;//存储漫反射
gColor.a = texture(texture_specular1, TexCoords).r;//存储镜面强度
}
延迟光照处理阶段
- 在客户端绑定GBuffer中的纹理,并传入每个光源的位置
//...
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, gAlbedoSpec);
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, gNormal);
glActiveTexture(GL_TEXTURE2);
glBindTexture(GL_TEXTURE_2D, gColor);
//...
void main()
{
vec3 FragPos = texture(gPosition, TexCoords).rgb;
vec3 Normal = texture(gNormal, TexCoords).rgb;
vec3 Diffuse = texture(gColor, TexCoords).rgb;
float Specular = texture(gColor, TexCoords).a;
for(int i = 0; i < NR_LIGHTS; ++i)
{
//...
}
FragColor = vec4(lighting, 1.0);
}
- 在模型之间加上若干光源
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/f5cb5ad7518198a653d423bcfc655173.gif)
副作用
不能混合
- 延迟渲染其中一个缺点就是它不能进行混合(Blending),因为GBuffer中所有的数据都是从一个单独的片段中来的,而混合需要对多个片段的组合进行操作
- 解决办法:将延迟渲染与正向渲染结合。
- 首先是进行延迟渲染,然后利用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);
for (GLuint i = 0; i < lightPositions.size(); i++)
{
//...
}
- 将模型之间的光源显示出来
![在这里插入图片描述](https://i-blog.csdnimg.cn/blog_migrate/0cc2f5d1ecf2d7ab5da105931e9c73d6.gif)
不能MSAA?
- 理论上,延迟渲染是可以进行MSAA的,但不是在光照处理阶段进行,而是在几何着色阶段进行。
- 实际上,在几何着色阶段进行MSAA时,会对显存造成极大的浪费,所以大家都不在延迟渲染上做MSAA
- 几何着色阶段一般都会使用多渲染目标(MRT),而MRT是硬件提供的一种能力。在十多年前的DX9时代,他不支持MSAA的MRT,这也是延迟渲染不能MSAA的一个重要原因。但从DX10以来,就支持了带MSAA的MRT
对比正向渲染
- 在复杂的场景中,延迟渲染的效率会比正向渲染有很大的提升,因为每个像素只运行一次片段着色器;而正向渲染每个物体的每个顶点都要运行多次片段着色器
- 然而对于简单的场景,延迟渲染的效率并不一定比正向渲染高
- 正向渲染可以实现的效果同样可以在延迟渲染中实现
- 延迟渲染有上面提到的副作用