解释TMP的SubMesh与材质实例化
解读
面试官抛出这个问题,通常不是想听“SubMesh 就是把字拆成几块”这种表面答案,而是考察候选人是否真正踩过 中文混排 + 表情 + 特效 + 动态字体 的坑,是否理解 DrawCall、内存、合批、热更新 在 TMP 管线里的连锁反应。国内项目普遍要求 iOS 低端机 60 FPS + 热更新不重启,如果 TMP 材质实例化策略不对,一次聊天弹窗就能让 DrawCall 暴涨到 100+,直接触发 GC 卡顿,面试时能把这一层讲清楚,才能体现“资深”二字。
知识点
- TMP 的 Mesh 生成管线:Text → Glyph → Quad → SubMesh
- SubMesh 的划分规则:材质槽位差异 触发拆分,同一槽位内连续 Quad 自动合并
- 材质槽位来源:
- 默认材质(Font Asset 指定)
- 富标签 <material> 索引
- 精灵标签 <sprite> 索引
- 颜色渐变 <gradient> 产生的临时材质
- 材质实例化时机:
- 首次解析到富标签 → TMP 自动 Material.Instantiate
- 运行时动态修改颜色/贴图 → 再次实例化
- DontDestroyOnLoad 的 Canvas 忘记清理 → 泄漏到下一次场景
- 合批条件:同一 Texture + 同一 Material 实例 + 同一 Shader 参数
- 国内常见坑:
- 美术把表情打成 512 张散图 → 每个表情一个 SubMesh,DrawCall 爆炸
- 策划在聊天配置里写
<material=1>但材质槽 1 为空 → TMP 回退实例化默认材质,产生 隐藏的一份冗余材质 - 热更新 DLL 里动态创建 FontAsset 并替换默认材质 → 旧材质实例残留在 CanvasRenderer 的额外材质数组,导致 “字体替换了但颜色还残留”
答案
TMP 的 SubMesh 是 在同一个 TextMeshProUGUI 组件里,按“材质槽位”划分的连续三角形块。生成逻辑是:
- 解析文本时,每遇到一个 <material=index> 或 <sprite=index> 标签,就切换一次材质槽位;
- 网格构建阶段,把相同槽位的连续 Quad 写进同一段索引缓冲区,形成独立 SubMesh;
- 最终提交渲染时,每个 SubMesh 对应一次 Material 实例的 DrawCall。
材质实例化发生在 第一次遇到非默认槽位 时:TMP 会 Material.Instantiate(Font Asset 的默认材质),并把新实例塞进 TMP_SubMeshInfo.material,保证 修改颜色、贴图、Stencil 参数 时不会影响原资源。
如果运行时继续动态修改该实例(例如代码里改颜色),不会再次实例化;但若重新赋值一个全新的 Material,TMP 会 再次 Instantiate,导致 内存与 DrawCall 双增。
国内项目要减少实例化带来的开销,有三条实践经验:
- 提前在 Font Asset 里配好所有材质槽位,避免运行时动态插值;
- 把表情合并到一张 2048 图集,用 <sprite> 索引调用,确保所有 SubMesh 共享同一 Texture,可被 SRP Batcher 合批;
- 场景卸载时主动调用 TMP_Text.DestroyMaterialInstances(),防止 DontDestroyOnLoad 的聊天窗把材质带到下一场景,造成 “字体已卸载但材质泄漏” 的闪退。
一句话总结:SubMesh 是 TMP 为了多材质需求自动拆分的网格段,而每一次拆分都伴随一次材质实例化;只有让“槽位数量 × 材质实例数量”最小化,才能把低端机的 DrawCall 压到个位数。
拓展思考
- 如果策划要求 “聊天超链接hover 时变色”,你会在 TMP 的哪一层做?
提示:hover 只改颜色不改材质槽位,可直接 material.SetColor("_FaceColor", …),不会触发新实例化;但若为了做 外发光 而切换 Shader,则必须 预先在 Font Asset 里放两份材质槽位,否则运行时 Instantiate 会打破合批。 - 在 Addressables 热更新 场景,FontAsset 与贴图都走远程加载,如何保证 旧材质实例不残留?
思路:监听 Addressables.Release 时,手动遍历 TMP_SubMeshInfo[],对 material.referenceCount == 0 的实例执行 DestroyImmediate,并 清空 CanvasRenderer 的 additionalMaterials 数组,否则下一次加载新字体时会出现 “紫块” 或 “颜色错乱”。