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

地图的制作与优化

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

===

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

一种是手动拼接的地图。

手动拼接的地形比较常见,由3D设计师制作出来的3D模型作为地面的地形放置在场景中,相当于是一个模型落在场景上一样,只是这个3D模型很可能是有碰撞体的,碰撞体即 Unity3D的 Mesh Collider 根据项目的需要而增加物理系统的组件,一般都会直接使用 Mesh Collider,除非是完全平面式的,不需要碰撞组件,而是自己开发的一套基于平面的2D地图障碍与寻路,完全脱离了传统的物理系统。

也有可能会使用Unity3D内置的地形,也一样相当于一个3D模型放置在场景中,Unity3D的地形可以任意的改变起伏,但地形的渲染消耗比较大,因此我们并不建议使用Unity3D内置的地形,而是建议地图设计师和3D模型设计师用 Maya 和 3DMax 等模型软件来构建地形的起伏,这种做法能更加容易的被设计师掌控以及自由发挥,也更加节省资源,对程序来说在处理性能优化时也更加容易一些。

其他3D物体,比如石头,小石块,草,树,动物等也用同种方式放置在场景中,只是放置的手法可能不同,有的可能通过嵌入到prefab的方式,而有的可能通过地图编辑器加入到地图数据文件中,再在加载的地图时,从文件数据中得到要加载的实体的资源位置,再实例化到场景中。

手动拼接的好处在于地形和地图是可以完全表达美术风格的,地形设计师和场景制作人员能根据自己的喜好和想象中的画面来自由得定制场景,他们可以任意的移动、旋转、缩放,甚至更换、修整、完善等,这能让他们对场景画面的把控有很大的自由度。

能自由发挥当然是好事,但也带来了诸多不可控性,由于场景内物件没有统一的标准,因此在场景制作时常常会大量使用,小块的地形,甚至带有动画的地形,特效,以及半透明效果来增强画面的效果,画面效果增强的同时导致性能急剧降低。地图上的小件就更不用说了,在给予场景制作团队自由发挥的同时,我们可以想象同时造成了性能无法承受的问题。

虽然这本质是没有制定和规范场景制作的要求的问题,但无论怎么规范,毕竟不是每个团队的执行力都是完美的,大部分的执行力都是差强人意的,所以都会或多或少的在优化阶段发现很多糟糕的、令人头疼的性能开销问题。

另一种是地图地形的程序拼接方式。

用程序拼接地图的主旨意图是希望能更好的为设计人员自由设计更好的地图而选择的方式,不仅仅是美术设计人员,还包括策划设计人员,需要在设计和性能上求得平衡点,这样所有的地图都由程序而生成了一个或者几个模型的结构铺满整个地图。

这种方式来制作地图最常见的要属2D的RPG游戏了,2D角色扮演类游戏中,几乎整个屏幕的地形都需要用地图编辑器来建立和拼合的,当需要这类地图出现时时,程序会将地图数据加载进来后,对每一块的地图都进行动态的拼合,这样一来,整个地图就只有一个drawcall,因为它只是同一个模型。

我们先来看看2D RPG游戏中,是如何动态拼合整个地图的

很多2D游戏都通过了Unity3D实现,其实2D也是有Mesh、顶点、UV的概念,和3D比起来只是少了1个维度而已。在2D游戏中的地图拼接的手法也都是通过生成顶点和三角来完成的。

我们可以设想整个地图就是一个大的矩形,地图中每个方块是这个大矩形中的一个格子,而每个格子都由2个三角形构成,既然我们能计算出每个方块的位置,我们就能计算每个三角的顶点和位置。

我们知道的是,整个地图有多大,也就是这个大矩形在什么位置、有多大,每个方块矩形有多大,也就是每个格子有多大。我们知道有多少格子,有多大的格子,就能计算出,总共有多少个顶点,每个顶点的位置在什么地方,也就能知道三角形该怎么拼接了。

简单得说,地图是一个大矩形,横切竖切N刀,成了一个个大小一样的小矩形或者正方形,这种有规则、大小一样、位置可寻的网格,是可以通过程序生成的。

