顶点着色器源码
其中暂时省略关于宏的部分
csharp
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) scale = lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))));
float weight = lerp(_WeightNormal, _WeightBold, bold) / 4.0;
weight = (weight + _FaceDilate) * _ScaleRatioA * 0.5;
float bias =(.5 - weight) + (.5 / scale);
float alphaClip = (1.0 - _OutlineWidth * _ScaleRatioA - _OutlineSoftness * _ScaleRatioA);
#if GLOW_ON
//暂时省略
#endif
alphaClip = alphaClip / 2.0 - ( .5 / scale) - weight;
#if (UNDERLAY_ON || UNDERLAY_INNER)
//暂时省略
#endif
// Generate UV for the Masking Texture
float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
float2 maskUV = (vert.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);
// Support for texture tiling and offset
float2 textureUV = UnpackUV(input.texcoord1.x);
float2 faceUV = TRANSFORM_TEX(textureUV, _FaceTex);
float2 outlineUV = TRANSFORM_TEX(textureUV, _OutlineTex);
output.position = vPosition;
output.color = input.color;
output.atlas = input.texcoord0;
output.param = float4(alphaClip, scale, bias, weight);
output.mask = half4(vert.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_MaskSoftnessX, _MaskSoftnessY) + pixelSize.xy));
output.viewDir = mul((float3x3)_EnvMatrix, _WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, vert).xyz);
#if (UNDERLAY_ON || UNDERLAY_INNER)
//暂时省略
#endif
output.textures = float4(faceUV, outlineUV);
return output;
}
第1部分解析
csharp
// 判断是否为粗体字(通过 的符号判断)
float bold = step(input.texcoord1.y, 0);
由前面的文章分析可知,texcoord1.y的符号代表是否加粗,负号为粗体
第2部分解析
csharp
float4 vert = input.position;
vert.x += _VertexOffsetX;
vert.y += _VertexOffsetY;

获取顶点位置,并应用用户定义的偏移
第4部分解析
csharp
float4 vPosition = UnityObjectToClipPos(vert);
// 计算当前像素在屏幕上的大小(用于后续的缩放计算)
float2 pixelSize = vPosition.w;
pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
_ScaleX和_ScaleY如图设置

1. float2 pixelSize = vPosition.w;
vPosition: 这是通过UnityObjectToClipPos(vert)计算出的裁剪空间 (Clip Space) 坐标。vPosition.xyz的范围通常是[-w, w]。vPosition.w是裁剪空间的w分量。
vPosition.w的含义 :- 在透视投影 中,
vPosition.w通常等于顶点的世界空间 Z 坐标(或与之成正比)。它代表了顶点到摄像机的距离。 - 在正交投影 中,
vPosition.w通常是一个常数(例如1.0),因为它不随距离变化。
- 在透视投影 中,
- 为什么用
vPosition.w作为pixelSize的初始值?- 这是一个非常关键的技巧 。
vPosition.w隐含了深度信息 。在透视投影中,距离越远,w越大,物体看起来越小(像素越少);距离越近,w越小,物体看起来越大(像素越多)。vPosition.w是计算屏幕像素大小的一个基础因子。
- 这是一个非常关键的技巧 。
2. pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));
这行代码是归一化 (Normalization) 过程,将 vPosition.w 转换为真正的屏幕像素大小。我们来分解右边的分母:
分母:float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy))
这个分母代表了"从裁剪空间到屏幕像素空间的总缩放因子"。
-
float2(_ScaleX, _ScaleY):- 这是 TMP 的自定义缩放参数。
- 它通常由
TextMeshPro组件根据lossyScale和字符的scale属性计算得出,并通过材质属性传递给着色器。 - 它代表了对象在 X 和 Y 轴上的相对缩放 。例如,如果
_ScaleX = 2.0,表示文本在 X 方向上被拉伸了 2 倍。
-
_ScreenParams.xy:- 这是 Unity 内置的变量,代表屏幕的宽度和高度(以像素为单位)。
- 例如,在 1920x1080 的屏幕上,
_ScreenParams.xy = float2(1920, 1080)。 - 它是从裁剪空间
[-1, 1]映射到屏幕像素空间[0, width]和[0, height]的基础比例因子。
-
UNITY_MATRIX_P:- 这是 Unity 的投影矩阵 (Projection Matrix)。
- 它定义了如何将视图空间 (View Space) 的坐标转换为裁剪空间 (Clip Space) 的坐标。
- 它包含了摄像机的视野 (FOV) 、宽高比 (Aspect Ratio) 、近裁剪面 (Near) 和远裁剪面 (Far) 等信息。
-
(float2x2)UNITY_MATRIX_P:-
这里将 4x4 的
UNITY_MATRIX_P强制转换为 2x2 矩阵。 -
这个 2x2 矩阵包含了投影矩阵中影响 X 和 Y 坐标缩放的部分。
-
具体来说,它通常是:
[ Pxx, Pxy ] [ Pyx, Pyy ]其中
Pxx和Pyy与 FOV 和宽高比有关。
-
-
mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy):- 这个乘法操作是核心。
- 它计算了在当前投影矩阵下,从裁剪空间
[-1, 1]映射到屏幕像素空间[0, width]所需的缩放比例。 - 简化理解:
- 裁剪空间的 X 范围是
[-1, 1](宽度为 2)。 - 屏幕空间的 X 范围是
[0, _ScreenParams.x]。 - 所以,从裁剪空间 X 到屏幕像素 X 的缩放因子大约是
_ScreenParams.x / 2。 - 但是,
UNITY_MATRIX_P会修改这个映射关系(尤其是在透视投影中,边缘会被压缩)。 mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy)这个操作,精确地计算了在当前投影下,裁剪空间单位长度在屏幕像素空间中对应的实际长度。
- 裁剪空间的 X 范围是
- 结果 :得到一个
float2,代表了 X 和 Y 方向上的总投影缩放因子。
-
abs(...):- 取绝对值,确保缩放因子为正数。这主要是为了处理某些特殊投影或矩阵配置。
-
float2(_ScaleX, _ScaleY) * abs(mul(...)):- 将对象的自定义缩放 与投影+屏幕的总缩放相乘。
- 得到最终的从裁剪空间到屏幕像素空间的总缩放因子。
3.最终的 pixelSize
pixelSize = vPosition.w / (总缩放因子)
- 物理意义 :
pixelSize现在代表了当前顶点位置的一个"世界单位"在屏幕空间中占据的像素数量 。- 例如,
pixelSize.x = 4.0意味着在 X 方向上,1 个单位长度对应 4 个像素。
- 例如,
第5部分解析
csharp
float scale = rsqrt(dot(pixelSize, pixelSize));
scale *= abs(input.texcoord1.y) * _GradientScale * (_Sharpness + 1);
其中_Sharpness 在如下位置调整,范围在-1到1之间

