体积光渲染技术

什么是体积光

遮光物体被光源照射时,在其周围呈现的光的放射性泄露,称其为体积光。例如太阳照到树上,会从树叶的缝隙中透过形成光柱。之所以称之为体积光,是因为这种特效下的光照相比以往游戏中的光照给人视觉上以空间的感觉。体积光让游戏玩家更真实的感觉。有时也指实现这一特效的技术。-《百度百科》

Volumetric lighting is a technique used in 3D computer graphics to add lighting effects to a rendered scene. It allows the viewer to see beams of light shining through the environment; seeing sunbeams streaming through an open window is an example of volumetric lighting, also known as crepuscular rays. The term seems to have been introduced from cinematography and is now widely applied to 3D modelling and rendering especially in the field of 3D gaming. -《Wikipedia-Volumetric lighting》

下图是我读书的时候在学校里面拍的体积光示意图。
1

游戏中的体积光效果

2
3

上面两张是从《辐射4》中截取的体积光示意图(来自Nvidia),而谈到体积光,不得不说的是一款很著名的独立游戏《inside》。这款游戏利用体积光(当然还有雾,glow等其他技术),把阴暗的氛围烘托得十分出色,是体积光应用的一个极好的例子。
4
5

基于Billboard的体积光

基于Billboard的体积光很简单,就是生成一个明暗条纹,加上一个遮罩图,再贴到两个或者若干个交叠的面片上即可。

6

这种方式比较简单,效果也还过得去。尤其是用于远景的体积光表示是很合适的。但是对于近景,尤其是观察角度比较自由的情况下就容易穿帮。

基于后处理的体积光

基于后处理的体积光渲染比较像Bloom,只不过做模糊的方法有点不一样。后处理实现体积光的方法流程为:

  1. 渲染出整个画面
  2. 提取画面中高亮的部部分(参数控制)
  3. 对高亮部分进行径向模糊(注意,bloom用的一般不是径向模糊)
  4. 将径向模糊后的高亮层和原图进行叠加

这个方法的效果不错,消耗也不大,而且效果可以独立控制,只是针对光源不在画面内的情况没办法处理。另外,体积光的那种颗粒感和不均匀的线条感很难得到。如果改用更复杂的后处理算法的话,消耗会增加得比较厉害。

而Unreal中采用bloom method渲染的体积光也是基于这个思路。

9

基于Occlusion的体积光

这种方案也是unreal采用了的一种方法,具体做法是:

  1. 从场景的深度图创建一张mask(参数控制阈值)
  2. 沿着光源射出去的方向,blur这张mask图
  3. 用blur后的mask图来mask场景中的雾效和大气效果

这个方法解决了后处理方案中,当光源不在画面中无法得到正确效果的问题。但是这个方法的问题是,体积光的效果取决于当前场景中的大气和雾效。下图是采用Occlusion渲染的体积光的一张示意图。

10

Ray-Marching和基于Ray-Marching的体积光渲染

作为基础知识,首先介绍一下Ray-Marching。

重要的放前面,要详细了解Ray-Marching可以阅读这篇博客 Ray Marching and Signed Distance Functions

简单来说,Ray-Marching主要是用于体绘制的一种方案,类似于Ray-Tracing,从观察者的位置向屏幕的每个点发射一条射线,每次步进一定的距离,直到到达物体表面,然后再进行光照计算。由于Ray-Marching步进的过程,可以进一步计算得到观察者和目标物体之间的光照结果,因而对于体绘制有特殊的帮助。关于Ray-Marching就先讲这么多。

有了Ray-Marching的基本概念,在pixel shader中我们可以模拟Ray-Marching的做法,绘制体积光。

11

  1. 从起点开始,沿着射线每次推进一点,采样每个点的亮度,所有经过的采样点上的散射亮度求和就是像素的颜色
  2. 散射光的亮度和离光源的距离成平方反比
  3. 迭代介质中的光散射亮度

这种做法,往往需要迭代几十次,因此消耗很大,而后有人利用线积分的解析公式替代迭代计算,本文就不具体推导公式了。

这种做法得出的效果有一些问题,比如各个方向上散射光强度一样,没有变化,不够真实,另外这种体积光也不会被遮挡。

因此,我们在计算散射光强度的时候,不应该只由到光源的距离来衰减光强,还应该乘以以下几个因子:

  1. 散射因子(HG公式), HG公式表示了每个方向散射出去的光线亮度应该是不一样的,并且散射出去的光线亮度总合应该和射到尘埃上的那束光线亮度一样,也就是能量守恒。公式内容可以自行google。
  2. 投光比因子:根据Beer-Lambert法子,根据物质的密度和到光源距离,可以描述入射光强度和透光强度的比值。
  3. 阴影因子:类似ShadowMapping等阴影算法,略去不详述。

经此计算后的体积光,有了更多的变化,也更加真实了。

13

Epipolar Sampling Based Light Shafts

对于基于Ray-Marching渲染体积光的方案,有很多改进的算法,本文介绍一种基于极坐标的方案。

改进算法的目标是比较一致的,就是想减少采样次数,从而减少计算复杂度。而基于极坐标采样的方案就是源自一个观察结果:体积光中光线的散射强度沿着光源射出去的线,变化是不大的;而在这些一条条射出去的射线之间,散射光强度是有明显变化的。

由此想到了从光源到屏幕边缘固定的一些点连线,只用Ray-Marching计算这些连线上某些点的散射光强度,再利用插值的方法计算其他点的散射光强度,并存在一张贴图中。最后在渲染场景的时候,从贴图中采样散射光的强度,并合成到画面上。

极坐标采样示意图如下。

14

极坐标采样的步骤是:

  1. 从光源到屏幕坐标连线,决定采样点在屏幕空间的位置;
  2. 在depth不连续的地方,补充采样点;
  3. 利用Ray-Marching计算散射光强度,并利用插值计算剩下点的散射光强度;

最后,渲染场景,再从记录了散射光强度的texture中插值采样即可。更具体的算法可以参考 IVB Atmospheric Light Scattering 和Epipolar Sampling for Shadows and Crepuscular Rays in Participating Media with Single Scattering 这篇文章。

这种算法的效果图如下:

15
16