《Unity3D高级编程之进阶主程》第十章,地图与寻路(四) 地图的制作与优化

地图的制作与优化

场景的制作占到所有项目制作中非常大的一块工作内容,我们来说说场景制作在技术层面上技巧。上节把地图编辑器学习了一下,这节我们就在地图编辑器之外说说,地图场景的制作与优化。

===

我们先来说说场景中的地图,准确的说应该是地形,地形是场景中比较关键的一部分,在项目中地形的制作分为几种:

定制式地形

定制式地形比较常见,由3D设计师制作出来的网格模型作为地面的地形放置在场景中,相当于一个模型放置在场景上一样,给这个网格模型加入MeshCollider碰撞组件(这里因为是静态的地形所以静态的碰撞体使用 Mesh Collider 性能还尚可,如果是动态物体则不能使用 Mesh Collider)。也有不需要网格模型的,比如使用高度图来计算坐标。

有的项目会使用Unity3D内置的地形,也相当于将一个网格模型放置在场景中,Unity3D的地形可以通过更改地形高度图来改变起伏,不同通常地图项目用 Maya 和 3DMax 等模型软件来构建地形,这种做法能更加容易的被设计师掌控以及自由发挥,也更加节省资源,对程序来说在处理性能优化时会更加容易一些。其他3D物体,比如石头,小石块,草,树,动物等也是同种方式放置在场景中。

定制式地形的好处在于地形和地图的制作不受限制,地形设计师和场景制作师能根据自己的喜好和想象中的画面来自由得定制场景,他们可以任意的移动、旋转、缩放、更换、修整、完善等,这能让他们对场景上的把控有很大的自由度,这种方式是比较常用的单一场景的制作方式。

程序拼接地形方式

用程序拼接地形的主旨意图是希望能在一定的规则下更好的为设计人员自由设计更好的地图而选择的方式,这样在规则下去做一些事情从设计和性能上求得一个平衡点,整个地图由程序生成,通常会一个或者几个模型的结构铺满整个地图。

用这种方式制作地图最常见的要属2D的RPG游戏,2D角色扮演类游戏中,几乎整个屏幕的地形都需要用地图编辑器来建立和拼凑,运行时程序会将地图数据加载进来后,对每一块的地图都进行动态的拼合,这样一来整个地图就只有一个drawcall,因为它只使用了一个网格(程序生成的)和一个材质球。对此种方式我们做一些分析:

我们先来看看2D RPG游戏中,是如何动态拼合整个地图。现在很多2D游戏都通过Unity3D来实现,其实2D也是有Mesh、顶点、UV的概念,和3D比起来只是少了1个维度,因此在2D游戏中的地图程序拼合手法也是通过生成顶点和三角来完成的。我们可以设想整个地图就是一个大的矩形,地图中每个方块是这个大矩形中的一个格子,而每个格子都由2个三角形构成,既然我们能计算出每个方块的位置,我们就能计算出每个三角的顶点和位置。简单来说地图是一个大矩形,它由很多个大小相同的小方块构成,小方块的大小可以通过配置文件的形式来调整,这样有规则、大小一致、位置可寻的网格可以通过程序生成。

我们用三角形的方式拼接每个方块,并调整地图方块上顶点的UV,UV指向的是地图贴图中的某一块,而地图贴图以图集的方式存储地图上不同类型的贴图。就这样所有的方块拼合成了一个大网格,每个方块上的顶点上的UV都指向地图贴图中的某一块地块内容,地块贴图内容全部集中在一张图集里。

