03 - 地形、大气和云的渲染
本文介绍了一些有关游戏中自然场景(地形、大气和云等)的渲染,力求真实感。
地形渲染[几何向]
地形绘制(Terrain)是一些大世界游戏的重中之重,通常分为两类:渲染地球上的地形;非地球上“真实”的地形。例如下图的微软飞行模拟和无人深空。
用高度贴图渲染地形
可以用有高度(Height)或等高线(Contour)信息的贴图来渲染地形,这种贴图也称 高度贴图(Heightfield)。高度贴图往往有“分形”性质,因此可以通过计算机程序化生成。
具体步骤如下:
- 给一片Mesh均匀划分网格,然后用相关信息对每一个顶点做变换。
缺点:不是很好表达开放大世界的地形,可用LOD优化。
优化:
Adaptive Mesh Tessellation:由于真正能被看到的是视椎体内和近处的地形,因此视椎体内的地形三角形应该细分,看不到&远处的地形三角形应该合并。
Error bound:在进行曲面细分/合并时,因此造成的地形误差应不超过一定的阈值(在观察空间上)。
曲面细分
基于三角形的曲面细分
这是最简单的一种曲面细分,由于这样子划分的结构像二叉树,因此也叫Binary Triangle-Based Subdivision:
在划分时要避免 T-Junctions 现象,即在相邻网格中,如果两者的划分程度差异过大,那么可能会造成“白边”现象:
因此相邻网格的划分程度差异不要太大了。
基于四叉树的曲面细分
这是最常用的一种曲面细分:
优点:
- 容易构建;
- 容易管理,数据读写符合硬盘处理方式(块读写);
缺点:
- 曲面细分不像三角形划分那样灵活;
- 网格等级的叶子结点需要持久化;
可以使用吸附法(Stitching)将多余的顶点吸附到其他顶点上,即退化三角形,以避免T-Junctions现象:
不规则网格表达地形
如果觉得均匀划分的网格浪费图元,也能采用 不规则网格(Triangulated Irregular Network, TIN) 来表达地形。它将相对平坦的区域用大块三角形简化,将顶点和地形特征(山脚、河边等对齐)。
该方法和上面Adaptive Tessellation方法的对比如下:
优点是容易运行时渲染,且在特定地形下的三角形比较小;缺点是需要一定的预处理,且可重用性低。
基于GPU的曲面细分
即使用硬件进行曲面细分:
早期使用GPU曲面细分,需要手写Hull-Shader
,然后硬件在Tessellator
阶段处理,最后手写Domain-Shader
才能曲面细分:
现在有了Mesh Shader
管线的概念,GPU曲面细分就更方便了,但需要DX12及以上的Win10系统:
实时动态地形
如何实现实时动态地形(Real-Time Deformable Terrain)也很重要,例如上图拖拉机的车痕,让游戏看起来更加真实。
非基于高度的地形
此外还有非基于高度的地形,例如山洞、隧道等。目前主要有两种渲染方法:给地形开洞,体素化地形。
给地形开洞
在顶点着色器中标记一些顶点是否为洞,这样它周围的三角形就不会被渲染,产生“退化”,此时洞为有锯齿的洞。最后再套一个空心半圆柱模型挡住锯齿即可。
体素化地形
这是很少用到的方法,它使用 Marching Cubes 算法为基础,将场景体素化,进而得到带洞的地形。
地形渲染[美术向]
有了真实地形的轮廓后,就需要给它上材质了。
使用PBR材质
可以使用PBR材质来渲染地形,但存在一些问题:如果用简单的alpha混合,会出现平滑过渡但一点都不真实的情况。可以用Height
贴图进行加权混合,选择高度高的材质作为最终混合的结果。此外,在画面移动时可能会出现抖动,可以添加一个bias
进行过渡差值。
材质泼溅技术
上面说到纹理的混合,需要用到材质泼溅技术(Texture Splatting):
从上图可以看到,纹理的过渡是不自然的,因此需要引入高度贴图Height Maps
。
此外,为了防止画面抖动,需要加上Bias
:
纹理数组
如何高效管理纹理,同时采样这么多种纹理?
可以从纹理数组中采样,用一个3D纹理存储纹理数组,前二维存储单张纹理,后一维存储纹理的索引。然后根据基底纹理(该2D纹理存储需要用到纹理的索引和混合比例)进行纹理混合渲染。
视差贴图和位移贴图
此外,和地形渲染相关的纹理贴图还有 视差贴图(Parallax Mapping)和 位移贴图(Displacement Mapping),如图:
视差贴图使用类似于光线步进的方法,让地表看起来比凹凸贴图(Bump Mapping)渲染出来的更真实。但代价是类光线步进方法导致的性能问题,此外边缘处的凹凸感也不太好。
位移贴图则通过着色点表面的高度信息对其纹理的采样位置进行变换,进而看到凹凸不平的表面。
该方法的缺点
该方法的缺点就是材质的采样和混合操作十分费时,且读取材质的方式(跳跃式读取)也对内存访问不太友好。
使用虚拟纹理
这种方法是现代游戏引擎所广泛使用的。
它将整个大场景“存储”到一张虚拟纹理中,实际上就是分块的场景索引。和Cache类似,它将频繁使用到的地形纹理块索引存储在页表中,然后读取基于视角LOD所需要的地形纹理数据。
此外,这些纹理数据都是被预处理(烘焙)至硬盘中的,用到的时候读取就行。如果是动态生成的话,就是实时虚拟纹理了。
内存调度算法
接下来简单看看虚拟纹理技术关于GPU显存、内存以及磁盘上的调度算法:
DirectStorage
:压缩数据从磁盘读取到内存,然后送到GPU显存,最终让显卡解压缩并使用。- DMA技术:直接将要读的数据从磁盘读到显存,前提是硬件支持。
可见第二种方法大大提高了运行效率。
浮点数精度溢出问题
随着浮点数所表示的值越来越大,会因精度不足而发生“抖动”。例如,当物体离摄像机越来越远时,该物体各顶点发生“抖动”现象越明显。
使用相对相机的渲染
可以使用相对相机的渲染技术来解决上述问题。根据物体和相机在世界空间的位置来调整物体离相机的相对距离,调整后相机的世界空间坐标为0,以避免浮点数精度溢出。
地形渲染[小装饰]
有了真实且美观的地形后,就要为空旷的地形点缀一些小装饰了,例如植被、道路、贴花等。
渲染树
有专门的LOD,离近时用高模渲染,离远时就是几张“纸片”(Billboard技术)。
渲染装饰物
要渲染草、石头等装饰物,可以用最简单的Mesh表达,也有专门的LOD。
渲染道路和贴花
- 基于样条曲线(Spline)的道路系统:在道路出现后,还要处理它周围的高度信息。例如《天际线》中的道路。
- 基于在虚拟纹理中的道路&贴花溅射系统:道路旁一般会崎岖不平,这种效果可通过贴花来实现。贴花Decal有法向凹凸效果,可用视差贴图实现。例如《战地》向墙开枪后,有弹坑。
有了这两种系统,就能规划道路,并让道路看起来更真实。
大气渲染
有了地形后,还要渲染大气和云,首先看看大气的渲染。
大气解析模型
这是大气的“Blinn-Phong”模型:
其中,\(\theta\)是观察方向和天顶的夹角;\(\gamma\)是观察方向和太阳所在方向的夹角。该方法的优点是效果不错且计算快。缺点是仅限于从地面观察大气,如果从太空观察就不行;参数是写死的,不能自由改变。
大气散射理论
为了更加真实地模拟大气,需要引入大气散射理论。
参与介质
大气层主要由各种气体分子和气溶胶组成,这些物质就是影响大气中光传播的参与介质。
光与参与介质的交互
如图,光与参与介质的交互主要有四种:
- 吸收部分光(Absorption):光线在经过参与介质后,可能会被吸收一部分。其中\(\sigma_a\)是吸收常数。
- 散射部分光(Out-scattering):光线在经过参与介质后,可能会发生散射。其中\(\sigma_s\)是散射常数。
- 自发光(Emission):光线在经过参与介质后,可能会被增强。
- 吸收其他介质的散射光(In-scattering):一些经由其他介质散射的光会被参与介质吸收,并随入射光一起散射出去。
可以将这四种交互方式总结为一个 辐射传递方程(Radiative Transfer Equation, RTE),上图中的辐射传递方程是一维情况的。
体素渲染方程
体素渲染方程,即Volume Rendering Equation, VRE。描述光从M点经由一个个参与介质在大气中传播的过程与结果。
其中,\(T(x)\)是传输通透度(Transmittance),表示M点的着色结果在经由大气传播后保留了多少;\(L_i(x,\omega)\)是上面的in-scattering方程。
真实的大气物理学
接下来看看真实的大气物理学。
参与者
太阳:一切光照的贡献者,看起来是白的,实际上由各种颜色的光(波长不同)组合而成。
大气成分:气体分子,波长小于这些光的。
气溶胶分子(Aerosols):波长接近这些光的。
散射模型
主要有瑞利散射(Rayleigh Scattering)和米氏散射(Mie Scattering)两种:
- 瑞丽散射:认为散射的方向分布和参与介质的大小有关。如果介质大小越小于光波长,散射方向分布越发散。
- 米氏散射:认为气溶胶的尺寸接近或大于光波长时,散射会有一定的方向性(沿着光)。
瑞利散射
短波长的光(如蓝光)比长波长的光(如红光)散射更强烈。
可以使用瑞利散射方程来描述光在参与介质中的散射。其中:
- \(F_{Rayleigh}(\theta)\)是Phase Function,描述拟合的几何形状。\(\theta\)是散射的角度。
- \(\sigma_s^{Rayleigh}(\lambda,h)\)是散射常数,\(\lambda\)为光的波长,\(h\)为点的海拔高度。
用瑞利散射模型可以解释天为什么那么蓝。白天,红光几乎直射到地面,蓝光一直在大气中散射,因此看到蓝天。傍晚,红光斜射进来,大部分蓝光经散射又回到太空,因此看到晚霞。
米氏散射
使用米氏散射方程描述光在气溶胶中的散射:
其中:
- \(F_{Mie}(\theta)\)是Phase Function,描述拟合的几何形状。\(g\)项是几何项,当\(g=0\)时,散射方向和瑞利散射相同,当\(g>0\)时,更偏向正向;\(g<0\)时,更偏向反向。
- \(\sigma_s^{Mie}(\lambda,h)\)是散射常数。
用米氏散射可以解释“雾气”和“光晕”现象。对于光晕,由于太阳光具有方向性,经过米氏散射直接汇聚到你的眼睛。对于雾气,所有波长的光均发生散射,从而产生雾气。
大气分子的吸收
大气中的\(\mathrm{O_3}\)和\(\mathrm{CH_4}\)分子会吸收长波长的光。例如海王星是蓝色就是因为它\(\mathrm{CH_4}\)含量高,吸收了红光。
实时大气渲染
单次散射和多次散射
单次散射:光线经过参与介质,根据通透性直接得到结果。
多次散射:光线经过参与介质,要经过多方向多次散射,最后在着色点汇总得到结果。
从上图中可以看出,多次散射比单次散射效果好。例如多次散射的山背面还有颜色,单次散射就直接黑了。
求散射用到的技术
接下来以单次散射为例,看看在实时大气渲染中求散射要用到哪些技术。
光线步进
可从眼睛到太阳处射出一条光线,并让它步进,用积分算出各点的折射程度并累加,然后使用结果即可。因此可以先预计算出一张存有大气散射结果的LUT,再随用随取。
预计算大气散射
这里看看如何求上面提到的LUT。首先是预计算通透度(Transmittance)LUT,可以用\(h\)和\(\theta\)查询:
具体做法是已知观察点\(X_v\),目标点\(X_m\)和大气层边界\(B\),那么\(X_m\)的通透度为 \[ \nonumber T(X_v\rarr X_m)=\frac{T(X_v\rarr B)}{T(X_m\rarr B)} \] 其中分子和分母均可在LUT上查到。
然后就能计算单次大气散射的LUT了:
对于单次散射的情况,可以先选取均匀的几个方向和高度进行计算,从而通过插值预计算出单次散射的所有结果至3D的LUT中,然后通过\((v,\mu_s,h)\)查询。具体做法是,将“太阳与天顶的夹角\(\eta\)”,“视线与天顶的夹角\(\theta\)” 以及 “太阳与视线的夹角\(\Phi\)”映射为\((v,\mu_s,h)\)。如果考虑雾效,还要对这张3D的LUT进行加工。
有了通透度LUT和单次散射LUT,就能计算多次散射的LUT了。通常计算3~4次就够用了:
当然,该方法也是存在一定问题的:
- 预计算十分耗时:多次散射的迭代计算并生成对应LUT的代价很昂贵。PC端有Compute Shader,可以在几ms~1s内完成;但手机端耗时很长。
- 动态环境变化失效:例如天气突然变成阴雨天,该方法就失效了。因此艺术家调整不了散射相关参数,且搞不了动态场景。
- 运行时渲染也耗时:运行时渲染的查表、采样、插值操作也会消耗一定的时间。
一种可扩展且可用于生产的天空和大气渲染技术
即A Scalable and Production Ready Sky and Atmosphere Rendering Technique,这是一种更先进的做法。
它假设散射是各项同性的,即均匀的,计算散射便成为计算衰减,于是多次散射只需求级数,而不是求积分。
它还通过固定观察位置和太阳位置以移除LUT的两个维度,提高了效率。
对于模拟透视效果,它通过光线步进生成3D的LUT实现。
计算简便,对艺术家友好,且效果不错。
云的渲染
云的种类
如上图,云有很多种类,主要是层云和积云。
渲染方法
使用建模+噪声
可以通过建模+噪声的方式渲染云,但这样质量虽然高,性能却很差。
使用Billboard
也能将云制成Billboard然后拖到场景中,这样效率虽高,但质量却很不好。
使用体素建模/体积云
目前广泛运用此方法,它通过程序来生成,质量高,效率取决于代码实现。
可以用Weather Texture来描述云的分布和厚度:
可以使用噪声让云的渲染更加真实:
噪声主要有两种,柏林噪声 和 细胞噪声:
有了上述贴图,就能用光线步进技术来渲染体积云了:
雾的渲染
深度雾
根据深度信息来设置雾:
高度雾
有了深度信息还不够,实际上雾还需要高度信息,例如山顶和山脚的雾是不同的。它根据观察者的高度和视线信息对雾的密度进行积分计算。
现代体积雾
在现代游戏中人们更倾向于使用体积雾,这种雾气更加真实,例如有丁达尔效应等。它根据视椎体远近计算不同的雾气,大体思路和计算体积云类似,中间计算结果存储在和屏幕等比例的3D纹理中。
参考资料
- GAMES104 (boomingtech.com)