Unity TMP_SDF 分析(五)片元着色器

片元着色器源码

省略宏相关部分

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 → 包含四个重要参数:
    • .xalphaClip(已废弃或未使用)
    • .yscale(像素密度缩放因子)
    • .zbias(距离场偏移,控制粗细)
    • .wweight(加粗权重)
  • 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.5outlineAlpha = 0
    • d > -outline * 0.5outlineAlpha = (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.25sqrt(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 边界更靠内,给描边留出空间。


为什么这样设计?

  1. 描边居中outline * 0.5 让描边一半在内一半在外,视觉更平衡
  2. 抗锯齿提前+ softness * 0.5 让抗锯齿从描边内侧就开始,避免硬边
  3. 小描边增强sqrt(min(1, outline)) 让细描边更明显
  4. 预乘 Alpha:保证颜色混合正确,无边缘渗色
  5. 先混合后透明:描边作为"装饰层"覆盖在字形上,符合设计直觉

相关推荐
mxwin3 小时前
Unity Shader Texture Bombing用随机旋转与偏移的多次采样,打破大地形纹理的
unity·游戏引擎
代数狂人3 小时前
《深入浅出Godot 4与C# 3D游戏开发》第二章:编辑器导航
3d·编辑器·游戏引擎·godot
zcc8580797623 小时前
Unity MVVM UniTask + 轻量级 ReactiveProperty
unity
zcc8580797624 小时前
Unity 自动生成UI绑定+MVVM 架构模板
unity
LF男男6 小时前
MK - Grand Mahjong Game-
unity·c#
呆呆敲代码的小Y6 小时前
【Unity实战篇】| YooAsset + UOS CDN 云服务资源部署,实现正式热更流程
人工智能·游戏·unity·游戏引擎·免费游戏
WarPigs6 小时前
unity多语言框架
unity
代数狂人7 小时前
《深入浅出Godot 4与C# 3D游戏开发》第一章:了解Godot与搭建开发环境
c#·游戏引擎·godot
RReality7 小时前
【UGUI】自定义 ListView 架构:设计、原理与可扩展性
unity·架构