伪代码:

		//生成地图
		void Generate_map()
		{
			//遍历每块矩形的方块位置
			for i to width_count then
				for j to height_count then
					mesh = generate_trangle_by_rectangle(i,j,_type, texture_info);
					AddCombine(mesh);
				end
			end
			CombineMeshList();
		}

		//生成矩形方块所需要的,4个顶点,4个索引,2个三角形,2个三角形的顶点索引,以及三角形的uv位置。
		void generate_trangle_by_rectangle(int _x, int _y, int _type, texture_info _tex)
		{
			//矩形的4个顶点
			point1 = vector2( (_x - 0.5) * width, (_y + 0.5) * height);
			point2 = vector2( (_x + 0.5) * width, (_y + 0.5) * height);
			point3 = vector2( (_x + 0.5) * width, (_y - 0.5) * height);
			point4 = vector2( (_x - 0.5) * width, (_y - 0.5) * height);

			//顶点增加后的索引位置
			point_index1 = add_point(point1);
			point_index2 = add_point(point2);
			point_index3 = add_point(point3);
			point_index4 = add_point(point4);

			//三角形的生成时的顶点
			trangle1 = [point1, point2, point3];
			trangle2 = [point3, point4, point1];

			//三角形顶点的索引信息
			trangle_index1 = [point_index1, point_index2, point_index3];
			trangle_index2 = [point_index3, point_index4, point_index1];

			//4个uv点位的信息
			point_uv1 = vector2(_tex.uv_x, _tex.uv_y);
			point_uv2 = vector2(_tex.uv_x + _tex.width, _tex.uv_y);
			point_uv3 = vector2(_tex.uv_x + _tex.width, _tex.uv_y + _tex.height);
			point_uv4 = vector2(_tex.uv_x, _tex.uv_y + _tex.height);

			Mesh mesh = new Mesh();
			mesh.trangles = [trangle1 , tangle2];
			mesh.trangles_index = [trangle_index1 , trangle_index2];
			mesh.uvs = [point_uv1, point_uv2, point_uv3, point_uv4];

			return mesh;
		}

上述伪代码中,对所有地图中的方块进行遍历,在遍历中生成了每个方块所需要的顶点、顶点索引、uv数据,进而生成三角形网格。遍历完毕后将所有网格数据合并,使得整个地图是一个一体化的网格,渲染时只产生一个Drawcall。用程序拼接的地图可以通过地图编辑器来改变地形地貌,拼接完成的地图在渲染上的代价也相当的小,假如想换个地图样式,只需更换贴图就可以非常方便。

我们需要注意整个地图需要些许工具链支撑,由于地图元素仅限于图集中的元素,每次增加、删除元素都需要操作地图图集,图集太大时就会有拆分图集的必要,把不同地图图集分为共享图集和各自地图的图集,这样就需要我们改变一个地图一个Drawcall的策略,渲染多个图集让多个Mesh凑成一个地图,每个Mesh代表一种类型的地图图集。

3D RPG角色扮演类游戏中,也常常使用这种技巧来绘制游戏地图,曾经在日本风靡一时的《白猫计划》就使用了这种方式。

这种程序拼合的地图,策划设计师、关卡设计师可以在地图编辑器上,任意的绘制、拼接、同种类型的不同样式的地图(即在同一张贴图内容中的模型和地图元素),因此大大缩短了大量的场景试错时间。它能大量的生成出不同样式的地图,不需要制作大量的不同类型的3D模型,大大缩短了项目时间进度,深受游戏制作人的喜爱。

那么在3D地图中是怎么拼接方块地形的呢?看上去比2D更加复杂的事情其实并不是。

首先,我们在制作3D地形前需要制定一下模型的规范。3D地图中不需要我们自己来拼接三角形,我们用固定的地形模型,把每个地形模型都做成一个固定大小的立方体。假设规定每块地形规定为 10 x 10,在制作和拆分地形模型时按规定来拼接,每个3D地形模型都必须以 10 x 10 的标准来定制。单个模型的尺寸大小按照项目的地图拆分来,也可以是20 x 20 或 30 x 30,只是当标准制定完毕后,地形模型的制作必须要按照标准来做,每个元素都相当于一个地图块。

其次,同一个地图上的地形模型的纹理贴图都最好并在一张图集上来制作,因为只有这样才能在合并模型才能合并材质球,并且在合并后让这么多的3D小方块只需要1个Drawcall渲染调用就可以绘制所有的地图,也只有这样才能解决太多模型需要渲染导致的爆量Drawcall问题。

然后,在3D地形模型制作时需要些提前的设计和考虑,因为地图都被拆分成了N种类型的地形立方体,所以在制作的初期需要对整个地形有哪些类型的需求进行设计和探讨。在读取地图编辑器编辑的地图数据后,除了拼合地形模型立方体,实例化场景地图时也需要增加些许逻辑,为的就是适应更好的模型拼合,就像我们在制作和拆分城墙时那样,当城墙处于拐角处时,我们需要选择不同的拐角模型代替,判断它的左右前后有没有其他城墙,如果有则应该选择适当的连接城墙来适应周边的模型,左连接、右连接、前连接、后连接、以及前后双向连接,左右双向连接等等总共9种情况,有不同的9种拐角城墙所代替,这样才能完美契合周围的地块模型。

