片元着色器源码
省略宏相关部分
csharp
fixed4 PixShader(pixel_t input) : SV_Target
{
UNITY_SETUP_INSTANCE_ID(input);
float c = tex2D(_MainTex, input.atlas).a;
float scale = input.param.y;
float bias = input.param.z;
float weight = input.param.w;
float sd = (bias - c) * scale;
float outline = (_OutlineWidth * _ScaleRatioA) * scale;
float softness = (_OutlineSoftness * _ScaleRatioA) * scale;
half4 faceColor = _FaceColor;
half4 outlineColor = _OutlineColor;
faceColor.rgb *= input.color.rgb;
faceColor *= tex2D(_FaceTex, input.textures.xy + float2(_FaceUVSpeedX, _FaceUVSpeedY) * _Time.y);
outlineColor *= tex2D(_OutlineTex, input.textures.zw + float2(_OutlineUVSpeedX, _OutlineUVSpeedY) * _Time.y);
faceColor = GetColor(sd, faceColor, outlineColor, outline, softness);
return faceColor * input.color.a;
}
输入结构
片段着色器接收来自顶点着色器的数据结构 pixel_t,其中关键字段包括:
input.atlas→ 字体图集UV坐标(用于采样距离场纹理)input.param→ 包含四个重要参数:.x→alphaClip(已废弃或未使用).y→scale(像素密度缩放因子).z→bias(距离场偏移,控制粗细).w→weight(加粗权重)
input.textures→ 包含两个纹理UV:.xy→ 面部纹理(_FaceTex).zw→ 描边纹理(_OutlineTex)
input.color→ 顶点颜色(通常为白色或用户指定颜色)
逐行解析
1️⃣ 初始化实例ID(多实例渲染支持)
csharp
UNITY_SETUP_INSTANCE_ID(input);
支持 GPU Instancing,允许多个相同材质的文本实例高效绘制。
2️⃣ 采样距离场纹理,获取原始距离值
csharp
float c = tex2D(_MainTex, input.atlas).a;
_MainTex是 字体图集纹理(Distance Field Atlas),存储的是每个像素到字形边缘的"有符号距离"。.a(Alpha通道)存储距离值:> 0.5→ 在字形内部(前景)< 0.5→ 在字形外部(背景)= 0.5→ 正好在边缘
💡 SDF 的核心思想:用一个浮点值表示"距离边缘有多远",从而实现任意缩放下的平滑边缘。
测试输出如下
csharp
float c = tex2D(_MainTex, input.atlas).a;
return fixed4(c,c,c,1);

即sdf图集

里面的这个2字