我们利用三角形的拼接方式拼接完矩形,再把地图方块的UV接上去,因为每块地形的图都不一样,所以我们需要在离线时就把所有的地形图都拼成一张图,即以图集的方式存储地图的方块。当我们将图接到网格上时,我们需要将指定的方块图的UV数值赋值给顶点,对每个方块里的2个三角形做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)
                    CombineAdd(mesh)
                end
            end
        }

        //生成矩形方块所需要的,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。

用程序拼接地图的好处是可以任意的通过地图编辑器来改变地形地貌,拼接完成的地图在渲染上的代价也相当的小,而且假如想换个地图的地形,只要更换贴图就可以了,什么样的贴图就可以产生什么样的地图。

不过同时也带来了很多弊端,必须要有成形的地图编辑器支撑,前期工具的工作量比较大,稍微提高了点门槛,并且地图元素仅限于贴图中的元素,当地图元素增加到一定范围,就要扩大地图的贴图时就要重新排列地图信息。这一连套的工具链包括地图编辑器是必要的前提条件。

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

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

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

首先,在3D地形模型制作时需要制定一下模型的规范。

3D模型的长宽必须和地图切割后的方块长宽是一致的。3D地图中,不需要我们自己来拼接三角形了,因为已经有了固定的地形模型,但对每个地形模型就需要有规范。假如规定每块地形都是 1 x 1,那么在制作和拆分地形模型时就要按规定来,每个3D地形模型都必须以 1 x 1 的标准来定制,否则不可用。当然并不一定要 1 x 1,也可以是2 x 2、 3 x 3等等,只是当标准制定完毕后,地形模型的制作必须要按照标准来做,每个元素都相当于一个地图块。

其次,所有地形模型的纹理贴图都必须并在一张贴图上制作。因为只有这样才能在合并模型才能合并,并且在合并后让这么多 1 x 1 的小方块只需要1个drawcall渲染调用就可以了绘制所有的地图。也只有这样,才能解决太多模型需要渲染导致的爆量drawcall问题。

然后,在3D地形模型制作时也是需要相当的考量的,因为这个地图都被拆分成了N种类型的地形方块,所以在制作的初期需要对整个地形有哪些类型的需求,需要有一个设计、交流、探讨、决策、改进的过程。

并且在读取地图编辑器编辑的数据后,实例化场景地图时也需要增加些许逻辑,这些逻辑是为了适应更好的模型拼合,比如当左边是某个模型时,为了能与它完美的拼接,当前这个格子上的模型必须是迎合它的那个,就像我们在制作和拆分城墙时那样,当城墙处于拐角处时,我们需要用拐角模型代替,还要判断它的左右前后有没有城墙,假设左边有城墙则应该选用带有连接左边城墙拐点的3D城墙模型,右边的、前面的、后面的、以及前后都有的,左右都有的,等等,总共9种情况都有不同的9种拐角城墙所代替,就是为了完美契合周围的环境而进行的额外的处理。

最后的合并模型步骤就简单的多了,切分开来后的模型,按地图数据文件所描述的位置,放在指定的位置上,并向上面所说的选择好适配的模型,最后调用Unity3D的 Mesh.CombineMeshes 合并所有模型。

在合并后可能发生衔接处的问题,用程序生成面片来补救很难做到完美的展示,我们还是建议调整3D模型在接缝处的面片与网格,只有这样才能调整出一个完美的或者说完全可掌控的地图造型。

常规场景的性能优化

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

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

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

    2.渲染管线调用次数太多,即drawcall太多,GPU排队渲染的队伍太长

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

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

    5.实时光的GPU开销太大

其实还有很多很多问题需要解决,比如实时阴影导致的 Drawcall 太多,增大了渲染调用的排队长度,以及透贴太多,Overdraw 重绘问题比较严重,还有比如单个物体需要的Pass渲染太多,一个物体需要绘制多次才能完成等等。这些问题也是很重要的,但这节我们重点解决这最重要的5个问题。

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

