一个模型中可以包含很多网格,一个模型可以由多个网格组成。在Unity3D中一个网格可以由多个子网格(Sub-Mesh)组成。
在渲染引擎的时候,每个子网格都要匹配一个材质球来做渲染,实际上一个子网格本身就是一个个普通的模型,它由很多个三角形构成,也需要材质球支持达成渲染。
为什么要有子网格?
美术人员在制作3D模型时,为什么不打网格编成一个,而要制作成多个子网格?
- 模型中的一部分网格用一种材质球来表现效果,另一部分用另一种。因为一个网格只能对应一个材质球所以要拆开。
- 模型中的莫部分贴图在众多模型中共同时使用的频率比较高,为了不重复制作以减少重复劳动,原本可以作为一个整体的模型会将公共材质部分单独拆分出来让一部分模型使用同一个材质球。
- 制作动画时,由于动画过于复杂,如果使用同一个模型去表现,则骨骼的数量就会成倍的增加,为了更好的表现动画,也为了能降低骨骼的使用数量,要拆分出来一部分模型,让它们单独成为模型动画的一部分。
从以上看多个子网格比单个网格要灵活的多,但它也有缺点,由于每个子网格都有材质球,导致子网格越多,drawcall 也越多。
子网格功能虽然强大,但也需要注意性能开销。
动态合并3D模型
我们知道每个网格模型都需要有一个材质球支持,导致每个模型都会产生一个drawcall,过多的drawcall,CPU会忙于发送状态数据给GPU,GPU大多数时间处于等待状态,这将导致画面帧率下降及强烈的画面卡顿。
实际项目中通常会遇到这样的问题:场景中会有很多物体,人物、建筑、路标、景观、树木、石头、碎块、花草等等,有的物体可能不只有一个材质球,相同物体共用一个,当然不同的物体也可能使用相同的材质球,这些物体如果不做任何优化处理,就会产生很多drawcall。
合并3D模型的主要目的就是减少drawcall,通过减少材质球的提交次数来完成优化,简单的说就是大拥有相同材质球的模型合并成一个模型和一个材质球,从而减少向GPU提交的drawcall数量,让GPU并行处理数据时更快更顺畅。
在Unity中分为动态合批和静态合批两种,使用它们的前提条件是模型物体必须具有相同的材质球,除了这个必要条件外还有其他条件。
动态批处理
开启动态批处理时,Unity会将场景中的某些物体自动批处理成为同一个drawcall,需要满足:
- 顶点数目要少于900个顶点网格。如果你的着色器使用顶点坐标、法线和单独的UV,那么顶点个数就要在300以内,如果着色器使用顶点坐标、法线、UV0、UV1和切线,则顶点要在180个内的网格。
- 两个物体的缩放单位要相同,比如A(1,1,1) B(1,1,2)就没法合批了
- 使用相同材质球
- 多管线(Pipeline)着色器会中断动态合批,支持多个灯光的前置渲染,它们增加了多个渲染通道,因此无法批处理。旧系统中的延迟渲染路径(灯光前置通道)关闭了动态批处理,因为它需要绘制物体两次。多有多个Pass的着色器增加了渲染管道,不会被动态批处理。
从上面看动态批处理的条件非常苛刻,真实的项目里很多模型时不符合动态批处理要求的。另外动态批处理要消耗CPU将所有物体的顶点转换到世界空间,合并网格也会带来开销。一味的减小drawcall并不是万能的,开销取决于很多因素,drawcall的开销主要是由图形API的调用造成的,如果节省的开销小于准备工作的开销,就得不偿失。例如,一个主机设备或流行的API(Apple Metal这样的),drawcall的开销通常很低,那么对于它来说动态批处理就没有什么优势了。
静态批处理
静态批处理允许引擎离线的情况下进行模型合并的批处理,减少drawcall。无论模型有多大,只要使用同一个材质球都会被静态批处理优化。它通常比动态批处理更有用(因为不需要实时转换顶点来消耗CPU),缺点是消耗了更多的内存。
Unity中需要对物体设置静态标记,也就是不能移动,缩放,旋转的物体。
如果一些物体之前共用一个模型,那么unity会复制这些物体的模型用来合并,所以使内存增大。个人觉得使用内存换CPU是值得的,然而如果用100GB的内存换千分之一的CPU效率那就不值得了。
静态批处理的具体流程是,将所有静态标记的物体放入世界空间->通过材质球分类标准将它们分别合并->构建成一个大的顶点集合和索引缓存。从而减少drawcall。
从技术上来说,静态批处理并没有节省3D API drawcall的数量,但它能减少因3D API之间的状态改变导致的消耗。大多数平台上,批处理限制在64000个顶点和64000个索引内(OpenGLES上为48000个,MacOS上为32000个),所以如果批处理过多,需要取消一些静态标记。
自己编写合并3D模型程序
Unity3D中:
- Mesh类有一个CombineMeshes的接口,提供了合并入口。
- MeshFilter类,是承载数据类。
- MeshRenderer类,是绘制网格的类
在使用之前我们还需要了解
MeshFilter和MeshRenderer中的mesh与shareMesh、material及shareMaterial的区别
mesh和material都是实例型的变量,对mesh和material执行任何操作都是额外复制一份后重新赋值,即使只是get操作,同样也会执行复制操作。而就是说,对mesh和material进行操作后,就会变成另一个实例。sharedMesh和shareMaterial与前面的两个变量不同,它们是共享型的。多个模型可以共同指定一个sharedMesh和sharedMaterial,当年修改了shareMesh或sharedMaterial里面的参数,指向同一个sharedMaterial和sharedMesh的多个模型就会同时改变效果。
materials和sharedMaterialsd的区别
materials是实例型,sharedMaterials是共享型,两个都是数组。material和sharedMaterial只针对主网格。而就是说material等于materials[0],sharedMaterial等于sharedMaterial[0]。
网格、MeshFilter、MeshRenderer的关系
网格时数据资源,它可以有自己的资源文件,通常时.FBX文件。网格里存储了顶点、UV、顶点颜色、三角形、切线、法线、骨骼、骨骼权重等提供渲染所必要的数据。
MeshFilter是一个承载网格数据的类,网格被实例化后存储在MeshFilter类中。MeshFilter包含两种类型,即实例型(mesh)和共享型变量(sharedMesh)。
MeshRenderer具有渲染功能,它会提取MeshFilter中的网格数据,结合自身的materials或sharedMaterials进行渲染。
Mesh.CombineInstance合并数据实例类
合并时需要为每个将要合并的网格创建一个CombineInstance实例,并往里放入mesh、subMesh的索引,lightmap的缩放和偏移,realtimeLightmap的缩放和偏移,以及世界坐标矩阵。它承载了所有需要需要合并的数据。
现在我们掌握了关于合并网格的理论知识我们来实现以下,这里我从Unity 商店下载了一个模型,我将角色手中的 剑和盾牌进行了合并,并生成了一个新的GameObject 效果如下:
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
public class CombineTest : MonoBehaviour
{
public List<MeshFilter> combineMeshFilers = new List<MeshFilter>();
// Start is called before the first frame update
void Start()
{
CreateCombineModel();
}
private void CreateCombineModel()
{
// 提取网格所需的资源
List<Material> materials = new List<Material>();
CombineInstance[] combines = new CombineInstance[combineMeshFilers.Count];
for (int i = 0; i < combineMeshFilers.Count; i++)
{
CombineInstance tmpInstance = new CombineInstance();
MeshFilter tmpFilter = combineMeshFilers[i];
tmpInstance.mesh = tmpFilter.sharedMesh;
tmpInstance.transform = tmpFilter.transform.localToWorldMatrix;
combines[i] = tmpInstance;
MeshRenderer renderer = tmpFilter.transform.GetComponent<MeshRenderer>();
for (int j = 0; j < renderer.sharedMaterials.Length; j++)
{
if (!materials.Contains(renderer.sharedMaterials[j]))
materials.Add(renderer.sharedMaterials[j]);
}
}
// 将所有网格合并成一个新的网格
Mesh mesh = new Mesh();
MeshFilter combineFilter = transform.AddComponent<MeshFilter>();
MeshRenderer combineRenderer = transform.AddComponent<MeshRenderer>();
mesh.CombineMeshes(combines);
combineFilter.mesh = mesh;
combineRenderer.materials = materials.ToArray();
}
}