游戏开发技术论坛

 找回密码
 立即注册
搜索
查看: 20950|回复: 0

腾讯魔方技术专家:开放世界中的水体渲染和仿真

[复制链接]

4万

主题

4万

帖子

7万

积分

管理员

Rank: 9Rank: 9Rank: 9

积分
73312
发表于 2021-12-23 09:34:54 | 显示全部楼层 |阅读模式
2021年11月22日-24日,由腾讯游戏学堂举办的第五届腾讯游戏开发者大会(Tencent Game Developers Conference,简称TGDC)在线上举行。本届大会以“Five by Five”为主题,邀请了海内外40多位行业嘉宾,从主论坛、产品、技术、艺术、独立游戏、市场及游戏社会价值7大专场共同探讨游戏产业趋势和多元价值,以开发者视角与需求为出发点,助力游戏行业良性发展,探索游戏的更多可能性。

我是腾讯魔方工作室群的陈家铭,今天和我的搭档、天美工作室群的胡有为一起,分享大面积深度可交互的水体仿真和渲染技术Aqua。

微信图片_20211223092244.jpg

1. Aqua项目简介

我和有为来自两个部门,是什么原因使我们一起合作研究这个水体方案呢?因为我们参加了腾讯游戏举办的开源计划Tech Future,目的是加强内部技术交流,推进前沿游戏技术开发。Aqua就是其中一个项目,它的的目标是研究可交互的水体仿真和渲染技术,由来自不同部门、岗位的小伙伴们,经过一年时间的业务时间开发,做出了一些成果,在本次大会分享。

微信图片_20211223092252.jpg

水体的仿真和渲染一直以来都是经常被研究的课题,比如育碧《刺客信条》的海洋,顽皮狗《神秘海域》系列中的水体,虚幻引擎 4.26版本发布的水体系统。

虚幻引擎的水体系统,和我们的目标比较接近,是在编辑时首先以曲线定义好各种水体范围,然后在运行时通过一个四叉树来构造水体的Mesh进行绘制。这个系统的功能很强大,但是跟我们希望做的全动态水体有点差异, 比如说:我们希望可以做到下雨的时候造成洪水,把整个场景淹没。又或者玩家可以通过技能,随意把水放到任何一个地方。

微信图片_20211223092253.jpg

所以,Aqua团队希望在这方面有所突破,开发了一套方案,并且制作了一个 tech demo。demo 里,可以看到的水体范围是1km x 1km, 而仿真的精度能达到 25cm, 渲染精度能做到6.5cm,玩家可以像刚才提到的用技能在场景中喷水,并且把篝火弄灭,同时喷出来的水也能留在场景里面。除了一些图形效果外,我们也实现了像载具、浮力等需要跟CPU沟通的一些特性,主要是为了验证方案的实用性。也可以在demo里看到,下雨会导致水位上升,然后洪水把场景淹没的一个事件。还有,除了可以模拟湖泊外,还可以模拟河流、海洋。

2.1 仿真:多层级水体仿真

Aqua背后的原理是怎样的呢?先讲讲 Aqua 的仿真算法选形。在游戏里面常见的水体算法有以下几:

PBD/SPH:通过粒子来最真实地模拟水体动态,一般都需要一个很巨大数量的粒子。所以当我们应用在大面积水体里面,比如说几百米,不论是仿真或者渲染要解决的性能问题都是非常困难。

Wave Particles:它的思路是用一个个粒子来代表一个波浪(Wave),然后把所有波浪都转成高度图来进行渲染,好处是仿真效果很可控,但是较难去模拟水体容量的改变。

网格法:最传统的,虽然它不能像 SPH 一样可以支持水躍 (Hydraulic jump),但大部份水体的特性都能够模拟出来,而且它有很好的特性和伸缩性。考虑到我们目标是要支持好几百米以上的超大范围,Aqua 最终选择了网格法作为方案基础。

但是不能简单用很高分辨率的网格进行仿真,因为内存、计算量在一般的 GPU 上都会扛不住。因此我们借鉴了Clip mapping 的原理来进行多层级仿真。思路是把整个仿真范围以不同精度的网格来覆盖,去掉一些肉眼看不到的细节来换取性能;每层 Clip 的中心大概与相机对齐,水体离相机越近便会有越高的精度。我们从面积最大的一层开始,之后就把图中红色的边缘部份同步给下一层;就能把 Clip 外部的重要影响传递进去。

