Unity TMP_SDF 分析(七)UNDERLAY_ON 与 UNDERLAY_INNER
2026/8/20 20:34:20 网站建设 项目流程

Unity TMP_SDF 分析(七):UNDERLAY_ONUNDERLAY_INNER

适用版本:Unity2021.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_INNER

Normal 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_ONUNDERLAY_INNER
材质面板NormalInner
是否与另一关键字同时启用
是否跳过主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 互斥关键字设置。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询