一、先理解:一个顶点里装了什么
很多人以为顶点 = 一个3D坐标点 实际上一个顶点携带【一堆属性数据】: 一个顶点(Vertex)包含: ┌─────────────────────────────────────┐ │ Position(位置) : 3个float = 12字节 │ ← 必须 │ Normal(法线) : 3个float = 12字节 │ ← 光照用 │ Tangent(切线) : 4个float = 16字节 │ ← 法线贴图用 │ UV0(主纹理坐标) : 2个float = 8字节 │ ← 贴图用 │ UV1(第二套UV) : 2个float = 8字节 │ ← lightmap用 │ Color(顶点色) : 4个byte = 4字节 │ ← 顶点着色用 │ BoneWeight(骨骼) : ... │ ← 蒙皮动画用 └─────────────────────────────────────┘ ↓ 一个"完整"顶点可能 60+ 字节内存计算:
Mesh内存 ≈ 顶点数 × 每顶点字节数 10000顶点 × 60字节 = 600KB 10000顶点 × 精简到28字节 = 280KB ↓ 省一半以上! 顶点数越多,精简收益越大🎯核心思路:不是每个网格都需要全部属性,用不到的属性去掉,直接减少每个顶点的字节数。
二、各顶点属性的作用与去除条件
1. Position(位置)— 永远保留
顶点的3D坐标,网格的骨架 → 绝对不能去,去了就没网格了2. Normal(法线)— 光照相关
作用:决定表面朝向,用于计算光照明暗 ┌──────────────────────────────────┐ │ 有法线:光照下明暗自然、有立体感 │ │ 无法线:无法正确计算光照 │ └──────────────────────────────────┘ 【可去除】的情况: ├── 纯色/自发光物体(不受光照) ├── UI元素(通常不参与光照) ├── 用了不需要法线的特殊shader └── 2D精灵 【必须保留】: └── 任何需要光照的3D物体(绝大多数)3. Tangent(切线)— 法线贴图相关
作用:配合Normal构成"切线空间",用于法线贴图(Normal Map) 法线贴图 = 用贴图模拟表面凹凸细节 ↓ 需要 切线空间 = Normal + Tangent 一起定义 ↓ 没有Tangent → 法线贴图无法正确工作 ┌──────────────────────────────────┐ │ Tangent占16字节,是较大的一块 │ └──────────────────────────────────┘ 【可去除】的情况:★常见优化点★ ├── 材质没用法线贴图(Normal Map) ├── 简单材质(纯色、只有主贴图) └── 大部分场景道具其实不用法线贴图 【必须保留】: └── 用了法线贴图的材质(角色、精细物体)💡实战:很多物体用了简单材质却带着Tangent,去掉能省16字节/顶点,收益明显。
4. UV0(主纹理坐标)— 通常保留
作用:贴图怎么"贴"到模型上的坐标映射 有贴图的物体都需要UV0 ↓ 【可去除】:纯色无贴图物体(极少) 【保留】:绝大多数(要贴图就要UV)5. UV1 / UV2(第二套UV)— Lightmap相关
作用:主要用于光照贴图(Lightmap)的独立UV展开 为什么需要第二套UV? 主UV(UV0):贴纹理,可能重叠/平铺 Lightmap UV(UV1):烘焙光照,必须不重叠 ↓ 两者需求冲突 → 用独立的第二套UV ┌──────────────────────────────────┐ │ 【可去除】的情况:★常见★ │ │ ├── 不用烘焙光照(实时光照/无光照) │ │ ├── 动态物体(不参与lightmap烘焙) │ │ └── 移动端很多不用lightmap的物体 │ ├──────────────────────────────────┤ │ 【必须保留】: │ │ └── 参与光照烘焙的静态物体 │ └──────────────────────────────────┘6. Color(顶点色)— 顶点着色相关
作用:给每个顶点存一个颜色,用于: ├── 顶点级着色(渐变、遮罩) ├── 混合多张贴图的权重 └── 某些特效 【可去除】:不用顶点色的物体(大部分) 【保留】:用顶点色做混合/特效的7. BoneWeight(骨骼权重)— 蒙皮动画
作用:骨骼动画中,顶点受哪些骨骼影响、影响多少 【只有】SkinnedMeshRenderer(蒙皮网格)需要 ↓ 静态物体(MeshRenderer)不该有这个三、Mesh Compression(压缩)原理
前面是"去掉整个属性",这里是"降低保留属性的精度"。
核心:用更少的位表示数值
默认:每个数值用 float(32位) 存储 压缩:用更少的位数近似表示 例:位置坐标 未压缩:float 32位,精度极高 压缩:可能用16位半精度,精度降低但省一半四个档位
┌──────────┬────────────────────────────────┐ │ Off │ 不压缩,全精度float,最大最准 │ ├──────────┼────────────────────────────────┤ │ Low │ 轻度压缩,精度损失小 │ ├──────────┼────────────────────────────────┤ │ Medium │ 中度压缩 │ ├──────────┼────────────────────────────────┤ │ High │ 高度压缩,最省但精度损失明显 │ └──────────┴────────────────────────────────┘ 压缩不同数据用不同精度: ├── Position:影响形状,压狠了会变形/抖动 ├── Normal/Tangent:影响光照,损失影响明暗 └── UV:影响贴图,压狠了贴图错位压缩的代价:精度损失表现
位置精度损失: 模型顶点位置"量化"到网格点上 → 大模型可能出现顶点抖动、缝隙 → 精细模型慎用High UV精度损失: → 贴图可能轻微错位、接缝 Normal精度损失: → 光照明暗过渡不够平滑⚠️注意:Mesh Compression 是存储压缩(磁盘/传输小),加载到运行时内存里通常还是解压状态。它主要减小包体,运行时内存收益有限。真正省运行时内存的是"去掉属性"。
四、关键区分:两种优化的不同收益
┌────────────────┬──────────┬──────────┐ │ │ 包体(磁盘) │ 运行时内存│ ├────────────────┼──────────┼──────────┤ │ Mesh Compression│ ↓↓ 明显 │ ~ 有限 │ │ (降精度) │ │ │ ├────────────────┼──────────┼──────────┤ │ 去除顶点属性 │ ↓ 一些 │ ↓↓ 明显 │ │ (删Normal等) │ │ │ └────────────────┴──────────┴──────────┘ 想省包体 → Mesh Compression 想省运行时内存 → 去除不用的属性 ★ 两者可以结合用五、实战:如何去除顶点属性
1. 模型导入设置(ModelImporter)
选中模型 → Inspector: Normals: ├── Import:用模型自带法线 ├── Calculate:Unity计算 └── None:不要法线 ← 去除 Tangents: ├── Import/Calculate └── None:不要切线 ← 去除(不用法线贴图时) Blend Shapes:不用可关 Optimize Mesh:优化顶点顺序(GPU缓存友好)2. 代码检查网格的属性
Meshmesh=...;Debug.Log($"顶点数:{mesh.vertexCount}");Debug.Log($"有法线:{mesh.normals.Length>0}");Debug.Log($"有切线:{mesh.tangents.Length>0}");Debug.Log($"有UV2:{mesh.uv2.Length>0}");Debug.Log($"有顶点色:{mesh.colors.Length>0}");// 估算每顶点大小3. AssetPostprocessor 自动化
publicclassMeshImporter:AssetPostprocessor{voidOnPreprocessModel(){varimporter=assetImporterasModelImporter;// 场景道具默认不要切线(省16字节/顶点)if(assetPath.Contains("/Props/")){importer.importTangents=ModelImporterTangents.None;}// 优化网格importer.optimizeMeshPolygons=true;importer.optimizeMeshVertices=true;}}六、决策速查表
根据物体类型决定保留哪些属性:
┌──────────────┬────┬────┬────┬────┬────┬─────┐ │ 物体类型 │Pos │Norm│Tang│UV0 │UV1 │Color│ ├──────────────┼────┼────┼────┼────┼────┼─────┤ │ 受光普通模型 │ ✅ │ ✅ │ ? │ ✅ │ ? │ ? │ │ 用法线贴图 │ ✅ │ ✅ │ ✅ │ ✅ │ ? │ ? │ │ 静态烘焙光照 │ ✅ │ ✅ │ ? │ ✅ │ ✅ │ ? │ │ 纯色/自发光 │ ✅ │ ? │ ❌ │ ? │ ❌ │ ? │ │ UI/2D精灵 │ ✅ │ ❌ │ ❌ │ ✅ │ ❌ │ ? │ │ 简单场景道具 │ ✅ │ ✅ │ ❌ │ ✅ │ ❌ │ ❌ │ └──────────────┴────┴────┴────┴────┴────┴─────┘ ✅需要 ❌可去 ?看需求 最常见的优化: 1. 去Tangent(没用法线贴图) ← 收益大(16字节) 2. 去UV2(不烘焙光照) 3. 去Color(不用顶点色)七、常见误区
| 误区 | 真相 |
|---|---|
| “Mesh Compression能省运行时内存” | 主要省包体,运行时通常解压 |
| “所有属性都留着保险” | 每个属性都占内存,用不到就浪费 |
| “去了法线也能光照” | 不行,法线是光照基础 |
| “Tangent必须有” | 只有法线贴图才需要,很多物体不用 |
| “Compression High随便用” | 精度损失可能导致模型抖动/变形 |
| “UV1和UV0一样” | UV1多用于lightmap,需求不同 |
八、核心要点总结
顶点数据精简两条路: 【路径1:去除不用的属性】(省运行时内存 ★) 一个顶点含多种属性,各自的去留: ├── Position:必须留 ├── Normal:受光就留,不受光可去 ├── Tangent:用法线贴图才留 ← 常见可去(16字节) ├── UV0:有贴图就留 ├── UV1/UV2:烘焙光照才留 ← 常见可去 ├── Color:用顶点色才留 ← 常见可去 └── BoneWeight:蒙皮动画才有 【路径2:Mesh Compression】(省包体) 降低保留属性的数值精度(Off/Low/Med/High) ├── 省磁盘/传输 ├── 运行时通常解压,内存收益有限 └── 压太狠会变形/抖动,精细模型慎用High 关键区分: ├── 省内存 → 去属性 ├── 省包体 → Compression └── 两者可结合 实战: ├── ModelImporter设置Normals/Tangents为None ├── AssetPostprocessor自动化 └── 最常见收益:去Tangent + 去UV2 + 去Color一句话:
一个顶点携带位置、法线、切线、UV、顶点色等多种属性,每种都占字节。去除用不到的属性(如没用法线贴图就去切线、不烘焙光照就去UV2)能直接减小运行时内存;而Mesh Compression是降低数值精度,主要减小包体,运行时内存收益有限——两者针对不同目标,可结合使用。