今天开始整理R星在《荒野大镖客2》中的大气渲染技术:R星在《荒野大镖客2》中用一套统一的方法体系实现了众多的volumetric效果(体积云、体积雾、体积光以及大气散射),本文主要介绍实现细节以及过程中遇到的一些问题。主要材料为Siggraph 2019上面的陈述PPT,由于光看PPT会有不甚清楚的地方,因此会参考网上其他同学的见解。
《荒野大镖客2》(以下简称RDR2)是一个开放世界的游戏,游戏设定中有动态的天气变化与24小时昼夜的轮转的需求,而这些需求所包含的效果实现中有很多地方需要使用volumetric-based的方法,RDR2在本文中给出的实现方式可以在主机上保持30fps的帧率。
RDR2的故事发生在世纪之交的荒野,地图中包含多个具有鲜明特征的气候区域,因此整个世界的渲染中,户外自然光照是其中的关键,而天空又对户外光照的效果有着至关重要的影响。
在之前的同系列游戏中,天空的渲染已经是一套完整的方案了,而在接下来的续集中,R星尝试针对当代主机配置给出一套更优秀的解决方案。
美术风格的形成受到了对应时代美国的几个风景艺术家的影响
如图所示,大块的云团跟环境存在穿插,并对光照的构成产生了强烈影响。
其中包含了很多体积效果:云层,雾效,以及light shafts(丁达尔效应)
先来看下这些体积效果背后的一些物理基础知识:
在所有进入到人眼或者说相机的光线中,这里只考虑单次散射的部分。光线(比如太阳光)在大气中传播,如图中的明黄色箭头所示,以大气介质中的一个粒子为例,假如这个粒子正好处于光线传播的路径之上,那么这个粒子对于光线的散射作用就会影响到光照进入到人眼后的效果。打到粒子上的光线,首先会被吸收一部分(这一部分会转化成其他能量,比如热能),用红色小圆圈表示,剩余的部分则会被散射开来。如果散射光线中存在着朝向相机的部分的话,那么这部分就会被计入到光线的in-scattering中,对应于上图中水平的橙色箭头,剩下的部分则会继续传播并与其他粒子或者表面再次产生散射交互,这种发生多次碰撞散射的特性被称之为multi-scattering,multi-scattering比较复杂,想要解析出这部分数据会比较困难,因此RDR2只考虑单次散射的作用。
下面对一些基本公式进行解释:
散射光强Scattered Light需要统计所有光源的影响,因此需要对光源作用进行累加。而每个光源的贡献则可以用三项来表示:P&V&L,其中L表示的是光源光线在散射点处的光强,V表示散射点处是否能够接受到光强(比如遮挡关系,Visibility),而P(Phase Function,相函数)表示的是在散射点处发生散射时,朝向各个方向散射的光强的比例分布,这三者相乘的意思就是给出当前光源在散射点处散射后朝向相机的光强数值。
Transmittance可以看成是光线在空气中传播的衰减比例,这个衰减比例符合Beer定律,总的衰减可以分成两项,一项是absorption吸收项,另一项则是out-scattering项,两项分别对应于两个衰减系数 ,总的衰减系数则是这两项之和;根据Beer定律,衰减比例跟光线的传播距离存在着指数关系,指数等于衰减路径上各点衰减系数的积分。
因此最终人眼接收到的光强等于两部分之和:直接入射光强与散射入射光强。直接入射光强指的是观察方向上对应点的原有光强乘上对应的衰减比例;而散射入射光强则对应于观察路径上各点的散射光强的积分。积分项为散射点处的散射光强乘上传播路径上的衰减比例再乘上散射点处的散射系数,这里之所以还要乘上散射系数(其实对应于散射比例)是因为前面说过,打在粒子上的光强会有两个去向:散射&吸收。因此在计算散射光强的时候必须要将衰减部分剔除出去。
这里给出RDR2的目标:
通过一套统一的方案来实现对尽可能多种类的体积效果的模拟,即用同一套光照数据模型来兼容多种不同材质。
这套方案应该要能够实现不同尺寸的体积效果,且能够实现interior效果跟exterior效果的无缝过渡。
这套方案要能够尽可能的跟真实世界的物理模型相契合。
要能够根据设备平台的性能进行自适应调整,以满足所有平台的30fps的目标。
这里给出了前人在实现volumetric效果上的一些工作,这些工作不论是从早期的解析模型到后来更精确的raymarching实现以及最近的frustum volume方法都只能处理单一的大气效果,不太符合RDR2希望达成的第一个目标。
在这些所有实现的效果中,通过精心设计过的raymarching方法实现的云层的渲染可以算是比较完美了。
大尺度的由于瑞利散射跟米氏散射而导致大气散射效果,则可以通过LUT来得到(这里写得太模糊,后面看看能不能回过头来补充)。
这里是RDR2的实现方法,主要分成三个部分:
体积效果对应的数据模型(体积效果计算需要哪些数据,这些数据是如何表示的)
数据模型是如何在渲染中使用的(数据在渲染中是如何使用的,即与公式中的参数之间的关联规则)
如何实现场景与数据模型之间的关联(数据的填充与更新是如何完成的)
先看下数据模型,数据模型主要包括两个方面:
云层跟雾效的实现
天空的散射与transmittance
云层的渲染位置是通过云层贴图(Cloud Map)来确定,云层贴图是一张覆盖全地图俯视角贴图,包含两个通道,每个通道对应于某个高度的云层的密度。
云层贴图的生成会考虑天气跟当前时辰的影响,准确而言,就是需要根据当前的天气以及时辰参数来对遮罩贴图(mask texture)以及噪声贴图(noise texture)进行调制。具体而言,采用什么样的调制方式,文中没有给出。
这里分别给出了无云层贴图,单层云层贴图以及双层云层贴图作用下的输出结果。
云层贴图只能给出云层在XY平面上的位置,如果两个通道的云层贴图分别对应于两个固定高度的云层的话(或者按照某种规则可以计算出当前点的云层高度的话),那么这里其实就已经知道了云层的三维位置了,但是云层的形状却还是模糊不清的,因此还需要得到不同高度所对应的云层的形状,从而实现对多种不同种类的云层的建模输出。
高度到云层形状的映射是通过一个一维的LUT实现的,LUT中包含了高度的数个梯度数据,而梯度数据可以映射到cumulus云层形状以及stratus云层形状(为什么只模拟这两种云层效果,这里需要叙述一下,猜测可能是因为cumulus(积云)与stratus(层云)属于中低空云,距离相机近,容易出效果,高空云可以考虑直接使用天空盒模拟)。
虽然采样只使用一维的LUT,但考虑到多种不同天气下的采样结果应该有所变化,因此实际的LUT是一个2D贴图,每一行对应一种天气下的采样数据。运行的时候,会有一个专有的渲染pass中将当前天气的LUT数据拷贝出来,这样做是方便通过贴图混合来实现多种不同天气之间的过渡。
跟Schneider15一样,云层的高频细节数据则是根据前面得到的云层贴图&LUT等低频数据按照一定算法雕刻得到。
具体的做法则是在采样点的xy位置以及xz位置分别对2D的displacement vector贴图(这个贴图是用于实现displacement mapping的普通贴图,数据作用跟法线贴图类似)进行一次采样,得到两个displacement vector,根据这两个vector可以求得一个准3D向量(实现过程见下面的伪代码),之后会用这个向量以及风力向量来对3D的噪声volume贴图进行偏移计算出对应点的噪声数值,并据此计算云层的外部细节。
float3 TimeAccumWindVector = float3(566.36108, 523.53986, -100.18636);
float fDisplacementScale = 0.0005;
float3 SamplePos = (BaseSamplePos + TimeAccumWindVector) * fDisplacementScale;
float3 DisplacementDetailxy = textureLod(sampler2D(DisplacementTex, SamplerState), SamplePos.xy, 0.0).xyz;
float3 DisplacementDetailxz = textureLod(sampler2D(DisplacementTex, SamplerState), SamplePos.xz, 0.0).xyz;
float3 DisplacementDetail = (DisplacementDetailxy * 2.0 - 1.0) + (DisplacementDetailxz * 2.0 - 1.0);
float3 PWNoiseUVW = (BaseSamplePos + TimeAccumWindVector * 1.5) * PWNoiseUVScale + DisplacementDetail * (fDisplacementOffset + fDisplacementScale * DisplacementHeightMapping);
这是只包含云层基础形状的效果图,图中给出了实现算法的shader代码:
首先对射线与云层贴图进行相交检测,这个过程通过SampleCloudMap来实现(猜测应该是使用Ray Marching完成),因为云层贴图有两个通道,因此得到一个2维的输出。
之后根据当前采样点的海拔数据对LUT进行采样,前面说到,LUT存储的是高度的数个梯度数据,从这个输出来看,梯度数据包含三个分量(不知道这仨怎么用的)。
最后通过Hermite插值计算云层的基础形状:输入的参数中有一个4D的全局变量g_CloudShape,全局变量中的xy分量对应的是第一层的数据,zw分量对应的则是第二层的数据,分别对应着云层形状分布的一个范围;插值的输入为全局变量加上高度梯度数据,插值的权重变量为云层贴图采样得到的云层密度分布数据;输出的结果应该是两个云层的密度数据。具体流程大概是这个样子的:
- 首先根据全局变量以及当前点的高度,分别计算出两层cloud对应的height fraction
- 根据两个height fraction数值,采样LUT,得到两层cloud的LUT density
- 对当前点的cloud mask数值(2D)进行计算转换为1D数值,以这个数值作为权重对LUT Density进行混合,得到当前点的cloud density。
这里给出云层细节的实现效果以及相应的实现代码:
细节通过一个rescale函数雕刻得到,rescale函数会对此前计算得到的基本云层形状的密度数据进行加工,根据噪声数据以及CloudLut.z进行缩放,其本质上是一个linstep或者inverse lerp函数,目的是将输入转换为[0,1]的输出,越接近于vMin则输出越趋近于0,反之越趋近于1,其中噪声数据用于控制什么范围的density会被降低到0,从而达到镂空雕刻的目的,而CloudLut.z用于控制噪声的软化程度,这个数值越大,得到的密度就越小,云层就越稀薄。
这是使用了displacement vector对噪声进行干扰之后的输出效果(没有给出displacement vector+wind offset对于noise的具体作用方式,即代码)。
此外,在需要降雨的时候,还可以根据云层贴图的密度计算出可能的降雨的位置,并将rain fog特效摆放到对应的位置上,当然,在放置的时候还需要根据cloud的高度位置对rain fog的高度进行调整。
局部雾效(这里的局部指的是玩家周边可以看到的,而非扫视全图的时候的雾效)可以通过查表实现,类似此前cloud map的处理,不过此时对应的是fog map,且只覆盖当前场景中玩家可能会涉及到的一些区域(从前后描述来看,应该是全局地图的可能性比较大,即用一张512的贴图实现8KM*8KM的地图区域)
Fog map的分辨率为512x512,由三个通道组成,分别对应fog start height, falloff distance以及fog density。
Fog map的生成则是噪声贴图在天气以及TOD参数作用下的结果,因此可以实现早晨水面上的雾气随着时间逐渐消散的效果。从这里的描述可以看到,fog map是会随着游戏的需要,在参数变化的时候进行数据更新的。
这里给出的fog效果图,分别对应于多种不同输入。
Fog map是全局数据,与之对应的是局部数据,通常通过fog volume实现。
fog volume的生成位置可以通过美术同学手动放置的方式来指定,也可以通过绑定到对应的粒子或者物件上的方式来指定。
fog volume的体积形状通常为球形或者方形,混合方式通常为Additive、Alpha blending。
假设当前fragment shader计算得到的颜色值为(comp_color, comp_alpha),而此前像素的输出颜色值为(frag_color, frga_alpha),那么:
Alpha Blending: comp_color * comp_alpha + frag_color * (1- comp_alpha)
Additive Blending: comp_color * comp_alpha + frag_color
二者的区别在于,是否需要对原有的像素颜色进行alpha衰减,从这个角度来看,alpha blending中的当前计算输出结果占比要高于additive blending,即Additive Blending作用下,背景会更通透些。参考资料
这是局部体积雾在室内观察的实现效果,此时的雾效采用的是alpha blending的方式实现。窗户外的雾效实现采用的是环境雾(environmental fog),而室内渲染想要出效果就需要使用大气散射实现,这两者的结合与过度是通过一个alpha volume完成的。
而这是雾效在户外的效果展示,此时的雾效通过Additive的方式渲染。对于如温泉以及爆炸效果等特殊表现需求,通常会使用这种additive volume的方式来添加雾效。
上面说的additive混合或者blend混合,其实指的都是一个frustum切割成三维的cell(寒霜的froxel的概念),在某个像素对应的射线上的多个不同深度的cell的数据的混合方式
Rendering一章主要介绍散射光照的计算实现,以及使用frustum跟raymarching方式实现散射渲染时,RDR2采用的一些实施策略,并在后面附上相应的性能表现数据。
对于近景而言,RDR2使用了一个跟frustum平齐的volume实现数据的采样;
而对于远景,使用的则是raymarching系统。这样做的目的是兼顾表现跟效率(frustum精度高消耗高,raymarching通过调整step可以实现高效渲染)
而不论是近景还是远景,这两套方案对应的光照以及数据模型都是相同的。
RDR2采用的shading model只考虑光路采样点上的单次散射,且为了简化考虑,光线的transimittance是wave-length independent的。
多次散射效果则是仿照Wrenninge13的研究结果使用两个直接光照octave来模拟。
具体是如何实现的呢?根据参考文献[1]中的说法,为了模拟multiscattering,这里的最直接的想法就是降低raymarching时从相机出发的每条射线上的每个采样点在计算不同光源的in-scattering时的衰减系数,但是这种减弱不是乘上一个固定的系数,而是通过对多盏不同的光源进行不同比例的衰减处理来实现的,具体说来就是,我们需要为每盏光源的相函数设定一个不同的各项异性参数g(),每盏光源的散射系数设定一个不同的贡献比例,最终的公式给出如下:
公式中N表示的是输入光源的阶数(octaves,即对同一盏光源,需要在几个位置对其进行采样),a表示的是衰减(attenuation),b表示的是光栅inscattering的贡献比例(contribution),c表示的是各向异性因子的衰减比例(eccentric attenuation),默认情况下,N可以取8,abc则都取1/2,在实际使用的时候都是开放给美术同学手动编辑调整的,据说可以得到不错的表现。
先来回顾一下散射光强计算公式:所有参与到散射输出的光源,在经过相函数P以及可见性函数V(散射采样点处是否对于光源而言是否可见)调制之后,求和输出。
RDR2的可见性结果可以通过shadow map获取,或者通过secondary ray T进行查询(前面说过云层有两层密度贴图,对应于两层云,secondary ray指的是太阳光在第二层云层处投射的射线)
RDR2的相函数的选取兼顾了效果以及性能,力图采用低消耗的相函数实现对高消耗散射效果的逼近。
最为流行的Henyey-Greenstein相函数,对于那些偏向于前向散射的媒介,比如薄雾会有比较好的效果,而对于云层这种需要考虑multi-scattering以及后向散射的介质,就做不到很好的模拟了。
因此,RDR2的相函数由多阶使用不同参数g的HG相函数加权而成,在每次迭代中,用于表征各向异性的参数g的值会逐渐减小,参数g对于相函数的影响是?这种多次迭代的目的是?
之后使用美术同学手动编辑过的权重值加上材质的衰减系数来对前面说到的多阶HG函数进行加权平均。
得到的相函数能够很好的给出前向散射的表现,不过对于后向散射还需要想其他办法。
RDR2给出的是一个并不物理的解决方案,基于Lambertian BRDF计算公式,将extinction clamp到,公式给出如上,整个后向散射的效果是在材质衰减属性的基础上通过主观对参数进行调制而成。
这里给出了相函数可视化效果展示,0阶相函数用红色表示,1阶用绿色表示,后向clamp结果用蓝色表示。这里给出了四种不同的体积效果所对应的多阶HG相函数叠加后的表现示意图:薄雾,浓雾,云层,烟雾。
0阶相函数作用下的云层效果。
0+1阶相函数作用下的效果。
添加了后向散射的云层效果。
对于散射方程中的第二项参数:可见性Visibility,RDR2是通过shadow map采样得到的,这里的Shadow Map包含了CSM,Local Light Shadow Map,Cloud Shadow Map以及Terrain Shadow Map等多张贴图数据,对于不同的Shadow Map对应于不同的光源信息,散射的求取也确实需要考虑不同光源的影响,因此对于每个光源都应该单独处理。
RDR2在云层材质渲染的时候,会沿着朝向太阳或者月亮的方向进行单次衰减数据lookup采样,以获取高频阴影信息,采样点的距离则是由天气参数控制的,以实现不同尺寸的云层形状的调制。
通过对地表高度图进行raymarching采样,可以求得地表的阴影贴图(获取地表阴影的目的是?为了方便后续判定当前点是否处于阴影中。为什么要使用raymarching而不是直接使用shadow map?因为,这里需要的不仅仅是被光点亮与否,还需要求取距离遮挡物的距离以及对应点的高度。),在这个过程中,会将raymarching的碰撞点的地表高度跟ray的长度存储下来,结果存在一张128*128的双通道贴图中,如右下角的贴图所示。
之后在判定某点是否被阴影覆盖的时候,只需要将shadow map上对应点的地表高度(为什么需要存储交点的高度?这里说的交点的高度,指的是交点处到其最高点的距离,可以用于后面进行对应地表位置上空的其他点是否会被地表阴影遮蔽的判断)取出来跟当前点的高度做对比就可以了
而之前存储的ray长度则可以用来对阴影进行软化处理
Cloud shadow map存储的是云层相对于太阳光的ESM
而ESM的深度数据是通过对从太阳出发的光线进行raymarching而得到,得到的深度数据会经过Transmittance的权重调制,整张Cloud Shadow Map的分辨率为768*768,共有6级mip
之后对shadow map进行模糊处理,结合mip采样处理来降低锯齿,抖动以及crawling异常效果。
对于环境光,正常的做法是对所有方向的transmittance数据进行采样并平均,不过这种方法消耗过高,RDR2使用的是对方向光(太阳光)在云层上的secondary ray sample(第二个射线采样点)进行重用(也就是对前面说到的第二张云层密度贴图进行lookup)的方式来降低消耗。
环境光可以从如下两个来源采样获得:
1.使用一个求取瑞利散射与米氏散射的加权平均的raymarcher获得,这个方式计算得到的数据会被渲染到一个低分辨率的paraboloid贴图上,且在这个贴图上会使用parallel reduction(并行计算优化?)
2.对整个frustum可以采样一个irradiance probe field(照度场),这个field会包含天光数据,以及经过大尺寸AO数据调制后的光线在其中的散射情况
无环境光下的表现
使用散射方式求取的天光paraboloid作为环境光来源时候的表现
使用Irradiance Probe + AO数据时的环境光表现,很明显,前者效果要好一些。
局部光源的的采样是通过light cluster volume实现,光线可见性则是通过阴影贴图求得,不再进行额外的追踪过程
对于局部光源而言,出于对性能的考虑,只使用单个的HG相函数即可。
RDR2实现的大气效果还支持彩虹效果,彩虹效果不是美术同学直接摆上去的,而是根据视角跟主光源(太阳月亮灯)的方向计算得到。
生成的彩虹将被看成是额外的光源,参与到大气散射的计算中去,光源强度取决于大气中水雾的密度参数。
闪电的实现则比较粗暴,每个strike都会被看成是一个点光源,之后会对每个采样点进行遍历,计算每个点光源对此采样点的影响
闪电对像素的光照影响则需要考虑如下要素:指数型的强度衰减,HG相函数以及简单的可见性控制(即用局部吸收来作为光照阴影的近似)。在这种计算方式下,可以保证闪电光照区域产生较多的形状与结构。
总结一下,这就是所有光源作用下得到的前向与后向散射的效果。
前面介绍的是在某个采样点上进行散射光强累加计算的算法,下面看下所有采样点的组织方式。
对于近景区间,RDR2使用的是frustum volume方法,也就是所谓的froxels方法,这种做法跟Wronski14以及Hillaire15的做法是一致的
frustum覆盖的深度区域是可以动态调节的(没有给出深度调节的依据与算法),最高可以达到160m,不过在某些情况下,比如室内,美术同学也可以将之调的比较小,以降低采样率过低导致的问题。
之后会通过三个阶段——每个阶段对应于一个volume——来完成数据的整理与搜集:方向光阴影volume,材质volume以及散射光volume,每个volume的分辨率都是160x88x64
Shadow volume的数据格式为16位浮点数,数据来源于前面提到过的CSM,Cloud Shadow & Terrain Shadow。为了降低锯齿表现,这个数据还会通过TAA进行一次滤波处理。
材质volume会被分割成两个子volume,每个子volume包含RGBA四个通道,从而可以存储8个材质参数
第一个子volume用于存储散射系数(RGB)与吸收系数(A)。
第二个子volume则用于存储:
各向异性参数g
自发光强度参数(RDR2不支持基于volume的自发光雾,而是提供了一个全局的自发光数值,之后这个数值会与每个volume的自发光强度参数来进行自发光强度的缩放)
环境光强度(主要用于内部环境的AO)
水汽密度(实现彩虹强度的缩放)
在材质accumulation pass中,会对所有的材质数据(包括全局雾,云层,fog map以及fog volume)进行采样与混合
Volume之间的混合顺序给出如下:
先进行additive的渲染
再进行alpha blend,这样做可以使得我们可以在建筑周边摆放少量的大尺寸的环境效果volume,之后用一些小尺寸的透明volume以实现一些全局alpha volume消耗很高的效果。
而为了支持可变密度边缘效果(比如火焰,爆炸以及烟雾等),会将粒子雾效volume放在最后进行。
经过这个Pass后,材质Volume的8个通道的数据就已经收集完成了(没有具体介绍实现方法呀),接下来就是进行优化。
跟shadow volume一样,这里会使用TAA来提高质量,不过仅限于散射系数以及吸收系数volume(为什么呢?另一个volume是不能还是不必?)。
另外,为了使用风力对噪声LUT进行animation控制,这里还需要使用风力速度作为reprojection的motion vector以实现对于云层等volume效果的扰动,不过这种处理方法在将数据reproject到一个外部的voxel位置的时候,就可能会导致内部效果的leaking问题。因此还需要维护一个速度幅度frustum volume,这个volume会基于alpha的透明度与粒子volume的不透明度对motion进行缩放
最后就是散射光照的volume数据,这个volume会对前面得到的shadow volume以及材质volume进行采样,并对直接光照(太阳,月亮以及局部光源)与环境光(主要对irradiance probe grid进行采样)的散射光强进行累加
之后,每个slice marching pass会对每个slice位置的in-scattering以及transmittance数据进行积分计算,得到的累加volume就可以用作前向或者延迟渲染的LUT
值得一提的是,在散射光照volume上不会进行TAA混合,因为在这个上面进行TAA不但会对动态光源产生重影,而且会在相机移动的时候对于各项异性雾效中的静态光源造成拖尾效应,因此为了保证质量,这里会对累加volume使用抖动LUT(dithered lookups)跟TAA并行的方式实现质量的提升。
这里是对frustum volume系统的总结,不过由于这些volume本身的分辨率较低,即使在TAA的作用下,想要得到高清细节也是不现实的
因此这类需要另外一个系统来接管volume系统在高清距离情景下的表现。Raymarching是一个内存消耗较低的方法,不过其迭代过程会需要较多的计算时间,因此这里可以选择舍弃一些散射效果,比如fog olume,irradiance probe以及局部光照来实现加速。
Raymarching中每条射线的长度是有限制的,其最大长度取决于深度buffer以及与地表和云顶的理论上的交点。
为了降低marching的消耗,这里仿照Schneider15对采样点的步进过程进行修正:
每次marching发生碰撞后,就会先回退至上一个采样点,并将步进长度减半进行再次marching
这个marching修正过程会有一个固定数值的迭代次数,因此最终追踪到的碰撞点距离理想点的位置不会差太远
射线的原点放置在frustum volume的最后一个slice上,使用蓝噪声进行偏移。
这里的目标分辨率是屏幕分辨率的一半,虽然很低,但是对于这里所需要的情况而言是足够了,不过在这个分辨率下,如果对于每个像素都生成一条射线依然太过昂贵,因此,这里会尝试使用更少的射线,并在后面进行一个重建过程来优化。
对于这个模式而言,会在半分辨率贴图空间中,每2x2个像素中取一个像素出来进行射线计算,之后剩下的3个像素则取用前一帧的结果。
为了避免结果相差过远,会在取用前一帧结果的时候对in-scattering和Transmittance数据进行clamp处理,clamp的范围为相邻的3x3像素组成的颜色AABB。
此外,还会通过放弃跟当前像素深度相差过大的像素的方式,来避免结果产生较大的深度断痕。这种做法对于前面颜色AABB的clamp方式起到了很好的补充,可以有效避免移动的时候产生的重影。在借用前一帧结果的时候,如果没有对应的有效数据,就会转而取用最接近的像素结果来补偿。
因此这里会考虑另外一种放置射线的方式,这种方式会尽可能的接近于真实的深度边缘,且能够保证有效的取用到上一帧的数据
首先,先来看下各个射线之间采用平均间隔的方式选取的结果,如第二帧所示,会很容易碰到这种情况:在某个像素的相邻区域,所有的射线均没能命中有效像素,这就会导致结果的闪烁。
从前面的结果看到,选用规整的采样模式来实现射线选取的方式得到的结果并不好。因此第二种尝试考虑使用间隔pattern的方式进行采样,即135等采用同一套pattern,246采用另一套pattern,得到结果如上所示。
这种方式可以有效解决前面一种pattern导致的闪烁问题。
不过这种方法在一些小尺寸的有效区域采样上,还是有问题。
如图所示,可以看到,在第3帧的时候,选取的射线将覆盖不到任何有效像素。
第三种尝试,还是使用棋盘格偏移,不过这次会同时考虑邻接像素的索引。对初始射线放置位置的深度进行整理,得到min/max值,并将这两个值跟当前射线位置处的min/max值进行比较,如果所有的邻接的射线像素采取的都是较大的深度值,那么当前射线就选取一个较小的深度值的位置,反之亦然。
采用这种模式得到的重建结果比较稳定(不过由于采样分辨率低以及clamp的原因依然会存在闪烁),不过在这个基础上还需要上采样到全分辨率贴图。
这个过程是通过对半分辨率贴图采用4taps的方式进行蓝噪声dithering得到。Dithering会根据情况选择对taps选用双线性滤波或者近邻滤波。
这个temporal模糊的kernel半径的选取会根据当前射线像素的Transmittance加权深度计算得到。这样做的原因是为了避免远距离像素位置的不必要的模糊处理,因为在远距离位置本身就比较模糊,还是希望保留更多的细节。
在对in-scattering、Transmittance累加volume进行LUT采样的时候也会采用相同的原则。
性能表现跟所使用的数据,场景的深度以及相机朝向都有很大关系,整个系统中消耗最高的就是raymarching,这个过程是通过shader loop完成的,因此消耗取决于射线长度以及迭代次数(采样点数)。因此,射线raymarching会根据需要及时结束,分阶段释放所需要的资源消耗知道整个过程结束。而由于当前的解决方案完全是基于计算的(compute-based),因此可以交叉分配raymarching跟frustum volume线程,还可以在部分pass上使用异步计算。
此外,由于方向光的阴影不会频繁变化,因此对于cloud的阴影贴图,每次只对4x4像素快中的一个射线进行更新;对于地表阴影贴图则借用TAA的方式来对效果进行美化
下面来看下如何将渲染结果跟主场景结合起来,尤其是如何对于环境光方向光以及反射效果造成影响
这里给出的天空散射方案是基于Bruneton07的预计算大气散射方案(Precomputed Atmospheric Scattering),Hillaire16中实现的臭氧层参数也是采用类似的思想实现的。
不过RDR2认为地球阴影对于开放世界TOD的效果提升比较显著,因此还支持了地球阴影。
LUT的会根据时间片来进行更新。天空散射以及transmittance数据会被缓存在另一个跟frustum一致的volume中,可以同时用于前向跟延迟渲染。
出于对性能的考虑,不会对每个采样点都进行天空散射的采样计算,作为近似,会在raymarching之后针对经由transmittance加权后计算得到的depth单独进行一次采样计算即可。
此外,为了支持light shaft效果,RDR2还将可见性项Visibility从天空散射积分中分离出来。在Raymarching过程中,会对通过对阴影贴图进行lookup而得到的方向光的light可见性进行累加,并根据当前采样点覆盖的面积以及当前射线对应的transmittance对这个累加项进行加权。射线raymarching终止后,再根据射线的全长度对该值进行归一化处理,之后使用这个归一化后的数值来对天空散射数值进行调制。
虽然这种做法并不符合物理规律,不过可以用较低的消耗给出比较令人满意的结果。
通过transmittance加权后的深度图。
通过累加,加权,归一化后得到的图像可见性效果图。
最终的组合结果。
到此为止,用于计算最终的大气散射的所有参数都已经准备就绪了。
在所有不透明物体绘制完成之后,透明物体绘制之前,就需要将in-scattering、transmittance volume还有raymarching结果跟天空散射等数据应用到场景里。
在高海拔区域,RDR2会添加一层卷云效果,这个效果会需要根据密度进行一次2D贴图采样,而光照模型则是跟之前描述的一样。
无云层条件下的表现效果。
添加了通过2D贴图采样得到的卷云表现效果。
添加了Raymarching得到的大气散射效果(远景)后的表现。
添加了近景Volume后的表现效果。
添加了天空散射、衰减后的表现效果。
通过前面的效果图可以看到,云层跟雾效对于主场景的影响还是很显著的,而这两者对于全局光的依赖都比较高,因此有必要加入全局光照的考虑。
而要实现这个效果,就需要维护32x32个probe,每个probe都会被编码成三阶的SH,probe会按照均匀网格的方式放置,放置的位置会同时对高度图以及弯曲的法线贴图(bent normal texture)进行对高度跟朝向进行采样
每个probe之后会投出射线(投射的射线会考虑表面梯度以及遮挡物的限制),在这些射线raymarch过程中进行in-scattering&transmittance的累加计算。
计算得到的结果会被用来计算对应方向在天穹上的瑞利、米氏散射项(结果会存成低分辨率praboloid)。
这种做法可以在洁净天气下实现蓝天的面光源效果,也可以实现阴天从云体中产生的间接光效果。
这里是效果图展示,注意观察图中从云层中裂开缝隙中透露出来的白色亮光以及云层跟山体接触区域的阴影。
这套probes为后续实现高分辨率irradiance probe field做好了铺垫。
为了对这个过程进行加速,RDR2还单独设计了一个分类pass,在这个pass中会对主场景的数据可见性进行计算,可见的probe会分配更多的射线,不可见的probe也不能完全不管,因为反射计算可能会需要用到这些数据。
射线的计算会通过多线程完成,每个线程负责一条射线,射线在半球上按照Hammersley排布,之后在共享内存中调用并行的reduction操作以生成SH系数。
Raymarch过程也是经过下述简化的,因此作实际上消耗不算很高,在PS4上大概是0.22ms左右:
移除高频噪声侵蚀过程
移除彩虹与闪电计算
Raymarch使用大步长进行,且不做碰撞后回撤再半分step的精修
计算的结果能够实现对环境光的模拟,不过这里还需要考虑对于方向光的影响。
对于可见性的计算,不需要再增加额外的raymarch射线,可以直接通过云层的阴影贴图来获得。
对于反射实现而言,可能还会需要添加额外的射线raymarch操作。
对于相机位置反射probe而言,只需要生成一个包含in-scattering&transmittance数据的低分辨率cubemap即可,因而可以跟天空的irradiance probe计算一样,直接使用简化版的raymarch来降低消耗。
对于水面的反射而言,可以进行一遍不带alpha以及volumetrics效果(这是什么意思?)的SSR采样,因为SSR采样得到的数据本身已经包含了当前相机位置到反射深度位置处的in-scattering&transmittance数据,因此这种做法可以避免上述数据被叠加两次
这里会使用另外的一套射线来计算in-scattering&transmittance数据(这是因为反射直接采样得到的数据跟通过反射得到的数据应该存在一定差异?),这个计算同样使用的是简化版的raymarch,且会通过TAA进行混合。
这种水面反射的实现方法还有一个效果,那就是可以得到正确的反射角度(反射角度指的是?),从而避免SSR方法中常见的边缘位置异常表现(具体一点呢?)
而且由于分辨率较低,因此整个过程消耗也比较低。