渲染面数太多分两种,一种是整个场景3D物体太多导致的面数多,一种是同屏展示的面数太多。

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

在进入渲染管线前每个3D物体能计算出或者已经计算好了一个包围盒,即Bounds,这个包围盒由8个顶点组成,加上旋转矩阵就能计算出每个顶点是否在摄像头的锥形体范围内。

裁切的算法简单说是这样的,如果有包围盒8个点中只要有一个点在锥形体内,就认为是需要渲染的不可被裁剪,如果8个顶点都不在摄像头的锥形体范围内,才认为是不需要被渲染的,然后Unity3D引擎会阻止这个模型渲染即在渲染调用前裁切掉。

这种Unity3D引擎上的裁剪,帮助我们屏蔽掉了很多不需要渲染的物体,虽然裁剪也会花去些许CPU,但比起要绘制一个3D物体来说要少的多的多。

这种裁剪的好处是,使得我们只要关心摄像头范围内的面片就可以了,但是如果是一个模型很大,或者常常我们项目中有合并的模型范围从南延伸到北贯穿整个场景,一旦这个模型很大,覆盖了整个场景,包围盒总有一段在摄像头内就不会被裁剪,这样就会浪费很多不必要的GPU计算。

除了Unity3D引擎内的裁剪,OpenGL即渲染管线也会对每个三角形进行裁剪,不过三角形的裁剪要比8个顶点的包围盒裁剪要费的多。所以我们尽量还是能用包围盒裁剪就用包围盒裁剪方式,将更少的三角形面片带入渲染管线。

传入渲染管线的所有三角形顶点管线对它们做的第一个动作就是裁剪,其次才是计算,网格根据摄像头锥形体进行裁剪,所有不在锥形体范围内的顶点都会被裁掉。但是这种裁切的效率比起8个顶点的包围盒效率要低得多。

场景面数太多时,如果没有或者很少有这种从南到北的模型时,渲染的压力就取决于摄像头中可展示的物体的数量,但是如果单个模型通常会覆盖很多天南地北的网格,那么GPU对裁切计算的压力就会增大。

后一种同屏面数太多的情况,就是因为3D模型都在摄像头范围内,要么就是模型太多,要么就是模型面数太多,要么就是摄像头太远而显示距离太大造成的。

最简单直接的方法就是,拉近摄像头,或者拉近摄像头显示距离,让更少的模型进入视野内。当视野范围内的物体少了,需要渲染的模型面数也就少了,自然CPU和GPU的消耗也就少了。虽然这是最拙劣的办法,也是我们需要调整和调节的参数,让视线范围在一个合理的范围内。

其次是减少模型面数,或者减少模型在场景中的数量,降低不必要的高品质。3D模型面数的降低,不是一两天就能完成的事,一个模型面数的降低也伴随着画面质量的降低,又要面数低又要品质高那是痴人做梦,所以我们找到一个平衡,也要制定一个容忍度,即前面几章提到的,要将模型面数在研发期间控制在一定范围内。模型数量在场景中的数量的降低,或者更直接的方式,去除了许许多多本来需要渲染的三角面,自然就省去了诸多CPU和GPU消耗,但也伴随着场景变得空旷不饱满不生动。

那么有没一种方法可以不更重制3D模型面数,不减少模型在场景里的数量,不缩短摄像头显示距离,也能优化性能的。有。

世上没有免费的午餐,想降低GPU的消耗,就得用增大内存来换。

LOD,即Level of detail,就是此道。用内存换GPU。

我们把每个3D模型,做成多个不同细节级别的模型,当摄像头拉远时,启用形状相似面数更少细节的模型,当摄像头拉近时,则启用形状相似面数更多细节的模型。

这样即使众多的模型在摄像头范围内需要渲染,也只有几个模型在靠近摄像头的,而大部分都是远离摄像头的。

远离摄像头的3D模型面数很少,即使数量众多,它们所展示的低等级的面数的总和也是可以接受的。