微信图片_20211223092254.jpg

整个系统的流程大概这样:每一层 clip 都会分为“数据收集”和“仿真”两个阶段,首先我们会收集clip范围内的仿真输入,包括场景或者地形的高度图,主要是用来跟水体进行遮挡或者碰撞。此外,还会收集一系列额外的仿真输入,同时转成贴图,以便给后面的 GPU 仿真使用。

接着,我们会利用 Compute Shader 来进行仿真的解算;我们实现了两种网格算法,分别是 LBM 和 Pipe Water, 这个我们在后面会有进一步的去分享。为了避免水体的速度太快而超出 CFL 条件,我们还要考虑 sub-stepping 的支持,通过缩小 delta time 来提升仿真的稳定性。完成了一级 clip 的仿真后,我们会进行边缘状态的传递。如此类推,当所有 clips 完成仿真之后,会把结果导出到一个TextureArray或者TextureAtlas里面,再交给渲染模块进行绘制。

微信图片_20211223092255.jpg

刚才提到我们需要收集场景或者是地形的高度图,实际上是通过虚幻的 SceneCapture功能来达成。用户预先定义好仿真在世界空间最小和最大的高度,当相机在水平移动超过一个阈值之后,我们会在最高点,从上往下拍一张深度图,再把深度反转,加上最低点的高度,就能得到场景朝上一面的高度。

另外,我们还要收集一系列额外的仿真输入,像水的容量或者速度的影响,这里的难点是如何把各种各样的影响统一成固定的输入,以便给 GPU 仿真使用。

微信图片_20211223092256.jpg

仿真算法的相关内容:到目前为止,我们实现了PipeWater和LBM两种网格算法,由于时间关系,细节参考下图所列的相关文章。虽然两个算法的公式都不太一样,但都是在求格子某几个特定方向的水体流量 F_i,比如说 Pipe 是求上下左右 4个方向,而 LBM 是求 D2Q9 中的9 个方向。仿真的 CS 会通过当前帧的 F_i , 加上与相邻格子的高度差、黏度、引力等等参数来算出下一帧的 F_i ,这样不停迭代。仿真 CS 会用 structured buffer 来保存 F_i, 把中间数据,像容量数据速度导出给渲染使用。

微信图片_20211223092257.jpg

在开始的时侯,我们提到方案中有一个边缘状态传递的操作,所谓的边缘状态就是指每一层 Clip 边界上的 F_i。在每一层 Clip 开始仿真之前,我们都必须要从上一层把对应这一层的边界 2 个格子宽度的 F_i 拷过来。这样我们就可以把 clip 范围以外的影响传递到这一层的 clip 里面。以下面两个视频为例,第一个视频演示了状态传递关闭,水就流不进场境中心的区域。而第二个视频里因为打开了状态传递,水就能正常流到场景中心里面了。

虽然我们用了多层级的策略来增加仿真的范围,但面对好几公里的超大场景,需要用另外一个技巧来增量去扩展仿真范围。我们把这个方法称成Scroll Update或者是 Sliding Window。首先每层 Clip 各自与相机对齐中心,这样当相机平移后就让 clip 的所覆盖范围也会自动跟随着去更新。由于覆盖范围有所更变,我们需要把更新前的 F_i, 按世界坐标拷到新的格子里,相机位置会按格子大小进行 snapping, 避免因为拷贝F_i时候造成跳变。为了避免太频密的更新,我们会等相机超过多个格子才触发更新。在移动方向出现 Cache miss 区域, 我们可以从面积较大的一级Clip 去读取 F_i,提高稳定性。

微信图片_20211223092258.jpg

跟一般的物理仿真一样,当水体流动速度太快,就很容易超出了 CFL 条件就会导致数值溢出的情况 (下图爆炸的水体)。这个在仿真精度提高的时候特别容易出现,因为仿真公式中的delta x相对地降低了, 速度就很容易超出Cmax的限制。所以只能通过 sub stepping 来降低 delta t 来解决,简单说就是在一帧里面多跑好几次的仿真。但这样对性能不太友好,因为计算量直接翻了好几倍。我们观察到,在多层级的方案中每层 Clip 的 delta x 都会增加了一倍,这就意味着Sub-step 次数可以减半。通过这样的优化,跟原来不分层级来比较,可以降低大概 ~90% 左右的计算量。

