文章目录

  • 前言
  • 关于LOD技术
  • 三角形瓦片的计算
  • 实现更大的细分程度
  • 基于四叉树的动态LOD技术
  • Unity ECS技术
  • 检测和更新瓦片LOD



前言

  上一节主要讲述了动态生成球体模型的思路及正二十面体的生成,这一节我们来讲讲如何实现实现。因为将来要是生成的是一个6371km并具有一定分辨率的球体,若将每个顶点都细分计算出来,这样的顶点数无疑会是个天文数字,如此巨大的开销在目前的主流PC设备上,想要流畅运行几乎是不可能的。并且unity中单个模型的顶点数也不能超过65535,所以必须用多个模型来组成一个球体,这些模型的管理也是一个比较大的性能开销。所以这里使用LOD技术,在不影响整体效果的情况下,对距离玩家近的部分,细节程度越高,顶点越密集;反之细节程度越低,顶点越稀疏。


关于LOD技术

  LOD技术(Level Of Detail)指用若干不同复杂度的模型来表示同一对象的技术。此技术主要根据视点距离对象位置的变化调用不同复杂度的模型,即在较远时调用低复杂度模型,在较近时调用高复杂度模型。采用该技术虽然会损失一定的逼真程度,但可以减少场景绘制的多边形的数量,节约了系统资源,与它提高的性能来说,这些损失是可以接受的。对于静态的LOD,同一个物体一般需要准备若干个不同精度的模型,根据与玩家的距离加载不同精度的模型,而动态的LOD需要在某些时机,比如玩家靠近或远离了一定距离,动态计算出适宜精度的模型。

unity 动态Enum Unity 动态lod_游戏引擎


静态的LOD当然性能更好,因为不用动态地去计算模型,直接从内存加载即可,是用空间换时间,对于一些比较复杂的模型一般也是用静态的LOD,unity也自带有静态LOD处理的功能。但这里由于模型本身就是动态生成的,所以必须要使用动态的LOD,并且我们的模型也比较简单,只是一个三角面,是可以计算出来的。

三角形瓦片的计算

  这里我们将一个细分的三角面作为一个最小的模型单元,具体需要细分多少可以自己定,细分后最高的顶点数不超过65535即可。这里我是设计了一个叫Tile(意为瓦片)的类,之后也将其称为“瓦片”以免与其他概念混淆。这个类通过三个顶点位置去计算由这三个顶点所围成的三角面,然后通过一个细分程度作为参数去进行细分。例如下图,细分3次,也就是把外面那个大三角形的边分成了3段:

unity 动态Enum Unity 动态lod_高速缓存_02

其中的每一个点也是比较容易就能求出来的,用点b减去点a即可得到向量AB,向量AB除以细分程度+1即可得到向量AD(以下公式的向量用大写字母表示),以此类推也可以得到DE,点a不用求,点d=a + 1* AB,点e=a + 1AD + 1DE,所以这三角形上的每个点=a + n* AD + m* DE,其中n <= 细分程度+1,m <= n。用一个双循环累加(当然乘以m或者n也行,这里用加法主要是因为加法比乘法快)就可以求出这个三角面的所有顶点了。

接下来要求它的三角形连接顺序。我们先从上到下给这个模型的所有顶点都上个标号,对应的也是顶点在数组中的下标:

unity 动态Enum Unity 动态lod_高速缓存_03

先通肉眼观察,这些三角形顺时针该怎么连,(0,1,2),(1,3,4),(1,2,4)……(8,9,13),(9,13,14)好像都没什么规律,但通过仔细可以发现:朝向相同的情况下,右边的三角形与左边的三角形的每个顶点的标号都是相差1,因此我们只需要求出左边那一列的三角形的连接顺序,就可以得到其右边的三角形连接顺序,问题就来到了然后求左边的三角形的连接顺序。再看看左边三角形的顶点0、1、3、6、10,发现规律0=0,1=0+1,3=0+1+2,6=0+1+2+3,10=0+1+2+3+4,将这些顶点按行排好,左边方向朝上的三角形的顶点标号=该顶点所在行数的等差数列和,行数从第0行开始,最后一行为细分次数+1。得到了这个规律,那另外两个点就好算多了,右下点=左下点+1,左下点=顶点行号+1的等差数列和。以此类推,也可以计算朝下的三角形。朝下的三角形的行号从1开始,最后一行=细分次数,右上点=左上点+1,左上点=该顶点所在行数的等差数列和,下面的点=该顶点所在行数+1的等差数列和再+1。最后通过循环计算出右边的三角形,循环次数为当前三角形顶点的行号,与此同时也可以计算顶点朝下的三角形连接顺序。

