本篇介绍一下常见的光照优化技术,包括延迟着色和分块着色,这些技术可以有效的提高渲染效率。
PS:这是本系列教程的最后一篇,完结撒花!
OpenGL 入门教程(11) 光照优化技术
引言
到目前为止,已经讲完了常用的光照模型及其常用的材质和投光物的相关知识。
本节讲提高性能的各种优化光照技术,基于帧缓冲的延迟着色技术和分块着色技术。
延迟着色(Deferred Shading)
目前我们用的光照方式叫做正向渲染(Forward Rendering)或者正向着色法(Forward Shading)。
简单地说,在场景中我们根据所有光源照亮一个物体,之后再渲染下一个物体,以此类推。
在前向渲染中,每个对象的光照计算是在片段着色阶段独立完成的,这意味着每个像素可能需要多次光照计算,特别是在场景中有多个光源时。这种情况下,性能开销会随着光源数量的增加而显著增长。
为了解决这个问题,提出延迟着色的渲染方式。
延迟着色法(Deferred Shading)/延迟渲染(Deferred Rendering)分为两个阶段,几何阶段(G-Buffer填充)和光照阶段(Lighting Pass)。
几何阶段 (G-Buffer)
之前我们讲过的帧缓冲在这里就起到作用了。
在这一步骤中,不是直接计算光照,而是将场景几何信息(如位置、法线、纹理坐标、漫反射颜色等)渲染到一组缓冲区中,这组缓冲区统称为G-Buffer(Geometry Buffer或者Geometry Buffer)。 每个像素对应这些信息的一个记录,为后续的光照计算准备数据。
我们以 冯 / 布林冯 模型为例子。
我们同时(或任意顺序)渲染以下几个帧缓冲,需要开启深度测试:
- 位置缓冲:存储片段的位置向量,用来计算光线/视角方向
- 法线缓冲:存储片段的单位法向量,用来判断平面的斜率
- 漫反射缓冲:存储片段的RGB漫反射颜色向量,也就是反照率(Albedo)
- 镜面缓冲:存储片段的镜面强度(Specular Intensity)值
由于镜面强度是一个浮点数,而漫反射也只用了rgb分量; 因此实际中可以合并这两个缓冲,即rgba的缓冲中rgb为漫反射,a为镜面强度。
这样我们就分开获得了各个属性的G缓冲。
几何阶段片段着色器示例如下:
1 |
|
光照阶段
一旦G-Buffer构建完成,渲染器会对从G-Buffer读取几何信息,然后针对每个像素进行光照计算。 由于光照计算是独立于几何体的,每个光源的贡献只需针对G-Buffer中的信息计算一次,然后直接应用到整个屏幕,大大减少了重复的像素着色计算。
这个着色器的原理和之前的布林冯的着色器一致,只是各信息直接从G-Buffer获取,而不是重复计算。
光照阶段片段着色器示例如下:
1 |
|
光体积(Light Volumes)优化
虽然延迟渲染少了一些几何阶段的重复计算,但是每个光源依然都作用了一遍。
我们可以通过光体积(Light Volumes)优化,在渲染时跳过不在光照范围内的光源,就可以跳过不必要的光照计算,提高计算效率。
光体积原理
光体积实质上是光源影响范围的可视化表示,通过计算光源的衰减半径,可以限定光照计算的区域,避免对场景中远离光源的像素进行不必要的光照计算,从而大幅节省计算资源。
光体积的大小基于光源的衰减方程计算得出。之前提到的光源的衰减遵循以下公式:
\[F_{light} = \frac{I}{K_c + K_l * d + K_q * d^2}\]虽然理论上我们想要F=0对应的距离d, 但显然这个式子没有零解。 我们只能取一个靠近0的阈值作为光照的边界,例如0.05或者其他数。
二次方程的求解这里就不赘述了。
当然,对于其他自定义的复杂光照的算法是一致的。这里本质是求解一个阈值对应的距离。
因此,使用不动点法,牛顿法,或者是其他的求解方法都是可以的。
使用光体积
由于 GPU 的并行执行特性,很多时候 GPU 执行 if 会变成执行两种情况再判断选择哪个。 这就使我们在着色器中使用 if 筛选光源变得无意义。
因此我们使用另一种方式。
在渲染阶段,把光源看作光体积半径的球体,将这些球体(光体积)作为额外的几何体进行渲染。由于球体的大小与光源的光体积半径一致,因此只对实际受到光源影响的像素进行光照计算。 这样可以确保只有位于光体积内的像素调用复杂的光照着色器,避免了对远处或不受光源直接影响的像素进行计算,极大地节省了计算资源。
设物体数量为m, 光照数量为n, 则时间复杂度从 O(mn) 降到了 O(m+n)。
需要注意的是,如果不开启面剔除,每个光都会被渲染两次; 但是开启后在光源内部可能会因为背面剔除使得该光不被渲染。
这可以使用模板缓冲解决。 在渲染光体积时,先使用模板测试标记出受光源影响的像素区域,然后再进行光照计算。 这样,即便观察者位于光源内部,模板缓冲也能确保正确渲染光源效果,避免了光源消失的问题。
延迟光照
虽然渲染光体积显著提升了处理大量光源的效率,但仍有进一步优化的空间。
可以对光体积也创建一个和 Gbuffer 类似的 buffer, 按照延迟渲染的思路做光照。
具体地说,先将所有光源的光体积渲染到一个额外的缓冲中,记录下光照计算的必要信息(如光照方向、颜色等)。 在后续的光照阶段,从这个缓冲中读取信息,对每个像素进行光照计算。这种方法避免了重复计算,提高了效率。
分块着色
虽然我们一路优化到现在,但是仍有进一步优化的空间。
试想一下,我们如果把屏幕分成多个块,那么可能左上角的块不会受到右下角的块中的光照,因此这里虽然整个屏幕对应的纹理受n个光照,但是分割后的每一块受到的光照数量都小于n.
这就是分块着色的思想。
切片式延迟着色(Tile-based Deferred Shading)与分区延迟着色(Clustered Deferred Shading)都是分块着色的,针对延迟着色技术的优化方案,它们通过将屏幕分割成多个小区域来减少全局光照计算的成本,从而提高渲染效率。
切片式延迟着色(Tile-based Deferred Shading)
切片式延迟着色是一种将屏幕空间分割成多个固定大小的切片(tiles)的技术。 每个切片通常对应于屏幕上的一个矩形区域。
在这一技术中,光照计算不是在整个屏幕空间上进行,而是针对每个切片独立进行。
在切片式延迟着色中,G-Buffer的构建阶段与标准的延迟着色相同。 但在光照计算阶段,每帧只处理当前切片内的像素,而不是整个屏幕。 这允许算法更高效地管理内存带宽和计算资源,因为只有当前切片的数据需要被加载到高速缓存中进行光照计算。
此外,切片式延迟着色还利用了光源相对于切片的局部性。 在光照计算阶段,每个切片仅考虑影响该切片的光源。 如果一个光源影响多个切片,则它的计算会被分散到这些切片的光照计算过程中,这样就可以避免在整个屏幕上重复计算同一个光源的贡献。
分区延迟着色(Clustered Deferred Shading)
分区延迟着色是切片式延迟着色的一种变种,它将屏幕空间分割成动态大小的分区(clusters)。 与切片不同,分区的大小可以根据光源的分布和强度动态调整,以进一步优化光照计算。
在分区延迟着色中,屏幕被分割成多个分区,每个分区可以包含不同的数量的像素。 这种动态分区的机制使得算法能够更加智能地分配计算资源。 例如,当一个区域内有大量光源时,可以将该区域划分为较小的分区,以便更精细地控制光照计算; 相反,当一个区域内的光源较少时,可以将该区域划分为较大的分区,以减少不必要的计算。
分区延迟着色同样在光照计算阶段利用光源的局部性,但与切片式延迟着色相比,它可以更灵活地适应场景中的光源分布,从而提供更好的性能优化。
总的来说,通过光体积、延迟光照和切片式延迟着色法等技术,延迟渲染能够在处理大量光源时保持高性能,同时维持良好的图像质量和抗锯齿效果。
至此,读者已经了解常见的延迟着色优化方法了。
到目前为止,我认为基础的 OpenGL 的使用方法已经介绍完了。
如果要进一步讲,要么是讲 PBR /光线追踪,要么是讲一些动画技术,要么是讲一些优化技术。
这些每一个都值得单独开一个系列教程了,放在 OpenGL 教程里太臃肿,也没有必要。
因此这个系列就到此为止了,感谢读者朋友的追读,完结撒花!
PS:接下来想先更一些数学相关的博文,一段时间后再更计算机系列的喔。