微信图片_20211223092259.jpg

最后,我们看看多层级仿真的效果比较:左图只有一级,而右图是分了两级来进行仿真,可以看到效果都是很接近的。而从我们 tech demo 的数据里面去看到, 多层级仿真不论在内存和性能上都有明显提升。

微信图片_20211223092300.png

2.2 仿真:作用器系统

接下来交给有为,介绍水体仿真的作用器系统和渲染部分。接下来会向大家介绍我们开发作用器系统的目的和挑战,以及如何解耦仿真,以及如何自定义自己的作用器材质,最后我们会展示一些Demo实际案例。

其实不论是LBM仿真算法还是Pipe仿真算法,本质上都是在计算水体高度场和速度场,假如没有其他的因素干扰,仿真结果最终会趋向平稳。

但是,如果在计算的过程中加入一些外部影响的话,就可以得到一些交互式的结果,比如角色在河里游泳产生的水波,以及下雨产生的涟漪,海风吹起的海浪等等,这些都是交互仿真,那我们如何实现这些交互仿真呢?答案就是作用器。

微信图片_20211223092301.jpg

我们曾经想过让作用器直接参与仿真ComputeShader的计算,但是这样会导致一个问题:作用器种类是非常多的,每个作用器都有自己的算法实现,这导致很难直接在仿真阶段通过一个统一的函数入口去直接影响仿真结果。这会使得仿真的shader变得及其复杂,也不方便以后的管理和维护。

于是我们使用了一个解耦手段,就是将所有作用器的结果都输出到作用图中,对于所有的作用器,即便他们的算法实现不同,但他们都输出统一的结果,就是刚体入水深度、速度和体积,然后我们会把这些结果分别保存在作用图的RGBA通道里面。然后我们就可以在LBM仿真或者是Pipe仿真的时候去采样这个RT作用图,分别将RGBA通道数据取出来去影响各个仿真参数,完成交互仿真过程。

微信图片_20211223092302.png

这里分享一个比较重要的内容,就是我们的作用器系统框架,该系统首先会每帧收集当前区域内有效的作用器,然后同时更新这些作用器Gameplay。然后收集它们产生的Instance信息,包括位置、大小、自定义数据等。同时会收集所有作用器可能访问的uniform Buffer。包括上一帧的仿真高度场、速度场、场景高度,以及自定义贴图数据等。然后通过Draw Instance方式,将每个作用器以相同材质进行合批渲染到四边形的方式绘制到RT上,最终每个作用器统一的输出体积,XY方向速度和刚体数据四个结果,分别保存到贴图的RGBA通道。那么对不同的仿真算法可以访问这个RT,对仿真结果加上外部影响。因为RT上输出的是统一的物理结果,所以,不论哪种仿真算法都可以使用,而无须关心作用器的类别,达到解耦的目的。

微信图片_20211223092303.jpg

那如何让使用者快速开发属于他自己的作用器呢?最初的设想是,让TA或者程序能像开发材质一样,在材质编辑器中通过连线的方式就可以实现自己的作用器算法,我们称之为作用器材质。如图所示,目前开放项目中已经通过自定义的方式实现了一部分常用作用器材质。

微信图片_20211223092304.jpg

同时为了丰富高度自定义内容,我们也封装了很多材质函数,以供开发者使用,比如访问每个作用器自定义的数据,uniform buffer数据,高度场,速度场贴图数据等。都能在材质编辑器中拖入使用。可以看到下面的截图,它是一个水源作用器的实现过程。

微信图片_20211223092306.jpg

大量不同作用器材质的使用会导致drawcall数量增加的问题,那么我们如何解决这个问题呢?刚刚的分享当中我已经提到了,我们这里也会对相同材质的作用器进行合批处理。我们可以看到截图里面,在开放项目中,我们大量使用了作用器去模拟水源,波浪等,但最终的drawcall调用次数只有3次。

微信图片_20211223092307.jpg

这里我们截取了开放项目中部分的作用器效果示例。可以看到借助作用器框架,可以方便的自定义实现多种Affector Material与水体进行交互的Gameplay玩法。

微信图片_20211223092308.jpg

3.1 渲染:基于GPU Driven的水体渲染