我们常提到的 Mipmap 也是这种方式在运作,对于距离远的物体,启用更小的纹理贴图,而与摄像头距离近的物体则使用更大的纹理贴图。这样在渲染时就传送进渲染管线的贴图就更小了,宽带压力小了,GPU消耗自然少了。

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

2.渲染管线太多,即 Drawcall 太多,渲染管线太多

除了面数带来的GPU计算压力,渲染管线太多也是造成GPU压力的一大问题。

渲染管线太多的问题,大都是因为场景中的模型物体太多,或者使用的Shader中的管线太多,或者这两者同时造成的。

其实这个问题是最好解决的,‘干掉’在场景中众多的模型物体,以及‘注释掉’Shader中不必要的管线。

当然这是最粗暴的做法,无疑将美术设计师,场景设计师的辛勤工作的成果给抛弃了。但也不是不可为,适当的‘干掉’一部分不必要模型的其实是有助于场景制作的。

那么我们能不能在不毁坏场景的前提下做些优化呢。可以的。

关键点就在于‘合并’二字。

为什么呢?因为一个屏幕内需要渲染的面片数量是一定的,即我们不减模型在场景中的数量,不减单个模型面数的情况下,一个画面上需要渲染的三角形面数是固定的数量。

我们每次调用管线渲染(即drawcall)传给渲染管线的三角形数量却比较少,所以调用的次数比较多。

我们能否合并这些面片数据,然后一次性得传递给管线,将原来需要很多次的管线调用变成一次,只有这样才能大大降低 Drawcall 的数量,降低渲染调用次数。

那么减少渲染管线即减少 Drawcall 的数量究竟有什么好处?

减少了渲染管线的数量,即降低了Drawcall的数量后,尽管并没有减少任何需要渲染的面片数量,只是每次传递给渲染管线的顶点和面片数增多了,传递的次数减少了,但是三角形数据进入渲染管线后,就可以开启GPU并行处理了,单行线变成了8车道甚至16车道单行线,速度加快8-16倍。

原本调用一次渲染,就要在队列中等待管线渲染完毕后才进行下一次渲染,合并后就不同了。不再是像以前那样调用这么多次Drawcall,每次渲染一个画面时Drawcall的队伍都要排的老长老长的,都等在那排着队等待一个个的交给GPU处理,效率低下。

合并后就不再是这样了,排队的数量少了,队伍短了,虽然每个排队的‘人’都很‘胖’(即数据量很多),但是一旦进入处理阶段,8-16条生产线并行处理这些数据,比前面排着老长得队伍一个个处理要来的高效的多。

所以我们要想方设法的合并各种模型。怎么合并?

其实Unity3D引擎自带有多种合批方式,前面几个章节有详细的介绍,这里我们简单提一下,Unity3D引擎中有动态合批、静态合批、GPU Instancing 三种合批方式,其中动态合批,合并的普通的模型,但要求比较高,包括模型面熟、Shader的Pass数等,静态合批也有自己的规则,它通过消耗的内存来换取CPU,而GPU Instancing 的原理是通过一次性传递更多的数据来达到合批Drawcall的目的。

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

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

其中实时合并也会消耗大量的CPU,因为它要读取多个模型,并创建新模型数据,不断的创建新模型数据,就会导致CPU的消耗。这里不再详细介绍,前面章节已经介绍过,这里简单点说一下,把相同材质球,相同贴图的模型,读取它们的模型数据,生成一个新的合并后的模型,然后隐藏原有的模型。如果很多模型材质球效果一样,但贴图不一样,我们可以在线下手动合并它们的贴图后再放到线上合并。

非实时合并则全是线下的手动合并,即大家常说的静态合并。Unity3D静态批处理的规则比较严格,即要求相同材质球相同贴图还要形同模型,而且内存消耗比较大,因此我们还是建议使用自己手动合并的方式,即手动拼接场景中静态不动的、有相同材质球、相同贴图的模型,手动拼接成一个模型,也可以用插件来操作,比如 MeshBaker,通过 MeshBaker 来制作和合并模型与纹理贴图。

静态合并Mesh的好处就是,游戏中不需要实时消耗CPU去合并模型,节省了不少CPU,也降低了很多Drawcall的数量。

