type
status
date
summary
tags
category
icon

一、网格模型基础

Unity3D模型中子网格的意义

在Unity3D中,一个网格可以由多个子网格(SubMesh)组成。在渲染引擎时,每个子网格都要匹配一个材质球来做渲染,说白了,一个子网格本身就是一个普通的模型,也由很多个三角形组成,也需要材质球支持来达成渲染。
那么有一个疑问,相比于制作成一个整体的网格,美术人员为什么还需要将网格拆分成不同的子网格呢?这是有原因的
  • 第一个原因是,美术人员在制作3D模型时,希望一个模型中的一部分网格用一种材质球来表现效果,另一部分网格则用另一种材质球来表现效果。这时就需要将模型拆分开,因为一个网格只能对应一个材质球,一个材质球只能表现一种效果。例如人物模型,
  • 第二个原因是,模型中的某部分贴图在众多模型中共同使用的频率比较高,为了不重复制作以减少重复劳动,原本可以作为一个整体的模型会将公共材质部分单独拆分出来让这一部分模型使用同一个材质球。
  • 第三个原因是,在制作动画时,由于动画过于复杂,如果使用同一个模型去表现,则骨骼的数量会成倍增加。为了能够更好的表现动画,也为了能降低骨骼的使用数量,要拆分出一部分模型,让他们单独成为模型动画的一部分。
以上三个原因是在制作模型中需要着重考虑的,通常情况下会用拆分模型的方式来处理。
子网格的功能虽然强大,但也需要注意性能开销。由于每个子网格都有材质球,导致子网格越多,增加的渲染管线调用(drawcall)也越多,并且也无法与其他网格合并,导致优化的一个重要环节被阻断。

网格类与接口

自己编写合并3D模型代码时,需要了解Unity3D的几个类和接口
  • Mesh类有一个CombineMeshes的接口,提供了合并3D模型的入口
  • MeshFilter类,是承载网格数据的类
  • MeshRenderer类,是绘制网格的类

mesh与shareMesh、material及shareMaterial的区别

mesh和material都是实例型的变量,对mesh和material执行任何操作,都是额外复制一份后再赋值,即使只是get操作也同样会执行赋值操作。也就是说,对mesh和material进行操作后,就会变成另外一个实例。
sharedMesh和sharedMaterial与前面两个变量不同,他们是共享型。多个3D模型可以共用同一个指定的sharedMesh和sharedMaterial,当修改sharedMesh或sharedMaterial里面参数时,指向同一个sharedMesh和sharedMaterial的多个模型就会同时改变效果

materials和sharedMaterials区别

与material和sharedMaterial一样,materials是实例型的,sharedMaterials是共享型的,只不过变成了数组形式。
materials和sharedMaterials可以针对不同的子网格,material和sharedMaterial只针对主网格。也就是说,material和sharedMaterial等于materials[0]和sharedMaterials[0]

网格、MeshFilter、MeshRenderer的关系

网格是数据资源,可以有自己的资源文件,比如XXX.FPX。网格里存储了顶点、UV、顶点颜色、三角形、切线、法线、骨骼、骨骼权重等提供渲染所必要的数据。
MeshFilter是一个承载网格数据的类,网格被实例化后存储在MeshFilter类中。MeshFilter包含两种类型,即实例型和共享型的变量。
MeshRenderer具有渲染功能,它会提取MeshFilter中的网格数据,结合自身的materials或sharedMaterials进行渲染。

mesh和subMesh区别

subMesh属于mesh,subMesh的所有顶点数据信息都来自mesh,subMesh自身保存了一个记录了索引的数组。为了进行区分,每一个材质对应的顶点部分对应的mesh,就组成了一个subMesh,即一种材质对应一个subMesh,如果一个模型只有一个材质,那么导出的subMes就只有一个

二、Unity3D合并3D模型

合并3D模型的主要目的就是减少drawcall,它是通过减少材质球的提交数量来完成优化的,说的简单点就是,把拥有相同材质球的模型合并成一个模型和一个材质球,从而减少向GPU提交drawcall的数量,让GPU并行处理数据时更快更流畅。
Unity3D引擎在合并模型优化drawcall上有自己的功能,既动态批处理静态批处理两种,使用他们的前提条件是模型物体必须具有相同的材质球。

