前一篇文章讲了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,防止贴图拉伸变形。
- 计算公式 :根据另一轴的模式套用公式 -> 等比反推 。
- 如果水平轴设为
MatchAspect,代码会先去检查垂直轴 的映射模式(是Character、Line还是Paragraph)。 - 套用前三种对应的公式,先把垂直轴的
UV.y算出来。 - 最后基于物理宽高的实际比例,等比推导出水平轴的
UV.x(以避免拉伸)。
- 如果水平轴设为
- (注:两轴不能同时设为 MatchAspect,否则会产生死循环并触发 Error)
五、结语
TMP 通过复用 uv2 通道,实现了顶点数据的极致压缩 与渲染控制的灵活扩展。
提示 :在 Shader 中使用
uv2时,记得先调用UnpackUV()解码,再根据需求实现效果。
参考资料:
- Unity TextMeshPro 源码(v3.0.9)