查看原文
其他

Unite 2018|玩转移动端大型世界开发

Unity Unity官方平台 2018-11-15

今天将分享Unite Beijing 2018大会上,Unity技术支持工程师牟宝玉的演讲,带你使用Unity玩转移动端大型世界的开发。


下面是演讲内容:

大家好,今天我们交流一下游戏中大型世界问题,分享我们初步的研究和方向。


世界多大呢? 8000×8000,如果一个单位对应一米的距离,8000就是8公里,开车的速度如果是每小时60公里,我们大概需要开车10分钟,而在这么大的地图中,我们可以设置很多剧情和任务。大世界的类型可能有很多种,可能是一个海岛,或者游戏中茂密的森林,一些山脉这种野外地形。也可以是一个大型的城市,城市中可以发生和外星人对战。



今天我将以大型山脉为例,与大家一起做一个探讨。



首先第一个面临的问题就是大世界怎么搭建?一般来说可以使用Unity的Terrain组件,同时也有一些丰富的插件做这方面的支持。比如我们可以用Terrain Composer2,我直接使用了一个现成的地形,接下来看一下演示基本的样子。


下图中的地图相近靠边的地方这样一个场景的全部概貌:远处是山,里面有5张地形、10种树、3种植被、1组5个建筑物、2种共498个石头。



石头数量太多,一个一个摆放没有那么多时间,位置就直接随机一下,我给每个石头加一个钢体,点击运行后石头慢慢的掉地上,掉地上后和地面结合非常好,直接把信息保存拽进来就可以使用了,善用Unity里面一些自带的功能也是比较有趣的。

 

这个Demo使用了内置的物理系统,实行一个平行光做实时阴影,地形进行了烘焙。使用了Wind Zone,加入二个屏幕后处理效果。这个Demo也从Asset Store资源商店中使用很多插件。例如:森林地形和动态物。


还有的是LOD,它的效果我感觉不是特别的理想,里面的小房子LOD的效果很好,我直接引入进来放在地形里面。左下角是一个移动平台上第一人称视角的插件,可以控制这个角色移动。接下来是PostProcessing,最后是地形切换的插件,接下来的地形我们切一切。



我们为了站在山崖上看地形的全貌,在iPhone 6s上渲染了一下,渲染是185个渲染批次,Tris是65.5k,紧接着三角面,所对应的顶点达到4万4千个,为什么会有这么多三角面呢?因为看的远,相机多远是1万5的,因为你这个位置看对面有点切掉一个部分。


我们在当前这个视角下看到哪些内容?这么多三角面都是代表了什么?我们逐步分析一下。



第一行是笔刷,5张贴图在地形里面对应5个控制通道,5个控制通道就是后面的3张贴图,第1张是RGB控制前3个笔刷,灰白控制第4个笔刷,第5个笔刷是由八通道控制。地形里面使用的树木比较多,下面列了10棵树,摆放和渲染的时候离的远是公告牌的样式,离的近的话占用很多。最下面是放了三个草。



上图是地图中的小房子,我们先看下层这部分。我们可以看到小房子总共有4个LOD的层级,这个层级已经很多,最多LOD0在上面也会看到,里面包括窗户,房间里的楼梯还有酒杯、桌子,所以这是非常细致的,也就是说这个房子是允许角色走进去的。那么有多近,可能5米或者10米需要用LOD0,远一点是LOD1或者2,如果距离非常远,站在非常远的地方看对面山顶有一个房子,这就是LOD3,所以可以看到LOD3达到32%,这允许玩家在非常远的地方看到一个房子的虚影。



然后是石头,这个石头其实也是需要做出更多的LOD,Demo没有做太多,只做了二个层级,一个石头200面,但是距离远会被直接裁掉。



通过关闭草和树木,计算出草和树占4.4k三角面,那么剩下的都是地形的。最后我们看一下剩下的地形部分,6.1万面还是非常的多。我们可以看到人物是在最下面站着,从下往上看,距离越近的地方越黑,网格越密,距离越远网格越稀疏,所以远处看山棱角比较分明,这也是一般的地形常用的做法,也是优化的手段,包括Unity里面一些使用的方案。


既然地形这么多的话,我们首先从地形上入手,把地形拆一拆,切一切。



首先我们想到拆分资源,如果我们做大型项目肯定都要拆分资源,它有很多优势。例如:地形拆了之后可以分块加载每次只加载几块,可以减少内存占用,也可以资源热更新、动态加载等。



我们先看第一个地形拆分里面的地表拆分。地形拆分我们先把Terrain拆分,这个插件直接把Terrain里的数据读出来,包括树和植被,自动做拆分。拆的时候很简单,把原始的地形选上,然后设置一下拆多少块,我这里写的8个块,横竖都是8个块,64个块。然后设置一下边缘混合的宽度,这样宽度就会显得更平滑一些,然后设置一下输出路径,点下面的拆分就可以了。


拆分之后由一个大的变成许多小的,一共变成64个,其实拆很简单,但是这么做它是怎么做到的?下面我们来谈谈Terrain底层数据。



