Unity3D高级编程主程手记 学习笔记五:3D模型与动画

1.美术资源规范

  一个项目中,资源的规范是非常重要的。资源不进行规范,会导致项目的性能变差,例如,模型过大,模型面数过多,压缩不够等都会导致加载变慢,游戏卡顿。所以,对与美术资源来说,并不是模型越精细就越好,应该是在一定条件的限制下努力做到更加好的美化。

  美术资源的大小规范:通常可以根据行业经验来确定一些资源大小规范,例如,手机游戏中人物不应该超过5000个面,贴图不应该大于512pix,骨骼数量不应该多余30个。但是当开始一个新项目时可能就需要重新评估资源规范,这里是一些规范方法:

    1)根据场景确定

    例如汤姆猫这种场景单一,可以将所有美术资源都分给主角是,主角的精细化就很有必要,这是主角模型可以分到10000个面也不为过,贴图大小也可以达到2048*2048。如第三人称RPG的话,同一视角下人物数量可能会很多,所以,角色的面数应该控制到3000以下,建筑因为大小的关系也可以区分规范,例如大型建筑7000,中型建筑5000,小型建筑在3000以下。贴图的话最好也都控制在512pix以下,小部件贴图可以控制在128以下。当然在超大型游戏里除了规范美术资源还可以使用mipmap或者LOD技术来优化。

    2)使用反推计算来得出规范

    举例,例如同屏展示面数需要控制在40W面以下时,就以40W作为一个基准,假设同屏角色有100个,50个建筑,除去地表3W面还有37W。平均每个物体2500面,然而我们并不想要平均,需要进行一个等级拆分。小物件为1级,小建筑2级,中建筑3级,大建筑4级,人物角色3级。所以100个角色可以有3000面,剩下的大型建筑5个,中型建筑20个,小型建筑20个,小物件10个。

    3)规范的自动检测系统

    在实际项目中,常常有让程序员或美术设计师以人工方式去寻找美术资源规范的情况,这是费时费力的,我们可以编写一个美术资源规范检测程序,每隔一段时间运行一次并打印一次报告,打印那些不规范的资源信息。