最后的合并模型步骤就相对简单的多,按地形模型放在地图数据文件所描述的指定位置上,并如上面所说的选择好适配的模型,调用Unity3D的API方法 Mesh.CombineMeshes 合并所有模型。在合并时也会有三角面数的限制,合并后的模型三角形数量不能超过2的16次方减1个面(即65535个面),如果超过则应该另起一个模型进行合并。

常规场景的性能优化

上面几节阐述了地图的拼接方式,其中用程序拼接合并地图块的方式确实大大降低了 Drawcall 的数量,提升了渲染的性能,但这也只是针对可拆分的地图类型项目,对大多数游戏类型来说地图是不可拆分成小块的立方体进行拼接的,因此我们还是需要更多的针对常规场景讲解优化的方法和技巧。

首先我们要清楚的是,是什么造成了场景的低渲染效率。我们在这里罗列一下主要的几个问题:

	1.同屏渲染面数太多,GPU压力太大。

	2.渲染管线调用次数太多(Drawcall太多)GPU的并行处理没有很好的发挥作用

	3.贴图太重太大占内存大,导致显存的带宽负荷太重

	4.动画太多,蒙皮的计算消耗的CPU计算量太多

	5.Shader开销太大GPU的计算量开销太大

其实还有很多很多问题需要解决,比如实时阴影导致的 Drawcall 太多,以及透贴太多导致Overdraw 重绘问题比较严重,还有比如物体需要的Pass渲染太多,一个物体需要绘制多次才能完成,这些问题也是很重要的,这节我们就来说聊聊场景的优化。

1.渲染面数太多,GPU压力太大。

渲染面数太多一般都是同屏展示的面数太多,整个场景的网格面数太多只会让内存上升,摄像机里同屏的渲染面数才是比较重要的。

Unity3D引擎会裁切掉在摄像头之外的物体,不让他们进入到渲染管线以减少消耗,虽然裁切也会消耗小部分CPU,但毕竟比起渲染整个物体要小的多。

很多45度固定摄像头视角的游戏并不会将摄像机抬起来让更多的画面进入摄像机,但有很多游戏摄像机的旋转角度是自由的,这导致在摄像机抬起时会让许多模型进入摄像机的渲染范围。大部分引擎包括Unity3D都有对模型物体在提交GPU前进行一次裁剪,在进入渲染管线前每个3D物体能计算出或者已经计算好了一个包围盒即Bounds,这个包围盒由8个顶点组成,加上旋转矩阵就能计算出每个顶点是否在摄像头的锥形体范围内。

裁切的算法中包围盒8个点中只要有一个点在锥视体内,就认为是需要渲染的不可被裁剪,如果8个顶点都不在摄像头的锥形体范围内,才认为是不需要被渲染的,这时Unity3D引擎会阻止这个模型渲染,在渲染调用前就抛弃它。因此我们要格外注意模型的包围盒,有些包围盒在模型制作时由于错误的操作导致包围盒并不适应模型大小,过大的包围盒导致GPU性能浪费。这种Unity3D引擎上的裁剪,帮助我们屏蔽掉了很多不需要渲染的物体,虽然裁剪也会花去些许CPU,但比起要绘制一个3D物体的计算量来说要少的多。

场景中如果是一个模型很大很长会让裁剪失效,常常在我们项目中有模型范围从南延伸到北贯穿整个场景,这样的模型包围盒会很大很长,常常让引擎的裁剪优化失效。一旦模型长到覆盖了整个场景,包围盒总有一段在摄像头内导致不会被引擎裁剪,这样就会浪费很多不必要的GPU计算,因为网格被传入后由GPU来负责对每个三角形裁剪,比起用包围盒的方式裁剪要费力的多。因此我们在场景中要考虑对过大过长的模型进行拆分,不能让它跨很多个区域范围,不过注意拆分的过细也不行,因为这样会带来更多的Drawcall,理解了裁剪原理会对场景物件的颗粒度大小的把控会更加清晰一些。

摄像机的远切面太长就会包含太多的物体在视野范围内,这导致我们前面说的引擎内裁减失效,更多的物体会被送去渲染,增大了GPU的压力。