首先Terrain下面有TerrainData,高度图、细节位置、树以及其它的控制图和纹理信息和数据信息都在Terrain数据里面,从里面可以获取到几乎Terrain的全部数据。里面这些对应的方法,例如:GetAlphaMaps是细节的数据,Layer是植被的数据,Height是高度,因为地形按照高度图做的计算。


Unity现在支持高度图的导入和导出,方便美术使用其它的工具制作地形之后,然后导出高度图,再导入Unity中,导完了之后然后设置地形里面即可。



在上图的代码里面直接访问,然后按照拆块的数量通过返回高度,这个高度区间是0-1的范围,届时会映射到地形里面设置的高度上。如果是600,就是从0-600的范围,会重新映射过去。GetAlphaMaps是一个三维,前二个是XY,因为用笔刷的时候,我这里用5个笔刷图,所以最后一个维度对应着5,这是一个对应关系,也可以说是一个权重值。


我们把数据获取后创建一个新的Terrain Data,把这些数据拽出来分块存进去。当读数据的时候肯定把Terrain Data的高度、分辨率还有混合贴图这些东西都读出来,包括在设置地形里面这些植被的密度,还有一些其它的参数,例如:各个分辨率,地表分辨率设置成1024,可能拆的时候除8,因为我这里拆了64块,1024代表一个轴的长度,除8再存进去,这样就可以拆出来64块。


在这里涉及到Lightmap的问题,UV2无法更改,只能重新拆一下,Unity里面也提供了读取的方法,拆好之后到时候重新影射回去就可以了。然后把LightMap信息读出来,然后把索引设置一下,让它们保持一一对应的关系。


拆了之后还有一个问题,就是往回拼。拼的时候需要设置一个邻接块,这主要是为了解决地块之间在不同的LOD级别产生接缝和漏白的问题,红箭头的地方就是拼缝的地方。



设置拼缝问题的时候,在函数里面需要有4个函数。例如:中间这个地块设置邻接,需要设置4个上下左右,对应的API是上下左右,它拼缝的时候如下面这个图的样子,把一些稀疏的网格和致密的网格按照这种形式拼在一起。



当我们拆分地面上一些树、房子这些资源,其实在拆分的时候也是按照地块来拆分的。每个地块对应一组资源,当然拆分的时候这些资源也有共用性,所以我们拆分之后把每个信息都尽量的独立出来,然后分类和保存好,然后资源解耦也要注意一下,尽量是用字符串把他们记好,不要用关联这种形式。



打包的时候涉及很多内容,一定要分档次,从最低端的手机一直到苹果iPhone X都是需要支持,支持的时候这个包里面有最低端也有高端,包括的资源量也不一样,有大贴图、小贴图,包括UI的贴图也会分二套。然后分的时候模型的面数,LOD的层级。以房子的为例,房子需要分很多级别,建议使用相关工具,贴图的尺寸注意一下大的小的,包括通过Texture Quality来设置当前载入的时候是直接按照一半来载还是按照更低的尺寸来载。


贴图的数量,贴图数量怎么理解?例如:说高端机型用PBR,里面用几张贴图,可能中端机型是一个普通的,那么这个时候一定要用单独的(英文)来打包。因为Unity目前有一个设计的问题。这个问题怎么说?当我们在一个材质里面切换的时候,相同的项会继续保留,用起来很方便,无论切换哪个只要属性的名字是对的是一样的名字,那么换贴图或者换别的,那么贴图自动回来,用起来很方便。相应的缺点就是这个哪怕看不见,其实打包的时候会引入进去。那么我们使用的时候要么打包前剔除,要么使用不同的材质球去分别设置。



粒子系统不要大规模使用,可以某些地方尽量少的的使用,因为粒子系统耗费很大,内存占用较多。还有地形参数,不同机型对应的参数不一样。我们尽量要把Bundle做的更加灵活,资源更加细,相应的依赖关系也总结好,因为一旦粒度做的细,就有很多关系,我们要使用一些算法,保证加载的时候这些相应都会被加载,如果已经存在的就不会重复加载,都要在算法上做一定的保证。资源冗余也要处理好。


我们要注意一下为什么同名?看一下它们的引用,可以帮助大家去定位资源为什么会有冗余?资源的名字尽量不要重复,一定要起名字。


打包完毕之后,准备加载,我们不会一次性加载,那么相应的加载时间短,内存占有小,加载速度会提升,点也是对应的,我们通过这些点去处理这些事情。为什么一步一步加载,因为按照打包粒度比较细,就会出现Bundle数量非常多,出现几千或者上万,这些都需要控制好。



首现是九宫格加载,这个加载方案适用于比较密集的地形,移动不会特别频繁,视角更倾向于俯视角。每次最多只显示9个场景的数据,当角色在中间的时候比如移动到边缘黄框,这个时候触发。例如:对上图做加载,左下角看第一个,那么到达第二个的时候周边九个都有了,然后往上走再继续加载周边的。


