第三章 模型与网格优化

模型和网格,说白了就是游戏里所有看得见的东西的「骨架」。你想想看,一个角色模型动辄几万面,一个场景里放上几十个这样的模型,手机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万时,中低端机就开始掉帧了。所以控制面数,本质上是在控制「每帧总面数」这个指标。

我的小技巧:在Unity里打开Stats窗口(Game视图右上角),看Tris这个数值。如果超过15万,就该考虑减面了。

减面的时候,我建议优先砍掉这些地方:

  • 角色背部、腋下、大腿内侧——玩家基本看不到
  • 圆形物体的分段数——比如圆柱体,16段和32段在手机上差别不大
  • 小物件的细节——比如纽扣、腰带扣,用贴图表现就够了
注意:不要无脑减面。我曾经把一个角色的手指从5根减到3根,结果被玩家骂惨了。关键部位的面数要保留,比如脸部、手部、武器。

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切换距离设置得太近了。建议每个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;
    }
}
注意:合并网格后,所有子物体的材质必须相同,否则无法合并。如果材质不同,可以考虑用Texture Atlas(纹理图集)来统一。

3.4 Mesh Baker工具使用

Mesh Baker是Unity Asset Store上的一款付费插件,但说实话,它值那个价。我用了好几年了,处理大型场景时基本离不开它。

Mesh Baker的核心功能:

  • 自动合并网格:把场景中相同材质的网格自动合并
  • 纹理图集生成:把多个小纹理合并成一张大纹理
  • LOD批量生成:一键生成所有模型的LOD
  • 网格压缩:批量压缩模型面数

使用步骤很简单:

  1. 在场景中选中所有要合并的物体
  2. 打开Mesh Baker窗口(Window → Mesh Baker)
  3. 点击「Create Combined Mesh」按钮
  4. 设置合并参数(比如纹理大小、是否生成LOD)
  5. 点击「Bake」生成合并后的网格

我记得有一次做一个开放世界手游,场景里有3000多棵树木。每棵树都是一个独立的网格,Draw Call直接飙到4000多。用Mesh Baker合并后,Draw Call降到了200左右,帧率从20帧直接跳到55帧。嗯,这就是工具的价值。

警告:Mesh Baker虽然强大,但不要滥用。合并后的网格如果太大(超过6万面),反而会影响性能。建议每个合并网格控制在3万面以内。

最后说一句,模型优化不是一锤子买卖。我建议你在项目初期就定好面数标准,然后每周做一次性能测试。发现问题及时调整,别等到上线前才慌慌张张地减面——那时候就晚了。