[MenuItem("校验工具/角色、模型、地形Prefab")]
static public void ModelPrefabValidate()
{
    //写入csv日志
    StreamWriter sw = new StreamWriter("模型Prefab检测报告.csv", false, System.Text.Encoding.UTF8);
​
    string[] allAssets = AssetDatabase.GetAllAssetPaths();
    foreach (string s in allAssets)
    {
        if (UIAssetPost.IsInPath(s, ModelFbxAssetPost.Character_Prefab_path){
         continue; ) GameObject obj
= AssetDatabase.LoadAssetAtPath(s, typeof(GameObject)) as GameObject; ​ //--检查Fbx,网格,设置 MeshFilter[] meshes = obj.GetComponentsInChildren<MeshFilter>(); if (meshes != null) { int vertexCount_sum = 0; ​ for(int i = 0 ; i < meshes.Length ; i++) { Mesh mesh = meshes[i].sharedMesh; SkinnedMeshRenderer smr = meshes[i].GetComponent<SkinnedMeshRenderer>(); // BatchRenderer br = meshes[i].GetComponent<BatchRenderer>(); if(mesh == null) { str_record = string.Format("丢失Mesh ,{0} ,{1}", s, meshes[i].name); } else { ModelImporter model_importer = null; string path_obj = null; UnityEngine.Object obj_fbx = null; ​ //检查fbx路径 path_obj = AssetDatabase.GetAssetPath(mesh); obj_fbx = AssetDatabase.LoadAssetAtPath(path_obj, typeof(GameObject)); ​ //检查fbx设置 model_importer = AssetImporter.GetAtPath(path_obj) as ModelImporter; if(!model_importer.optimizeMesh) { str_record = string.Format("Fbx设置中 optimizeMesh off 没开起来 ,{0} ,{1}", path_obj, obj_fbx.name); } if(model_importer.importMaterials) { str_record = string.Format("Fbx设置中 importMaterials on 被开起来了 ,{0} ,{1}", path_obj, obj_fbx.name); } if(!model_importer.weldVertices) { str_record = string.Format("Fbx设置中 weldVertices off 没开起来 ,{0} ,{1}", path_obj, obj_fbx.name); } if(model_importer.importTangents != ModelImporterTangents.None) { str_record = string.Format("Fbx设置中 importTangents on 被开起来了 ,{0} ,{1}", path_obj, obj_fbx.name); } if(model_importer.importNormals != ModelImporterNormals.Import) { str_record = string.Format("Fbx设置中 importNormals off 没开起来 ,{0} ,{1}", path_obj, obj_fbx.name); } if(smr != null && model_importer.isReadable) { str_record = string.Format("Fbx设置中 isReadable on 开起来了 SkinnedMeshRenderer 即动画不能开write ,{0} ,{1}", path_obj, obj_fbx.name); } if(!path_obj.Contains("_write") && model_importer.isReadable) { str_record = string.Format("Fbx设置中 isReadable on 开起来了 但文件名没有 _write 后缀,{0} ,{1}", path_obj, obj_fbx.name); } if(path_obj.Contains("_write") && !model_importer.isReadable) { str_record = string.Format("Fbx设置中 isReadable off 没开起来 但文件名有 _write 后缀,{0} ,{1}", path_obj, obj_fbx.name); } vertexCount_sum += mesh.vertexCount; } } ​ if(vertexCount_sum > MESH_VERTEX_MAX) { str_record = string.Format("网格顶点数大于 {0},{1} ,{2}", MESH_VERTEX_MAX, s, vertexCount_sum); } } ​ ... ​ //检测命名是否合法 if (!UIAssetPost.IsFileNameLegal(s)) { str_record = string.Format("文件命名不合法, {0}", s); } ... } } ... }

     为了不重复造轮子,这里也可以使用UWA本地资源管理工具UWA | 致力于游戏VR和AR应用提供项目研发解决方案 | 简单优化、优化简单 | 侑虎科技 (uwa4d.com)

2.合并3D模型

  2.1 网络模型的基础知识

    1.Animation 还是Animator?

    Unity3D已经不再对Animation系统进行维护了。但不代表不能用,只是它的性能和功能上都会落后一些。那为什么要使用Animator呢?

    新动画系统Mecanim中有动画组件Animator。Mecanim系统使用多线程计算,比单线程的Animation效率更高。Unity对Mecanim开放了优化选项Optimize GameObject,开启后Animator和MeshSkining的Cpu占用会降低。Animator的功能更多,可以让不同角色都使用同一套动画资源Animator的状态机让动画在不同条件下可以轻松切换,不同Layer让人物可以根据情况融合播放动画。

    2.Unity3D中子网格的意义

    一个模型中有多个网格,在Unity中网格里面有多个子网格SubMesh。在渲染时每个子网格都要匹配一个材质球。子网格的好处是可以针对不同的部分使用不同的材质球来实现视觉效果,从功能上看,使用子网格会更加灵活。但是子网格越多,渲染管线调用的DrawCall也就越多。所以子网格虽然强大,但是要注意它对性能的消耗。

    3.动态合并3D模型

    实际项目中,往往会遇到大量的3D物体,如果没有进行合并,它们每个3D物体都会产生一个drawcall,这样的话当场景多的时候drawcall就会变多,CPU会忙于发送状态数据给GPU导致GPU忙等,这样就会产生非常严重的卡顿。为了解决这一问题,我们想要做到的就是将这些3D物体进行合并,将多个模型减少成一个,材质球也只使用一个。

    Unity在合并模型从而优化drawcall上有自己的功能,即动态批处理和静态批处理,后面做详细解释。

  2.2 动态批处理

    动态批处理意味着随时可进行的模型合并批处理,当开启动态批处理(Dynamic Batch)时,Unity3D可以将场景中的某些物体自动批处理成为同一个drawcall,如果它们使用的是同一个材质球,并且满足一些条件,动态批处理就会自动完成,不需要额外操作。需要满足动态批处理的条件如下:

    1)动态批处理的物体顶点数目要在一定范围之内,动态批处理只能应用在少于900个顶点的网格中。如果你的Shader使用顶点坐标、法线和单独的UV,那只能动态批处理300个顶点的网格;如果Shader使用顶点坐标,法线,UV0、UV1和切线则只能处理100个顶点的网格。

    2)两个物体的缩放比例一定要相同,假如两个物体不再同一个缩放单位上,它们就不会进行动态批同步。

    3)使用相同的材质球

    4)多管线着色器会中断动态批同步

    一味的减少drawcall也不是万能的,如果节省的开销小于准备工作的开销就可以不进行多余操作(例如主机的API对drawcall消耗就很小,动态批处理的效果就不会很好)。

  2.3 静态批处理 

    静态批处理允许引擎在离线的情况下进行模型合并的批处理,以减少drawcall。无论模型有多大,只要使用同一材质球,都会被静态批处理。它通常比动态批处理更有用,但也会消耗更多的内存。

    为了让静态批处理能起作用,我们需要对物体设置静态标记,并且该物体之后不能动,旋转,缩放。

    使用静态批处理需要增加额外的内存来村粗合并的模型,是一种内存换CPU的方案。静态批处理的具体做法是将所有标记为static的物体放入世界空间,以材质球分类的标准将他们合并,并构建一个很大的顶点集合,将所有物品在同一批drawcall处理。

    绝大多数情况下,批处理被限制在64000个顶点和索引内

       2.4 自己写合并3D模型的程序

    很多时候动态批处理限制太多,静态批处理又不能实现我们的需求,这时候就需要使用自定义批处理,也就是我们自己实现合并3D模型。

    自己编写合并3D模型的代码时,需要调用到Unity的几个API,下面进行介绍。

    1.子网格的含义

    子网格就是网格中拆分出来的子模型,子网格需要用到多个额外的材质球,而普通的网格只有一个材质球。

    2.MeshFilter和MeshRenderer中的mesh与shareMesh、material及shareMaterial的区别

    mesh和material都是实例型变量。对mesh和material执行任何操作,都是额外复制一份之后再赋值。sharedMesh和sharedMaterial不一样,是共享型变量,多个3D模型可以共用同一个指定的sharedMesh和sharedMaterial。

    3.materials和sharedMaterals

    与之前一样,前者是实例型变量,后者是共享型变量,不同之处是现在都是数组形式。与没有加s的区别就是这是针对子网格的,而2中介绍的是针对主网格的。

    4.网格、MeshFilter、MeshRenderer的关系

    网格是数据资源,有自己的文件如XX.FBX,内部提供渲染所必要的数据。MeshFilter是一个承载网格数据的类,网格被实例化后存储在MeshFilter类中。MeshFilter包含两种类型,实例型与共享型如前文所诉。MeshRenderer则是具有渲染功能的组件,他会提取MeshFilter中的信息进行渲染。

    5.CombineInstance 合并数据实例类

    合并时需要为每个将要合并的网格创建一个CombineInstance实例,并往里面放mesh,subMesh的索引,lightmap的缩放和偏移,realtimeLightmap的缩放和偏移,世界坐标矩阵。合并时需要将CombineInstance数组传入合并接口,即通过Mesh.CombineMeshs接口进行合并。