触发加载的时候会有一定的卡顿,这个卡顿需要处理,怎么处理?根据不同的机型可能有参数的不一样,例如:资源加载有资源加载的序列,资源卸载有资源卸载的序列,高端的机型多加载一些。



如果出现卡顿,首现要考虑缓冲池。这样的好处不会那么频繁的加载卸载资源,因为玩家可能走过去或者走过来,如果提前加载,玩家发现走这边不会有明显的卡顿,然后会有更多的时间去加载相应的缓冲数据,使游戏相对更流畅一些。物体不需要的时候,直接放到缓冲池,当玩家确切走到边缘又迈出需要加载的一步,再加载就可以了。然后把不常用的资源清掉,缓冲池最大的数量注意控制好,根据不同机型进行调整。



第二个反感是全地形加载。全都加载一次性的。为什么这么做?例如我们在室外,相机一动可以看到最远处的山,这座山有多远?如上图所示,蓝色箭头是最边缘的山,就是在8000这个位置。右侧是红箭头,红箭头可能看不清楚,就是看山的侧边有几个小小的黑点,看着很虚,这是树的公告牌的片,因为山不可能光秃秃的,所以我们看到山的树影,我们一般尝试跑到那个位置,可以在那个位置做伏击,有树做掩护。



我们看的非常远,看到这么多东西,那么机器能不能抗住?


下图iPhone 6S上内存分布,加载耗时长一点,这是它的缺点,5张地形纹理刷,10种树,3种植被,石头等等这些,平行光,实时阴影。所以这里可以看到左上位置的RenderTexture达到29.9兆,纹理部分达到60兆,Mesh是5.1兆,其它就是一些公告牌显示的草还有远处这些树,这些三角面。



GPU渲染的压力比较大,这里可以看到TILER达到53%,下面列出来二项BlitCopy,这是渲染效果,它们的排名靠前是很正常的,对于全屏效果的优化,每个项目可能考虑自身的情况做一定的优化。


一次性加载之后有的优点是地形统一管理,通过调整Piexl Error和Base Map Dist,从65k调32k,从疏密程度已经降低很多了,当然这是以牺牲效果为代价的。


首先第一个Pixel Error,这是一个高度图和纹理在地形上影射的精度,从下图中的二个图,我们可以看出来左侧网格非常的致密,可以说精度比较高,也就是这个值比较小,或者我们可以理解越小网格越致密,显示出来的地形细节更丰富。如果比较大的话那么允许这个精度上调的更放宽,允许有更多的棱角出现,那么就会使网格变成右边,在远处看着非常的稀疏,稀疏之后相应的渲染开销也会减少很多。



第二项是细节纹理的距离,因为创作地形的时候我们会混合有多个笔刷,多个笔刷会生成一张大的比较完整的贴图,这张贴图主要是用来影射当前整个地形的,这个贴图包括已经绘制好的石头、草、沙子,相当于画很多层把这个图压下来,一般距离相机稍微远的地方,对显示精度要求不会那么高,就不会拿笔刷采样4-5次,生成精度非常高的效果,看着相对有点模糊。


下面还有几个参数比较好理解,例如:草的密度,显示树的距离,显示树的时候起始位置还有和长度等等。



这里树的距离是2000,希望远处看到树的影子,这样希望远处场景看起来有比较好的效果,如果把这些值设的越小公告牌越多,渲染压力越小。


然后是各种分辨率,高度图就是普通的贴图,但是它在Unity里面并不是以贴图的形式存储的。Detail Resolution设的越小的话那么决定下面草的细节,一个像素点对应的面积就会更大,那么这一个面积里面更大,里面显示的草就会更少。这个图的大小要适度,如果小的话,你发现地形的草无论怎么调就是那么小,缺点是渲染压力大一些。Per Patch这个值可以调的大一点,可以调128。最后是根据笔刷贴图调整的数值,是以贴图形式的,最后是base texture Resolution,把所有的笔刷像合成一样合到一起。



最后是优化,减少笔刷数量和纹理。因为少一个纹理至少可以减少一个采样,采样在GPU也是非常耗时的。Unity地形每4个笔刷纹理需要1个控制纹理,每个纹理对应一个通道,如果我们数量减下来的话,这个通道数使用少一些。


树也需要可以尝试减少它的使用细节,我们可以尝试减少草的数量。建筑这些都是需要LOD进行控制,你在远处看到一个很简单的轮廓,走到近处房子玻璃是透明的,里面的人也能够看见了,达到这个效果。包括粒子也需要做几个等级,按照不同的需求做加载。



最后展望一下,如果我们把树的2000调大一点或者LOD细分的时候,能够再把这些网格数量压一些,让在更低端的手机可以运行,比如让速度提高一些或者加入流式加载,让地形加载更加顺畅,控制起来更简单一些。甚至于说用机器学习,直接生成美术、程序、策划,都满意的地形!

 

推荐阅读

 

点击“阅读原文”访问Unity官方中文论坛

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存