《荒野大镖客2》作为去年最佳叙事和最佳表演的得主,在环境氛围的渲染和角色动画上都展现出了无与伦比的效果。 而它的天气系统也为游戏中的故事情节增色不少,让玩家在享受游戏的同时,也能深刻感受到第二次工业革命时代美国西部的氛围。
Red Dead Redemption 2 拥有我在次世代主机上见过的最佳画面
今年SIGGRAPH R星也来分享了Red Dead Redemption天气系统开发过程中的一些技术细节。 看完觉得干货很多,于是自己做笔记,供自己学习参考。 由于本人经验不足,加上分享中的一些技术细节与自己的项目关系较大,部分翻译和理解可能不准确,欢迎大家在评论区提出。
原文链接/s2019/slides_public_release.pptx
首先说一下游戏的整体风格目标和参考。 Red Dead Redemption 2 是原始故事的前传。 它发生在 18 世纪中叶。 当时美国西部的城镇还很少,所以自然风光成为了比赛的主体,球队也功不可没。 于是找了很多当时的山水画作为参考。
地形参考
体积云
首先,大致解释一下什么是基于物理学的体积散射模型,当光射到体积的粒子上时,一部分会被吸收或反射。 朝向相机位置散射的光子在光路上是内散射,而其他方向的光子是外散射,这也会影响其他物体表面或粒子的光照。 这两部分的算法与下图中公式中的P(Phase function)有关。
这通常被称为多重散射。 通常,为了简化模型,只考虑参与透射的外散射部分(透射率)。
对于每一个粒子,它的散射光计算可以分为Visibility和Phase function。 具体的Phase函数后面会提到。 在这里UI界面,很高兴知道它是一个概率密度函数。
透射部分的计算采用比尔定律的指数衰减函数,它描述了光通过体积介质的强度,并受体积本身的消光系数影响。
Integration是最终的光照计算公式3D道具,可以看出它包括emission、scattering和transmittance
这里有EA在Siggraph的15年分享,可以看出是同一套光照模型。
设计理念尽可能接近现实中的物理效果,实现了支持各种天气效果的系统。
整个系统的结构分为三大块。
数据模型
Cloud Map的RG通道分别存储两个云层,分别通过mask和noise map生成,同时受游戏中的天气和时间参数影响
Cloud Height Map,RGBA8 存储基于高度的形状信息来改变云和雾的高度。 Red Dead Redemption 支持层云和积云,采样也受天气参数影响。
Cloud Detail参考Horizon在2015年分享的做法,在xy和xz方向对2D位移矢量图进行两次采样,然后根据环境中的风力得到3D噪声LUT中的贴图。
贴出代码后,可以清楚的看到他们的云方式。 这里可以看到Cloud Height Lut的z通道也是用来控制噪声变化强度的。
float2 c = SampleCloudMap(ray.p);
float3 cloudLut = SampleCloudLut(altitude);
float density = smoothstep(g_CloudShape.xz + cloudLut.xy, g_CloudShape.yw + cloudLut.xy, c.xy)
float rescale(vMin, vMax, v)
{
return saturate((v-vMin)/(vMax-vMin));
}
density = rescale(noise, CloudLut.z, density);
根据云图,也可以实现这种雨汽效果
Localized Fog是通过Fog Map实现的,它像Cloud Map一样覆盖了游戏中的所有可玩区域。
Fog Map的三个通道分别存储了雾的起始高度、衰减距离和密度,产生时也受天气和时间的影响。
全局高度雾和局部雾的组合效果在屏幕上提供了良好的体积感。
局部雾的另一种实现是让艺术家在场景中放置雾体积。 基本原理是粒子配合 alpha 混合或添加剂。
渲染
他们使用了和Guerrilla一样的坚果壳结构,近处使用froxel,远处使用ray marching。 具体型号可以看他们在Eurographics 2018的分享。
Nubis:简而言之实时体积 Cloudscapes:///#ie=UTF-8&wd=/read/nubis-realtime-volumetric-cloudscapes-in-a-nutshell
Shading Model:解释一下前面的公式,P代表相位函数,L代表亮度,V代表能见度。 注意这里说的是用两束光octave来估计multi-scattering,下面的公式有详细解释。
Phase Function:描述给定波长的光被粒子以不同角度散射的现象,即光子的概率密度函数。
Red Dead Redemption 2中使用了Henyey-Greenstein相位函数(下图中的HG),g为各向异性值,存储在材质体积中。 但传统的HG只对haze等强散射体有效,不能很好地进行多重散射和后向散射,需要改进。
可以看到这里在计算phase function的时候,使用了多个HG function累加来计算multi-scattering的效果,使用weights让美工改变效果。 最后只用两个八度来累加演奏。
然而,效果仍然缺少后向散射部分,所以这里是对最终结果的假效果。
后向散射也受消光系数的影响
最终的结果很棒unity爆炸粒子效果,它会随着天气和一天中的时间而发生很大变化。
Visibility:这里主要通过shadow map获取物体的可见性。 对于云,将对主光源(太阳或月亮)的消光进行额外采样,以获得更高频率的阴影。 采样距离还受天气和时间参数的影响。
Terrain Shadow Map是通过对地形的高度图进行raymarching得到的。 得到的地图为RG16F,分别存储射线相交时检测到的地形高度和Ray Length。 然后将R通道的Intersection height与terrian shadow map进行对比,得到最终的visibility结果。 射线长度可以用来调整比较阈值,以提高软阴影效果。
Cloud Shadow Map使用ESM存储直接光照和阴影(如果你不知道什么是ESM,可以到下面的链接阅读Nvidia的这篇文章),它的深度是通过从光源到cloud,结果会被 blur 存储在 mip map 中,以避免出现锯齿。
高级软阴影贴图 /presentations/2008/GDC/GDC08_SoftShadowMapping.pdf
对于 Ambient Light,这里的 Visibility 重用了上面 cloud raymarch 的结果。 远距离 raymarch 渲染将对低分辨率抛物线纹理进行采样并执行平行缩减。 对于附近的frustum物体,直接使用烘焙的Irradiance probe filed,里面存储了计算需要的光照信息。
Local Lights的方法比较节省性能,直接通过对光簇体积进行采样得到。 可见性也是通过阴影贴图完成的。 出于性能考虑,仅使用一个HG函数来计算单次散射模型。
Rainbows的具体方法就不多说了,直接在体积中调整雨水密度即可生成(下面材质体积会说)。
Lightning(闪电,初见时理解为照亮QAQ)比较简单粗暴。 每一次闪电都会生成一个点光源到一个列表中,然后迭代计算每个点光源。
场景中的散射光源大概就是这些。
以上就是如何计算物体表面某个采样点的散射光。 接下来介绍如何管理场景中的渲染对象。
上面说了,附近的物体会被froxel管理。 一般由平截头体空间形成的体素大小为160x88x64,但在一些室内场景中会适当缩小。 最后,生成了三个裁剪空间体积。
转贴之前EA的分享,作为参考和对比
Shadow Volume采用R16F格式,存储了所有直接光照的阴影信息,包括CSM、Cloud shadow map和Terrain shadow map。 这里还使用了时间滤波来解决高频阴影细节的欠采样。
Material Volume比较大,分为两个Sub Volume:
第一卷存储散射系数和吸收系数,第二卷的R存储相函数中使用的各向异性值,G存储全局发射值(这里提到不支持每体积发射雾rendering pipeline ),B通道存储环境强度,主要用于室内ao效果,最后一个A通道是前面提到的雨密度,用于调节彩虹效果的强弱。
在 Material accumulation pass 中,对所有材料进行采样和混合。 顺序是先做alpha additive,再做alpha blend,这样更容易做一些城镇建筑的细节。 为了支持室内不同的粒子效果(如火、冒烟、爆炸等),将粒子放到最后计算。
这里也提到,所有的物质来源都是利用噪声进行耕作,并受到风的影响。
与阴影体积一样,时间过滤也用于此处的第一个子体积。
这里也提到了用风力作为运动矢量来绘制室内场景会存在泄漏问题,所以制作组保存了一个速度量级体积来解决。
最后介绍的是Scattered Light Volume,由采样材质和阴影体积产生,存储所有直射光(太阳光和Local Light)和环境光(通过采样前面提到的irradiance probe grid)的散射光强度。 用于计算累积过程中的散射和透射率。
这里没有使用Temporal,而是使用dithered lookup和TAA的方法。 主要原因是当光源移动时,会出现光源残像,而且对于一些密集的体积,相机移动时也会出现拖尾现象。
上面介绍了游戏中的整个froxel。 但由于体积有限,一些高频细节难以实现,精度不够,所以制作组选择同时使用Raymarching。
射线长度是通过与深度缓冲区、地平面和云穹顶相交来计算的。 为了减少计算开销,使用了15年地平线共享的方法。 每次与云相交,步长一分为二,然后从中点继续迭代。 重复直到达到预设的i_max次数,然后继续按照这个步长迭代。 下图也可以清楚的看到raymarching的步长。
地平线的实时体积云景:零
光线从froxel结束的位置发出,根据blue noise做offset。 目标分辨率只有屏幕分辨率的一半,但即便如此开销仍然很大,所以重建被分配到 4 帧。
下图展示了一系列的重构方法。 这个比较复杂,以后再补充。 建议阅读原文。
最后,需要将半分辨率帧拉伸到全分辨率。 使用的方法与 Scattered Light Volume 相同。 使用了抖动和TAA,可以看出效果非常好。
在最终的渲染开销统计中,raymarching确实是开销最大的部分。 因为所有的实现都是基于计算机的,所以很多可以重叠执行,也可以和宿主平台上的异步计算机很好的结合,节省带宽压力。
场景整合
天空散射的预计算参考了EA在2016年的分享,并在此基础上加入了地球阴影,提高动态时间效果。 根据时间段更新LUT,缓存在一个volume中,参与后续forward and deferred lighting的计算。
/s2015/The%2520Real-time%2520Volumetric%2520Cloudscapes%2520of%2520Horizon%2520-%2520Zero%2520Dawn%2520-%2520ARTR.pdf 中基于物理的天空、大气和云渲染
出于性能考虑,不是在每个采样点都做,而是只在raymarching后的加权深度做。 为了突出体积光效果,visibility计算被分开计算在阴影区域,结果会随着透射衰减。
渲染完所有不透明物体后,开始渲染半透明物体,利用之前得到的散射光量、raymarch、sky scattering/transmittance参与计算。 这里也会使用Cirrus云层作为密度查找unity爆炸粒子效果,否则与之前的云着色模型一致。
组合渲染在近景和远景细节方面都无可挑剔。
天空的环境光,晴天时蓝天以此实现。 使用32x32大小的探针进行挖矿,每256平方米放置一个。 生成的光照样本用于该方向的瑞利/米特散射计算。
对于Sky Irradiance probes部分的优化,visible probe采样到的光更多,但是invisible probe也不能忽略。 使用大步长进行采样,没有进行细化。
反射立方体使用低分辨率立方体贴图来存储内散射和透射率。 为了减少开销,raymarching采样部分也和sky irradiance probe一样。
水的反射是SSR、反射探头、平面反射和高度图追踪的混合体。 因为使用的分辨率不高,所以最终的开销也很低。 同样,temporal blend 也用于 raymarch 中的 trick。
最后的总结是一个非常清晰的架构思路。
对于一些未来的改进方向,我期待体积阴影和波长相关的透射率......
感谢Star R的图形和灯光团队和娜姐(她在近几年主持过SIGGRPAH
参考
这里我直接找出每个ref的链接,很好找
个人想法
去年自己玩《大表哥2》的时候,就被它逼真的自然环境渲染惊艳到,经常在路上做任务的时候停下来看看风景。 今年Siggrpah得知他们要来分享,立马下载了Slide分析他们的做法,看了之后还做了很多笔记,于是想着发出去分享,或许也有一定的参考价值给别人。 但是由于本人水平有限,感觉有些细节的理解很不准确。 也欢迎大家看完原分享后有什么不同的地方指出来让我指正。
来源知乎作者:佛罗伦萨木笛·育碧(Ubisoft)·技术美工