3️⃣ 解包缩放、偏移、权重参数
csharp
float scale = input.param.y;
float bias = input.param.z;
float weight = input.param.w;
这些值由顶点着色器计算并传递:
scale:基于屏幕像素密度和字体缩放计算出的"反走样因子",越大表示当前像素越小,需要更精细的抗锯齿。bias:控制字形"膨胀"或"收缩"。正值让字形变粗,负值变细。weight:加粗权重,受_FaceDilate和_WeightBold影响。
4️⃣ 计算标准化距离(Signed Distance)
csharp
float sd = (bias - c) * scale;
bias - c→ 将采样距离与偏移值比较,得到"相对边缘距离"。* scale→ 根据当前像素密度缩放,保证在不同分辨率下边缘宽度一致。
✅
sd > 0→ 像素在字形内部✅
sd < 0→ 像素在字形外部✅
sd = 0→ 像素正好在边缘 ------ 抗锯齿就发生在这里!
5️⃣ 计算描边宽度与软化度(乘以 scale 保持分辨率无关)
csharp
float outline = (_OutlineWidth * _ScaleRatioA) * scale;
float softness = (_OutlineSoftness * _ScaleRatioA) * scale;
_OutlineWidth:描边厚度(0~1)_OutlineSoftness:描边羽化程度(0=硬边,1=完全模糊)* scale:确保描边在不同 DPI 下视觉宽度一致
6️⃣ 初始化基础颜色
csharp
half4 faceColor = _FaceColor;
half4 outlineColor = _OutlineColor;
faceColor.rgb *= input.color.rgb; // 顶点颜色调制(支持Color Gradient等)
支持通过顶点色实现渐变、动画变色等效果。
7️⃣ 采样面部与描边纹理(支持动态UV动画)
csharp
faceColor *= tex2D(_FaceTex, input.textures.xy + float2(_FaceUVSpeedX, _FaceUVSpeedY) * _Time.y);
outlineColor *= tex2D(_OutlineTex, input.textures.zw + float2(_OutlineUVSpeedX, _OutlineUVSpeedY) * _Time.y);
_FaceTex/_OutlineTex:用户可指定的叠加纹理(如金属、发光、图案等)+ _Time.y * Speed→ 实现 UV 滚动动画(如跑马灯、流动光效)*=→ 与基础颜色相乘(Multiply Blend),常用于遮罩或纹理叠加
8️⃣ 核心:GetColor ------ 根据距离场生成最终颜色
csharp
faceColor = GetColor(sd, faceColor, outlineColor, outline, softness);
核心函数会看到细节
9️⃣ 最终输出:乘以顶点 Alpha(支持透明度动画)
csharp
return faceColor * input.color.a;
input.color.a→ 顶点 Alpha 通道,可用于淡入淡出、闪烁等效果- 最终颜色 = 渲染结果 × 透明度
核心函数
⚙️
GetColor是一个在TMPro.cginc中定义的函数,逻辑如下:
csharp
fixed4 GetColor(half d, fixed4 faceColor, fixed4 outlineColor, half outline, half softness)
{
half faceAlpha = 1-saturate((d - outline * 0.5 + softness * 0.5) / (1.0 + softness));
half outlineAlpha = saturate((d + outline * 0.5)) * sqrt(min(1.0, outline));
faceColor.rgb *= faceColor.a;
outlineColor.rgb *= outlineColor.a;
faceColor = lerp(faceColor, outlineColor, outlineAlpha);
faceColor *= faceAlpha;
return faceColor;
}
函数签名如下
hlsl
fixed4 GetColor(half d, fixed4 faceColor, fixed4 outlineColor, half outline, half softness)
d→ 标准化距离(Signed Distance) ,由(bias - c) * scale计算得来d > 0:像素在字形内部d < 0:像素在字形外部d = 0:正好在边缘(抗锯齿发生区)
faceColor→ 面部(文字本体)颜色outlineColor→ 描边颜色outline→ 描边宽度(已乘以 scale,分辨率无关)softness→ 描边羽化/抗锯齿软化程度(也乘以 scale)
💡 注意:参数名
d就是之前我们说的sd(signed distance)
1️⃣ 计算面部透明度(faceAlpha)
hlsl
half faceAlpha = 1 - saturate((d - outline * 0.5 + softness * 0.5) / (1.0 + softness));
如果想让文字"从边缘开始淡入",而不是一刀切。
outline * 0.5→ 描边占一半宽度在内侧,一半在外侧(居中描边)d - outline * 0.5→ 把"有效字形边界"向内收缩半个描边宽度(因为描边会覆盖一部分字形)+ softness * 0.5→ 再向内偏移半个软化值,让抗锯齿区域提前开始(更柔和)/ (1.0 + softness)→ 归一化软化范围,确保在softness=0时是硬边缘saturate(...)→ clamp 到 [0,1]1 - ...→ 反转:距离越大(越内部),透明度越高
效果如下
- 当
d很大(深内部)→faceAlpha ≈ 1 - 当
d很小(外部)→faceAlpha ≈ 0 - 在
d ≈ outline*0.5 - softness*0.5附近 → 平滑过渡(抗锯齿)
📌 这个公式巧妙地将 描边宽度 和 软化度 融入抗锯齿计算,确保描边和字形边缘自然衔接。
2️⃣ 计算描边透明度(outlineAlpha)
hlsl
half outlineAlpha = saturate((d + outline * 0.5)) * sqrt(min(1.0, outline));
描边是从字形边缘"向外扩展"的区域。
d + outline * 0.5:d是当前像素到原始边缘的距离+ outline * 0.5→ 把"描边外边界"设为d = -outline * 0.5- 所以当
d >= -outline * 0.5时,就在描边区域内
saturate(...)→ 确保透明度在 [0,1] 之间d < -outline * 0.5→outlineAlpha = 0d > -outline * 0.5→outlineAlpha = (d + outline * 0.5)线性增加
* sqrt(min(1.0, outline))→ 非线性强度控制
思考 :为什么乘以 sqrt(min(1.0, outline))?
这是个视觉补偿技巧:
- 当
outline很小时(如 0.1),线性透明度会让描边看起来"太弱" sqrt(x)在 x<1 时 > x,起到"增强小值"的作用min(1.0, outline)防止 outline > 1 时过度增强- 最终效果:小描边更明显,大描边不溢出
✅ 举例:
outline=0.25→sqrt(0.25)=0.5,描边强度翻倍!
3️⃣ 预乘 Alpha(Premultiply Alpha)
hlsl
faceColor.rgb *= faceColor.a;
outlineColor.rgb *= outlineColor.a;
🎨 预乘 Alpha(Premultiplied Alpha) 是图形学中常用技巧:
- 优点:混合更正确,避免边缘颜色渗出(color bleeding)
- 公式:
RGB = RGB * A,之后混合时直接加,不用再乘
TextMeshPro 采用预乘 Alpha 保证描边和字形混合时颜色过渡自然。
4️⃣ 描边与面部颜色混合
hlsl
faceColor = lerp(faceColor, outlineColor, outlineAlpha);
原理如下
lerp(A, B, t)=A * (1-t) + B * t- 当
outlineAlpha = 0→ 完全显示faceColor - 当
outlineAlpha = 1→ 完全显示outlineColor - 中间值 → 平滑过渡
✅ 关键设计:描边是"覆盖"在面部之上的,不是"环绕"。所以先混合颜色,再应用面部透明度。
5️⃣ 应用面部透明度(最终抗锯齿)
hlsl
faceColor *= faceAlpha;
- 将上一步混合后的颜色,乘以面部透明度
- 实现字形边缘的抗锯齿(从透明到不透明的平滑过渡)
6️⃣ 返回最终颜色
hlsl
return faceColor;
此时 faceColor 已包含:
- 正确的 RGB(预乘 Alpha)
- 正确的 A(由
faceAlpha控制) - 描边与面部的自然混合
- 抗锯齿边缘
算法流程图(简化版)
输入 d, faceColor, outlineColor, outline, softness
↓
计算 faceAlpha = 1 - saturate((d - outline*0.5 + softness*0.5) / (1+softness))
↓
计算 outlineAlpha = saturate(d + outline*0.5) * sqrt(min(1, outline))
↓
预乘 Alpha:faceColor.rgb *= faceColor.a, outlineColor.rgb *= outlineColor.a
↓
混合颜色:faceColor = lerp(faceColor, outlineColor, outlineAlpha)
↓
应用抗锯齿:faceColor *= faceAlpha
↓
返回 faceColor
视觉效果分区示意图
假设 outline = 1.0, softness = 0.5
d值范围 | 效果
------------------|-------------------
d < -1.0 | 完全透明(背景)
-1.0 < d < -0.5 | 描边区域(透明度从0→1)
-0.5 < d < 0.25 | 描边+面部混合 + 抗锯齿(faceAlpha从0→1)
d > 0.25 | 纯面部颜色(不透明)
📌 注意:因为
faceAlpha的计算中减去了outline*0.5 - softness*0.5,所以字形"视觉边界"比原始 SDF 边界更靠内,给描边留出空间。
为什么这样设计?
- 描边居中 :
outline * 0.5让描边一半在内一半在外,视觉更平衡 - 抗锯齿提前 :
+ softness * 0.5让抗锯齿从描边内侧就开始,避免硬边 - 小描边增强 :
sqrt(min(1, outline))让细描边更明显 - 预乘 Alpha:保证颜色混合正确,无边缘渗色
- 先混合后透明:描边作为"装饰层"覆盖在字形上,符合设计直觉