欢迎光临
我们一直在努力

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

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


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

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

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

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

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 设置,其类型定义如下:

public enum TextureMappingOptions
{
Character = 0,
Line = 1,
Paragraph = 2,
MatchAspect = 3
};

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


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

Character 模式

如图所示:

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

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

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 模式:以行为单位

请添加图片描述

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 的生成逻辑

// 更新当前行的包围盒
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 模式:以段落为单位

在这里插入图片描述

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 的计算

// 遍历所有字符,更新全局包围盒
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,绝不会让贴图变形。

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] 区间。

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 编码方法

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 缩放、粗体标识、字符宽度调整)

五、结语

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

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


参考资料:

  • Unity TextMeshPro 源码(v3.0.9)
赞(0)
未经允许不得转载:171主机测评 » Unity TMP_SDF 分析(二)数据来源2
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址