最简单直接的方法就是,拉近摄像头的远切面距离,减少模型进入视野内的数量。视野范围内的物体减少,需要渲染的模型面数也就减少了,CPU和GPU的消耗自然下降,让视线范围在一个合理的范围内是我们需要关注的摄像机参数。

这种方法毕竟不能大用,因为它缩短了视野直接裁减三角形,让画面不那么细腻美观,我们很多时候希望看到更远的地方。我们既希望能看到更多更远的物件,也想降低渲染带来的压力。世上没有免费的午餐,想降低CPU和GPU的消耗,至少得用大量内存来换。LOD,即Level of detail,就精通此道,用内存换CPU和GPU。

LOD需要我们把3D模型做成多个不同细节级别的模型文件,每个级别的模型都比上一级模型的面数少一些,这样面数少的模型用于远距离的渲染,而面数多的则用于近距离渲染。当摄像头拉远时,启用面数更少的模型,当摄像头拉近时,则启用面数更多的模型。这样即使众多的模型在摄像头范围内等待渲染,也只有几个面数多的模型靠近摄像头,而大部分模型都离摄像头比较远启用的是面数比较少的模型。这些远离摄像头的3D模型面数很少,即使数量众多,它们所展示的低面数的总和也是可以接受的,当同屏需要渲染的面数少时,GPU需要处理的三角面也少,虽然Drawcall数量没变化,GPU需要处理的渲染指令数量没有变化(渲染状态和渲染指令时串行处理的)但处理的量少了,处理速度自然就快了。

我们常提到的 Mipmap 其实也点LOD方式的运作意味,只是Mipmap的主要意图不是优化性能而是物体像素比导致的画面瑕疵问题,由于Mipmap会根据远近来选择贴图大小,因此在绘制场景时也节省了不少GPU与内存之间传输数据的带宽,我们将在后面的渲染章节更详细的讲解,这里只是简单陈述一下。Mipmap和LOD有差不多的理念,对于离摄像机远的物体启用更小的纹理贴图,对离摄像机距离近的物体使用正常的纹理贴图,只是Mipmap使用的不是距离而是渲染时的像素与贴图像素块的大小比例。

当然天下没有免费的午餐,提升了CPU和GPU的性能就得增大内存的消耗,内存消耗太多也会有得不偿失的时候,因此在使用LOD时我们考虑分多少级的细节级别上也需要斟酌一下。针对不同的项目分级的层数不同,拿大型MMORPG来说,那种可以在天上飞的游戏,看到的物体自然就很多,如果LOD分级少了画面则会显得突兀,而中小项目LOD分级太多则加大了内存量和制作时间得不偿失。

2.渲染管线调用次数太多(Drawcall太多)GPU的并行处理没有很好的发挥作用

除了渲染面数太多带来的GPU压力,渲染调用次数太多也是造成GPU瓶颈的一大问题。Drawcall太多的问题大都因为场景中的模型物体太多引起的,也可能附带着Shader中的Pass太多使得渲染压力更大。

这个问题是最简单粗暴就是‘干掉’在场景中众多的模型物体,以及‘注释掉’Shader中不必要的管线。这无疑将美术设计师和场景设计师的辛勤工作的成果给白白的浪费了,我们尽量不考虑这种方式,不过适当的移除些许不必要模型的其实也有助于场景制作。

我们希望在不毁坏场景的前提下做些优化,关键点就在于‘合并模型’。我们不希望破坏场景的美观,不想让设计师们辛苦设计的场景毁于性能优化,为了两全其美我们需要合并渲染。在制作完场景后同屏内需要渲染的面片数量已经确定,我们在不减模型在场景中的数量以及不减少单个模型面数的情况下,同屏画面上需要渲染的三角形面数已经不会变化。从整体上看当我们每次调用管线渲染(即调用Drawcall)都会传送给渲染管线一些三角形,如果传送的三角形数量比较少,则调用的Drawcall次数就会增多,因为三角形数量时确定的。我们需要合并这些三角形面片然后一并传递给管线,将原来需要多次的调用管线变成一次,只有这样才能大大降低 Drawcall 的数量,降低渲染调用次数。

那么减少渲染管线即减少 Drawcall 的数量究竟有什么好处?减少了渲染管线的数量,即降低了Drawcall的数量后,尽管并没有减少任何需要渲染的面片数量,只是每次传递给渲染管线的顶点和面片数增多了传递的次数减少了,三角形数据在进入渲染管线后可以开启GPU并行处理,原本单行线处理变成了多线处理,将像是单车道扩展为8车道那样速度成倍的增加。