实现更大的细分程度

  我们现在有了一个可以细分出具有一定面数的瓦片,理论上把20个瓦片细分到一定程度,然后贴在正二十面体的各个面上,最后乘上一个半径即可得到一个球体的模型,但是如果是需要生成一个半径非常巨大的球体,那需要细分的程度会非常高,这会导致单个瓦片模型的顶点数超出65535,并且我们需要实现在离玩家近的距离上细分程度高,越远细分程度越低。那既然单个瓦片的细分次数有上限,我们就需要用很多瓦片去铺满正二十面体的各个面,那么如何去铺这些瓦片呢?让我们再来研究一下三角形:

unity 动态Enum Unity 动态lod_数据_04

可以发现,当一个三角形细分一次,会得到四个小三角形。而正二十面体的每个面都是一个三角形面,细分后得到的每个小三角形也可以再次细分成4个更小的三角形,只要细分的次数够多,最后再贴上瓦片,就能得到我们想要的细节程度。而贴在这些三角形上的瓦片,为了方便实现动态LOD,将使用四叉树进行管理。为了节省性能,这颗四叉树只有叶子节点是真正存储有瓦片模型的,其他节点只存储节点间的父子关系。因为当一个瓦片被细分后,原来的模型应该消失,只有细分出来的小瓦片显示出来就足够了,也就是它的叶子节点。

基于四叉树的动态LOD技术

  要实现离玩家远时细节程度变低,离玩家近时细分程度变高,首先要知道,某个三角形离玩家多近?知道离玩家多远后该降低还是升高自己的细分程度?降低或升高为多少?为了解决这个问题,我们需要引入一个概念叫LOD等级,LOD等级越高细分程度越高,反之亦然。

我们可以通过当前瓦片与玩家的最短距离,决定LOD等级。当玩家距离瓦片为0,也就是站在瓦片上时,LOD等级最高。距离越远,LOD等级逐级下降,直到LOD等级为0,LOD等级为0时也就是不进行细分,直接是一块很大的瓦片。那最大LOD等级又如何计算呢?这得取决与两个因素:1、每个瓦片的细分程度;2、最高瓦片模型分辨率;3、星球的半径。比如当最高瓦片模型分辨率为3米,玩家脚下的瓦片,与玩家的距离是0,它的LOD等级最高,细分程度要达到瓦片模型上每个顶点的距离在3米左右,在距离玩家较远的模型,LOD等级逐级下降,分辨率也逐渐减小,由3米,到6米,到12米……程倍数增加。最后LOD为0的分辨率多少米我们不用关心,因为太远了也看不到。 如果按照距离来划分LOD,比如最高LOD等级为8时,小于300m为LOD 8,小于600m大于300m为LOD 7,小于1200m大于600m为LOD 6……以此类推,每段LOD等级的持续距离都是比它大的LOD等级的持续距离的总和。同时利用LOD等级来决定四叉树的深度,其具体用法后面再详细说明。

unity 动态Enum Unity 动态lod_unity_05

unity 动态Enum Unity 动态lod_unity_06