动态批处理

开启动态批处理(Dynamic Batch)时,Unity3D可以将场景中的某些物体自动批处理成为同一个drawcall,如果他们使用的是同一个材质球,并且满足一些条件,动态批处理就会自动完成。
需要满足的动态批处理条件如下:
1)动态批处理的顶点数目要在一定范围内,动态批处理只能应用在少于900个顶点的网格中
  • 如果你的着色器使用顶点坐标、法线和单独的UV,那么只能动态批处理300个顶点内的网格
  • 如果你的着色器使用顶点坐标、法线、UV0、UV1和切线,则只能动态批处理180个顶点内的网格
2)两个物体的缩放比例一定要相同
3)使用相同材质球的模型才会被合并
4)多管线(Pipeline)着色器会中断动态批处理
  • 那些支持多个灯光的前置渲染,它们增加了多个渲染通道,因此无法进行动态批处理
  • 旧系统中的延迟渲染路径(灯光前置通道)关闭了动态批处理,因为它需要绘制物体两次
  • 所有多个Pass的着色器增加了渲染管道,不会被动态批处理
可以看出来,动态批处理的条件很苛刻,项目中很多模型是不符合动态批处理要求的。需要说明的是,一味的减少drawcall并不是万能的,开销取决于很多因素,drawcall的开销主要是由图形API的调用造成的,如果节省的开销小于准备工作的开销,则是得不偿失的。

静态合批

静态批处理允许引擎在理想情况下进行模型合并处理,所以通常比动态批处理更有用(因为不需要实施转换顶点来消耗CPU),但也消耗了更多的内存。
使用静态批处理需要增加额外的内存来存储合并的模型。在静态批处理时,如果一些物体之前共用一个模型,那么Unity会复制至这些物体的模型来合并,在Editor或Runtime中都会执行这个操作。
静态批处理的具体做法是,将所有有静态标记的物体放入世界空间,以材质球为分类标准将它们分别合并,并构建成一个大的顶点集合和索引缓存,所有可见的同类物体就会被同一批drawcall处理,从而实现优化的效果。
从技术上来说,静态批处理并没有节省3D API drawcall的数量,但它能减少因状态改变导致的消耗。在大多数平台上,批处理被限制带64000个顶点和64000个索引内,所以,倘若超过这个数量,则需要取消一些静态批处理对象。

总结

  • 动态批处理的条件是,使用同一个材质球,顶点数量不超过900个,有法线的不超过300个顶点,有两个UV的不超过150个顶点,缩放比例要一致,着色器不能由多个通道
  • 静态批处理的条件是,必须是带有静态标记的物体,不能移动和旋转,不能缩放,不能有动画
动态批处理的规则是极其严格的,能用在具体场景中的模型通常是相对比较简单的,它对顶点限制很多,而且缩放比例也要相同,渲染管道也只能有一个。
虽然静态批处理的使用范围更广一些,但要求物体是静态的,不能移动、旋转和缩放。这个局限性太强,只有完全不动的场景中的固定物体才能使用静态批处理。
动态批处理限制太多,静态批处理又不能满足我们的需求,所以很多时候只能自己手动合并模型来替代Unity3D的批处理。

三、自定义合并3D模型

在Unity3D中我们想要实现自己合并网格时,需要为每个将要合并的网格创建一个CombineInstacne实例。CombineInstance承载了所有需要合并的数据,合并时需要将CombineInstance数组传入合并接口,即通过Mesh.CombineMeshes接口进行合并。下面是具体步骤
1)建立合并数据数组。源码如下:
2)填入合并数据。源码如下:
3)将所有网格合并成单独的一个网格,源码如下
或者合并后保留子网格,源码如下
合并完整的代码如下
从上面代码可以看到,合并网格在Unity3D引擎并不困难。合并模型能够减少材质球的使用数量,减少drawcall,减轻GPU的压力,但同时也消耗了CPU,如果在项目中每帧都合并网格,会让原本减少drawcall的优势反而被额外的消耗掩盖。自定义合并网格的优势在于我们业务逻辑中知道什么时候应该合并。
Unity 资源管理模块 AssetBundle基础CLR via C#(第四版) 笔记 反射原理
  • Twikoo
  • Cusdis