定义CombineMeshs接口,源码如下
public void CombineMeshs(CombineInstance[] combine, bool mergeSubMeshes = true, bool useMatrices = true, bool hasLightmapData = false);

//
1)建立合并数据数组 CombineInstance[] combine = new CombineInstance[m.MeshFilter.Count]; // 2)填入合并数据 for (int i=0 ;i< mMeshFilter.Count;i++){ combine[i].mesh = mMeshFilter[i].sharedMesh; combine[i].transform = mMeshFilter.transform.localToWorldMatrix; combine[i].subMeshIndex = i; //表示Material的索引位置 0,1,2等 } // 3)将所以网格合并到一个单独网格 new_meshFilter.sharedMesh.CombineMeshes(combine); // 或者保留子网格 new_meshFilter.sharedMesh.CombineMeshes(combine,false);

3.状态机

  状态机的也是对现实抽象的一种实现,可以定义不同的状态并且设置好跳转方式。

  状态机的功能和数量都会因为项目的不同而不同,只有事件机制和控制类是不变的。

  事件机制的原理是,当状态满足转换条件时,在即将退出状态前向当前状态发起事件,告诉当前状态机停止运行。等待退出逻辑处理完毕后,再向新状态发起事件。

  游戏项目中可以使用到状态机的地方:

  1)场景切换

  把每个场景看成是一个状态,触发不同条件切换不同场景

  2)人物行为状态切换

  人物一般只有一个动作状态,比如攻击状态、防守状态、死亡状态。又比如说人有跑步状态,但是我们有需求要求人边跑边吃东西。这时我们可以基于边跑边吃再重新设置一个状态,或者是在跑步状态中添加吃东西的参数,让跑步这个状态机播放不一样的跑步动画。

  3)宝箱、机关等具有多动画的元素

  可以把机关的每一个动画都看成是一个状态。其实Animator组件控制动画的过程就是采用了这种思路

  4)AI

  AI可以有巡逻,攻击,激怒的等多个状态,这些状态都可以使用状态机进行管理。

4.3D模型的变与换

  模型是3D游戏的基础,在模型的世界里,众多的顶点构成了3D模型,各个顶点之间连接组成了多个三角形和多边形。多边形也可以由三角形构成,且三角形操作起来会更加容易,除了细分着色器和几何着色器这两个不常用的着色器外,其他重要阶段的顶点都是以三角形为单位的。

  通常使用索引三角形网格表示法来进行表示,一共包含了两个列表,一个是顶点列表存储了网格所有的顶点,另一个是索引列表存储了形成三角形的全部索引。除顶点外我们还需要构成模型的其他信息,例如纹理,UV,法向量等。我们可以建立一个与顶点列表大小相同的列表用来存储。同时三个顶点组成了一个三角形,方向也很重要,这里规定了顺序索引法。确保能顺利计算出面的朝向。

  完整叙述一下网格数据从制作到渲染的全过程:

  首先,美术人员制作3D模型并导出成Untiy能够识别的格式,.fbx文件,其中已经包含了顶点和索引数据,然后在程序中将.fbx实例化成Untiy的GameObject,它们身上附带的MeshFilter组件存储了网格的顶点数据和索引数据,MeshRenderer或者SkinMeshRender用于渲染模型,这些顶点数据通常会和材质球合并,在渲染时一起送入图形卡,这里送入时不会由索引数据送入,而是三个顶点组成的三角形顶点送入图形卡。接着图形卡负责处理我们送入的数据,然后渲染帧缓存,并输出到屏幕。

  4.1 切割模型

   切割模型,就像是实现水果忍者游戏里的那样的效果。通过划屏幕从而将一个模型分为两个部分。具体实现只需要区分顶点是否在同一侧即可。在屏幕上划一下代表了平面,滑动时我们可以知道滑动起点和滑动终点,从而知道切割平面的法线,以及从摄像机近切面出发的平面方向,因此可以利用这两个数据点积得到结果。

  public float pointDotClipplane(Vector3 point){ return Vector3.Dot((point - touhEnd), planeNormal); } 

  切割后模型中空部分也需要缝合,缝合的主要目的是新生成的点能有规律的组成新的三角形。详情:GitHub - hugoscurti/mesh-cutter: Simple mesh cutting algorithm that works on simple 3d manifold objects with genus 0

  4.2 扭曲模型

   扭曲模型相当于将3D模型变形,例如,将模型拉伸或压缩。我们知道模型网格是由三角形构成的,三角形由顶点组成,要变形,就要移动顶点的位置,且有时不止移动一个点。

  顶点的移动相对比较简单,就是取出顶点数组,修改坐标再放回去。难点就是准确的找出修改的订单,二是不同的点修改的值不同,要找出修改顶点的偏移量。下面介绍几个案例。

  1)爆炸凹陷

  首先找出爆炸返回的地面网格,取出地面网格上的所有顶点数据,求出爆炸范围球体内的顶点,这些顶点就是需要修改的顶点。

  然后,进行顶点凹陷计算,凹陷算法的目的是将顶点位置修改到爆炸球体的表面上,这个算法相当于把一个顶点对应到一个球体的面上。可以使用球坐标来计算这个过程:

     x=cosa*cosb , y=cosa*sinb , z=sina 

