【Siggraph 2016】Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite

临江仙
[明代][杨慎]
滚滚长江东逝水,浪花淘尽英雄。是非成败转头空。青山依旧在,几度夕阳红。
白发渔樵江渚上,惯看秋月春风。一壶浊酒喜相逢。古今多少事,都付笑谈中。

今天来介绍一下寒霜引擎工作室在Siggraph 2016年关于大气与天空渲染的实现方案,原文PPT地址。

寒霜引擎在2014年完成PBR升级,其画质得到了极大提升,图中给出的两张图片,一张是实拍,一张是PBR渲染效果,可以看到渲染结果基本上可以以假乱真。



然而即使这样逼真的物理渲染效果,依然存在着许多问题,比如环境光照依然是一些静态效果经过渐变后得到的:

  • 天空渲染,通常是通过HDRI贴图作为天空盒,或者与一个渐变的天空颜色系统相结合来实现
  • 大气效果的实现,则是通过深度/高度雾以及可选的light shaft效果来完成
    -云层则是使用高分辨率的平面贴图或者全景(panoramic)贴图来完成,motion flow作为可选项。
    这些方式得到的天空效果并不是不好,不过想要实现如现实世界中一样的随着时间从早晨到晚上的动态天空变化就比较困难了,而且为了使得上面的各个元素的效果能够逻辑自洽也需要比较多的人力成本。

寒霜想要实现上图一样的效果:

  • 如物理一般真实的天空效果
  • 能够跟随时辰而动态变化的天空渲染效果(从图中看不出来)
  • 跟随太阳位置而变化的光照效果
  • 能够与当前大气环境相匹配的云层表现
  • 可能还要有能够完美自洽的动态天气效果

关于实现这样一套天空系统,想要得到的愿景包括以下几点:

  • 帧率不得低于60
  • 渲染结果必须是PBR的,包括以下两点:
    • 物体材质不能跟光照存在耦合关系,即不能将物体材质固定在某一种光照情况之下,而需要能够跟随光照变化实时的自然的变化
    • 物体材质需要完全按照自然界真实的数据参数来设定,比如光照强度等都应该与实际测试所得数值保持一致
  • 需要保证各个组件之间的交互是相互一致的:
    • 云层加厚应该要能影响到大其散射的效果
    • 场景中透明和不透明物件变化,可能会导致全局光照的变化

这里放了一个《镜之边缘-催化剂》的视频,在视频中,随着时间的变化,太阳位置也相应发生变化,与之相对应的变化还包括场景中的光照效果,天空的颜色,云层,全局光照,light shaft效果等,这就是寒霜引擎所表述的PBR天空效果。

这个视频是Bioware所实现的动态云层变化的尝试效果。

PBR天空的实现,总体来说可以分成如图所示的四大块:

  • 天空
  • 大气
  • 太阳
  • 体积云

先来看一下天空跟大气渲染的一些实现思路。

Rayleigh散射
Mie散射

在这里,将大气层当成一个覆盖在地球周边的球状穹顶,其中的大气粒子密度随着海拔的递增按照指数形式衰减。

光照与半径小于光照波长的粒子的交互可以通过Rayleigh散射来模拟(白天天空呈现蓝色,因为Rayleigh散射与波长的四次方成反比,因此蓝紫光的散射强度较高,早晨跟傍晚呈现橘红色,因为此时太阳光在大气层中传播路径较长,短波都散射掉了,只剩下长波直达人眼);

半径远大于波长的粒子与光波的交互可以通过Mie散射来模拟,这类粒子大多聚集在近地位置,其出现的原因大多与天气有关,比如雾霾天气,比如雨天,沙尘暴等。Mie散射可以看成是跟光波波长无关(实际上还是有关系的,不过基本可以忽略),发生Mie散射后的相函数在光线传播路径方向上有较高强度,而不是以散射粒子为中心均匀分布,太阳周边的光晕就是Mie散射后的结果。

具体应该采用哪种散射模型,可以根据如下的公式来计算:

  • x << 1, Rayleigh 散射
  • x ~= 1, Mie 散射
    x >> 1, Geometry散射(这个可以看成是反射/折射了,彩虹的出现就源于此)