1. float scale = rsqrt(dot(pixelSize, pixelSize));
这行代码将之前计算的 pixelSize 向量转换为一个标量缩放因子。
pixelSize: 如前所述,pixelSize是一个float2,代表了当前顶点位置在 X 和 Y 方向上,一个"世界单位"对应的屏幕像素数量 。- 例如,
pixelSize = float2(4.0, 4.0)表示 1 个单位 = 4 个像素。
- 例如,
dot(pixelSize, pixelSize):- 计算向量的点积,即
pixelSize.x * pixelSize.x + pixelSize.y * pixelSize.y。 - 这等于
pixelSize向量长度的平方 (|pixelSize|²)。
- 计算向量的点积,即
rsqrt(...):rsqrt是 Reciprocal Square Root 的缩写,即1 / sqrt(x)。rsqrt(dot(pixelSize, pixelSize))等价于1 / sqrt(|pixelSize|²) = 1 / |pixelSize|。
- 物理意义 :
|pixelSize|是一个综合的"每单位像素数"。1 / |pixelSize|就是"每个像素对应的世界单位长度"。- 这个值越小,说明像素越"密集"(高分辨率、放大状态),我们需要更精细的 SDF 采样。
- 这个值越大,说明像素越"稀疏"(低分辨率、缩小状态),我们可以使用更粗糙的 SDF 采样。
结论 :scale 的初始值是一个基础缩放因子,它反比于屏幕空间的像素密度。它确保了 SDF 采样能适应摄像机和缩放的变化。
2. scale *= abs(input.texcoord1.y) * _GradientScale * (_Sharpness + 1);
这行代码对基础 scale 进行最终调整,使其适应具体的字体样式和渲染需求。
-
abs(input.texcoord1.y):- 这是最关键的调整项。
- 如Unity TMP_SDF 分析(一)数据来源1 中,详细讨论的,
input.texcoord1.y(即uv2.y) 存储了SDF 缩放因子。 abs(...)确保我们取其绝对值,因为负号可能用于标记"程序化加粗",而我们只关心其大小。- 来源 :这个值通常由
TextMeshPro组件根据lossyScale、字符scale和m_charWidthAdjDelta计算得出,并通过PackUV方法写入uv2.y。 - 作用 :它代表了该字符或文本对象的"内在"缩放级别。例如,一个大号字体需要比小号字体更高的 SDF 精度。
-
_GradientScale:- 这是一个材质属性 (Material Property)。
- 它是一个全局的、可由用户调整的缩放因子。
- 用途 :
- 艺术调整:美术人员可以根据需要微调所有 TMP 文本的锐利程度。
- 适配不同纹理 :如果 SDF 纹理的分辨率或生成参数不同,可以通过
_GradientScale来补偿。 - 性能/质量平衡 :减小
_GradientScale可以让边缘更模糊(性能更好),增大则让边缘更锐利(质量更高)。
第6部分解析
csharp
if (UNITY_MATRIX_P[3][3] == 0)
scale = lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))));
其中_PerspectiveFilter在如下图设置,范围在0到1之间

