TMP 文本样式笔记 1. 项目里的 TMPEffectHelper 这个项目里的 TMPEffectHelper 是一个挂在 TextMeshProUGUI 上的文本特效组件,主要负责:
它的核心思路是:把文本视觉效果从 LeanText 中拆出来,独立管理。
原来的职责大概混在一起:
1 2 3 4 LeanText -> 语言 -> 字体 -> 描边
现在更像是:
1 2 3 4 5 6 7 LeanText -> 语言 -> 字体 TMPEffectHelper -> 描边 -> 阴影
LeanText 在字体加载或切换完成后,如果文本上挂了 TMPEffectHelper,就调用:
1 tmpEffectHelper.Apply();
重新应用描边和阴影效果。
2. TMPEffectHelper 的大致结构 简化结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 [ExecuteAlways] [RequireComponent(typeof(TextMeshProUGUI))] public class TMPEffectHelper : MonoBehaviour { public bool overrideOutline = true; public Color outlineColor = Color.black; public float outlineWidth = 0.1f; public bool isOuterStroke = true; public float faceDilate = 0f; public bool overrideUnderlay = true; public Color underlayColor = new Color(0, 0, 0, 0.9f); public float underlayOffsetX = 0.8f; public float underlayOffsetY = -0.8f; public float underlayDilate = 0f; public float underlaySoftness = 0.1f; private TextMeshProUGUI _text; private void OnEnable() => Apply(); }
这些参数可以直接在 Prefab / Inspector 上调整。
3. 它使用的 TMP 技术 TMPEffectHelper 使用的是 TextMeshPro 的 SDF 材质能力。
描边:TMP Outline 描边使用 TMP Shader 里的 Outline 参数:
1 2 3 ShaderUtilities.ID_OutlineColor ShaderUtilities.ID_OutlineWidth ShaderUtilities.Keyword_Outline
对应效果:
1 2 3 _OutlineColor:描边颜色 _OutlineWidth:描边宽度 Keyword_Outline:启用描边功能
阴影:TMP Underlay 阴影使用 TMP Shader 里的 Underlay 参数:
1 2 3 4 5 6 ShaderUtilities.ID_UnderlayColor ShaderUtilities.ID_UnderlayOffsetX ShaderUtilities.ID_UnderlayOffsetY ShaderUtilities.ID_UnderlayDilate ShaderUtilities.ID_UnderlaySoftness ShaderUtilities.Keyword_Underlay
对应效果:
1 2 3 4 5 6 _UnderlayColor:阴影颜色 _UnderlayOffsetX:阴影 X 偏移 _UnderlayOffsetY:阴影 Y 偏移 _UnderlayDilate:阴影膨胀 _UnderlaySoftness:阴影柔和度 Keyword_Underlay:启用阴影功能
注意:这里的“阴影”不是 Unity UI 的 Shadow 组件,而是 TMP Shader 自带的 Underlay 效果。
4. Apply 的核心逻辑 Apply() 的核心流程可以理解为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public void Apply() { if (_text == null) _text = GetComponent<TextMeshProUGUI>(); if (_text == null || _text.fontSharedMaterial == null) return; if (overrideOutline) { _text.SetOutlineColor(outlineColor); _text.SetOutlineWidth(outlineWidth); if (isOuterStroke) _text.SetFaceDilate(faceDilate); else _text.SetFaceDilate(-outlineWidth); } else { _text.DisableOutline(); _text.SetOutlineWidth(0f); _text.SetFaceDilate(0f); } if (overrideUnderlay) { _text.SetUnderlayColor(underlayColor); _text.SetUnderlayOffset(underlayOffsetX, underlayOffsetY); _text.SetUnderlayDilate(underlayDilate); _text.SetUnderlaySoftness(underlaySoftness); } else { _text.DisableUnderlay(); } _text.UpdateMeshPadding(); _text.SetMaterialDirty(); _text.SetVerticesDirty(); _text.RecalculateMasking(); }
这里有几个重点:
overrideOutline 控制是否接管描边。
overrideUnderlay 控制是否接管阴影。
isOuterStroke 控制外描边/偏内描边。
UpdateMeshPadding() 用来避免描边或阴影被裁切。
SetMaterialDirty()、SetVerticesDirty() 用来通知 TMP 刷新渲染。
5. 写入 TMP 材质参数 描边宽度的写法类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void SetOutlineWidth(this TextMeshProUGUI text, float width) { Material mat = text.fontMaterial; if (mat.HasProperty(ShaderUtilities.ID_OutlineWidth)) { mat.SetFloat(ShaderUtilities.ID_OutlineWidth, width); mat.EnableKeyword(ShaderUtilities.Keyword_Outline); } text.UpdateMeshPadding(); text.SetMaterialDirty(); }
描边颜色类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 public static void SetOutlineColor(this TextMeshProUGUI text, Color color) { Material mat = text.fontMaterial; if (mat.HasProperty(ShaderUtilities.ID_OutlineColor)) { mat.SetColor(ShaderUtilities.ID_OutlineColor, color); mat.EnableKeyword(ShaderUtilities.Keyword_Outline); } text.UpdateMeshPadding(); text.SetMaterialDirty(); }
阴影颜色类似:
1 2 3 4 5 6 7 8 9 10 11 12 public static void SetUnderlayColor(this TextMeshProUGUI text, Color color) { Material mat = text.fontMaterial; if (mat.HasProperty(ShaderUtilities.ID_UnderlayColor)) { mat.SetColor(ShaderUtilities.ID_UnderlayColor, color); mat.EnableKeyword(ShaderUtilities.Keyword_Underlay); } text.SetMaterialDirty(); }
阴影偏移类似:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public static void SetUnderlayOffset( this TextMeshProUGUI text, float offsetX, float offsetY) { Material mat = text.fontMaterial; if (mat.HasProperty(ShaderUtilities.ID_UnderlayOffsetX) && mat.HasProperty(ShaderUtilities.ID_UnderlayOffsetY)) { mat.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, offsetX); mat.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, offsetY); mat.EnableKeyword(ShaderUtilities.Keyword_Underlay); } text.UpdateMeshPadding(); text.SetMaterialDirty(); }
6. 配置驱动样式 项目里有活动标题样式配置,例如 wxcc.style。
格式是:
1 fontColor|outlineColor|outlineWidth*10000|underlayColor|underlayDilate*10000
示例:
1 #FFFFFF|#1A4684|3000|#913A0D|3000
含义是:
1 2 3 4 5 字体颜色:#FFFFFF 描边颜色:#1A4684 描边宽度:3000 / 10000 = 0.3 阴影颜色:#913A0D 阴影大小:3000 / 10000 = 0.3
业务中应用时大概是:
1 2 3 4 5 6 7 8 9 titleLabel.color = data.StyleFontColor.Value; effectHelper.outlineColor = data.StyleOutlineColor.Value; effectHelper.outlineWidth = data.StyleOutlineWidth; effectHelper.underlayColor = data.StyleUnderlayColor.Value; effectHelper.underlayDilate = data.StyleUnderlayDilate; effectHelper.Apply();
这个链路是:
1 2 3 4 5 配置字符串 -> 解析成颜色和数值 -> 写入 TMPEffectHelper -> Apply -> 修改 TMP 材质参数
7. 这个项目方案的优点 TMPEffectHelper 的优点:
把描边、阴影从 LeanText 中拆出来,职责更清晰。
使用 TMP 原生 SDF 材质能力,不需要复制多个 Text 节点。
Prefab 上能直观看到文本特效参数。
支持少量配置驱动文本样式。
调用了 UpdateMeshPadding(),考虑了描边/阴影被裁切的问题。
适合使用在:
1 2 3 4 活动标题 重点按钮 特殊标题 少量需要配置驱动的文本
8. 当前实现的隐患 当前实现的风险主要来自:
1 Material mat = text.fontMaterial;
text.fontMaterial 可能会让 TMP 为当前文本创建一个独立材质实例。
如果大量文本都这样做,可能导致:
1 2 3 4 5 材质实例数量增加 相同样式无法复用 Canvas 合批变差 Draw Call 增加 内存压力增加
准确地说,这不是 Shader 实例爆炸,而是 Material instance 变多。
所以当前 TMPEffectHelper 更适合少量特殊文本,不适合无脑挂满所有普通文本。
一般工程的 TMP 文本样式处理 9. 一般工程的思路 一般项目不会让每个文本自由设置描边、阴影参数,而是先约定一套统一文本样式。
例如:
1 2 3 4 5 6 Title_Gold Button_White_BlackOutline Desc_Normal Warning_Red Quality_Purple BattleDamage_Crit
业务代码或配置只选择样式:
1 TextStyle.Apply(text, TextStyleId.Title_Gold);
而不是散写:
1 2 3 描边色 #1A4684 描边宽 0.3 阴影色 #913A0D
这样可以保证:
风格统一
材质数量可控
美术和程序沟通成本低
后期改版方便
性能更可预期
10. 背后的核心技术:SDF 字体 TextMeshPro 的核心技术是 SDF 字体。
SDF 全称是 Signed Distance Field,可以简单理解为:
1 2 字体贴图中记录的不是单纯的“有没有字”, 而是“当前像素距离字形边缘有多远”。
因为 Shader 知道距离边缘的远近,所以可以很方便地做:
TMP 的这些效果都依赖 SDF 材质系统:
1 2 3 4 5 Outline:描边 Underlay:阴影/底影 Glow:发光 Face Dilate:字面膨胀或收缩 Softness:柔边
11. 常见方案一:TMP Material Preset 固定样式通常使用 TMP Material Preset。
它是在 Unity 编辑器里提前做好的 TMP 材质预设。
例如同一个中文字体,可以提前做几份材质:
1 2 3 4 Chinese_White_BlackOutline.mat Chinese_Gold_BrownOutline.mat Chinese_Red_Shadow.mat Chinese_Normal.mat
Prefab 上的 TextMeshProUGUI 直接引用这些材质。
优点:
不需要运行时创建材质。
美术可以直接调整材质参数。
材质数量可控。
合批和性能更可预期。
适合大量固定 UI 文本。
适合:
1 2 3 4 5 按钮文字 普通标题 通用描述 奖励数量 品质颜色
12. 常见方案二:Runtime Material Cache 如果样式来自活动配置,不能全部提前做好,就可以运行时生成材质,但要缓存。
核心思路是:用样式参数组成 key。
1 font + faceColor + outlineColor + outlineWidth + underlayColor + underlayOffset + softness
相同 key 只创建一份材质。
示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public static Material GetMaterial(TMP_FontAsset font, TextStyle style) { string key = BuildKey(font, style); if (cache.TryGetValue(key, out Material cached)) return cached; Material mat = new Material(font.material); if (style.useOutline) { mat.SetColor(ShaderUtilities.ID_OutlineColor, style.outlineColor); mat.SetFloat(ShaderUtilities.ID_OutlineWidth, style.outlineWidth); mat.EnableKeyword(ShaderUtilities.Keyword_Outline); } if (style.useUnderlay) { mat.SetColor(ShaderUtilities.ID_UnderlayColor, style.underlayColor); mat.SetFloat(ShaderUtilities.ID_UnderlayOffsetX, style.underlayOffsetX); mat.SetFloat(ShaderUtilities.ID_UnderlayOffsetY, style.underlayOffsetY); mat.SetFloat(ShaderUtilities.ID_UnderlayDilate, style.underlayDilate); mat.SetFloat(ShaderUtilities.ID_UnderlaySoftness, style.underlaySoftness); mat.EnableKeyword(ShaderUtilities.Keyword_Underlay); } cache[key] = mat; return mat; }
应用时:
1 2 3 4 text.color = style.faceColor; text.fontSharedMaterial = TextStyleManager.GetMaterial(text.font, style); text.UpdateMeshPadding(); text.SetMaterialDirty();
这里重点是使用:
1 text.fontSharedMaterial = cachedMaterial;
而不是:
1 text.fontMaterial.SetFloat(...)
前者复用材质,后者容易让每个文本生成自己的材质实例。
13. 推荐的工程结构 比较稳的结构是:
1 2 3 4 TextStyleId -> TextStyleManager -> TMP Material Preset / Runtime Material Cache -> TextMeshProUGUI
固定样式:
1 2 TextStyleId -> 直接映射到 TMP Material Preset
动态样式:
1 2 3 4 配置参数 -> 解析成 TextStyle -> Runtime Material Cache -> 复用材质
普通颜色变化:
特殊文本:
14. 总结 这个项目里的 TMPEffectHelper 做的是:
1 2 3 把描边、阴影组件化 通过 TMP Outline / Underlay 修改字体材质参数 支持 Prefab 配置和少量活动配置驱动
它的方向是对的,适合管理少量重点文本效果。
但当前实现直接修改:
如果大规模使用,可能导致材质实例过多。
一般工程更推荐:
1 2 3 4 固定样式用 TMP Material Preset 动态样式用 Runtime Material Cache 业务层只选择 TextStyleId 普通颜色只改 text.color
最终目标是: