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 范围区别? line 用 descender/ascender,mesh 用实际顶点
用途区别? 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);
    }
}

编码原理:

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

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


四、总结

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

uv2 的完整用途:

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

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

1. Character 模式(字符基准)

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

  • 计算公式 :固定值分配 。直接将顶点的 UV 强制写死为 0 或 1。
    • 左下/左上顶点: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,代码会先去检查垂直轴 的映射模式(是 Character、Line 还是 Paragraph)。
    2. 套用前三种对应的公式,先把垂直轴的 UV.y 算出来。
    3. 最后基于物理宽高的实际比例,等比推导出水平轴的 UV.x(以避免拉伸)。
  • (注:两轴不能同时设为 MatchAspect,否则会产生死循环并触发 Error)

五、结语

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

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


参考资料:

  • Unity TextMeshPro 源码(v3.0.9)
相关推荐
阿松爱学习6 小时前
【Unity开发】Aspose.PDF的使用
unity·pdf·c#
淡海水8 小时前
02-04-原理篇-GC Roots与对象图遍历
算法·unity·c#·图·gc·遍历·roots
辻弋2014 天前
五年前的旅行视频糊成马赛克?Video2X用Real-ESRGAN逐帧重建细节,但只支持Windows、集显用户建议直接放弃
服务器·数据库·windows·游戏引擎·电脑
Martina_03215 天前
AI生成3D场景导入 Unity/Unreal 后,NPC 总串层?用6步检查导航高度与跨层连接
人工智能·游戏·数学建模·3d·unity·自然语言处理·aigc
小贺儿开发5 天前
Unity 局域网遥控幻灯片展示工具 3.0
科技·http·unity·工具·ppt·通信·网页
WarPigs7 天前
Unity对象不应该使用null传播的问题
unity
每日出拳老爷子8 天前
【VR】【Unity】抓走转放系列21|一天搭完门和抽屉工具槽:小场景按这个次序拼装
unity·vr·驱动·排错·snap zone
淡海水8 天前
02-02-原理篇-分代GC
算法·unity·c#·游戏引擎·.net·gc
小贺儿开发9 天前
Unity 文物新生 会讲故事的文物展墙
人工智能·科技·unity·ai·视频·互动·演示
淡海水9 天前
02-03-原理篇-大对象堆
unity·c#·游戏引擎·.net·gc