1. if (UNITY_MATRIX_P[3][3] == 0)
UNITY_MATRIX_P: Unity 的投影矩阵 (Projection Matrix)。UNITY_MATRIX_P[3][3]:- 在透视投影 矩阵中,
[3][3]位置的值通常是0。 - 在正交投影 矩阵中,
[3][3]位置的值通常是1。
- 在透视投影 矩阵中,
- 作用 :这是一个高效的条件判断 ,用于区分当前摄像机是透视模式 还是正交模式 。
- 如果
== 0,说明是透视投影,需要进行额外的视角校正。 - 如果
!= 0(通常是1),说明是正交投影 ,跳过此校正(使用之前计算的scale)。
- 如果
2. dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))
这是计算视角校正因子的核心。
input.normal.xyz: 顶点的法线向量 。对于一个标准的文本网格(平面),这通常是(0, 0, 1)或(0, 0, -1),指向 Z 轴正方向或负方向。UnityObjectToWorldNormal(...): 将法线从模型空间 (Object Space) 转换到世界空间 (World Space)。这考虑了对象的旋转。WorldSpaceViewDir(vert): 计算从顶点位置 指向摄像机位置 的视线向量 (View Direction) ,结果在世界空间。normalize(...): 将视线向量归一化为单位向量。dot(...):- 计算世界空间法线 与归一化视线向量 的点积 (Dot Product)。
- 点积的结果等于
|A||B|cosθ,其中θ是两个向量之间的夹角。 - 因为两个向量都是单位向量(或接近单位向量),所以
dot的结果就是cosθ。
abs(...):- 取绝对值。因为法线可能指向
(0,0,1)或(0,0,-1),我们关心的是夹角的大小 ,而不关心方向。abs(cosθ)确保结果在[0, 1]范围内。
- 取绝对值。因为法线可能指向
- 物理意义 :
- 当视线垂直于文本平面 时(正视),
θ = 90°,cosθ = 0。 - 当视线平行于文本平面 时(侧视),
θ = 0°,cosθ = 1。 - 所以,
abs(dot(...))的值越大,表示视角越倾斜,文本平面在屏幕上的"投影面积"越小,有效像素密度越低,越容易模糊。
- 当视线垂直于文本平面 时(正视),
3. lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(...)))
这是一个线性插值 (Linear Interpolation) ,用于根据视角倾斜程度动态调整 scale。
lerp(A, B, T): 当T=0时返回A;当T=1时返回B;T在[0,1]之间时返回A和B的插值。T = abs(dot(...)): 视角倾斜程度,0(正视) 到1(侧视)。B = scale: 这是未校正 的原始scale值(基于距离和内在缩放)。A = abs(scale) * (1 - _PerspectiveFilter):abs(scale): 确保scale为正。(1 - _PerspectiveFilter): 这是校正强度的控制因子。_PerspectiveFilter: 这是一个材质属性 (Material Property) ,通常范围是[0, 1]。_PerspectiveFilter = 0: 完全禁用校正。A = abs(scale) * 1 = abs(scale),lerp在abs(scale)和scale之间插值,效果很弱。_PerspectiveFilter = 1: 最大校正。A = abs(scale) * 0 = 0,lerp在0和scale之间插值。
4.结论如下
在正交投影 (Orthographic) 下,物体的大小不随距离变化,scale 的计算相对简单且一致。
但在透视投影下:
- 近处 的文本看起来更大,像素更密集,需要更小的
scale值来保持边缘锐利。 - 远处 的文本看起来更小,像素更稀疏,需要更大的
scale值来避免锯齿。
然而,还有一个更隐蔽的问题:倾斜角度 (Viewing Angle)。
- 当你从一个倾斜的角度看一个文本平面时(比如抬头看高处的招牌),文本平面与视线的夹角会变小。
- 这会导致文本在屏幕上被"压扁",其有效像素密度降低,即使距离不远,边缘也可能变得模糊。
这段代码就是为了解决这个视角倾斜带来的模糊问题。