Unity TMP_SDF 分析(二)数据来源2

前一篇文章讲了uv.y的数据来源,本篇来看看uv.x的数据来源


一、uv2 的结构:每个字符的四个顶点

在 TMP 的顶点生成过程中,每个字符由四个顶点构成:

  • 左下(vertex_BL
  • 左上(vertex_TL
  • 右上(vertex_TR
  • 右下(vertex_BR

uv2 存储在这四个顶点中,用于后续 Shader 中的效果控制。

csharp 复制代码
public partial class TextMeshProUGUI
{
    protected virtual void GenerateTextMesh()
    {
        #region Handle UV Mapping Options
        switch (m_horizontalMapping)
        {
            case TextureMappingOptions.Line:
                characterInfos[i].vertex_BL.uv2.x = 0; // 示例,实际根据不同模式计算
                // ...
        }
        #endregion
    }
}

uv2 分别存放每个字符四个顶点的坐标,用来实现逐字、逐行、段落级的视觉效果。


二、TextureMappingOptions:四种映射模式

uv2 的计算方式取决于 m_horizontalMapping 设置,其类型定义如下:

csharp 复制代码
public enum TextureMappingOptions 
{ 
    Character = 0, 
    Line = 1, 
    Paragraph = 2, 
    MatchAspect = 3 
};

不同模式下,uv2.x 的归一化范围不同,从而实现不同的视觉效果。


1. Character 模式:以字符为单位

如图所示:

  • 字符 A:uv2.BL = (0,0)uv2.TR = (1,1)
  • 字符 g:uv2.BL = (0,0)uv2.TR = (1,1)

✅ 所有字符都拥有相同的 UV 范围,彼此独立。

csharp 复制代码
case TextureMappingOptions.Character:
    characterInfos[i].vertex_BL.uv2.x = 0; // + m_uvOffset.x;
    characterInfos[i].vertex_TL.uv2.x = 0; // + m_uvOffset.x;
    characterInfos[i].vertex_TR.uv2.x = 1; // + m_uvOffset.x;
    characterInfos[i].vertex_BR.uv2.x = 1; // + m_uvOffset.x;
    break;
  • 每个字符的 uv2.x 独立归一化到 [0,1]
  • 适用于逐字动画、颜色渐变等效果

2. Line 模式:以行为单位

csharp 复制代码
case TextureMappingOptions.Line:
    if (m_textAlignment != TextAlignmentOptions.Justified)
    {
        characterInfos[i].vertex_BL.uv2.x = (characterInfos[i].vertex_BL.position.x - lineExtents.min.x) / (lineExtents.max.x - lineExtents.min.x) + uvOffset;
        characterInfos[i].vertex_TL.uv2.x = (characterInfos[i].vertex_TL.position.x - lineExtents.min.x) / (lineExtents.max.x - lineExtents.min.x) + uvOffset;
        characterInfos[i].vertex_TR.uv2.x = (characterInfos[i].vertex_TR.position.x - lineExtents.min.x) / (lineExtents.max.x - lineExtents.min.x) + uvOffset;
        characterInfos[i].vertex_BR.uv2.x = (characterInfos[i].vertex_BR.position.x - lineExtents.min.x) / (lineExtents.max.x - lineExtents.min.x) + uvOffset;
        break;
    }
    else // 两端对齐(Justified)特殊处理
    {
        characterInfos[i].vertex_BL.uv2.x = (characterInfos[i].vertex_BL.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
        characterInfos[i].vertex_TL.uv2.x = (characterInfos[i].vertex_TL.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
        characterInfos[i].vertex_TR.uv2.x = (characterInfos[i].vertex_TR.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
        characterInfos[i].vertex_BR.uv2.x = (characterInfos[i].vertex_BR.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
        break;
    }
lineExtents 的生成逻辑
csharp 复制代码
// 更新当前行的包围盒
m_textInfo.lineInfo[currentLine].lineExtents.min = new Vector2(
    m_textInfo.characterInfo[m_textInfo.lineInfo[currentLine].firstCharacterIndex].bottomLeft.x,
    m_textInfo.lineInfo[currentLine].descender
);

m_textInfo.lineInfo[currentLine].lineExtents.max = new Vector2(
    m_textInfo.characterInfo[m_textInfo.lineInfo[currentLine].lastVisibleCharacterIndex].topRight.x,
    m_textInfo.lineInfo[currentLine].ascender
);
  • X 范围 :从行首字符的 bottomLeft 到行尾字符的 topRight
  • Y 范围 :从 descender(降部)到 ascender(升部)

形成一个包围盒 abde

Line 模式适用于整行统一效果,如波浪、扫描、渐变。


3. Paragraph 模式:以段落为单位

csharp 复制代码
case TextureMappingOptions.Paragraph:
    characterInfos[i].vertex_BL.uv2.x = (characterInfos[i].vertex_BL.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
    characterInfos[i].vertex_TL.uv2.x = (characterInfos[i].vertex_TL.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
    characterInfos[i].vertex_TR.uv2.x = (characterInfos[i].vertex_TR.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
    characterInfos[i].vertex_BR.uv2.x = (characterInfos[i].vertex_BR.position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset;
    break;
  • 使用 m_meshExtents(整个文本的包围盒)进行归一化
  • 适用于多行文本的整体效果控制

m_meshExtents 的计算
csharp 复制代码
// 遍历所有字符,更新全局包围盒
if (m_textInfo.characterInfo[m_characterCount].isVisible)
{
    m_meshExtents.min.x = Mathf.Min(m_meshExtents.min.x, m_textInfo.characterInfo[m_characterCount].bottomLeft.x);
    m_meshExtents.min.y = Mathf.Min(m_meshExtents.min.y, m_textInfo.characterInfo[m_characterCount].bottomLeft.y);
    
    m_meshExtents.max.x = Mathf.Max(m_meshExtents.max.x, m_textInfo.characterInfo[m_characterCount].topRight.x);
    m_meshExtents.max.y = Mathf.Max(m_meshExtents.max.y, m_textInfo.characterInfo[m_characterCount].topRight.y);
}
问题 回答
m_meshExtents 是什么? 整个文本的总包围盒
lineExtents 是什么? 每行的包围盒
X 范围区别? mesh 是全局最值,line 是行内首尾字符
Y 范围区别? linedescender/ascendermesh 用实际顶点
用途区别? line 用于行级效果,mesh 用于全局效果

4. MatchAspect 模式:匹配宽高比

它和 Paragraph 一样,也会把所有的多行文字(包括逗号、换行)视为一个整体的包围盒,但它的核心区别在于:它强制保持 UV 的 X 轴和 Y 轴缩放比例为 1:1,绝不会让贴图变形。

csharp 复制代码
case TextureMappingOptions.MatchAspect:
    switch (m_verticalMapping)
    {
        case TextureMappingOptions.Character:
            characterInfos[i].vertex_BL.uv2.y = 0; // + m_uvOffset.y;
            characterInfos[i].vertex_TL.uv2.y = 1; // + m_uvOffset.y;
            characterInfos[i].vertex_TR.uv2.y = 0; // + m_uvOffset.y;
            characterInfos[i].vertex_BR.uv2.y = 1; // + m_uvOffset.y;
            break;

        case TextureMappingOptions.Line:
            characterInfos[i].vertex_BL.uv2.y = (characterInfos[i].vertex_BL.position.y - lineExtents.min.y) / (lineExtents.max.y - lineExtents.min.y) + uvOffset;
            characterInfos[i].vertex_TL.uv2.y = (characterInfos[i].vertex_TL.position.y - lineExtents.min.y) / (lineExtents.max.y - lineExtents.min.y) + uvOffset;
            characterInfos[i].vertex_TR.uv2.y = characterInfos[i].vertex_BL.uv2.y;
            characterInfos[i].vertex_BR.uv2.y = characterInfos[i].vertex_TL.uv2.y;
            break;

        case TextureMappingOptions.Paragraph:
            characterInfos[i].vertex_BL.uv2.y = (characterInfos[i].vertex_BL.position.y - m_meshExtents.min.y) / (m_meshExtents.max.y - m_meshExtents.min.y) + uvOffset;
            characterInfos[i].vertex_TL.uv2.y = (characterInfos[i].vertex_TL.position.y - m_meshExtents.min.y) / (m_meshExtents.max.y - m_meshExtents.min.y) + uvOffset;
            characterInfos[i].vertex_TR.uv2.y = characterInfos[i].vertex_BL.uv2.y;
            characterInfos[i].vertex_BR.uv2.y = characterInfos[i].vertex_TL.uv2.y;
            break;

        case TextureMappingOptions.MatchAspect:
            Debug.Log("ERROR: Cannot Match both Vertical & Horizontal.");
            break;
    }
  • 根据垂直映射方式(m_verticalMapping)决定 uv2.y 的归一化范围
  • 用于保持 SDF 纹理的各向同性,避免拉伸模糊

三、UV 归一化与 Pack 编码

在计算完原始 uv2 后,TMP 会对其进行归一化处理,确保即使原始坐标超过 1,也能映射到 [0,1] 区间。

csharp 复制代码
float x0 = characterInfos[i].vertex_BL.uv2.x;
float y0 = characterInfos[i].vertex_BL.uv2.y;
float x1 = characterInfos[i].vertex_TR.uv2.x;
float y1 = characterInfos[i].vertex_TR.uv2.y;

float dx = (int)x0;
float dy = (int)y0;

x0 = x0 - dx;
x1 = x1 - dx;
y0 = y0 - dy;
y1 = y1 - dy;

// 将归一化后的 UV 打包进一个 float
characterInfos[i].vertex_BL.uv2.x = PackUV(x0, y0); characterInfos[i].vertex_BL.uv2.y = xScale;
characterInfos[i].vertex_TL.uv2.x = PackUV(x0, y1); characterInfos[i].vertex_TL.uv2.y = xScale;
characterInfos[i].vertex_TR.uv2.x = PackUV(x1, y1); characterInfos[i].vertex_TR.uv2.y = xScale;
characterInfos[i].vertex_BR.uv2.x = PackUV(x1, y0); characterInfos[i].vertex_BR.uv2.y = xScale;

PackUV 编码方法

csharp 复制代码
public abstract class TMP_Text : MaskableGraphic
{
    protected float PackUV(float x, float y)
    {
        double x0 = (int)(x * 511);
        double y0 = (int)(y * 511);
        return (float)((x0 * 4096) + y0);
    }
}

编码原理:

  • xy 归一化到 [0,1]
  • 乘以 511 → 得到 [0,511] 的整数(9 位精度)
  • x0 * 4096 + y04096 = 2^12,因此 x 占高 9 位,y 占低 9 位
  • 最终打包为一个 float,在 Shader 中通过 frac()floor() 解包

✅ 这种编码方式极大节省了顶点数据量,是 TMP 高效渲染的关键设计之一。


四、总结

映射模式 归一化范围 适用场景
Character 每个字符独立 [0,1] 逐字动画、独立效果
Line 当前行内归一化 行级波浪、扫描
Paragraph 全文本归一化 段落级渐变、扭曲
MatchAspect 匹配字体图集宽高比 保持 SDF 清晰度

uv2 的完整用途:

  • uv2.x:归一化位置(由映射模式决定)
  • uv2.yxScale(SDF 缩放、粗体标识、字符宽度调整)

以下是这 4 个模式 UV 计算公式的简要总结:

1. Character 模式(字符基准)

每个字符独立映射,不论字符多大,贴图都在单个字符内部完整铺满一遍。

  • 计算公式固定值分配 。直接将顶点的 UV 强制写死为 01
    • 左下/左上顶点:UV = 0 + uvOffset
    • 右下/右上顶点:UV = 1 + uvOffset

2. Line 模式(行基准)

以当前文本行的范围(lineExtents)为基准,贴图在每一行横向/纵向上完整铺满一遍。

  • 计算公式当前顶点在行内的相对比例
    • UV = (当前顶点坐标 - 当前行最小值) / 当前行总尺寸 + uvOffset
  • X轴示例
    UV.x = (Position.x - lineExtents.min.x) / (lineExtents.max.x - lineExtents.min.x) + uvOffset

3. Paragraph 模式(段落整体基准)

以整个文本生成的总包围盒(m_meshExtents)为基准,贴图把多行文本作为一个大整体,只从头到尾铺满一遍。

  • 计算公式当前顶点在整体文本中的相对比例 (会额外考虑排版的对齐偏移 justificationOffset)。
    • UV = (当前顶点坐标 + 对齐偏移 - 整体最小值) / 整体总尺寸 + uvOffset
  • X轴示例
    UV.x = (Position.x + justificationOffset.x - m_meshExtents.min.x) / (m_meshExtents.max.x - m_meshExtents.min.x) + uvOffset

4. MatchAspect 模式(防拉伸等比基准)

没有独立的基础计算公式,它的计算必须**"寄生"**在另一个轴的设定上,核心目的是保证 UV 的 X 和 Y 比例为 1:1,防止贴图拉伸变形。

  • 计算公式根据另一轴的模式套用公式 -> 等比反推
    1. 如果水平轴设为 MatchAspect,代码会先去检查垂直轴 的映射模式(是 CharacterLine 还是 Paragraph)。
    2. 套用前三种对应的公式,先把垂直轴的 UV.y 算出来。
    3. 最后基于物理宽高的实际比例,等比推导出水平轴的 UV.x(以避免拉伸)。
  • (注:两轴不能同时设为 MatchAspect,否则会产生死循环并触发 Error)

五、结语

TMP 通过复用 uv2 通道,实现了顶点数据的极致压缩渲染控制的灵活扩展

提示 :在 Shader 中使用 uv2 时,记得先调用 UnpackUV() 解码,再根据需求实现效果。


参考资料

  • Unity TextMeshPro 源码(v3.0.9)
相关推荐
Mao_Hui2 小时前
Unity3d实时读取Modbus RTU数据
开发语言·嵌入式硬件·unity·c#
相信神话20212 小时前
《酒魂》游戏开发实战——从设计思想到 Godot 实现(单机完整版)
游戏引擎·godot
心疼你的一切5 小时前
【Unity-MCP完全指南:从零开始构建AI游戏开发助手】
人工智能·unity·ai·游戏引擎·aigc·mcp
示申○言舌6 小时前
基于知识库(RAG)系统打造由大模型(LLM)驱动NPC游戏的技术设想
游戏·unity·大模型·知识库·rag·智能npc·npc记忆
前端不太难10 小时前
OpenClaw 如何运行 Claw 资源文件
c++·开源·游戏引擎
avi911111 小时前
UnReal-UE5虚幻蓝图如何修改
ue5·游戏引擎·虚幻·虚幻引擎·ue·蓝图·蓝图逻辑
国家一级摸鱼选手1 天前
MCP(Model Context Protocol)学习笔记
unity·ai·mcp
会思考的猴子1 天前
Unity3D发布后软件界面右下角出现Trial Version
unity
ellis19701 天前
Unity资源管理框架Addressables[五] 构建
unity