图形引擎实战:浅谈移动端无尽海面的网格生成

国内手游市场发展如火如荼,各类手游开发项目层出不穷,在这之间,不乏一些需要制作海洋效果的项目。说到海洋模拟,笔者脑海中立刻蹦出一些关键词,“Vertex Displacement”、“FFT”、“Gerstner Waves”等,这些都是讨论“如何让海面起伏更真实”会涉及到的话题,大家在网络上一搜,可以很容易找到各路大佬的分享,笔者也从这些文章中收益匪浅,但是今天,笔者想分享的并不是上述的主题,而是想跟大家讨论一下“在如何让的海面网格以支持复杂起伏”的问题。

无尽海面的网格生成虽不像海浪起伏模拟一样涉及很多高深的数学、巧妙的优化,却也是制作海洋效果不可或缺的第一环。特别是当我们把运行平台限制在“移动端”之后,基于性能与兼容性的考虑,要求我们在这一问题上投入更多的关注。因此,笔者基于曾经的项目经验,在本文中就这一话题针对一些典型的实现方式进行简单讨论,如有错误的地方,请大家不吝赐教。文中涉及的代码与场景演示工程,请从文后链接下载。

简单网格片

当我们进行游戏场景搭建的时候,会将做好的模型放置到具体的位置,使用模型自带的UV信息进行贴图采样,这样就保证了模型不管在哪里,除了光照效果外,其他外表都是不变的。那么我们应该如果制作海洋的模型呢?难道需要提前做一个无限大的片再放置到场景中吗?答案是——不需要。

因为不管海洋有多大,我们能看到的总是眼前视锥体之内有限的一块区域,因此让海洋无限大的最简单的方式,是创建一个片模型,让其保持Y轴高度(海面高度)不变的情况下,跟随摄像机进行XZ平面的位移,这样玩家就仿佛永远也走不出海洋了。但是这样会引出第二个问题,模型的UV不会变,模型片一直跟着玩家水平位移,那岂不是和摄像机永远相对静止(就像下面视频所示)?

视频见链接:https://zhuanlan.zhihu.com/p/558150619

要解决这一问题,我们肯定不能再使用模型UV去进行贴图采样了,取而代之的应当使用世界空间水平坐标(WorldPosition.xz)进行一定的缩放以及时间变换后作为采样UV,这样能保证同一空间位置的采样结果是稳定的(就像下面视频所示)。使用世界空间水平坐标作为采样UV是绝大部分海洋模拟方案采用的一个方式。

视频见链接:https://zhuanlan.zhihu.com/p/558150619

从上图中可以发现,我们所使用的模型非常简单,因此无法应用复杂的顶点动画从而产生波浪,但是却不影响通过叠加多层法线扰动产生假波动(如下图所示)。这种方式可以很好地满足不要求真实顶点波动的海洋需求,性能消耗点全在Fragment Shader中。

那么问题来了,当我们需要表现非常复杂的海浪时,该怎么办?

基于曲面细分

复杂的顶点运动,必然需要细密的网格支持,而我们一般都希望摄像机前的效果最好,随着距离增加可以逐渐淡化细节。针对这一需求特点,我们首先会想到曲面细分)。使用曲面细分,我们很容易让眼前区域的水片模型顶点变得细密,同时按距离逐渐恢复到模型原始细密度。在Unity中可以这样实现:首先声明使用曲面细分

在Shader中进行细分计算,下图演示的是在minDist与maxDist之间,根据距离的大小进行细分,tess的值控制细分的粒度。

其效果如下图所示。

视频见链接:https://zhuanlan.zhihu.com/p/558150619

曲面细分很好用,但是当我们想用在移动设备上时,可能会有些犹豫。从兼容性上说,从OpenGLES3.2开始支持曲面细分,可以认为现在用户手里绝大部分安卓手机都是可以使用这一特性的,但是不排除某些特立独行的机型或者安卓模拟器存在特殊情况。从实际操作上来说,目前也确实基本上没见过在手游项目中使用曲面细分的案例。不过笔者使用上述演示Demo在XiaoMi9SE设备上测试运行是没问题的。