关于大气散射,目前主要有以下几类实现方法:

  • 解析模型,这类方法的限制在于只支持近地散射,不支持外太空散射,或者其参数调整存在限制等
  • 光谱模型,精度较高,但是消耗同样也高,即使现代硬件平台想要硬撑也很困难
  • 天空大气近似模型,Bruneton尝试将大气散射中的大部分数据通过预处理的方式生成到贴图中,在运行时直接采样实现实时渲染,这种方式效率高,效果好,能够支持multiple scattering,关于这种方法还有一些变种,比如Elek给出的方法以放弃地表投影对大气的影响来减少一维数据,而Yusov则给出另一种参数化方式来进一步提升渲染的精度。

寒霜这边选择采用Bruneton模型来实现大气效果:

  • 采用RGB渲染,而非光谱模型
  • 使用Elek的3D LUT方案,放弃地表对于大气投影的效果
  • 使用Yusov的参数化模型

这里简单介绍一下LUT的生成算法,具体可以参考Bruneton & Elek的论文。

如图所示,LUT的三个参数给出如下:

  • 相机高度h
  • 相机与zenith(头顶正上方)的连线以及与太阳中心的连线的夹角θl
  • 相机与zenith(头顶正上方)的连线与相机观察方向v的夹角θv

根据这三个参数,可以构建出transmittance贴图(用于查询从A点到B点的散射强度衰减系数)以及单次散射贴图,而多次散射贴图则可以通过对这几个参数进行重复迭代计算得到(参考Bruneton 08)。

这里统合了各个参考文献给出的一些参数数据,包括不同散射类型的散射系数Scattering(散射系数通常会跟温度以及压力有关,这里仅仅给出平均值),散射传播途径中的由于吸收与outscattering而导致的衰减系数Extinction以及大气浓度随着海拔高度变化的distribution数据。

这里还给出了OZone(臭氧层)的相关参数),臭氧层是清晨或者傍晚的时候天空依然能够保持湛蓝的主要原因(否则蓝光就被散射到太空了),这里给出的数据是通过测量得到的。

本文中的一些符号约定

另外需要注意的是,这里给出的Rayleigh散射数据并不包含全光谱的,知识给出了三原色RGB的数据(波长分别为:680, 550 and 440nm)

轻度Rayleigh散射效果
普通情况下的Rayleigh散射效果
重度Rayleigh散射效果
无Mie散射效果
普通天气下的Mie散射效果
重度污染天气的Mie散射效果
OZone Scattering Close
OZone Scattering Open

这里给出了臭氧层散射开关效果对比,通常来说臭氧层处于海平面往上32KM的位置,不过按照这个规律来计算臭氧层散射得到的效果跟预期的有出入(猜测是因为使用的是RGB三原色而非全光谱渲染的原因),因此最终在渲染的时候将其粒子分布等同于Rayleigh散射的粒子分布。

如上图所示,最终的渲染输出实现可以按照以下流程完成:
1.先渲染不透明物体
2.再渲染天空,并渲染天空的过程中从散射LUT中提取大气散射数据,并将之作用到天空各个像素的输出颜色之上

在上述渲染中,用到的散射LUT是两张3D的大气透视贴图,3D贴图的覆盖范围都正好契合进相机的Frustum,如上图所示,每张贴图的分辨率为32x32x16,其中散射贴图存储的对应位置的散射光照亮度数据,而衰减贴图存储的则是从当前点到相机这段距离的传输衰减系数。

这里需要注意的是,此处存储的散射数据是未曾考虑散射遮挡情况的,因此其实现效果中是不支持light shaft的,要想实现效果令人满意的light shaft,需要增加的计算量较高,性价比太低。

大气散射数据的计算是在渲染天空的阶段进行的,对于不透明物体,这个数据是在ps中叠加到渲染结果中的,而对于透明物体上的大气散射则是在vs中叠加上去的。这样做的好处在于,既能让透明物体的渲染效果能够跟整体场景相一致而不至于太突兀,又不会因此导致较高的消耗。

这里放了一个太阳位置随着时间而变化的动态效果视频,在这个视频中,随着太阳位置的变化,大气散射效果,天空环境贴图,局部反射贴图以及动态GI效果也会相应变化。

这里来介绍一下这个实现方案的性能消耗情况,总的来说,一旦LUT贴图生成完成了,太阳位置就可以自由移动而不需要考虑新的消耗(太阳位置移动不是会导致阳光方向发生变化么,这个参数的变化难道不会触发LUT的更新吗),只是当LUT的参数发生变化时,才需要触发LUT的更新,而LUT的更新是一个消耗比较高的操作。