这个主题我们会向大家分享以往的传统做法,然后我们如何创造性地应用CDLOD来实现GPU驱动的水体网格渲染,包括如何在GPU上构建四叉树,如何实现遮挡剔除、超高网格密度、顶点变形等。传统的水体网格渲染,我们都知道我们一般会使用一个平面网格,加一个高度图来还原水面的波浪运动。在移动端甚至不需要真实的波浪,而是仅仅通过法线和UV动画来模拟波浪。

微信图片_20211223092309.jpg

实现算法就是我们去还原波浪的高度以及还原顶点的位置,很简单,我们只需要在VertexShader中采样高度图,来还原顶点的Z坐标值。并通过采样邻近高度场数据来重新计算每个顶点的切线空间,最后把顶点的切线空间插值到光栅化阶段。

微信图片_20211223092310.jpg

在实现GPU驱动之前,我们也调研了UE4.26 的水体插件,其中截帧分析了海洋的例子,可以看到,Unreal为了表现不同的海洋网格密度,它使用了6种不同的网格拓扑结构。虽然用了同一个水体材质,并且instance方式绘制,但也产生了6DrawCall。而且,UE4.26的水体分为海洋,河流和湖泊不同的水体Actor,这也会导致DrawCall数量的增加。另外UE4.26的水体是以CPU的方式驱动,而我们的目的是做GPU Driven。

微信图片_20211223092311.jpg

最终我们在方案的选择上,我们采用了CDLOD的技术,CDLOD全称是基于连续距离的细节等级。它本来是用来解决大世界地形渲染优化问题的,我们利用这次机会,将它很好的实践到了大型水体渲染当中。CDLOD它有很多优点,它具有变化平滑,不会产生裂缝,Lod之间等级差不超过1的特点,并且非常适合做基于四叉树节点的剔除。

微信图片_20211223092312.jpg

在GPU上面去构建四叉树的算法原理其实就是一个自顶而下的过程,从根节点开始,每个根节点是ComputeShader中的一个线程。如果当前节点在递归的这一级,又在下一级,它就确定了这个Quad。如果即在当前递归的这一级,又在下一级,那么意味着子节点需要继续递归到更低级别去确定,如此循环往复最终递归到0级。在GPU上构建去四叉树会遇到比较多的问题,我们遇到的第一个问题,首先在ComputeShader上面是不能使用递归函数的,它有递归函数使用的限制。

微信图片_20211223092313.jpg

然后,我们如果想跳过这个限制,比如在shader代码中写大量的嵌套循环来解决递归函数问题,又会使得shader变得超级复杂,也会将寄存器耗尽。而且,一般的构建过程当中,递归复杂度会随着每一级递减,也就是说,根节点那一级他是最复杂的,也是最耗时的,0级是最不耗时的,如果我们把所有的级别都用统一的shader代码,就不能很好的平均每个线程执行时间。

为了解决这个问题,我们采用了分批次执行ComputeShader,每一级构建都会有自己对应的ComputeShader变种。每次使用上一次构建未确定的节点的输出作为这次的输入,这样使得每一次构建都只关心它当前这一级,从而大大降低了shader复杂度,平均了线程间的执行时间。

微信图片_20211223092314.jpg

在构建的过程当中,我们也会先判断四叉树节点是不是应该被剔除,以减少Instance数量。通过采样水面高度的仿真结果,我们可以构建一个节点内的Quad内的Min/Max Height信息,从而构建一个WorldSpace下面的立方体。我们的剔除分为两种,一种是视锥剔除,另外一种是HZB剔除。视锥剔除会判断立方体的8顶点是否在裁剪空间内,而HZB则根据立方体的屏幕空间尺寸选择合适的Z-buffer mipmap做深度判断进行剔除。

微信图片_20211223092315.jpg

对于构建好的节点,如果直接渲染的话,其密度是很低的,那么我们如何去构建一个高密度的网格呢?如下图所示,颜色相同的区域具有相同的网格密度。这里我们使用了2种网格密度,预先生成了2种面片mesh,第二面片的顶点数是第一个的1/4。

以全尺寸32和半尺寸16为例,我们去拿32和16去填充非边界和边界的四叉树节点,通过这种方法,我们仅使用2种拓扑结构就可以表达多种网格密度,DrawCall数量最大为2。

微信图片_20211223092316.jpg