其中,a,b为经纬度,这样就能计算出从原始顶点到球中心点的方向矢量了,进而计算出经纬度,进而得到修改后的球面位置。修改顶点数据后即可,索引和UV都不需要改变。

  2)球体拉伸与反弹恢复

  在拉伸球体时,要计算所有点与拉伸点的距离,距离越大,顶点的偏移也就越小。并且不是线性衰减的,可以用一个公式: res =f/(d*d),d是距离,f是拉伸距离,res是需要移动的距离。

  球体拉伸后又放开恢复时,可以假定球体的屁股是被固定的。同样,拉得越远的回弹速度也就越快,也就是说,我们需要记录下原来没被拉伸时顶点的坐标位置,它们与原来的位置相减就是反弹速度的基础变量。反弹力度也可以用一个公式表示:res = (d/max)*(d-2)*k。

  在不断计算的过程中,回弹的恢复力度会由于顶点与原来点位距离的缩小而缩小,之后又由于反弹过度而不断放大,有一个来回反弹的过程,最后恢复到不再移动的平静状态。

  3)模拟制陶工艺

  一个陶器模型在转盘上不断的转动,人可以通过点击来实现对模型的拉伸,这就是制陶工艺,我没记错早些年的手机上曾经有过几款类似的游戏很火,现在我们来看下实现原理把。

  制套工艺的原理很简单,就是当你在触碰时,根据手指滑动的方向,将范围内的顶点按照手指滑动的方向偏移,且这里有一个衰减范围,离手指越近,拉伸的距离也就越大。

  4.3 简化模型

   模型过大,虽然更加精细,但是也有可能造成游戏卡顿,简化模型是LOD的比较常用的方法,我们当然可以手动的去化简模型,但是时间人力成本等因素让我们使用程序来进行化简(随便一搜就会有很多的插件)。

  4.4 蒙皮骨骼动画

   游戏场景中有模型也就会有预支对于的动画,那3D模型和3D模型动画的区别在哪呢?

  MeshRenderer和SkinedMeshRenderer这两个组件分别用于渲染3D模型和3D模型动画,它们的模型数据都存储在MeshFilter中,因此它们都依赖MeshFilter组件。其中,MeshRenderer只负责渲染模型,我们也可以称它为普通网络渲染组件,它从MeshFilter中提取网格顶点数据。而SkinedMeshRenderer虽然也渲染模型,也从MeshFilter中提取模型网格顶点数据,但蒙皮网格主要用于渲染动画服务,所以蒙皮网格除了3D模型数据外,还有骨骼数据及顶点权重数据。

  蒙皮网格在渲染时与3D模型渲染的步骤是一样的。如果蒙皮网格上没有骨骼数据,那他也就是一个单纯的3D模型。

  下面来介绍下骨骼动画的一些原理,首先,使用骨骼去影响网格顶点的,我们称之为骨骼动画。骨骼动画数据主要由一些骨骼点和权重数据组成,游戏角色中,骨骼动画的骨骼数量通常都不会超过100个。骨骼动画由骨骼点组成,骨骼点可以认为是带有相对空间坐标点的数据实体,骨骼动画中可以有许多个骨骼点,但根节点只有一个,在现代手机游戏中,人物骨骼动画大概30个,PC中大概75个。在Unity的蒙皮网格组件中,bones变量用于存储所有骨骼点,骨骼点在蒙皮网格中是以Transform数组形式存储的每个顶点都有一个BoneWeigh结构实例,用来判断自己被哪些骨骼点影响。Untiy3D中的Quality setting中,我们可以设置被影响的骨骼数。

  制作蒙皮动画的步骤:第一步是使用3DMax等3D建模软件在几何模型上构建一系列的骨骼点,并计算几何模型的每一个顶点受到这些骨骼点影响的权重值。第二步是动画师通过3D模型制作一系列的动画。第三步导入Unity中后播放动画。

  总之,在制作该类型动画时要尽可能的让骨骼少一些,让面数少一些。

  4.5 人物3D模型动画换皮换装

   首先,为了达到模型动画的动态拼接,必须规定角色的所有动画和部件只使用同一套骨骼。由于骨骼点的移动影响网格顶点,更换了模型的网格依然可以根据骨骼点计算出偏移量,但如果骨骼更换了,那么就不行了。

  其次,把骨骼和模型部件拆分开来,骨骼文件里只有骨骼数据,每个部件的模型文件只包含它自己的模型和顶点数据,以及顶点上的骨骼权重数据。用Untiy中的说法就是一个模型可以拆分为多个.fbx文件,其中一个只负责骨骼数据,其他的用来存放个每个部件的模型。

  当我们想更换人物的某个部件模型,只需要把原有部件的模型实例删除,再实例化出我们需要的模型部件,并把骨骼数据复制即可。但是这种方式虽然简单,但是消耗的drawcall是很多的,假设人被拆分成了五个部件那就有五个材质球,就需要五次drawcall才能把人渲染出来,加重了性能损耗。

  更好的办法是使用Mesh.CombineMeshes(),将模型进行合并。对于贴图,则将多个贴图在合并时动态的进行合并。

  4.6 捏脸

  1)更换身体部件

  跟换身体部件的步骤与上诉的更换衣服的操作是一样的。

  2)更换贴图

  直接更换材质球里面的贴图即可。

  3)骨骼移动,旋转,缩放

  由于模型的网格是通过骨骼点来变化的,所以只要骨骼点移动,旋转,变换了人物也会变。所以我们只需要记录骨骼的数据就可以了。但是新的问题来了,骨骼点是跟随动画一起动的,动画数据里的关键帧决定了骨骼点的变化,就算是人工修改了骨骼点,在播放动画时也会强行恢复到动画数据。解决办法是额外增加一些骨骼点,这些骨骼点是专门为用户提供捏脸操作的骨骼点。动画数据中不会存在这些骨骼点,这也就导致了改变体型的时候骨骼点不会影响动画只会影响网格。

  4)用两个不同网格顶点的线性差值做脸部动画

  只使用增加骨骼点的方式是不行的,因为我们的脸部不能有太多的骨骼点。那该怎么办呢?我们可以制作两个极端的脸部表情,用网格顶点差值的方式对原始网格数据里的顶点进行改变,每个差值都会带来顶点的变化,并形成一个网格形状,从而形成不同脸部表情的动画。

  4.7 动画优化

   前面介绍了关于蒙皮动画太消耗CPU的问题,通常所有蒙皮网格的变化都是由CPU计算得到的,这就使得CPU的负担比较重,因为游戏中的动画量通常很大

  Unity3D中有一个CPU Skinning的选项,开启后,引擎会使用多线程+SIMD来对蒙皮网格的计算做加速处理,由于每个顶点的变化都是独立于骨骼点之上的,相邻的顶点之间并不会互相影响,因此可以使用多线程将一个模型的网格顶点拆分成多个顶点进行计算,多线程的使用将提高蒙皮网格计算的速度。

  CPU Skinning并没有减少CPU的运算量,只是加快了计算速度,提高了运算效率。

  所以除了引擎上优化还有一些方法用来降低CPU的运算量。

  1)使用着色器代替动画

  天上的云,地上的草,飘动的红旗,都是一些我们可以用来使用着色器代替动画的。这些着色器大部分都会利用时间因子,噪声算法、数学公式(sin、cos)来表达顶点的偏移量。除了顶点动画外,我们还可以使用UV来制作动画,例如流动的水属于UV位移动画,火焰效果可以根据不同断的更换UV范围达到序列帧动画效果的UV序列帧动画等。这种方法可以将CPU消耗转化为GPU消耗并且消耗会更小。

  2)离线制作加速动画

  我们可以为提前每帧都渲染一个模型,之后每帧都展示不同的模型,这样就可以通过切换模型来实现渲染动画的效果了。这样的方法,例如场景只有2-3个模型在播放动画,那么为了完成2-3个模型就需要150个模型来播放动画,这样内存就需要额外增加150倍。但如果场景中的模型数量非常多的话,假设现在有20个,这20个模型每帧都需要通过计算的方式来计算出动画,且要重复计算100次,就远比用150个模型来代替的性能消耗要高。这是一种内存换CPU的方式,适用于同一场景中动画太多的情况。且会存在大量的drawcall,因为每个模型至少有一个drawcall,多个模型也就有多个,GPU压力并没有减少

  3)GPU instancing 加速动画

  GPU instancing是显卡的一种特性,大部分图形API都能提供的一种技术,其表象为当我们绘制1000个物体时,它只将模型数据及1000个坐标提交给显卡,这1000个物体不同的位置、状态、颜色整合成一个Per Instance Attribute的Buffer提交给GPU,这使得GPU可以在着色器中区别对待传入的网格数据。这样的好处只需要提交一次就能绘制所有不同位置的物体,大大减少了提交次数,对于绘制大量相同模型的情况,这种技术可以提高效率。同时也能避免因合批而造成的内存浪费。

  它的主要特点就是,只提交一次就能绘制多个物体,把原本的提交1000次流程简化成了只提交一次。当然,它的使用也是有条件的:1.模型的着色器要能够支持GPU Instancing2.模型的位置、角度可以不一样,但是都必须使用同一个网格数据(着色器,材质球,网格)

  4)离线LOD动画与LOD网格

  我们可以提前制作不同等级的动画资源,更具玩家的配置性能选择性的播放动画资源,与经典的LOD一致。