为了降低LUT更新的消耗,这里会将整个LUT的更新消耗分散到多帧完成,甚至可以在原LUT贴图上进行,每帧只进行部分区域的更新,输出的结果在相邻数据插值即可,得到的效果还算平滑。通过这样处理之后,LUT更新的消耗基本上就可以接受了。

太阳渲染

前面说到,美术同学通常只需要给出地表处的阳光照度,之后程序这边根据头顶正上方光照的衰减系数以及太阳的solid angle,就可以计算出太阳在外太空的亮度数据。

得到太阳在外太空的亮度数据之后,就可以计算出当前时刻任意地表的阳光照度数据。

这里假设太阳在天空中的尺寸非常之小,在这个假设下,可以用一个与solid angle的简单点乘来近似模拟solid angle范围内的积分操作。

太阳亮度数据与照度数据可以通过如下过程计算得到:
首先计算出头顶正上方的大气层衰减系数,之后将这个衰减系数以及solid angle代入到公式1来近似求得外太空的阳光亮度数据:

之后计算出太阳中心位置到当前地表的衰减系数,将之代入到公式2,求得当前地表位置的光照照度数据:

拿到太阳在外太空的亮度数据之后,就可以据此绘制太阳disk效果了,不过如果不考虑大气对于太阳光线传播过程中的衰减的话,绘制得到的效果就会过亮,如上图所示。

这个问题也很好解决,只需要对太阳disk上的每个像素的照度数据乘上对应的衰减系数即可。

体积云实现

目前业界有比较多的实现体积云的算法:
Harris给出的实现是公告板+粒子系统的方式,这种算法性能较高,不过其效果已经无法满足当代游戏的要求了。

Bouthors给出的是一种raymarching+高精度贴图+网格模型的混合实现算法,这种算法得到的结果具有较高的精度,不过性能消耗相应也不低,此外这种算法给出的参数调整起来不是很方便。

Yusov给出的是一种基于粒子系统的实现算法,这种算法会预先计算出粒子元素上的散射积分结果并存储下来,在运行的时候根据参数进行采样即可。这种算法的不足在于其只能实现特定风格的云层效果,而不能根据需要实现各种不同风格的云层效果。

Schneider在15年提出的基于分层的raymarching实现算法(地平线零所使用的算法)可以实时根据天气情况以及空间位置生成动态的云层效果,寒霜引擎的云层实现的基础就是这种算法。

寒霜引擎在Schneider算法的基础上做了一些改进,主要包括以下一些细节的修正:光照,散射能量守恒,动态天气调整以及随着一天内时辰而变化。

Transmittance
Ambient
Sun Scattering
Cloud self-Shadow
Phase function
Reality Comparison

云层的实现效果主要基于物理的散射,吸收以及相函数等材质参数来控制调整,其实现的效果与下面几项细分参数之间的关系用文字阐述如下:
Transmittance用于给出云层后的遮挡情况(Occlusion of what is behind cloud)
Ambient lighting(比如天光等)将会与云层产生散射交互
Light scattering,太阳光导致的大气散射效果也需要考虑进去,不过单只加上这一项,其效果可能不佳,还需要考虑由于衰减而导致的云层自阴影(cloud self shadow)影响(参考Hillaire15)
Phase function,云层上发生的大气散射效果并不是沿着各个方向均匀发生的,而是带有一定的方向性,通常沿着太阳光方向的散射强度会高一点,从而导致通常我们说的silver lining效果。

这里给出了Cloud Ray marching实现代码,Ray marching的输出结果正确的应该是将散射数据与衰减数据相乘并沿着ray marching路径进行积分,不过在shader中是不可能积分的,因此改成累加,这里给出的算法是将两个ray marching点之间的step范围的散射数据用一个常量来近似,不过如果采用直接相乘的方式其实是不正确的,上面给出的积分公式就可以看出,在当前step范围内的积分结果并不等于衰减系数与端点散射强度的乘积。这里有个ShaderToy 的Demo可以用来验证这个观点。

这里给出错误计算方法的效果图,因为并非能量守恒,因此对于Ambient的散射来说,其结果尤其怪异,可以看到效果底部数据过于明亮了。

