前言
有时候想想物理法则真的很厉害,因为它有着去中心化的运行方式。计算机科技领域的算力在自然法则面前还差距很远。
对光线追踪有所了解的可能会知道,现有的实时光线追踪技术还是大量建立在AI推测和降噪算法的基础上,本身底层能做到的每帧采样率(基于“蒙特卡洛积分”方式,需要一定的采样数)其实很低。这个数量级要每帧模拟物理世界中的分子级别的运算还是不可能的。
例如衣服打湿的效果,玻璃上的雨水效果之类,这些视觉效果在游戏引擎中是不存在基于物理法则的“水滴”的。前者通常是一个基于打湿程度的整体布料渲染系数变化(辅助一些雨滴粒子之类),后者则往往通过一个实时生成的透明效果层来实现。
在离线渲染中,水体的物理模拟可以通过弹球系统来进行,当单个弹球足够小时其实已经比较接近水分子系统了;而在实时渲染中,水体能实现“看起来像”已经耗费了很多人类的智慧,这里面也有巨大的效果和性能的trade off。各种不同的实时渲染水体几乎还都是一事一议的来制作和优化的,例如杯子中的酒、空中的雨滴和大海中的波浪要考虑的问题就千差万别。
从这个意义上来说,现在和今后很长一段时间内游戏中其实不存在“真”的水体。所以很多时候玩家觉得游戏中的水看起来假,其实就是开发者没有足够的能力或精力去处理水体这个课题,没有做到很好的模拟和抽象;而那些水体做的好的游戏,要么是把它作为重要视觉目标来对待,要么就是他们有着长足的积累和足够的性能空间。
本文综合了比较成熟但可能不是最新的一套水体方案,大部分内容拆解自几篇SIGGRAPH论文和一套成熟的Unity水体插件(文末会附资料链接)。另外由于篇幅和个人阅历的原因,本文和此系列的其它文章一样也只能起到一个抛砖引玉的作用。
下面让我们逐章节开始:
1 基础形体:网格——Mesh
理论上最低限度可以表达水体的多边形就是一个平面,上面运行着有一定噪声扰动、法线扰动及光照计算的半透明(或不透明)着色。而稍微有点写实程度要求的水体,则不可避免要使用多边形网格来描述水面的形体。
由于水体渲染肯定是要大量使用多边形网格的,一般来说会有这些基本原则:
- 最小顶点数
- 保证靠近视点的细节(LOD)
- 稳定性(不同帧之间)
- 在视点移动时不产生噪点
- 适配各种不同视点
这里面很多原则其实和大世界地形渲染是共有的问题意识。下面介绍一种被称为Geometry Clipmaps的设计及其后续改进的内容。
图中展示了这种多层结构的基础多边形分布
Geometry Clipmaps要点:
- 几何(密度)延视点分布
- 顶点位置始终对齐到世界空间的格子上(整体呈矩形分布)
- 经过接合的几何LOD层级(*这一点后续又有所改进,变成网格不是必须接合的)
下面通过一组GIF演示一下这个方案的基本效果:
随视点变化的网格密度(LOD)
网格通过动态划分来切换LOD
通过这一方案,就可以达到近处的网格密度高、远处相对较低,整体比较有序接合的效果;后续要对这个网格系统做基于波函数或顶点变换的形变也能继续进行。得益于现代GPU,实时运算这种数量级的多边形变换对于PC和主机已经不是问题。
由于这个方案的网格区域也不是无限大的,对于最外层网格更外侧的区域需要一定的外延填充策略。这个方案就简单的做了一下外延拓展,如果对于纹理外延拓展有了解的应该对这个模式不陌生。
图中展示了外延填充后的网格
最后再来看一下摄像机远近对于网格缩放的影响:
基于远近的网格缩放策略
之所以需要不断动态折腾这套网格LOD,是因为毕竟算力还是应该分配到最合适的方面。这里面还有一个部分重叠(Overlay)的问题,由于篇幅原因不展开了。
最后来对比一下这套方案和《神秘海域4》中采用的大地形CDLOD(Continuous Detail LOD)方案:
CDLOD——几何距离;CDClipmaps——曼哈顿距离(坐标里程)
*Taxicab distance直译是出租车距离,其实国内更常见的叫法应该是曼哈顿距离(Manhattan Distance)。
CDLOD——几何距离、四叉树、不能保证边缘几何正确、LOD之间没有形体关联。
CDClipmaps——曼哈顿距离、最小化CPU消耗、边缘几何有明确方案、LOD间有形体关联。
2 波和浪:模拟——Wave & Foam Simulation
有了网格系统后,海水的波和浪都是基于对网格的整体形变来运作的。只是一般来说波的影响范围没有浪那么大,因此两者也有着不同的预运算和实时运算方式;由于实时的流体计算还是开销太大了,因此实时运算也往往采用波函数等简化方案。
1)基于预计算纹理的波形
在《刺客信条3》中,开发者就通过离线预计算的FFT(Fast Fourier Transform 快速傅立叶变换,这里就是表现成预计算纹理)模拟了逼真的海浪效果。
To produce the waves a statistical model based on fourier transform was used, based on Jerry Tessendorf’s work (simulating ocean water). The team precomputed two sets of tiling and cycling wave displacements. These were stored as rgb textures. The water textures were projected from the camera position, thus distant waves are smaller and closer waves larger. The resulting waves are an input for vertex buffers.
译:为创建这些波浪我们采用了一个基于快速傅立叶变换的静态模型,基于Jerry Tessendorf之前关于海洋水流模拟方面的成果。团队预计算了两组平铺、可循环的波纹替代,并存入RGB纹理中。水体纹理从摄像机位置被按照近大远小的策略进行投射。最终结果的波形被输入到顶点缓冲数据中。
图中是《刺客信条》当时实现的效果及纹理缓冲中的内容
*作为一款十年前的方案,这种波浪现在已经是单机甚至手机3D游戏的起点甚至标配了。
2)基于波函数的波形
对于实时波函数,这里介绍一个Gerstner波函数。它演进自正弦波函数,公式如下:
看起来玄乎,其实逻辑并不复杂,大概就是定义了一组波的陡度和间隔等参数,带入进去可以得到一组波型(甚至法线也可以高效的计算出,这里不展开了)。
图中是通过Gerstner波函数得到的波形示例
*更多的2D波函数可以参考文末的链接。
3)基于预计算形体,以“粒子”方式作用的波形
有时候,波形也被预存成形体变换的数据(网格、深度图、法线图等),以类似“粒子”的方式作用到水体网格上。形体变换数据会基于参数输入(玩家操作、风向等)、已有波浪和发射源相关参数等属性进一步运算,在进行形变后得到最终结果。
下面视频展示了基于“粒子”方式的波形示例:
海浪的情况部分和波类似,目前主要的成熟方案还是基于预计算的。
1)基于替代纹理(displacement texture)的海浪
例如在Jacobian的替代纹理方案中,采样值大于1代表拉伸、小于1代表挤压,而小于0的值代表海浪局部重叠(折进去)。如果结合数据缓冲区,还能更好的实现海浪基于时间的指数级衰减效果。
基于替代纹理的海浪示例
2)基于形变数据(deformation data)的海浪
严格上说形变数据和替换纹理没有质的变化,只是形变数据不一定保存成纹理的形式,且有着更丰富的数据维度。
这部分我直接摘一段SIGGRAPH2022中《地平线:西之绝境》的分享内容。
We began by doing a Houdini sim of a breaking wave. From that we extracted a cross-section for each frame. The upper image is an example. The cross section line is expressed as a deformation from a flat surface, as shown by the arrows in the lower image. The XYZ offset is converted into RGB, the thin green and purple strip underneath shows the result.
译:起初我们在Houdini(一款3D图形软件)中做海浪的模拟,并从中导出了每一帧的横截面——上图就是一个示例。横截面的线条代表了在一个平面的基础上,应用基于下图所示的箭头为方向的形变。XYZ的偏移被转换为RGB值,图中的绿色和紫色的细线代表了这一结果。
We repeat the process for all the frames of the animation. Each frame gave us a horizontal strip of pixels. They’re assembled into a vertical stack to form a single 2D texture for the whole animation, as shown here. A couple of example frames are shown here, with the corresponding row of the deformation texture outlined.
译:我们对(海浪)动画的所有帧重复以上处理,每一帧都为我们提供了一条像素数据。它们被垂直堆叠整合到到一张2D纹理,如图所示包含了整个动画的信息。这里展示了一组示例帧和它们对应的形变数据——纹理中对应被圈出的那一行。
*后面还提到了一些基于这个数据组织方案的具体困难和处理,这里不展开了。这个方案的亮点是以2D纹理的方式,通过切片还原的方式存储了海浪的形变。
形变数据的应用
The two waves shown here are moving left right towards the shore. The lefthand wave is a younger version with has lower animation parameters, the righthand one is older and starting to break. To create the shape we apply the cross-section deformation along the curve of the wavefront. The deformations take a flat water surface and bend it to match their cross section. The cross sections are shown at a few places, the number is the V coordinate of the deformation texture.
Looking at the righthand one, near the camera at 0.27 the water is just starting to rise, further along at 0.4 it starts to steepen, at 0.5 it’s curling over and turning into foam.
译:这里展示了左右两道朝着岸边推进的波形。左侧的波形更新且有着更低的动画系数,右侧的波形更老并开始破碎了。通过沿着波阵面曲线应用横截面形变数据,我们创建了海浪形体——这些形变作用把海平面弯曲变成了符合截面数据的形状。图中多处展示出了横截面数据,其中的数字表示形变纹理中的V坐标数据(代表在动画序列中的位置)。
以右侧的波浪为例,靠近摄像机在0.27数字的位置,海水即将抬升;更远处0.4数字的位置海浪开始变陡;在0.5数字的位置海浪开始卷曲并变成浪花。
*关于波阵面散布曲线和浪花形体细节这里不展开了。
《地平线:西之绝境》呈现的水体海浪效果
3 水体表面渲染——Water Surface Render
水体渲染的基础还是半透明(或不透明)着色,只是基于设计的视觉模型不同需要考虑的维度不同。
以《刺客信条3》中的“简单”水体着色为例,有如下参数项:
- 漫反射——Diffuse
- 高光——Specular
- 法线贴图——Normal map
- 反射——Reflection
- 深度和变色——Depth & tint
- 折射与次表面散射——Refraction and ‘SSS’
- 复杂的浪花——Complex Foam
下面通过一组图片展示这些特性逐步叠加上去的效果:
基础水体——漫反射、颜色
高光
反射——屏幕空间或CubeMap
折射+次表面散射(近似)
其中水的深浅主要是通过颜色和透明度的变化来体现的:
渐变纹理与深浅变化
而实时的SSS效果则往往是一种近似方案(trick)。例如在《刺客信条3》中他们的方案是:
To provide the effect the team used a simple SSS test based on viewing position, sun position and surface slope, this would then vary localized ‘shallowness’ or tint/opacity. It is remarkable how effective it is to fake SSS by just making the underside of back lit waves ‘less shallow’ (tint/transparent).
译:为提供这一效果,团队采用了一项简单的基于视点位置、主光源位置和表面斜率的SSS检测,以局部改变水体的“深浅度”或颜色与透明度。通过使波浪的背面“看起来更浅”的这一处理有着显著的效果。
SSS的关\开对比
*育碧的这一方案中还引入了一个基于距离的不透明边界,在边界外的水体可以都以不透明方式来渲染,以提升性能。
4 海浪渲染:深度剥离——Depth Peeling
做稍微复杂的半透明渲染都离不开深度剥离(之前一篇介绍透明渲染的文章中也简单介绍过)。简单来说就是先基于深度从近到远排序出若干个透明层,再对它们进行从远到近渲染的过程。
下面比对一下深度剥离关闭与开启的效果:
1透明层——无深度剥离
5透明层
可以看到不采用深度剥离时,无法正确表现浪花的背面及水体中更多的层次。
简单归纳一下水体深度剥离的一些细节,如下:
- 先进行顺序无关的水体渲染,获得最靠前的水体层的深度。
- 在得到最大深度层时,从后向前逐层渲染——依据两层之间是否在水体中计算吸收和散射值。
- (中间层)可以应用水下视觉的后处理效果。
- 渲染最前层采用传统折射着色(非多层折射的),但有正确的散射值。
*水体深度剥离的特殊之处在于光线可以一段在水体,一段在水体外,相应的计算参数是不同的。
5 水下渲染——Under Water Render
对于观察点在水体中的情况,额外还需要考虑体积渲染+后处理。在水体中看水面,和水面中看水中计算上没有本质区别;有了水面层的渲染结果后,水体内部的体积渲染还需要考虑一些水下的视觉特征:
1)水质特点——水下吸收度和散射度
简单点的水下视觉可以用类似距离雾(Distance Fog)的方式来进行能见度过渡。
如果追求更接近物理和更精确的视觉,一般要充分考虑水质对于不同波长的吸收和散射状态。
图中展示了不同水质情况的散射和吸收状况
要比较精确的做到这一效果在实时渲染领域还是比较困难的,一般都是通过经验公式或对实验结果做LUT(Look Up Table)来实现。
2)太阳光束(God Ray)
关于表现大气中的体积光,之前我有一篇文章介绍过——简单来说就有屏幕空间后处理和体积渲染两种思路。
但是水体由于比较空气来说是更复杂的散射介质,通常就无法通过屏幕空间后处理的方式加上太阳光束了;至于体积渲染的思路,则可以完全沿用实现大气中太阳光束的那些方案。
3)焦散(Caustics)
图中展示了焦散(Caustics)效果的示例
Games101课程的闫老师很不喜欢焦散这个翻译,因为其实这是一种折射后“重聚焦”的效果。很多时候有些翻译确实有一定偏差(如果听过重轻老师说“碎拍”的翻译应该也有同感)。
下面简单介绍一种提供焦散效果的方式:
- 首先,在帧缓冲中渲染一张(从水面向水底的)环境纹理,RGB通道存储世界空间的XYZ值,Alpha通道储存深度
环境纹理示例
- 光线从水面折射点开始对环境纹理求交点,类似RayMarching思路,原理如图:
从水面交点计算折射后,逐像素和环境纹理的深度比较
和环境纹理有交点,则射线检测结束
- 基于这一折射后的映射关系得到焦散纹理:
焦散纹理示意
- 在渲染的场景中应用焦散纹理,就可以得到近似的焦散效果。
*这一方案当然也可以有很多可优化的方向,不过其计算范围始终是纹理坐标范围,必要时以半精度或四分之一精度来运算都能有不错的近似效果。
图中展示了某个水下渲染组合方案的效果
结语
虽然努力覆盖了很多方面,但其实大家也能发现这种概括既不全面又不详细——确实如此,因为实际上无论是渲染技术还是学术上,想在其中比较困难的例如波浪模拟、水下渲染真实性这些方面有所突破,可能都要付出数年的研究。这也是为何人类社会需要论文的原因,有价值的科研论文才是人类科技研究传递的基础。
由于篇幅原因本文还未涉及动态物体和水体的交互中的一些其它细节。例如人游泳,既然不能做到流体力学计算,那该如何进行近似而有效的模拟——诸如此类其实还会有很多课题(特别基础的喷个形变粒子就糊弄过去的肯定是能实现的,但更复杂的动态要考虑的就多了)。这类一事一议的课题在实时渲染领域还有很多。
反过来说,如果水体只是提供一种氛围,那么不做波浪形体、不做水下效果都是很常见的情况。这也是有些游戏通过卡BUG到了水下之后,发现并没有水的原因。
最后是一些资料链接:
Crest: Novel Ocean Rendering Techniques in an Open Source FrameworkPPT下载
Crest的Unity工程
Rendering Water in Horizon Forbidden West的SIG页
SIG2008的Real Time Physics 的PDF
Nvidia:Effective Water Simulation from Physical Models
Fast Terrain Rendering with Continuous Detail on a Modern GPU
Assassin’s Creed III: The tech behind (or beneath) the action
Underwater Rendering with Realistic Water Properties (PDF)
Real-Time Mixed Reality Rendering for Underwater 360° Videos
Real-Time Underwater Spectral Rendering(2024)
Wave_equation Wiki