原本调用一次渲染就要在队列中等待管线渲染完毕后才进行下一次渲染,合并后就不同了。不再是像以前那样调用这么多次Drawcall,渲染画面时Drawcall的队伍都要排的老长,都等在那排着队等待GPU处理。合并后就不再是这样了,排队的数量少了队伍短了,虽然每个排队的数据都很‘胖’(数据量很多),但是一旦进入GPU处理阶段上百条生产线并行处理这些数据,因此我们要想方设法的合并各种模型减少Drawcall。

Unity3D引擎自带有多种合批方式,前面几个章节有详细的介绍这里我们再简要的阐述一下,Unity3D引擎中有动态合批、静态合批、GPU Instancing 三种合批方式,其中动态合批在合并的模型时要求比较高,包括对模型面数、法线、切线、Pass数等都有要求,静态合批则放宽一些但也有自己的规则,它通过消耗的内存来换取CPU,而GPU Instancing 的原理是通过对一个模型传递更多的信息数据从而让它绘制在不同的位置与不同的样式以达到减少Drawcall的目的。

这里我们不讨论通过Unity3D引擎里的功能来做‘合并’的操作,我们希望能够自己掌控‘合并’效率和效果。部分原因也是因为引擎自带的合批方式通常都是通用的合批方式,规则比较严格,合批率比较低,我们假设搁置它而选择自己动手丰衣足食。

‘合并’分实时合并,与,非实时合并两种方法

实时合并会消耗大量的CPU,因为它要读取多个模型,并创建新模型数据,不断的创建新模型数据,就会导致CPU的消耗。这里由于前面模型的章节已经详细阐述过因此我们简单阐述一下,合并模型时要求模型使用相同材质球,读取所有具有相同材质球的模型数据,生成一个新的合并后的模型并隐藏原有的模型。倘若我们在制作时发现很多模型材质球使用的Shader和Shader的参数一样,只是贴图不一样,我们可以离线合并它们的贴图后再放到实时渲染时进行合并。

非实时合并则全是线下的合并,即大家常说的静态合并。Unity3D静态批处理的规则比较严格,即要求相同材质球相同贴图还要相同的模型,而且内存消耗比较大,因此我们还是建议使用自己手动合并的方式,即可以自己手动拼接场景中静态不动的、有相同材质球的模型,也可以用程序或插件来拼接,比如 MeshBaker,通过 MeshBaker 来制作和合并模型与纹理贴图。静态合并Mesh的好处就是,游戏中不需要实时消耗CPU去合并模型,节省了不少实时开销的CPU,同时也降低了很多Drawcall的数量。

合并这档子事当然也会过犹不及的,如果把所有的在场景内的物体全部合成为一个模型,无论摄像头能不能看到的地方都合并了,那么GPU的压力也同样会很大,因为这样的话我们前面提到的引擎自身做的第一层包围盒裁剪就不能生效了。渲染时会一股脑的将整个模型数据塞进渲染管线中由GPU来裁剪,这样会增大GPU的计算压力,GPU要裁剪整个地图的所有模型面片,计算量会是巨大的。因此我们在静态合并时也要适度,即合并附近距离不太远的、可以称为一块范围内的模型,这样即减少 Drawcall 次数,也为引擎裁剪让出了空间,降低了GPU裁剪的压力。

3.贴图太重太大占内存大,导致显存的带宽负荷太重

贴图太多宽带压力太大导致的问题,主要是因为GPU在渲染时需要将内存的纹理拷贝到显存中去才能使用在GPU中渲染造成的。所以拷贝的消耗和显存的消耗也是很大的。现代设备显存只存在于主机和PC游戏中,在手机中没有显存的概念,它只有内存,手机设备中的显存都是内存内部的拷贝,即手机中会为GPU预留出一块用于渲染的内存块作为缓存。