实际使用的话,我们可能需要考虑这个问题:使用类似于FFT技术生成拟真度非常高的波浪时,需要极细密的网格密度,同时,在Vertex Shader中的计算(需要做顶点置换操作)也会非常复杂,这种情况下,海面顶点数可能成为性能消耗瓶颈,然而使用曲面细分加密网格是在一个Mesh上操作的,结果是整个网格全部会走一遍复杂的Vertex Shader。假设在视野中看到的海面网格部分需要8万个顶点才能满足密度要求,以FOV=60为例,那细分后的整个网格大概是48万顶点,对于手游来说或许有些高了。要避免这个问题,我们可以把思路放到上,也可以想办法。

Lod模型拼接

渲染管线在实际开始渲染前,会做一步视锥剔除的操作,目的是将不被当前视野看到的对象排除出去,进而减轻后面渲染管线的工作负担。因此如果我们生成的海面是由多块Mesh、多个游戏对象组合而成,那么渲染管线就可以自动为我们剔除看不见的区域了,同时,我们手动控制海面的组合,也方便进行LOD控制,我们可以让离摄像机近的区块网格密度高,而离摄像机越远,网格密度逐渐减小。

一种典型的海面LOD模型拼接方式如下图所示。

海面网格中间核心位置最密,每往外一层,网格密度减少一半,由于模型大小一直不变,很容易将这些对象组合到一起。

具体的实现细节,我们首先根据自己的需要确定LOD层数,以及每一层级对应的网格密度,并将需要的Mesh创建好。下面的案例代码将所有LOD的Mesh大小限制为1(-0.5~0.5),方便设置对象缩放。

使用生成的各级Lod模型,我们直接通过两层循环创建最后放到场景中水块对象即可。

最终效果如下面所示。

视频见链接:https://zhuanlan.zhihu.com/p/558150619

通过这种拼接方式,每帧我们只会渲染出现在视野中的水块,很好地解决了我们优化顶点数量的需求,但是同时也带来了两个新的问题:

(1) 海面由几十上百个对象组成,导致渲染批次增加;

(2) LOD不同级别结合处由于顶点没有完全重合,在顶点置换过程中,很容易出现裂缝,如下图所示。

对于问题(1),我们可以通过使用GPU Instancing的方式合并批次,案例中7级LOD拼接成的水面渲染批次占用5个,因此只要Lod级别合适,并且没有太多与水块数量相关的功能渲染,批次问题不大。对于问题(2),如果在具体项目中瑕疵不大,可以通过一个非常简单的方式解决,那就是让外围LOD对象逐级下沉一定的高度,来让裂缝被高一级的LOD遮挡从而不容易观察到,就像这样:

但是如果想要完美解决裂缝问题,那就需要用上别的方法了……

无缝Lod模型拼接

一如前面所说,海面Mesh出现裂缝是由于LOD结合处存在不重合的顶点导致,那么显而易见,消除这一裂缝的办法,那就是消除结合处不重合顶点。根据这一思路,我们这里介绍一种典型的无缝LOD拼接方案[1]。

该方案的解决思路是让LOD之间的顶点在Vertex Shader中进行基于距离的位置偏移,从而达到LOD Mesh之间的自然过渡。为了达到该目的,首先我们创建的Mesh结构需要做些变化,不再是斜着的三角形划分,而是交叉式的三角形划分,如下图所示。

这种结构可以很容易从当前密度向下一LOD级别过渡,主要是让多出来的顶点向交叉斜线四周偏移直至与剩余顶点重合,下图是其过渡的动画过程。

视频见链接:https://zhuanlan.zhihu.com/p/558150619

根据这一规律,我们可以在shader中利用顶点的WorldSpacePosition计算其在当前LOD级别应该要应用的过渡值,并进行世界空间偏移。具体的Shader计算过程与注释如下面代码所示。

最终呈现出来的Mesh LOD拼接过渡效果:

视频见链接:https://zhuanlan.zhihu.com/p/558150619

投影网格

以上所介绍的海面Mesh生成方式,都是将海洋作为场景中的固定物体,所有顶点经历正常的空间变换最终进行片元渲染,但是大家有没有想过,难道所有物体的渲染都需要走这样固定的过程吗?

