前言
这周本来的选题是关于模糊(Blur)效果的。由于这个系列一直力求的是精准地概括一些有趣的渲染知识,因此查资料后放弃了只谈模糊——因为模糊的N种分类及其原理(文末会给出链接),已经有大神谈得又准确又易读了。
因此这一期转而介绍实时渲染中一个组合了多种中间技术的效果——体积光散射,对应到太阳作为光源时就往往被称为太阳照射(散射)效果,也俗称太阳光束。
这一方案的实现过程中集合了阴影纹理映射、光线步进、模糊效果及屏幕空间后处理的思想,这些思想的基本原理可能不难理解,复杂的是其后续不断演进迭代的技术方案。
1 Blur
模糊效果(Blur)的应用想必大家都不陌生,例如游戏里转视角时会有动态模糊等。简单来说模糊是一种基于屏幕空间的后处理效果,即对于渲染出的结果进行逐像素的修改——主流的模糊算法往往都是基于一个模糊核(Kernel)逐像素进行计算的结果,部分算法依据其思想还会有动态范围的模糊核。但无论是哪种方式,其核心思想都可以理解成把一个或少量像素的颜色以更广泛分散的权重“抹”到更大一片像素区域上得到的结果。
这里展示几种常见的模糊算法,具体的算法都可以去看后面提供的资料:
1)高斯模糊——Gaussian blur
(图中展示了高斯模糊的应用原理,红色是原像素,蓝色是变换后的像素)
(图中展示了高斯模糊的效果)
高斯模糊是一个低通道过滤(Low-pass filter)方案。除了通过模糊柔化画面等应用外,高斯模糊还可以用来做边缘查找;另外,在其基础上还演变出了Bloom视觉效果,这一点应该也都不陌生。
(图中是Bloom效果的简要流程)
2 )KawaseBlur
Kawase Blur最初专用于Bloom效果,但也可以推广作为专门的模糊算法使用,且在模糊外观表现上与高斯模糊非常接近。
(图中是Kawase Blur在单个pass的运算方式)
(图中是kawase Blur多次pass的运算方式)
从图中可以看出,Kawase Blur的Blur Kernel是随迭代次数移动的。另外,其多次pass后的渲染效果接近高斯模糊,但具有更好的性能。
基于这个思想后来还衍生出了双重模糊(Dual Blur),主要是在采样精度和采样方式上进行了改进。
3)散景模糊——Bokeh Blur
之前有一篇文章提到过景深效果,因此景深效果的视觉原理就不介绍了。这里的Bokeh Blur可以理解成用近似的失焦分布方式来作为一个模糊核。
(Bokeh Blur的像素分布可以理解旋转扩散的,如图所示)
(图中展示了一些相机不同参数拍摄的散景模糊效果)
需要注意,仅仅从屏幕空间后处理的方式是无法达到上图的效果的。Bokeh Blur可以整体将画面模糊,但无法准确定位出清晰部分的范围。
后续人们提出了光圈模糊(Iris Blur)作为组合方案,作为达成景深效果的近似。但这种方案划定出的清晰区域仍然只是近似的。
(图中展示了引入椭圆遮罩范围后的光圈模糊效果)
4)径向模糊——Radial Blur
径向模糊可以给画面带来很好的速度感,其基本思想是按一定的空间方向进行采样扩散,得到的视觉效果是很多“线”。
(图中展示了图形选件中做出的径向模糊效果)
后面提到的基于后处理的太阳光束方案就改进自Radial Blur,因为在模拟某一点发射出的很多线的形式上是很类似的。
5)动态模糊——Motion Blur
动态模糊与前几种模糊不同,其算法依据的数据不仅仅是当前帧渲染的像素及帧缓冲。
常见的动态模糊有两种方式,一种是对于历史帧的混合,另一种是基于逐像素的速度向量( Velocity Vector)进行推算。后者需要的帧缓冲更少。
(图中左侧是加入了动态模糊的效果)
2 Shadow Map
阴影纹理映射(Shadow Map)是一个运行时产生的逐帧更新的纹理,因此需要占用一定缓存。对于传统渲染管线来说,shadow map记录的是某一光源发出的“可见性”(Visibility)采样结果,所以会先以光源为“摄像机”进行一次阴影纹理的绘制(Shadow Pass),绘制范围可以依据主摄像机的视锥体进行换算得出(即shadow map需要包含主摄像机能渲染出的所有物体)。
(图中展示了常见的基于深度缓冲的shadow map原理)
只是此时不是进行着色而是记录“距离光源最近的遮挡深度”。有了这一中间结果后,再在主摄像机着色时对比每一个片元计算距离光源的距离,并和shadow Map中存储的深度进行比对,如果小于则说明被遮挡住了,就应该表现为在阴影中。(最早的shadow map还不是存储的深度,由于过于久远这里就不提了)
绘制shadow map和普通的渲染绘制共享一些问题与解决方案,如剔除、空间划分优化等。
基于中间各步骤换算的一些采样精度损失(主要是因为像素是离散的),这一方案有一些会导致失真的问题,常常通过引入偏移量来近似解决。
游戏引擎后续迭代出了级联阴影纹理(Cascaded Shadow Maps)技术以提高局部阴影的精度,主要是针对采样这一步提供了不同的采样分辨率;另外这种方式表现出的阴影是“硬阴影”,要实现“软阴影”是一套不同的算法(通常是 Percentage-Closer Filtering PCF),思路上很接近某种模糊算法,这里不展开了。
由于其相对较大的计算量,shadow map方案仅仅适用于数量不多的实时光源,多小的额外光源如点光源即使是实时光照着色,也往往无法绘制实时阴影。
由于shadow map是一个大而化之的实时渲染方案,如果对细节精度有要求就会引入各种优化方案。例如类似落日时那种太阳光与地表近似平行的状态下,表现其阴影很难做到很好的效果;再比如有些物体自阴影往往难有很好的精度,例如人体的服饰或头发的阴影等。相关的优化方案这里不展开了。
3 Ray Marching
光线步进(Ray Marching)其实可以理解为是一种前光线追踪时代的间接光照算法。需要说明的是,光线在参与介质(Participating Media)中的传播是可能有更多次弹射和相当复杂的衰减模式的,而在实时渲染领域中所有对于光线弹射基本都进行了近似,例如认为只弹射一次、衰减通过给定系数推算等。
Ray Marching是完全基于屏幕(或者说摄像机视角)进行运算的,需要计算的对象全部都是由方程来表示,这个方程是用来表示空间中任何一个点与该物体表面的最小距离。因此我们可以抛开多边形网格,直接通过方程计算建模得到完美平滑的效果,或者用来模拟液体、变形、融合、体积云等等多边形难以达到的效果。
其最基本思想就是基于符号距离函数(Signed distance Functions),以一定的步长策略进行采样。虽然计算量偏大,但Ray marching确实可以在一次绘制的Shader片元函数中完成。基于这一策略可以一定程度上计算折射、反射、AO等光照效果。
这里不展开讲基于距离场的step策略(即使每次都步进固定距离这一算法也成立,只是很浪费),着重演示的参与介质中的着色策略——参与介质可以简单理解成液体或者云这种有一定内部结构但整体透光的物体。
(图中展示了ray marching一次step关注的几个方面)
图中蓝色的射线是marching的方向,粉色范围是用函数定义的参与介质的范围,橙色是从采样点到光源的射线——需要通过这个橙色的射线得到和介质的交点,并计算光线在介质内的衰减。
(图中演示了ray marching多步采样的流程)
最后逐步迭代渲染得到的结果,实际上是基于黎曼和思想的一个用采样来近似求积分结果的问题——最终的结果得到的就是从视点看来符合光传播特性的一个颜色值。
光线步进通常有2种方向策略,分别是从后向前的和从前向后的,两者的核心计算公式不同,但整体思想接近。一般认为从前向后的方案比较好,因为可以在运算结果已经完全不透明时就提前停下来。(这里和之前讲的颜色半透明混合略微不同,是一类采样近似算积分的问题,因此可以从前向后)
*光线步进每一步的射线不一定只是向光源,例如之前讲过屏幕空间反射就是向对应位置的片元。
更多细节都可以参考Wiki和其它资料,这里也不会是这个系列第一次提到Ray Marching(最多只能算开了个头),但至少讲到这里构造一个视觉上是God Ray的要素都已经齐备了。
(图中展示了通过Ray marching能达到的渲染效果)
4 Volumetric Light Scattering as a Post-Process
目前主流的游戏引擎中内置的Sun Shaft方案是基于后处理的。
最常见的方案是将游戏内的太阳(主光源)作为中心,以一定步长采样光源并衰减(调整后的径向模糊 )+ Bloom。 下面摘一段Nivdia的GPU Gem原文中的算法说明:
Given the initial image, sample coordinates are generated along a ray cast from the pixel location to the screen-space light position. The light position in screen space is computed by the standard world-view-project transform and is scaled and biased to obtain coordinates in the range [-1, 1]. Successive samples L(s, , i ) in the summation of Equation 4 are scaled by both the weight constant and the exponential decay attenuation coefficients for the purpose of parameterizing control of the effect. The separation between samples' density may be adjusted and as a final control factor, the resulting combined color is scaled by a constant attenuation coefficient exposure.
另外一个方案需要计算屏幕空间遮挡(Screen-Space Occlusion),其基本思想就是用一个Pre-Pass(或者是Stencil 模板方式)将光源前的遮挡物渲染或描述出来,在最终采样光源并混合时在屏幕空间考虑遮挡物的影响。基于这些实现的方式,此方案在应对更多光源时或不同光源位置时有其相应的局限。(这里不展开了,后面会附加原文链接)
(图中b表现了预渲染出的遮挡物信息)
(图中展示了《巫师2》中基于这一方案得到的效果)
5 Volumetric Light Effects in Killzone:Shadow Fall
现在能找到的较早提出Ray Marching方案的光线散射来自《Killzone:Shadow Fall》分享的文档。为避免个人概括水平不足错失了原文的信息,这里也摘录一段基础算法的介绍:
To render the volumetric light effects, we start by rendering a shape for each light that represents the light's volume. ... For the sunlight we will render a fullscreen quad, as the volume of the sunlight covers the entire scene. We render each shape with a special volumetric light shader. For each pixel the shader will calculate the line segment of the view ray that intersects the light's volume. We then enter the ray-march loop, which takes multiple light contribution samples across this line segment. Artists can define the number of ray-march steps we take on a per-light basis. For each ray-march step the sample position is calculated, and the sample is lit using the same code we would normally use to light the geometry. When performing the lighting calculations, we assume we are lighting a completely diffuse white surface that is facing the light direction to get the maximum contribution of the light at the sample's position. Because we use the same code that is used to calculate the lighting for a regular surface, we automatically support all light features we have, such as shadow mapping, fall-off ranges, textures to define the light's color, etc. To improve rendering performance, we disabled all shadow filtering features.... Finally, all ray-march samples are added together, and the effect is rendered additively to the scene.
文章中提到了如何定义光照体积的范围,以及为何渲染每一个采样步骤时可以完全采用普通光照着色的特性(如Shadow map),最后采样结果是被累加并以叠加(Additive)方式渲染到屏幕上。这段叙述大部分思路也和前面介绍到的Ray Marching章节一致,只是提到了更多技术细节——例如日光的体积范围如何划定(原文还提到了其它光源的划定,这里没摘录);如采样shadow map时不需要考虑filtering就已经有足够好的效果,这样还可以提升性能等。
这部分引用略去了提到散射系数(Scattering Factor)的内容,这是考虑光照着色时需要考虑的细节算法内容,但这里不展开了。
(散射系数用来表现光线穿过介质时的散射程度)
一般来说光线散射效果是基于特定散射介质的,但那又是另一个话题——即散射介质本身还有一些特性,例如自发光、光线通过系数与散射系数等,例如体积雾、体积云等。
(图示的游戏中已经不仅是Sun Shaft,而是拓展到Light Shaft了,有很多光束)
结语
其实游戏史上有不少游戏,虽然不好卖或不那么好玩,但确确实实带来了图形学上的创新与突破。典型的一个例子就是我们都熟悉的育碧的《刺客信条》系列,再比如本文中提到的《杀戮地带》系列;虽然很多思想学术上早就有论文了,但恰如其时的引入工业界和民用又是另一回事了。如果听过重轻老师的《游戏帝国 第二季》,应该对这一点也能有体会。
如果一个游戏要引入太阳光照表现一个相对真实的世界,基于后处理的太阳光束已经足够使用(相匹配的效果还有镜头光斑 Lens Flare)。而如果要呈现更丰富限制更少的体积光渲染,由于要使用Ray marching,目前来说这类体积渲染效果对于移动端和低性能设备还是略显昂贵,但也不是完全不能用;国内的一些多端游戏,在PC端的采用的渲染管线技术往往是高于手机端的,就比如体积云、动态AO、大气散射等等,有些效果在手机性能更好时会逐渐下沉到手机端。
最后想谈一个21世纪对待知识信息的观念,这也是我自己近些年越来越深的感受:在这个信息爆炸的时代,人们可以很快接触到过去60年甚至更长时间积累下来的知识,这一点在图形学上就完全是如此,但肯定没能力全部细致的学完。因此,多大程度上对知识做概念性的把握,多大程度做细节原理的把握,其实这一直是一个不好把握的尺度——例如本篇谈到Ray Marching,即使其基本思想不复杂,但延伸出应用和优化中又有着无穷的细节和流变,但是人的精力又不支持把它们全部搞懂,很多时候也没有必要(因为技术演变过时,或是对个人经济状态没有贡献等原因)。
我个人的想法是,起码要把握到算法思想一层,至于更深的数学层即使没能力去把握,这也不妨碍随时对一个技术方案拿起来就能融会贯通的使用和修改,同时也不会把根本的概念都理解错误。
这个文章系列是把我个人的这种理解又精简成玩过游戏的非从业人员也能通过文字看懂的信息,我希望每一篇都是比较有效的“长求总”。
最后是一些资料的链接:
知乎上大神的一篇讲模糊的长文
Nvidia GPU Gem3书中动态模糊的篇章
Nvidia 早期的一篇分享多层ShadowMap的文档
Nvidia GPU Gem3书中多层ShadowMap的篇章
讲RayMarching比较详细的一篇剑桥大学的文档
ShaderToy中基于ray marching的经典蜗牛案例
Nvidia GPU Gem3书中对于后处理体积光散射的篇章
网络读书站引用的Volumetric Light Effects in Killzone: Shadow Fall(国内网上看到的翻译多少 有些问题,建议看看原文)
The Eurographics Association 收录的学术文章 Real-time Volumetric Lighting in Participating Media