最终渲染的时候,最关键的就是要确定哪些顶点是需要变形,我们会为每一级设定一个当前这一级对应的节点尺寸大小的变形区域,处于区域内的顶点将进行变形。因为之前按照2的幂次方规则,使用了2种不同的面片进行网格密度填充,所以这些面片的顶点位置是奇数的是需要变形的。

微信图片_20211223092317.jpg

另外我们也总结了构建分辨率和该分辨率下对应下的构建消耗时间对照表,无论场景多大,我们的构建四叉树花销的消耗时间只与构建分辨率有关,这样可以稳定性能上限。构建分辨率和网格密度可以随意搭配,根据实际项目需要进行配置,达到性能和效果的平衡。在目前的开源项目中,以512的构建分辨率+32的网格密度作为默认配置,如果想要性能更好,可以选择256 +16甚至更低。但是这样会导致网格精度会下降,可能会出现锯齿、走样。

微信图片_20211223092318.jpg

最后我们来看下实际Demo中的效果,角色近处始终维持比较高的网格密度,而远处比较稀疏的网格会随着角色摄像机移动而进行平滑过渡。

微信图片_20211223092320.jpg

3.2 渲染:基于Height Blend的动态地表浅水

接下来介绍如何改进UE4原生高度混合算法,并在此基础上实现静态浅水,然后结合仿真结果进一步实现了动态浅水。最后我们将展示在游戏Gameplay过程中的实际效果。

开放世界除了海洋,湖泊,河流这些常规水体。浅水也是一个很重要的能体现环境细节表达,特别是池塘洼地,雨后积水的路面。UE4的地形渲染,我们知道都是多层Layer进行Blend的结果。其中的基于高度的混合,可以让地表表现出缝隙或者低矮处混合。于是,我们设想在Landscape中通过插入一个浅水层,与其他层去做Height Blend,以表达浅水效果。

微信图片_20211223092321.jpg

先来分析下UE原生的heightblend算法,这个算法很简单,它将每层的权重乘以高度值,然后累加,再归一化百分比,最后混合每个层的结果。这个算法很简单,但是它导致不同layer在整个权重区间都在Blend,效果显得很脏。

Layer weight 和 height map value 数值空间在[0,1]范围,这个范围很小,不是真实物理单位,它会丢失计算精度。并且在接缝处无法实现均值过渡,以及无法控制过渡阈值。

下图右侧是这个算法的原生效果。改进的做法,第一步把高度值映射到真实物理空间,然后乘以对应的权重,接下来我们会筛选一个权重最大的层,也就是最高的层。然后将每层权重与最高层的差值除以一个过渡阈值,这样就得到一个平滑过渡的权重,最后将这个平滑过渡的权重去做归一化百分比,最后混合每层得结果。这个算法的优点在于,首先将在[0,1]的height map value 映射到实际物理单位下,扩大了计算精度。在接缝的地方会是一个趋向于平均实现了均值过渡,以及可以控制过渡阈值。我们可以看到右边的截图是改进后的效果,可以看到基本达到了做浅水的要求。

微信图片_20211223092322.jpg

另外很重要的是,我们不希望浅水的HeightBlend过程被固定死在C++代码中,而是开放让美术或者TA能在材质编辑器中通过连线的方式去进行编辑。UE默认的地表材质节点,Layer Blend是无法解耦浅水和其他层的混合操作的。所以,我们开发了自己的材质节点叫做Height Blend。可以看到截图里面,我们自己开发的这个节点除了输出其他层的混合结果以外,会将浅水层的权重信息单独输出,这样我们便能在材质中控制最终浅水的混合效果。

微信图片_20211223092323.jpg

我们把最终的浅水混合操作都封装在了一个材质函数中,之前我们有提到,我们开发了一个自己的地表材质节点,它会将浅水层的权重单独输出,我们拿到这个权重后,就可以分别去混合浅水的颜色,法线和PBR信息了。法线和PBR的混合比较简单 就用了比较常规线性插值,而颜色方面我们则考虑了干燥地表向潮湿区域过渡的效果,以及潮湿区域向浅水过渡的效果。

微信图片_20211223092325.jpg

刚才讲的其实都是静态浅水,进一步增加动态浅水,我们又加入了LBM的仿真结果,下图左侧是加入了LBM结果后的效果。我们把LBM的仿真结果去影响高度混合的高度值,可以看到它与周围环境产生了交互效果,并且真实反应了HeightBlend。然后,我们也测试了Pipe仿真和Rain Affector一起作用的效果,可以看到除了表面涟漪以外,还可以看到下图右侧,积水体积是不一样的,从而真实反应了仿真对积水体积的影响。

