Unity TMP_SDF 分析(七):UNDERLAY_ON与UNDERLAY_INNER
适用版本:Unity
2021.3.23f1c1|TextMeshPro3.0.9|UGUI1.0.0分析范围:项目中的标准
Assets/TextMesh Pro/Shaders/TMP_SDF.shader及其关联源码;Mobile、SSD、Surface 等变体不直接套用。系列导航:第 7/9 篇|前置:
GLOW_ON辉光路径|下一篇:BEVEL_ON法线、光照与反射
Underlay 使用一套偏移、膨胀和柔化参数再次采样_MainTexSDF Atlas,再把结果合成到 Face/Outline 下方。
一、关键字是互斥变体
Shader 声明为:
#pragma shader_feature __ UNDERLAY_ON UNDERLAY_INNER材质面板对应三种互斥状态:
None → 两个关键字都不启用 Normal → UNDERLAY_ON Inner → UNDERLAY_INNERNormal Underlay 的效果示例:
Inner 模式选择UNDERLAY_INNER变体;Normal 与 Inner 是互斥选项。
Inner Underlay 会乘(1 - faceColor.a),所以 Face/Outline 完全不透明时效果会被遮住。调低 Face Alpha 后更容易观察:
效果示例:
二、完整相关代码
顶点着色器准备一套额外参数(节选):
#if (UNDERLAY_ON || UNDERLAY_INNER) float4 underlayColor = _UnderlayColor; underlayColor.rgb *= underlayColor.a; float bScale = scale; bScale /= 1 + ((_UnderlaySoftness * _ScaleRatioC) * bScale); float bBias = (0.5 - weight) * bScale - 0.5 - ((_UnderlayDilate * _ScaleRatioC) * 0.5 * bScale); float x = -(_UnderlayOffsetX * _ScaleRatioC) * _GradientScale / _TextureWidth; float y = -(_UnderlayOffsetY * _ScaleRatioC) * _GradientScale / _TextureHeight; float2 bOffset = float2(x, y); output.texcoord2 = float4( input.texcoord0 + bOffset, bScale, bBias); output.underlayColor = underlayColor; #endif片元着色器根据互斥关键字选择一种合成方式:
#ifndef UNDERLAY_ON clip(c - input.param.x); #endif #if UNDERLAY_ON float d = tex2D( _MainTex, input.texcoord2.xy).a * input.texcoord2.z; faceColor += input.underlayColor * saturate(d - input.texcoord2.w) * (1 - faceColor.a); #endif #if UNDERLAY_INNER float d = tex2D( _MainTex, input.texcoord2.xy).a * input.texcoord2.z; faceColor += input.underlayColor * (1 - saturate(d - input.texcoord2.w)) * saturate(1 - sd) * (1 - faceColor.a); #endif三、CPU 端_ScaleRatioC
TMP_ShaderUtilities.UpdateShaderRatios()的计算如下(节选):
floatscale=mat.GetFloat(ID_GradientScale);floatfaceDilate=mat.GetFloat(ID_FaceDilate);floatweight=Mathf.Max(mat.GetFloat(ID_WeightNormal),mat.GetFloat(ID_WeightBold))/4.0f;floatoffsetX=mat.GetFloat(ID_UnderlayOffsetX);floatoffsetY=mat.GetFloat(ID_UnderlayOffsetY);floatdilate=mat.GetFloat(ID_UnderlayDilate);floatsoftness=mat.GetFloat(ID_UnderlaySoftness);floatrange=(weight+faceDilate)*(scale-1.0f);floatt=Mathf.Max(1,Mathf.Max(Mathf.Abs(offsetX),Mathf.Abs(offsetY))+dilate+softness);boolisRatioEnabled=!mat.shaderKeywords.Contains("RATIOS_OFF");floatratioC=isRatioEnabled?Mathf.Max(0,scale-1.0f-range)/(scale*t):1;mat.SetFloat(ID_ScaleRatio_C,ratioC);Ratio C 先扣除 Weight 与 Face Dilate 占用的 Atlas 梯度范围,再根据以下 Underlay 参数压缩剩余预算:
- X/Y Offset 中绝对值较大的一个
- Dilate
- Softness
Ratio C 限制 SDF Padding 使用范围,顶点 Shader 的scale负责屏幕尺度换算。
四、Underlay 颜色使用预乘 Alpha
float4 underlayColor = _UnderlayColor; underlayColor.rgb *= underlayColor.a;Shader Pass 使用:
Blend One OneMinusSrcAlpha所以 Underlay RGB 先乘自身 Alpha,后续可以通过加法形式合成:
underlay + base × (1 - underlayAlpha)当前 Shader 的实际表达式还额外乘了(1 - faceColor.a),用来保证 Underlay 位于已生成的 Face/Outline 之下。
五、bScale:Underlay Softness
float bScale = scale; bScale /= 1 + ((_UnderlaySoftness * _ScaleRatioC) * bScale);可写为:
bScale = scale / (1 + softness × ratioC × scale)- Softness 为
0时,bScale = scale。 - Softness 增大时,
bScale减小。 - 片元阶段使用
atlasAlpha × bScale - bBias生成覆盖率;较小的bScale令 Atlas 距离变化斜率更缓,Underlay 边缘更柔。
这套 Softness 只作用于第二次偏移采样,不会直接修改主 Face/Outline 的scale。
六、bBias:Weight、Dilate 与柔化后的阈值
float bBias = (0.5 - weight) * bScale - 0.5 - ((_UnderlayDilate * _ScaleRatioC) * 0.5 * bScale);片元阶段组合为:
float d = shiftedAtlasAlpha * bScale; float underlayCoverage = saturate(d - bBias);因此bBias是已经转换到d所在尺度的覆盖阈值:
weight让 Underlay 与主字形的字重基准保持联系。- 正
_UnderlayDilate会降低bBias,使d - bBias增大,覆盖区域扩张。 - Softness 已经通过
bScale同时影响斜率和阈值。
主路径计算sd = (bias - c) * scale,Underlay 路径计算c × bScale - bBias,两者使用各自的符号和代数形式。
七、Offset 如何变成 Atlas UV 偏移
float x = -(_UnderlayOffsetX * _ScaleRatioC) * _GradientScale / _TextureWidth; float y = -(_UnderlayOffsetY * _ScaleRatioC) * _GradientScale / _TextureHeight;_GradientScale / _TextureWidth与_GradientScale / _TextureHeight把材质 Offset 换算为 Atlas UV 尺度,Ratio C 负责限制可用范围。
代码中的负号表示采样坐标向设置方向的反方向移动;视觉上得到的 Underlay 图形会相对原字形向 Offset 指定方向偏移。这与“移动图形”和“反向移动采样点”之间的关系一致。
最终传递的数据为:
input.texcoord2.xy → 偏移后的 Atlas UV input.texcoord2.z → bScale input.texcoord2.w → bBias八、为什么 Normal Underlay 跳过主clip()
#ifndef UNDERLAY_ON clip(c - input.param.x); #endif只有UNDERLAY_ON会跳过主 Atlas 的早期 clip:
- Normal Underlay 需要在原字形外侧显示偏移后的形状。
- 当前片元的原始 Atlas 值
c可能低于主 Face/Outline 的阈值,但偏移 UV 采样却可能命中 Underlay 字形。 - 若先按原始
c丢弃,这些外部阴影片元就永远无法执行第二次采样。
UNDERLAY_INNER只需要原字形内部附近的片元,因此它与无 Underlay 情况一样保留这次主clip()。
九、Normal Underlay 合成
float d = tex2D( _MainTex, input.texcoord2.xy).a * input.texcoord2.z; faceColor += input.underlayColor * saturate(d - input.texcoord2.w) * (1 - faceColor.a);拆解为:
shiftedCoverage = saturate(shiftedAtlasAlpha × bScale - bBias) underlayContribution = premultipliedUnderlayColor × shiftedCoverage × (1 - baseFaceOutlineAlpha)(1 - faceColor.a)使 Underlay 只填充 Face/Outline 尚未覆盖的部分,形成位于文字下方的阴影或外扩底层。
十、Inner Underlay 合成
faceColor += input.underlayColor * (1 - saturate(d - input.texcoord2.w)) * saturate(1 - sd) * (1 - faceColor.a);它由三个遮罩共同决定。
1. 偏移字形的反向覆盖
1 - saturate(d - bBias)Normal 模式使用偏移字形“被覆盖”的区域;Inner 模式反过来选择偏移字形“未覆盖或覆盖较弱”的区域。
2. 原始字形内部遮罩
saturate(1 - sd)第五篇已经确认sd < 0更靠原字形内部:
- 内部
sd <= 0时,该遮罩通常为1。 - 向外移动、
sd增大后逐渐降为0。
这一步把反向偏移覆盖限制在原字形内部附近,从而形成内部阴影/内凹效果。
3. 基础颜色剩余透明度
1 - faceColor.a即使前两个遮罩有贡献,只要 Face/Outline 已经完全不透明,最终 Underlay 仍会被乘为零。这正是降低 Face Alpha 后 Inner Underlay 才明显的原因。
十一、两种模式对照
| 项目 | UNDERLAY_ON | UNDERLAY_INNER |
|---|---|---|
| 材质面板 | Normal | Inner |
| 是否与另一关键字同时启用 | 否 | 否 |
是否跳过主clip(c-alphaClip) | 是 | 否 |
| 偏移覆盖 | saturate(d-bBias) | 1-saturate(d-bBias) |
| 原字形内部遮罩 | 无 | saturate(1-sd) |
| 与 Face 的层级 | 都乘(1-faceColor.a),位于其下 | |
| 常见效果 | 投影、外部底层 | 内阴影、内凹遮罩 |
十二、源码索引
Assets/TextMesh Pro/Shaders/TMP_SDF.shader:Underlay 顶点参数与两种片元合成。Scripts/Runtime/TMP_ShaderUtilities.cs:_ScaleRatioC。Scripts/Editor/TMP_SDFShaderGUI.cs:None / Normal / Inner 互斥关键字设置。