延迟着色(Deferred Shading)

什么是延迟着色?

我们先说一下前向渲染(forward rendering),它是对场景中所有物体对象进行渲染并执行光照计算。但是光照计算其实是一项非常耗性能的操作,在前向渲染中,我们会做很多次像素着色器的调用,但是其中大部分是不必要的。比如场景中有四个物体,互相之间存在叠压关系,按照前向渲染的流程,先渲染了一个物体之后,它的一部分被后一个渲染的物体挡住了,那么被挡住的这部分就是做了无效的计算,这就会很浪费资源和计算能力。

如果场景中有大量的灯光,那么像素数*灯光数这种渲染逻辑将是非常糟糕的。

比如我们看到下面这张图片,是一个比较精细化的场景,

然后他的前面出了一个简单的立方体,遮挡了整个场景的一大部分,那么我们在渲染后面的精细化物体时做的计算就白做了。

对于非常大的场景,如果我们通过对每个渲染对象都执行光照计算,那么将会产生大量不必要的计算和着色器调用,前向渲染会大大降低性能。所以延迟着色就是通过确保不会发生不必要的着色计算来帮助我们防止这种情况发生。

这里提一下前向渲染也有它的优势,如果需要在场景中使用多个着色模型,甚至是每个几何体都使用不同的着色模型和渲染技术,前向渲染是可以很好的支持的。因为前向渲染这种逐个渲染的特点,它非常适合渲染半透明物体。

延迟着色如何工作

我们在进行着色计算的时候,想要的是只给用户呈现最终实际可见的像素。那么我们如何做到这一点?

我们先需要记录我们的光照计算在多渲染目标(MRTs,multiple render targets)中需要的所有变量和值。这样我们可以先渲染整个场景,同时捕获着色所需的结果。然后,我们渲染一个全屏三角形,我们只对实际需要它的像素执行着色计算!

因为我们把着色计算进行了推迟,所以这项技术被称为延迟着色。

延迟渲染就是一种解决大量光照渲染的方案,其本质是在对三角面进行光栅化的阶段,先不进行任何光照计算。直到三角面光栅化完成,留下能看到的所有像素,再对这些可见的像素进行光照计算。

做延迟渲染我们需要两个通道(Pass)。

我们捕获着色所需的所有值/信息的渲染通道称为几何通道(Geometry Pass)G通道(G Pass)。我们通常存储的值是:

  • 位置(positions)
  • 法线(normals)
  • 反照率颜色(albedo color)
  • 金属/粗糙度值(metal/roughness)
  • 环境光遮蔽值(ao)
  • 自发光(emissive)等

我们将这些结果保存在多个纹理中(即多个渲染目标,MRTs),这也称为G BufferGeometry Buffer

然后,我们执行着色/光照通道将结果渲染到全屏三角形或四边形,并使用存储在G Buffer中的值来计算场景光照。这种方式没有浪费计算,因为存储在G Buffer中的像素都是我们可见的片段,且每个像素的着色只计算一次。

使用G Buffer中的值执行PBR着色计算后的最终结果:

延迟着色的优点和缺点

首先说下优势。

我们只对当前帧重要的像素执行着色计算,当场景中所有物体的几何信息都写入 G Buffer 之后,光照计算就和场景中有多少物体,多少三角形没有关系了。如果场景相当复杂且有很多对象并且有很多灯光,这种方式将会非常有用。

G Buffer数据也可用于其他技术。例如,屏幕空间环境光遮蔽(Screen Space Ambient Occlusio,SSAO) 可以利用G Buffer中的视图空间法线和位置数据来围绕视图空间表面法线随机定向半球,然后使用此结果采样到位置缓冲区中以找到给定像素的遮挡因子。

然而这种技术有一些明显的缺点。

几何缓冲区非常占用内存(对于R16G16B16A16_FLOAT 格式,每个像素需要64位,而在1920 x 1080纹理中为1,65,88,800字节)。

如果我们的场景只有几盏灯,前向着色可能比延迟着色具有更高的性能,而且内存带宽利用率要低得多。这是我们需要记住的事情,因为它取决于场景/应用程序。

此外,我们所有的对象都必须使用相同的着色算法(即使用相同的管线状态)。虽然我们可以通过为每个对象存储材质和着色特定数据来解决,但这不是一个非常优雅的解决方案。

此外,混合变得更难实现。我们需要将渲染器分为两个阶段:不透明对象的延迟渲染通道和透明/半透明对象的前向渲染通道。

参考资料