微信图片_20211223092326.jpg

最后,是一些Gameplay中的有趣应用。可以看到主角在游戏世界中可以任意释放技能,这个技能可以在地表上残留一层浅水。浅水和地表形成高度混合的侵蚀效果,并且浅水会随时间慢慢吸收掉。接下来由家铭继续分享。


3.3 仿真结果应用

刚才了解到水体基础绘制的原理,现在会进一步讲解仿真结果是如何应用到更多水体的细节效果里。首先是水体表面的细节法线,有了细节法线,水流动的感觉更加强烈了;而这个效果也不难去实现的,我们只需要利用水体的速度场当作是 flowmap 去采样Detail Normal的就可以了。由于仿真出来的速度是在世界空间里面定义的,我们需要进行一个简单的 缩放 和 Clamp的操作。另外我们也需要考虑到速度为零的时侯把细节法线 Flatten。

微信图片_20211223092327.jpg

还有就是泡沫效果, 跟其他方案通过分析水体高度图的 Jacobian 不一样。我们发现水体出现碰撞的地方通常都会有比较高的旋度,由于我们是二维的速度场,所以算出来的旋度是一个纯量。因此我们可以把旋度视为左边的公式的一个2维 旋度的计算方法, 在里面f 就是速度是x坐标 ; 而g就是速度的y 坐标; 而k就是一个c轴,可以忽略。我们再一次把速度场当成是 flow map 去采样泡沫的贴图,再乘以遮罩之后就能得到下图效果。

微信图片_20211223092328.jpg

除了水体表面的效果外,水体周边的物体也会因为沾到水而改变材质颜色。为了计算沾水的程度,我们会利用一个 double buffering 的技巧。首先我们把当前帧 Buffer1 的水体高度拷到 Buffer2 里面, 然后读取上一帧Buffer2时候保存的高度,做一个缩减,再跟新的水体高度取 Max 再放回Buffer 1里面 所记录的水体高度就可以算出一个沾水权重。在渲染场景物体的时侯,我们会利用物体的世界坐标的C去减去Buffer 1 所记录的水体高度就能算出一个沾水权重。物体计算光照的时候PBR 参数,就利用这个权重从干和湿两组 Preset里面去插值出来。

微信图片_20211223092329.jpg

还有是一个GPU 粒子与水体交互的例子。在视频里面演示的落叶效果不仅可以漂浮在水上,也可以随着波浪而旋转。这个效果的 粒子系统里面,每个粒子可以分为:掉落、漂浮还有消失三个状态。掉落时粒子会以 场景高度 和 水体高度 判断它是否已经掉落到水里面,如果是就切换到漂浮状态。漂浮时就简单的以水体高度作为它在世界空间里面的高度;还加上之前提到的旋度来控制叶子旋转。假如粒子是掉落到地上它就会变成一个消失,就是简单的把它设成一个透明,而后在等好几帧让粒子系统让它回收到池子里面就可以了。

微信图片_20211223092330.jpg

再分享两个与水体高度有关的后处理效果;首先是 WaterLine ,这个效果会出现在水面跟水底交接的边缘部分。而它的思路就是在屏幕空间去找出接近水面的像素,再插值成WaterLine的颜色。利用投影距阵,我们可以计算 Near Plane 上每像素的世界坐标, 再用这坐标去采样水体高度, 这样我们就可以算出 Near Plane 对应像素与水面的距离,再把这个距离通过 falloff的函数 算出一个插值的权重就能得出左边截图的WaterLine 效果。

微信图片_20211223092331.jpg

延伸刚才 Water Line 的思路,我们可以算出在视线上不同距离的世界坐标, WP利用WP我们就可以通过一些噪声 或者是水体压场模拟出该点的一些Scattering的亮度。再以WP采样水体的高度,我们就可以知道这一点在水下深度,再放到一个 falloff的函数里面就可以算出因为这个水深而做成的散射的缩减。这样,我们在视线上 Ray March 4 个点,同时算出它散射的一个积分,就能得到视频里的 Light Shafts 效果。

微信图片_20211223092333.jpg