其实手机与PC机的架构完全不一样,这导致内存的存取方式也不同,PC机中向GPU传输纹理贴图是从内存向显存拷贝,这其实意味着贴图纹理有两份,一份在内存,一份在显存,当引擎调用渲染指令时会向GPU传输纹理贴图,通常GPU会拷贝一份纹理到现存中,不过这也是短暂的,显存只是相当于缓存作用,引擎的每次渲染调用都会重置渲染状态,向GPU传输纹理贴图,如果纹理贴图已经存在于显存中则不用再传输,如果显存不够则要清除掉显存中一部分内容以空出空间来应付当前的指令。显存理论上说是越大越好,这样每次渲染调用就不用被覆盖,在下一帧渲染时就可以重复利用前面已经传输过的纹理。

现代手机设备,安卓与IOS的架构则完全不同,手机中完美没有显存这个缓存硬件,只能从内存中分离出一部分来使用。而且在安卓和IOS的架构下也不再存在着纹理在内存和显存的拷贝,CPU和GPU的纹理地址指向同一个物理地址。当手机设备需要纹理时,系统从纹理文件中直接加载进入指定的物理内存不再拷贝到其他地方。

除此之外有两份内存的情况也时常在引擎内发生,Unity3D中贴图有个选项 Read/Write,当Write被勾上时就会在引擎的内存中有另一份拷贝,这是由于Write属性的贴图随时会被更改像素内容,为了不影响GPU贴图的渲染更好的在CPU与GPU之间运作,系统采用三重缓存机制,即系统会另起了一份贴图来使得CPU与GPU之间无障碍协作。因此我们在贴图设置时,首先要注意 Read/Write 选项是否需要被开启,绝大部分贴图是不需要被开启的,这些贴图被开启后造成的2倍内存也是不必要的。

贴图太大太多会影响渲染,主要影响的是GPU的拷贝过程,最好的办法是缩小贴图和压缩贴图。看起来挺简单的,其实我们在实际项目中,我们不能为了性能随意去缩小和压缩贴图,这样为导致项目因贴图质量太糟糕而影响画面效果。我们需要针对每个部分的贴图逐一去了解和设置参数,或者用Unity Editor脚本对某些文件夹下的贴图统一做强制性的设置。其实每个功能部分的贴图都有其用途,比如UI中的贴图分,大部分是图集和Icon图片,图集一般都是无损质量,不同级别的设备也会离线LOD的方式做多份拷贝,每份的设置都会有所区别,大部分UI贴图都不需要 Mipmap采样,Icon图也是一样。针对低端设备会对UI贴图做些压缩,压缩后UI的质量会有所降低,换来的是性能速度会快很多,这是由于无损的贴图在内存上和压缩的贴图通常有5-10倍的差距,贴图传入GPU所执行的拷贝过程会消耗一部分算力。

3D模型的贴图也是其自己的特点,模型纹理贴图通常都是2的幂次的大小存在。2的幂次大小的纹理GPU处理起来比较顺手,以前GPU只能处理2次幂大小的贴图,现在GPU已经强大到可以处理非2次幂大小的贴图了,但速度和效率还是会相对慢一些。这些贴图也通常是可以压缩的,并且大都需要带有 Mipmap 采样生成标记的。要压缩多少压缩到什么比例才适合,可能并没有绝对统一的标准,大部分项目都会针对设备平台去压缩,以及针对高中低端设备做压缩设置,如果你没有很好的压缩策略可以选择中位数策略即Normal Compress普通压缩。

贴图的大小也比较重要,每个项目在开始前最好能好好的规范一下,我们在模型章节里讲了许多规范的制定,只有遵守良好的规范项目才能稳步前进,毕竟不是一个人在努力,大家的努力要转化为有效的合作与更长远的发展。幸运的是,美术设计师无论将贴图做的多大,我们都可以在Unity3D里重新设置成我们需要的大小,Unity3D会将所有贴图都重新制作导出成我们指定大小的贴图和格式。因此即使前期贴图太大而导致的问题,可以在Unity3D的项目中将贴图设置回我们希望的大小尺寸。

4.动画太多,蒙皮的计算消耗的CPU计算量太多

模型动画确实是最令人头疼的一块内容,它是动态的而且时时刻刻都是计算网格的位置,不像静止的3D物件即使它们有时会出现或消失也不会消耗网格算力。而模型动画则不同,它们时时刻刻都在你眼前动来动去,即使不再屏幕上,大部分时候也需要一直保持动画的计算过程。

对于模型动画的优化,在模型动画章节中讲的比较详细,这里主要是在前面讲解内容基础上以实际的应用场景简单的讨论一下。