当然过犹不及么,如果把所有的在场景内的物体全部合成为一个模型,整个地图,无论摄像头能不能看到的地方都合并了,那么GPU的压力也同样很大,因为这样的话我们前面提到的引擎自身做的第一层包围盒裁剪就不生效了。渲染时会一股脑的将整个模型数据塞进渲染管线中,GPU在裁剪时的压力就会很大,因为它要裁剪整个地图的所有模型面片,计算量也是巨大的。

因此我们在静态合并时也要适度,即合并周围距离不太远的、可以合并的模型。这样即减少 Drawcall 次数,也同时降低了GPU裁剪的压力。

3.贴图太多太大,显存的带宽负荷太重

贴图太多,宽带压力太大,导致的问题,主要是因为GPU在渲染时,需要将内存的纹理拷贝到显存中去才能使用在GPU中。所以拷贝的消耗和显存的消耗也是很大的。不过,显存只存在于主机和PC游戏中,在手机中没有显存的概念,只有内存,因此都是内存和内存的拷贝。

在手机中只有内存与内存的拷贝,那么岂不是内存中的贴图纹理有两份?是的,如果引擎没有做额外处理的话,按照常理GPU的工作流程确实会是拷贝一份纹理到另外一段内存中,导致一份纹理占用了两份内存。但是毕竟引擎还是做了很多优化的,Unity3D就做了这方面的优化,当贴图没有勾选Read/Write 时Unity3D就认为该贴图是不会被改变的,当GPU需要该贴图时只是把原来在内存中的值传进去,而不是拷贝整个纹理,而当我们 勾选了 Read/Write 后Unity3D就认为该贴图是会被改变的,因此当GPU需要该贴图去渲染的时候,则会重新拷贝一份纹理贴图,这份纹理贴图专门给GPU渲染使用,避免GPU的纹理贴图随时被更改。

因此我们在贴图设置时,首先要注意 Read/Write 选项是否需要被开启,绝大部分贴图是不需要被开启的,这些贴图被开启后造成的2倍内存也是不必要的。

贴图太大太多,最好的办法是缩小贴图和压缩贴图。

看起来挺简单的,其实我们在实际项目中,我们不可能随意的去缩小和压缩贴图,这样很容易导致项目因贴图质量太糟糕而影响画面效果。

我们需要针对每个部分的贴图逐一去了解和设置,其实每个功能部分的贴图都有其用途。比如UI中的贴图分,图集和Icon图片,图集一般都是无损质量,因为要保障画面,也不需要 Mipmap采样,Icon图也是一样。虽然也不是不能压缩,不过压缩后确实UI的质量会遭到比较显著的降低,针对低端机时确实可以这样去做,因为无损的贴图在内存上和压缩的贴图通常有5-10倍的差距。

又比如3D模型的贴图,这些模型纹理贴图,通常都是2的幂次存在,因为2的幂次的纹理在GPU中处理起来比较顺手,而他们也通常是可以压缩的,并且带有 Mipmap 采样生成标记的。要压缩多少,压缩到什么比例才适合,每个项目都不一样,因人而异,大部分时候我们都是指压缩到中位数即Normal Compress普通压缩。

贴图的大小确实比较重要,每个项目在开始时都要好好的规范一下,因为只有遵守良好的规范,项目才能将内存限制在可控范围内。

幸运的是,美术设计师无论将贴图做的多大,我们都可以在Unity3D里重新设置成我们需要的大小,Unity3D会将所有贴图都重新制作导出成用户指定大小的贴图和格式。因此即使前期贴图太大而导致的问题,也可以在Unity3D的项目中将贴图设置回我们想要的大小。

4.动画太多,蒙皮的计算量太大

动画确实是最令人头疼的,因为它是动态的,而且时时刻刻都是动态的,不像3D物件,虽然它们会出现消失,但大部分情况下还是静止不同的。而动画则不同,它们时时刻刻都在你眼前动来动去,即使不再屏幕上,大部分时候也需要一直保持动的状态。