最后要分享的是跟玩法里面有紧密关系的浮力效果。有别于之前介绍的,浮力是在 CPU 上计算再给到物理引擎的刚体上。根据公式,浮力跟物体浸在水中的体积有关,所以我们需要在 CPU 访问物体位置对应的水深来算出它浸入体积。我们需要一个有效的机制从 GPU 里读取仿真结果,因此 Aqua 实现了一个延迟一帧的 GPU 读取功能。大概的流程是我们会在每帧都先记录所需要查询的位置,之后合批发给一个 CS 从Texture Atlas 中读取仿真结果并保存到一张分辨率很低的 Staging RT 里。等 CS Dispatch 完成之后我们才进行 read back, 最后在通过下一帧 C++ 回调来通知查询结果。这样就是可以尽量降低GPU 读取所带来的带宽消耗和延迟。有了水深,我就可以计算浮力。在 tech demo 里的浮木就是利用这个 GPU 读取框架来计算木头两端的浮力,再以AddImpluseAtLocation 应用到木头的刚体上。此外,当木头每一端检查到它是浸在水里,我们会用读取到的水体速度在这一端在给一道力;这样就能做出刚体随水流漂浮的一个效果了。

微信图片_20211223092334.jpg

今天我们为大家分享了 Aqua 团队所开发的开放世界水体方案,里面包括了多层级仿真系统、作用器系统、GPU Driven 的 CDLOD 水体渲染;还有浅水效果渲染,以及各样的仿真结果应用。在经历了一年不长也不短的开发,Aqua 支持了不少功能和特性,但当中也有不完美或者可以续继优化的地方。比如说:我们目前还没有做音效的支持;又或者是方案还没可以支持像地穴这种复杂地形的水体模拟。希望后面我们有机会再继续优化和打磨 Aqua, 让它变得更尽善尽美。

微信图片_20211223092335.jpg

Q & A

问:水体仿真的网格精度如何控制?

陈家铭:这个问题很好,在我们开发tech demo 的过程里面发现,其实以25厘米来代表一个格子的精度来仿真,不论在性能或者效果上都能达到一个比较好的平衡。由于在我们的demo 里面,我们是分了三层 clips 来进行仿真;所以对应的精度就大概是25cm,50cm 以及是1m左右。当然,我们也可以按着游戏的一个需求来提升仿真精度,但是当精度每提升一倍,就代表着substep 次数要增加一次,主要是为避免超出 CFL 条件导致了数值爆炸的情况出现。

问:水体渲染的网格是固定大小还是自适应?

胡有为:刚才我的分享当中也提到了,就是说我们的网格大小,其实是由两个决定的,第一是构建分辨率,第二是网格密度的设置,我们现在是以512的构建分辨率+32的设置去决定生成网格大小的,当然您也可以选择其他的,比如说256+16。目前这个配置是在运行前在项目中设置好的,我们并没有实现说在运行时runtime的时候去改变这个,当然我们理论上也是可以做到的。

问:水体光照渲染是如何避免走样的呢?GPU Driven如何适配到移动端?

胡有为:这个问题我们之前也遇到了,就是在水面很远的地方,我们确实也出现了一些锯齿、就是走样。那我们如何解决这个问题的呢?我们通过将逐顶点的切线空间的还原变成逐像素的,就是在像素着色器里面去还原切线空间,同时我们还原切线空间的时候,其实可以不用采样邻近像素的高度场,而是跨多个邻近像素,比如说两个、三个,可以让它的切线空间再次平滑。

目前在移动端去实现GPU Driven还是有点困难,因为我们目前大家都知道移动端的硬件架构,对于ComputeShader来说目前停留在API的支持阶段,移动端的架构并没有做出升级或者改变,所以说在移动端去适配GPU Driven还是比较困难,如果要在移动端去使用CDLOD,可能还是要去回滚到CPU驱动的方式,我们把在GPU上面去构建的算法,把它移植到CPU端,然后像变换大小这些信息可以用Vertex Streams顶点流的方式去做到instance渲染。自定义的数据就只能通过烘焙到贴图或者更新到贴图的方式,因为移动端目前是对struct buff的支持是不太好的。

文/陈家铭 胡有为
来源:腾讯游戏学堂

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

作品发布|文章投稿|广告合作|关于本站|游戏开发技术论坛 ( 闽ICP备17032699号-3 )

GMT+8, 2022-5-26 16:59

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表