Unity TMP_SDF 分析(三)顶点着色器中的屏幕空间缩放
Unity TMP_SDF 分析(三):顶点着色器中的屏幕空间缩放
适用版本:Unity
2021.3.23f1c1|TextMeshPro3.0.9|UGUI1.0.0分析范围:项目中的标准
Assets/TextMesh Pro/Shaders/TMP_SDF.shader及其关联源码;Mobile、SSD、Surface 等变体不直接套用。系列导航:第 3/9 篇|前置:纹理映射 UV 与 PackUV|下一篇:字重阈值、裁剪阈值与顶点输出
本篇分析VertShader()的前半部分:Bold 标记、顶点偏移、pixelSize、SDFscale以及透视过滤。Glow、Underlay、Bevel 和 UI 裁剪分别放在后续文章中。
一、相关源码
以下为TMP_SDF.shader顶点着色器的相关部分(节选):
pixel_t VertShader(vertex_t input) { pixel_t output; UNITY_INITIALIZE_OUTPUT(pixel_t, output); UNITY_SETUP_INSTANCE_ID(input); UNITY_TRANSFER_INSTANCE_ID(input, output); UNITY_INITIALIZE_VERTEX_OUTPUT_STEREO(output); float bold = step(input.texcoord1.y, 0); float4 vert = input.position; vert.x += _VertexOffsetX; vert.y += _VertexOffsetY; float4 vPosition = UnityObjectToClipPos(vert); float2 pixelSize = vPosition.w; pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy)); float scale = rsqrt(dot(pixelSize, pixelSize)); scale *= abs(input.texcoord1.y) * _GradientScale * (_Sharpness + 1); if (UNITY_MATRIX_P[3][3] == 0) { float viewDot = abs(dot( UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))); scale = lerp( abs(scale) * (1 - _PerspectiveFilter), scale, viewDot); } // 后续计算在第四篇继续分析。 }开头的 Unity 宏用于初始化输出结构,并传递实例 ID、立体渲染等平台相关数据,是 Shader 平台适配流程的一部分。
二、用texcoord1.y的符号识别模拟 Bold
float bold = step(input.texcoord1.y, 0);HLSL 的step(edge, x)在x >= edge时返回1,否则返回0。这里等价于:
input.texcoord1.y <= 0 → bold = 1 input.texcoord1.y > 0 → bold = 0根据第一篇的数据来源分析:
texcoord1.y的绝对值是 SDF 缩放量xScale。- 当字符需要由 Shader 模拟 Bold 时,C# 会把
xScale取负。 - 使用专用替代 Typeface 时通常不会通过负号再次模拟加粗。
step(..., 0)在输入恰好为0时也会返回1。正常字符的有效缩放量通常不为零;零缩放属于退化情况。
三、顶点偏移与裁剪空间位置
float4 vert = input.position; vert.x += _VertexOffsetX; vert.y += _VertexOffsetY; float4 vPosition = UnityObjectToClipPos(vert);input.position是对象空间中的文本顶点。Shader 先应用材质属性_VertexOffsetX/Y,再通过UnityObjectToClipPos()转换到裁剪空间。
这些参数的作用如下:
_VertexOffsetX/Y是 Shader 阶段施加的整体对象空间顶点偏移。- 偏移发生在对象空间,因此它会随对象的 Transform 和摄像机投影一起变换。
vPosition是齐次裁剪空间坐标;它还没有执行透视除法。
四、vPosition.w的准确含义
float2 pixelSize = vPosition.w;这行代码把标量vPosition.w同时赋给pixelSize.x和pixelSize.y。
vPosition.w是齐次裁剪空间的 W 分量:
- 在常见透视投影矩阵中,它与视空间深度成比例。
- 在常见正交投影中,它通常为常量。
- 它参与后续透视除法
clip.xy / clip.w,因此可以反映透视投影下随距离变化的屏幕尺寸。
离摄像机更远时,透视投影下的|w|通常更大,同样的对象空间长度覆盖的屏幕像素更少。
五、pixelSize表示什么
完整公式为:
float2 pixelSize = vPosition.w; pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));1._ScreenParams.xy
_ScreenParams.x/y是当前渲染目标的像素宽度和高度。它把投影尺度与实际屏幕分辨率联系起来。
2.(float2x2)UNITY_MATRIX_P
强制转换后的 2×2 部分包含投影矩阵对 X、Y 的主要缩放关系。对常规无偏斜投影,可近似理解为使用P00和P11分别修正水平、垂直投影尺度。
3._ScaleX与_ScaleY
它们是标准 TMP SDF Shader 的材质属性,默认值均为1,用于额外修正 X/Y 方向的 SDF 屏幕尺度。
在本项目的 TMP 3.0.9 源码中,它们作为 Shader 属性 ID 和材质面板参数使用。字符及对象的主要缩放信息通过texcoord1.y传入。
4. 最终物理意义
pixelSize表示一个屏幕像素对应的对象/投影空间长度估计量。
在其他条件不变时:
对象更近或显示更大 → pixelSize 更小 对象更远或显示更小 → pixelSize 更大 屏幕分辨率更高 → pixelSize 更小该公式使用 Shader 内部统一的常数约定,主要用于生成相对稳定的 SDF 屏幕缩放。
六、从pixelSize得到 SDFscale
float scale = rsqrt(dot(pixelSize, pixelSize));展开后为:
dot(pixelSize, pixelSize) = pixelSize.x² + pixelSize.y² rsqrt(value) = 1 / sqrt(value) scale = 1 / length(pixelSize)因此scale是pixelSize长度的倒数,更接近屏幕像素密度:
pixelSize 小 → scale 大 → 当前字形在屏幕上较大 pixelSize 大 → scale 小 → 当前字形在屏幕上较小七、字符缩放、Atlas 梯度与锐度
scale *= abs(input.texcoord1.y) * _GradientScale * (_Sharpness + 1);1.abs(input.texcoord1.y)
这是 C# 写入 UV2 的xScale绝对值。它补入当前字符、对象层级以及 UGUI Canvas 模式相关的缩放信息。负号只承担 Bold 标记,所以这里取绝对值。
2._GradientScale
_GradientScale描述字体 Atlas 中可用的 SDF 梯度范围。TMP 创建字体材质时通常会根据 Atlas Padding 写入该值;例如相关源码会使用atlasPadding + 1一类的换算。
Face Dilate、Outline、Glow、Underlay 等效果都依赖_GradientScale表示的有限距离范围;修改该值会同步改变这些效果的尺度关系。
3._Sharpness + 1
_Sharpness的 Shader 属性范围为[-1, 1]:
0:乘数为1,保持基础尺度。- 正值:增大
scale,令距离过渡更陡、更锐。 - 负值:减小
scale,令过渡更宽、更柔。
它调整距离场转换的斜率,Atlas 分辨率和纹理采样次数保持不变。
八、透视投影下的视角过滤
if (UNITY_MATRIX_P[3][3] == 0) { scale = lerp( abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot( UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert))))); }1. 投影类型判断
在 Unity 常见投影矩阵约定下:
UNITY_MATRIX_P[3][3] == 0 → 透视投影 UNITY_MATRIX_P[3][3] != 0 → 通常为正交投影因此视角过滤只在透视投影分支执行。
2. 法线与视线点积
viewDot = abs(dot(worldNormal, normalizedViewDirection));这里两者都在世界空间。其方向关系为:
正视文本平面:|N · V| 接近 1 掠射或侧视: |N · V| 接近 0abs()用于忽略文本正反面法线方向的符号。
3._PerspectiveFilter如何影响结果
令P = _PerspectiveFilter、T = |N · V|,则:
scale' = lerp(scale × (1 - P), scale, T)- 正视时
T ≈ 1:结果接近原始scale。 - 侧视时
T ≈ 0:结果接近scale × (1 - P)。 P = 0:两端相同,等于关闭视角修正。P越大:掠射角下的scale降低得越多,SDF 过渡更宽,可减少倾斜文本的闪烁和过锐锯齿。
侧视时该公式会减小距离斜率,实施更强的透视过滤。
九、数据关系总结
对象空间顶点 └─ _VertexOffsetX/Y └─ UnityObjectToClipPos() ├─ vPosition → SV_POSITION └─ vPosition.w └─ 投影矩阵 × 屏幕尺寸 × _ScaleX/Y └─ pixelSize(每像素局部尺度估计) └─ 1 / length(pixelSize) └─ × |texcoord1.y| × _GradientScale × (_Sharpness + 1) └─ scale └─ 透视视角过滤最终的scale会通过output.param.y传给片元着色器,用于把 Atlas 中的归一化距离转换为近似屏幕像素尺度的距离。
十、源码索引
Assets/TextMesh Pro/Shaders/TMP_SDF.shader:VertShader()、Shader Properties。Scripts/Runtime/TMPro_Private.cs:3D 文本写入texcoord1.y的路径。Scripts/Runtime/TMPro_UGUI_Private.cs:UGUI 写入texcoord1.y的路径。Scripts/Runtime/TMP_FontAsset.cs:字体材质_GradientScale的初始化。Scripts/Runtime/TMP_ShaderUtilities.cs:Shader 属性 ID 与比例参数计算。