对于动画的优化,其实上几个章节中有讲的比较详细,这里只是简单的点一下。

动画的消耗点最大的地方是CPU的蒙皮计算,如果有100个动画在屏幕中播放,由于每帧对蒙皮的计算都是全局性的,因此CPU会极大的消耗在蒙皮计算上。

蒙皮计算,其实质就是骨骼与顶点的计算,骨骼动画用骨骼点去影响顶点,每帧都需要计算骨骼点与顶点的偏移、缩放与旋转。

如果动画模型的顶点数量很多,骨骼数量很多,顶点关联着骨骼点,骨骼点影响着顶点,那么计算量就会很大,消耗的CPU自然就会很多。

为了能节省计算量,我们可以减少顶点数,又或者减少骨骼点的数量,抑或两者都进行削减,就能使得CPU降低消耗。

让3D模型设计师和动画师去减面和减动画骨骼,工作量毕竟是比较大的,涉及到所有3D模型和动画。在削减的时候还需要顾及画面,不能太糟糕,所以时间会比较长。这是我们程序无法控制的,我们能做的就是从程序上尽量的降低开销。

由于每帧都要计算,所以很费CPU,如果能把计算好的每帧顶点偏移量存起来,播放的时候直接偏移过去就好了,于是就有了 Animation Instancing。它就是把所有动画文件中的数据都导出放入贴图中,由可编程的顶点着色器来完成顶点的偏移。这样我们就不需要计算了,在设置顶点的位置就可以了,省去了大量的CPU计算消耗。

但是毕竟一个会动的模型,是无法合并Mesh,理论上说应该说每帧都合并一次对CPU消耗的代价太大,所以每个动画模型都需要至少一个Drawcall来支撑,如果Shader中有多个Pass管线,就有更多,比如描边,实时阴影等。

假设有100个这样的动画,就需要至少100个Drawcall来支撑。消耗还是太大,能不能合并Drawcall,就像合并Mesh一样,相同的材质球合并成为一个Drawcall呢?

Unity3D的 GPU Instancing 为我们提供了合并的可能。它可以合并相同材质球,相同模型的Drawcall。它原理是将多个模型的相同顶点,相同贴图,相同材质球,在不同位置的物体一次性提交给GPU,让它只绘制一次就能将所有物体都绘制在帧缓存中,进而体现在屏幕上。

这种方式的条件虽然有点苛刻,但恰好能和 Animation Instancing 配合的很好,Animation Instancing 并没有改变3D模型,贴图,材质球,能与 GPU Instancing 能很好的结合,两者使用起来能减掉很多有用相同模型和材质球的动画物体,还能将计算蒙皮的CPU消耗省去,可以说是绝佳的搭档。

与之相似的还有,另外一些场景中的草和树的摇动,我们可以用顶点动画和 GPU Instancing 结合来省去Drawcall和蒙皮计算的CPU消耗。

其实和 Animation Instancing 一样的原来,只是草和树是纯顶点算法计算出来的摇动,而3D模型动画的 Animation Instancing 则是在离线下将动画数据导进纹理贴图中在顶点着色器中偏移顶点罢了。

前者用了算法计算出顶点,而后者用纹理贴图当做数据载体直接偏移顶点。前者消耗了GPU来代替CPU计算,后者消耗了内存和少量GPU代替CPU,前者更加灵活但也受到算法的局限性,后者则死板一些但性能开销更少。各有利弊,在实际项目中也需要根据实际情况做选择和混用,我们在使用时应该衡量他们在项目中的限制和作用,尽量做最适合的选择。

5.实时光的GPU开销太大

烘培是解决实时灯光太多,GPU开销太大时比较好的解决手段。对于场景制作与优化,烘培是永远绕不过去的技巧,它能帮助我们省去很多灯光的实时开销,以及实时阴影的巨大开销。

下一节我们就来专门来讲讲,烘培及其优化。

感谢您的耐心阅读

Thanks for your reading

  • 版权申明

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

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

    Copyright attention

    Please don't reprint without authorize.

  • 微信公众号