5.资源的加载与释放

  5.1 资源加载的方式

   资源的加载分为阻塞式加载方式和非阻塞式加载方式。阻塞式就是指当前资源文件加载完成后才能执行下一条语句非阻塞式是开启另一个线程加载文件,主线程则可以继续执行下面的程序,当加载完毕时再通知主线程

  阻塞式加载方式有:

    1)Resource.Load

    当Unity项目被构建时,Untiy3D会将Resources文件夹下的所有资源文件打包成为一个或几个资源文件放入包内。当我们在程序中调用Resource.Load时,则可以从这几个资源文件中查找,并提取数据作为资源放入内存。使用该方法时,Unity在打包时会压缩该资源包的文件,会使得包大小减少,同时调用Load()时增加了解压的算力损耗,所以大部分项目都不会使用该方式。

    2)File read + AssetBundle.CreateFromMemory + AssetBundle.Load

    我们还可以先通过文件操作加载文件,再通过AssetBundle加载数据,但是这种方式十分消耗性能,但是可以让我们加入一些自定义的功能,例如在加载AssetBundle前做加解密操作,由于AssetBundle前自主加载了文件,因此该文件数据在变为AssetBundle前都可以自主操作。

    3)AssetBundle.CreateFromFile + AssetBundle.Load

    还可以直接通过加载文件变为AssetBundle。这是项目中最常用的,因为既没有压缩解压,也不用一下子把资源都全部加载到内存中。AssetBundle.CreateFromFile接口并不会把整个资源文件都加载进内存中,而是先加载文件中的数据头,通过数据头中的数据去识别各资源在文件中的偏移位置。当我们调用Load时,会先从数据头中找到数据的偏移位置,再将数据加载到内存中。因此,这种方式是按需加载,合理的利用了内存,也降低了CPU消耗。

  非阻塞式加载方式

    1. AssetBundle.CreateFromFile + AssetBundle.LoadAsync

    2.WWW + AssetBundle.Load

    3.WWW + AssetBundle.LoadAsync

    4.File Read all +AssetBundle.CreateFromMemory + AssetBundle.LoadAsync / .Load

    4.File Read async +AssetBundle.CreateFromMemory + AssetBundle.LoadAsync / .Load

  这几种是文件读取和AssetBundle异步加载来完成的。第一种用的比较多,因为大多数资源文件在游戏前进行比对和下载了,没有必要再通过WWW的形式下载。

  实际项目时,我们没有必要为了异步而去使用异步加载。大部分情况我们在使用阻塞式加载方式时会有一个问题,那就是某一帧资源特别多,会导致游戏卡顿。为了能够更加平滑过渡,我们需要把资源加载和实例化的时间跨度拉长

  具体怎么做?

  可以先获取需要加载的所有资源,然后放入队列中,每次加载限制N个,已经加载过的就直接通知逻辑程序实例化,还未加载的则调用加载程序,并将调用后的加载程序放入“加载中”队列,不开协程而是在Update帧更新去判断“加载中”队列是否有已经加载完成的资源,每加载完毕一个资源,就从加载中队列里将其移除,再通知逻辑程序进行实例化,在实例化期间,我们需要注意为每帧分配数量合适的实例化,实例化太多,也容易造成集中消耗CPU算力的现象,从而导致卡顿。这样直到队列中的请求加载完毕,才会继续下一批N个加载请求。当然,这里也需要做出判断,例如,对于已经在加载队列里的资源,是否不再重复加载。

  5.2 卸载AssetBundle的引用计数方式

     使用引用计数的方法就是判断是否释放资源的很好依据。我们在每加载一个AB包时就对其引用计数器+1,销毁时-1。实现细节,对于Texture贴图这种不需要实例化的资源,最好不要再次引用,每次需要Texture时,查看AB包是否有加载。同时卸载时要设计一个卸载倒计时,避免卸载完立即使用造成额外的性能损耗。

 

  5.3 AssetBundle的打包与颗粒度大小

     Unity中的AB包封装做的很好,打包时会自动计算AB包之间的依赖关系,Unity中AB包的颗粒度很容易缩放,先看两种极端情况。

    1.所有的资源都打在一个包里面。这种情况下不需要引用计数辅助,对文件操作次数很少,I/O效率非常高,并且只有一个文件的原因解压效率也是最高的。此时文件热更新时下载的资源必须重新全部下载。

    2.还有一种把所有的资源都单独打包一份。此时为了能有效地控制内存,AB包之间的依赖和引用计数在这里会有非常大的作用。此时文件的操作数量会很大,I/O操作时间会很大,下载时间会被拉长,下载完成后加压过程也会被拉长。

    实际开发中,会采取颗粒度适中的情况,中和上述的优点和缺点。

posted @ 2023-07-09 10:50  CatSevenMillion  阅读(624)  评论(0编辑  收藏  举报