这里给出的是另一种错误实现方式,这种方式先计算了衰减系数,再做乘法,得到的效果整体偏暗,且云层看起来很脏。

这是按照前面推导的积分公式计算得到的效果,其效果满足能量守恒的定律,看起来就比较正常了。

上面的效果图说明了,错误的算法在使用较多的采样点的时候可以逐渐逼近正确的输出结果。

云层渲染中一个非常重要的元素就是光照散射在云层上的相函数,正确的相函数是实现各种真实感效果(silver lining,pseudo-specularity,glory haloes等)的关键,上图中蓝色部分是云层相函数的示意图。

寒霜引擎中是通过两个HG lobe来实现对Cloud Phase Function的拟合的,不过如图所示,其实现的效果在backward scattering(绿色方框标注区域)的逼近效果比较差,寒霜那边的程序同学认为是缺少了对multi scattering部分的考虑导致。

在最后,程序这边将HG lobe的控制参数暴露给美术同学,美术同学可以在编辑的时候自行调整,从而修正相函数forward scattering跟backward scattering的能量,这种做法其实也比较符合现实世界中云层的相函数是随时间与外界环境参数而变化的事实。

single forward scattering
2 lobe, backward + forward,由于部分backward scattering+energy conservation,光照强度会有所降低
single forward scattering + sun behind the camera, ambient make cloud flat
backward + forward scattering

在进行云层渲染的时候,希望能够做到不论在何种天气与环境参数下,都能够保持云层跟周边环境的一致性,上面的截图中,由于没有给云层施加大气散射计算,导致其效果跟周边环境对不上,从而看起来略微怪异。

Average Depth效果

不过如果要为云层上的每一点都进行Aerial Perspective(简称AP)计算的话,消耗又会过高,此处寒霜引擎给出了一种折中的做法,即依据对应的transimittance对Depth进行加权(加权范围为以当前Cloud像素为中心,以某个值为半径的,云层面向相机这一面,即front interface上的采样点),以求得Cloud front interface的平均depth值,并取这个depth处的AP数据作为最终输出的AP数据(包括散射数据与Transmittance数据)

Depth加权计算公式

上面给出了单次AP计算之后的Cloud效果,看起来比前面的要好多了,需要注意的是,当云层覆盖面积不断增加直至完全盖住天空,在这个过程中AP对于sky/sun的scattering数据将逐渐减少(比较合理,被盖住了,哪里来的inscattering?),而更多的需要考虑阳光穿过云层之后的光照数据。

因此这里需要将云层的覆盖率纳入AP计算的考量,不过由于云层会有天光与阳光穿透的光照输出,因此在计算AP的时候也应该将云层的散射亮度作为AP计算的光源。

在实际计算的时候,为了实现上述思路,会以相机为中心,up vector作为法线,对半球进行积分,积分的结果包括cloud coverage与cloud scattering luminance,假设阳光的phase function在各个方向都是均匀分布的,那么就可以将之前计算得到的Sun scattering数据乘上1 - cloud coverage作为Sun scattering part,之后加上cloud scattering part,作为最终总的scattering part。

transimittance数据不受上述修正影响,因为这个数据只依赖于空气中的粒子浓度与传播距离,而这两项数据都没有发生变化。

另一种实现思路则是通过cloud shadow来模拟大气层中的cloud volumetric occlusion,这种实现对于不同位置的cloud coverage数据变化较大的情况会更合理一些。

image.png

这里是实现结果,在原文给出的视频中,太阳未被遮挡前,大其散射带着阳光的橙色(Mie Scattering),被云层挡住后,就逐渐被穿过云层的天光+阳光scattering所取代,而呈现一种淡蓝色(天光),当然,如果云层足够厚的话,大气散射的效果可能会偏白(穿破云层的天光以及阳光贡献较小了)。

XBox One性能表现

下面介绍一下随着太阳升起时最终的天空效果表现

image.png

这里用寒霜的大气散射模型模拟了一下火星的天空效果,从视频中可以看到火星上红色的天空与蓝色的日落效果,说明这个模型还是比较成功的。

结论
生效产品
Future Work

你可能感兴趣的:(【Siggraph 2016】Physically Based Sky, Atmosphere and Cloud Rendering in Frostbite)