可以看到,离玩家越近时,顶点越密集,反之越稀疏。

  现在完成了LOD的计算,但目前还不能算“动态LOD”,因为玩家不可能是一直站着不动的,我们还需要根据玩家位置不停地更新瓦片的LOD等级,当一些瓦片的LOD等级发生改变时,还需要重新去生成这些模型。写到这时我不禁产生一个疑问:那既然每个模型都是一样的三角面,只是它们的缩放和位置和旋转有所不同,那是不是可以直接只计算一个,然后通过这个模板去复杂去其他的瓦片模型,最后改下缩放和旋转和位置,贴到球面对应的位置上不就可以了吗?模型的三角形连接顺序确实可以不用重复地算,但模型的顶点,还是要重复算的,因为到后面,为了生成出地形而不是一个光溜溜的球面。我们还是需要遍历每一个顶点,还要计算每个顶点的高度,既然每次都要遍历这些顶点,与其去做这些麻烦的变换,还不如直接边创建这些顶点边计算高度,从而让它看起来像是一块地表,而每块地表都是不一样的。

  要去动态的更新这些模型,我们需要用到Unity的ECS技术去检测需要更新的瓦片,再用Compute Shader或者Unity Job系统去动态生成瓦片模型。

Unity ECS技术

  ECS(Entity Component System),中文翻译过来是实体组件系统,是截至目前Unity的一项比较新的技术,目前仍处于体验版本。它不同与我们平常的面向对象的编程范式,它是一种面向数据的编程范式。
  Entity:并非是一个分配的内存中的对象,它只是一个ID号,作用类似于容器的索引。Component:与Entity相关的数据,是一个struct。System:处理Component数据的逻辑。
  ECS运行效率非常快,主要原因是它能够充分利用CPU的高速缓存,提高数据在CPU的高速缓存命中率,从而大大提高了CPU访存速度。CPU操作数据会先从高速缓存中取得数据,速度非常快,如果在高速缓存中没读取到需要的数据,也就是没有命中高速缓存,就会去内存中读取。由于CPU到高速缓存读取数据的速度远远高于从内存中读取的速度,所以提高高速缓存的命中率就可以提高运行效率。与传统的面向对象中new出来的GameObject相比,Component体积可以设计得更小,组织更严密,能更多的塞进高速缓存里。例如当我们需要频繁操作一个GameObject的坐标数据,在传统面向对象模式下会把整个GameObject加载进高速缓存,而我们真正需要频繁访问的只有坐标数据,其他数据却也加载进高速缓存,白白浪费了高速缓存。而ECS中使用的是Component,只需要把要操作的数据组成一个Component,大大避免了浪费高速缓存从而存储更多数据提高缓存命中率。
  当然ECS也有一些不足,就是目前的Unity ECS还不是很完善,各类资料较少,与之配套的很多系统也没完善,用起来对代码的设计要求更高。
  ECS目前(2022.9)仍然处于频繁更新中,很多教程刚出可能很快就会过时了,推荐看官方文档(源码是用0.50.0版本):https://docs.unity3d.com/Packages/com.unity.entities@0.51/manual/install_setup.html

检测和更新瓦片LOD

  定时检测瓦片与玩家距离,当瓦片的LOD等级高于当前距离玩家合适的LOD等级,则向上找到对应LOD等级合适的父瓦片,重新显示父瓦片的模型,然后删除四叉树结构下该父瓦片下的子瓦片,这里的删除是指将其放回对象池,防止频繁地产生GC,这样就可以达到降低细节程度的目的。

unity 动态Enum Unity 动态lod_高速缓存_07

  当瓦片的LOD等级低于当前距离玩家合适的LOD等级,则删除自己的瓦片模型,细分一次,最后再细分出来的4个三角形内贴上子瓦片,并加入到四叉树结构中。在贴的时候需要注意的是瓦片模型的顶点要多出来一圈,使之与相邻的瓦片部分重合到一起,防止两块瓦片之间LOD等级不同时出现裂缝。

unity 动态Enum Unity 动态lod_数据_08

  至于模型的计算,这也是个比较耗性能的点。计算这些模型的顶点虽然简单但它数量巨大,并且做随机地形的话还要为每个顶点计算一个随机的高度值,所以又不得不使用Unity Job系统或者Compute Shader,可以查看源码看看它的使用方式(在专栏简介有GitHub链接)。