第三章 模型与网格优化
模型和网格,说白了就是游戏里所有看得见的东西的「骨架」。你想想看,一个角色模型动辄几万面,一个场景里放上几十个这样的模型,手机GPU直接就冒烟了。我在早期做项目时就吃过这个亏——一个武侠手游,角色模型平均2万面,结果在小米4上跑起来跟幻灯片似的。嗯,从那以后,我对模型优化就特别上心。
3.1 模型面数控制
模型面数,就是构成3D模型的三角形数量。每个三角形,GPU都要去计算它的位置、光照、纹理。面数越多,计算量越大。
我个人习惯把移动端模型面数分成三个等级:
| 设备等级 | 主角模型 | 普通NPC | 场景物件 |
|---|---|---|---|
| 高端机(iPhone 12+) | 8000-15000面 | 3000-5000面 | 500-2000面 |
| 中端机(iPhone 8-11) | 5000-8000面 | 2000-3000面 | 200-800面 |
| 低端机(iPhone 7及以下) | 3000-5000面 | 1000-2000面 | 100-500面 |
这些数字不是拍脑袋定的。我做过大量真机测试,发现当单帧渲染面数超过20万时,中低端机就开始掉帧了。所以控制面数,本质上是在控制「每帧总面数」这个指标。
减面的时候,我建议优先砍掉这些地方:
- 角色背部、腋下、大腿内侧——玩家基本看不到
- 圆形物体的分段数——比如圆柱体,16段和32段在手机上差别不大
- 小物件的细节——比如纽扣、腰带扣,用贴图表现就够了
3.2 LOD(Level of Detail)技术
LOD,说白了就是「离得近用高模,离得远用低模」。这个技术太实用了,我几乎每个项目都会用。
Unity的LOD系统用起来很简单:
// 在脚本中动态设置LOD
using UnityEngine;
public class LODController : MonoBehaviour
{
private LODGroup lodGroup;
void Start()
{
lodGroup = GetComponent<LODGroup>();
// 设置LOD切换距离
float[] distances = new float[] { 10f, 25f, 50f };
lodGroup.SetLODs(new LOD[] {
new LOD(0.5f, GetComponent<Renderer>()),
new LOD(0.3f, lowResRenderer),
new LOD(0.1f, veryLowResRenderer)
});
}
}
我个人习惯用3级LOD:
- LOD0:原始模型,100%面数,用于近距离(0-10米)
- LOD1:减到50%面数,用于中距离(10-25米)
- LOD2:减到20%面数,用于远距离(25-50米)
为什么是这三个比例?我测试过很多次,发现50%和20%这两个节点,在视觉上几乎看不出差别,但性能提升非常明显。你想想看,一个场景里如果有100个角色,每个角色从1万面降到2000面,那就是从100万面降到20万面——这差距太大了。
避坑指南:我曾经在LOD切换时没处理好,导致角色在跑动时模型突然「跳变」。后来发现是LOD切换距离设置得太近了。建议每个LOD之间留5-10米的缓冲区间,并且用Cross Fade(交叉淡入淡出)来平滑过渡。
3.3 网格压缩与合并
网格压缩,就是减少模型文件在内存中的大小。Unity提供了Mesh Compression选项,在模型的Import Settings里可以找到。
| 压缩级别 | 内存节省 | 质量影响 | 推荐场景 |
|---|---|---|---|
| Off | 0% | 无 | 主角、重要NPC |
| Low | 约30% | 几乎不可见 | 普通NPC、中型物件 |
| Medium | 约50% | 轻微 | 场景装饰、小物件 |
| High | 约70% | 明显 | 远景、不重要的物体 |
网格合并,则是把多个小网格合并成一个。为什么要合并?因为每个网格都是一个Draw Call。100个独立的小石头,就是100个Draw Call。合并成一个网格,就变成1个Draw Call。这差距,你品品。
Unity里合并网格的代码:
using UnityEngine;
using System.Collections.Generic;
public class MeshCombiner : MonoBehaviour
{
public void CombineMeshes()
{
MeshFilter[] meshFilters = GetComponentsInChildren<MeshFilter>();
CombineInstance[] combine = new CombineInstance[meshFilters.Length];
for (int i = 0; i < meshFilters.Length; i++)
{
combine[i].mesh = meshFilters[i].sharedMesh;
combine[i].transform = meshFilters[i].transform.localToWorldMatrix;
}
Mesh finalMesh = new Mesh();
finalMesh.CombineMeshes(combine);
// 把合并后的网格赋给父物体
GetComponent<MeshFilter>().mesh = finalMesh;
}
}
3.4 Mesh Baker工具使用
Mesh Baker是Unity Asset Store上的一款付费插件,但说实话,它值那个价。我用了好几年了,处理大型场景时基本离不开它。
Mesh Baker的核心功能:
- 自动合并网格:把场景中相同材质的网格自动合并
- 纹理图集生成:把多个小纹理合并成一张大纹理
- LOD批量生成:一键生成所有模型的LOD
- 网格压缩:批量压缩模型面数
使用步骤很简单:
- 在场景中选中所有要合并的物体
- 打开Mesh Baker窗口(Window → Mesh Baker)
- 点击「Create Combined Mesh」按钮
- 设置合并参数(比如纹理大小、是否生成LOD)
- 点击「Bake」生成合并后的网格
我记得有一次做一个开放世界手游,场景里有3000多棵树木。每棵树都是一个独立的网格,Draw Call直接飙到4000多。用Mesh Baker合并后,Draw Call降到了200左右,帧率从20帧直接跳到55帧。嗯,这就是工具的价值。
最后说一句,模型优化不是一锤子买卖。我建议你在项目初期就定好面数标准,然后每周做一次性能测试。发现问题及时调整,别等到上线前才慌慌张张地减面——那时候就晚了。