当然不是!只要我们想,我们甚至可以构造顶点位置处于屏幕空间范围的三角面,并在Vertex Shader中直接输出进行渲染。本节要介绍的海面Mesh动态生成方法——投影网格[2]就是类似的思路,其核心点在于,事先构造一个屏幕空间(-1~1)的网格,进入Vertex Shader后,首先利用一个特殊的反投影矩阵,计算一条起点在反投影摄像机近裁剪面,终点在反投影摄像机远裁剪面的射线,然后计算这条射线与海平面(高度固定的水平面)的交点,这个交点就是实际的海平面上世界空间顶点位置,然后使用这个位置进行正常的摄像机空间变换和投影变换就可以正常渲染了。也就是说,实际构造的网格保存的是屏幕位置信息。如下图所示,红色网格是我们事先构造的Mesh,蓝色射线是计算的世界空间射线(从反投影摄像机近裁剪面指向远裁剪面),A点则是与高度为_WaterHeight的水平面交点,因此整个黑色的网格就是当前视角下的海面世界空间网格。

上述计算在Shader中表示为:

这一步计算只要理解了还是比较简单的,难点在于反投影矩阵(_MatrixVPInverse)的确定。要计算正确的反投影矩阵,我们首先在世界空间构建一个投影器Projector。我们对投影器的要求是其始终朝向水面并且完全覆盖主摄像机视野范围内的水面波动,具体如下图所示。

因为海面是上下起伏的,所以使用baseUp与basedown分别表示海面波动的上界与下界,投影器需要覆盖从basedown到baseUp与主摄像机视锥体空间的交集,我们使用主摄像机视线方向一定距离的点在水平面上的投影点作为投影器的注视点,来保证投影器始终朝向水面。

并以此确定投影器空间矩阵。

接着,我们计算视锥体12根线段分别与baseUp平面、basedown平面的交点,以及处于basedown到baseUp空间内的视锥顶点,加入一个待处理的列表。下图代码中lineIndices数组为{ 0, 1, 1, 2, 2, 3, 3, 0, 4, 5, 5, 6, 6, 7, 7, 4, 0, 4, 1, 5, 2, 6, 3, 7 },表明视锥体的12根线段,IntersectionWithPlane函数用来求线段与某一高度水平面的交点。

将获得的visibleFrustumList列表中的点投影到海面上之后,再使用当前Projector视图矩阵和主摄像机投影矩阵进行投影变换,获得投影的最大最小值。因为是对前文所描述的覆盖区域进行投影的原因,投影结果将不会处于[-1,1]之间,而是比该范围大,我们使用这些值构造一个“扩大变换”矩阵,来对原始投影矩阵进行校正,获得最终我们需要的反投影矩阵matVPInverse。

最终网格动态效果如下面所示。

视频见链接:https://zhuanlan.zhihu.com/p/558150619

从视频可以看出,投影网格的方式是对海面顶点的极致控制,它存在典型的特点:

(1) 使用固定数量的顶点生成比主摄像机当前视野稍大一点的网格,最大限度利用了顶点;

(2) 由于投影的特性,生成的网格自带近处细密,越远越稀疏的LOD特征。

因此,投影网格是一种非常简洁的海面生成方案。

总结

本文简单介绍了在移动端生成无限海面网格的几种典型方式及其具体实现,希望能对大家实际工作带来一些帮助。演示案例请从这里下载——>Example.rar_免费高速下载|百度网盘-分享无限制 (baidu.com)

[1] Crest: Novel ocean rendering techniques in an open source framework, SIGGRAPH 2017

Real-time water rendering Introducing the projected grid concept, Claes Johanson,2004

欢迎加入我们!

感兴趣的同学可以投递简历至引擎部们招聘邮箱:CYouEngine@cyou-inc.com

全部评论

相关推荐

11-02 09:49
已编辑
货拉拉_测试(实习员工)
热爱生活的仰泳鲈鱼求你们别卷了:没事楼主,有反转查看图片
点赞 评论 收藏
分享
头像
11-18 16:08
福州大学 Java
影流之主:干10年不被裁,我就能拿别人一年的钱了,日子有盼头了
点赞 评论 收藏
分享
3 收藏 评论
分享
牛客网
牛客企业服务