模型动画的消耗量最大的地方是CPU的蒙皮计算,如果有100个动画在屏幕中播放,CPU会极大的消耗在蒙皮计算上。蒙皮计算,其实质就是骨骼与顶点的计算,骨骼动画用骨骼点去影响顶点,每帧都需要计算骨骼点与顶点的偏移、缩放与旋转。如果动画模型的顶点数量很多,骨骼数量很多,由于顶点关联着骨骼点,多个骨骼点影响着顶点,那么计算量就会很大,消耗的CPU的算力自然就会很多。为了能减少计算量,我们减少顶点数是最直接的办法,又或者减少骨骼点的数量也是,这样能使得CPU降低消耗。

让3D模型设计师和动画师去做减面和减动画骨骼的工作绝非易事,它涉及到画面质量削减平衡,在削减的时候设计师们需要顾及画面质量。这些都是我们程序员无法控制的,我们能做的就是从程序上尽量的降低开销。由于每帧都要计算网格的形状所以很费CPU,如果能把计算好的每帧顶点偏移量存起来,播放的时候直接偏移过去就会好很多,这样就不需要计算了只需要存储与偏移。我们可以把每个动画网格分批计算好另存为一个文件,这样有多少帧动画就会有多少个具体的网格存放起来,每次播放动画时,直接拿出这些网格一个个替换就省去了网格顶点的计算。

先不说空间的占用量,毕竟这么多模型网格无法合并Mesh,每个动画模型都需要至少一个Drawcall来支撑,如果Shader中有多个Pass管线,就有更多,比如描边,实时阴影等。假设有100个这样的动画,就需要至少100个Drawcall来支撑。消耗还是太大,能不能合并Drawcall,就像合并普通Mesh一样,相同的材质球合并成为一个Drawcall呢?Unity3D的 GPU Instancing 为我们提供了合并的可能。它可以合并相同材质球,相同模型的Drawcall。它原理是将一个模型以不同的状态,在不同位置渲染只需提交一次GPU。

SkinMesh Instancing 就借此解决了这个问题,它建立在GPU Instancing功能之上的动画优化方案。它把动画的模型数据在离线状态下计算好并存储在贴图中,这样解放了CPU的算力并转移到GPU中渲染。它把动画文件中的数据与模型数据结合分批计算好网格顶点的偏移量并存储在贴图中,由可编程的顶点着色器来根据参数来完成顶点的偏移,这样我们就不仅不需要计算而且还能一次提交就能渲染很多个位置的模型,每次渲染只需将顶点偏移就可以了,省去了大量的CPU计算蒙皮的消耗。

与之相似的,也在着色器中解决动画问题的还有一些场景中的草、树、飘带和红旗的摇动,我们可以用顶点动画蒙皮计算的CPU消耗。这些摇动的动画是纯顶点算法计算出来的摇动,它用了一些摇动算法计算出顶点的偏移位置,使用消耗GPU算力来代替CPU算力,因为GPU它是并行的且更擅长顶点计算。

当然这几种方法也各有利弊,在实际项目中需要根据实际情况做出选择甚至混合使用,我们在使用时应该衡量他们在项目中的限制和作用,尽量做最适合的选择。

5.Shader开销太大消耗GPU算力

???

片段着色器中计算复杂逻辑,将计算转移到顶点着色器中 使用过多复杂的数学函数,使用近似公式代替 使用变量精度多大,减少不必要的精度 使用过多ifelse,编译器并不会分支 使用需要等待的功能,避免使用等待缓冲需要功能 使用过多pass,合并计算 使用mutile compile后的初始化,变体导致编译出有很多的shader,引擎在识别功能时会选择对应的shader,这导致很多shader没有被初始化的会被临时初始化 AlphaTest在手机设备上使用会让pre depth test 失效,导致性能开销反而增加 尽量能够在离线计算好的数值,尽量离线计算完后,使用常量来代替实时计算消耗

参考资料:

《Triple Buffering》apple developer 三重缓存机制

· 书籍著作, Unity3D, 前端技术

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

    本文为博主原创文章,未经允许不得转载:

    《Unity3D高级编程之进阶主程》第十章,地图与寻路(四) 地图的制作与优化

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号,文章同步推送,致力于分享一个资深程序员在北上广深拼搏中对世界的理解

    QQ交流群: 777859752 (高级程序书友会)