当前位置: 首页 > news >正文

Unity TMP_SDF 分析(三)顶点着色器中的屏幕空间缩放

Unity TMP_SDF 分析(三):顶点着色器中的屏幕空间缩放

适用版本:Unity2021.3.23f1c1|TextMeshPro3.0.9|UGUI1.0.0

分析范围:项目中的标准Assets/TextMesh Pro/Shaders/TMP_SDF.shader及其关联源码;Mobile、SSD、Surface 等变体不直接套用。

系列导航:第 3/9 篇|前置:纹理映射 UV 与 PackUV|下一篇:字重阈值、裁剪阈值与顶点输出

本篇分析VertShader()的前半部分:Bold 标记、顶点偏移、pixelSize、SDFscale以及透视过滤。Glow、Underlay、Bevel 和 UI 裁剪分别放在后续文章中。


一、相关源码

以下为TMP_SDF.shader顶点着色器的相关部分(节选):

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) { float viewDot = abs(dot( UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))); scale = lerp( abs(scale) * (1 - _PerspectiveFilter), scale, viewDot); } // 后续计算在第四篇继续分析。 }

开头的 Unity 宏用于初始化输出结构,并传递实例 ID、立体渲染等平台相关数据,是 Shader 平台适配流程的一部分。


二、用texcoord1.y的符号识别模拟 Bold

float bold = step(input.texcoord1.y, 0);

HLSL 的step(edge, x)x >= edge时返回1,否则返回0。这里等价于:

input.texcoord1.y <= 0 → bold = 1 input.texcoord1.y > 0 → bold = 0

根据第一篇的数据来源分析:

  • texcoord1.y的绝对值是 SDF 缩放量xScale
  • 当字符需要由 Shader 模拟 Bold 时,C# 会把xScale取负。
  • 使用专用替代 Typeface 时通常不会通过负号再次模拟加粗。

step(..., 0)在输入恰好为0时也会返回1。正常字符的有效缩放量通常不为零;零缩放属于退化情况。


三、顶点偏移与裁剪空间位置

float4 vert = input.position; vert.x += _VertexOffsetX; vert.y += _VertexOffsetY; float4 vPosition = UnityObjectToClipPos(vert);

input.position是对象空间中的文本顶点。Shader 先应用材质属性_VertexOffsetX/Y,再通过UnityObjectToClipPos()转换到裁剪空间。

这些参数的作用如下:

  • _VertexOffsetX/Y是 Shader 阶段施加的整体对象空间顶点偏移。
  • 偏移发生在对象空间,因此它会随对象的 Transform 和摄像机投影一起变换。
  • vPosition是齐次裁剪空间坐标;它还没有执行透视除法。

四、vPosition.w的准确含义

float2 pixelSize = vPosition.w;

这行代码把标量vPosition.w同时赋给pixelSize.xpixelSize.y

vPosition.w是齐次裁剪空间的 W 分量:

  • 在常见透视投影矩阵中,它与视空间深度成比例。
  • 在常见正交投影中,它通常为常量。
  • 它参与后续透视除法clip.xy / clip.w,因此可以反映透视投影下随距离变化的屏幕尺寸。

离摄像机更远时,透视投影下的|w|通常更大,同样的对象空间长度覆盖的屏幕像素更少。


五、pixelSize表示什么

完整公式为:

float2 pixelSize = vPosition.w; pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));

1._ScreenParams.xy

_ScreenParams.x/y是当前渲染目标的像素宽度和高度。它把投影尺度与实际屏幕分辨率联系起来。

2.(float2x2)UNITY_MATRIX_P

强制转换后的 2×2 部分包含投影矩阵对 X、Y 的主要缩放关系。对常规无偏斜投影,可近似理解为使用P00P11分别修正水平、垂直投影尺度。

3._ScaleX_ScaleY

它们是标准 TMP SDF Shader 的材质属性,默认值均为1,用于额外修正 X/Y 方向的 SDF 屏幕尺度。

在本项目的 TMP 3.0.9 源码中,它们作为 Shader 属性 ID 和材质面板参数使用。字符及对象的主要缩放信息通过texcoord1.y传入。

4. 最终物理意义

pixelSize表示一个屏幕像素对应的对象/投影空间长度估计量。

在其他条件不变时:

对象更近或显示更大 → pixelSize 更小 对象更远或显示更小 → pixelSize 更大 屏幕分辨率更高 → pixelSize 更小

该公式使用 Shader 内部统一的常数约定,主要用于生成相对稳定的 SDF 屏幕缩放。


六、从pixelSize得到 SDFscale

float scale = rsqrt(dot(pixelSize, pixelSize));

展开后为:

dot(pixelSize, pixelSize) = pixelSize.x² + pixelSize.y² rsqrt(value) = 1 / sqrt(value) scale = 1 / length(pixelSize)

因此scalepixelSize长度的倒数,更接近屏幕像素密度:

pixelSize 小 → scale 大 → 当前字形在屏幕上较大 pixelSize 大 → scale 小 → 当前字形在屏幕上较小

七、字符缩放、Atlas 梯度与锐度

scale *= abs(input.texcoord1.y) * _GradientScale * (_Sharpness + 1);

1.abs(input.texcoord1.y)

这是 C# 写入 UV2 的xScale绝对值。它补入当前字符、对象层级以及 UGUI Canvas 模式相关的缩放信息。负号只承担 Bold 标记,所以这里取绝对值。

2._GradientScale

_GradientScale描述字体 Atlas 中可用的 SDF 梯度范围。TMP 创建字体材质时通常会根据 Atlas Padding 写入该值;例如相关源码会使用atlasPadding + 1一类的换算。

Face Dilate、Outline、Glow、Underlay 等效果都依赖_GradientScale表示的有限距离范围;修改该值会同步改变这些效果的尺度关系。

3._Sharpness + 1

_Sharpness的 Shader 属性范围为[-1, 1]

  • 0:乘数为1,保持基础尺度。
  • 正值:增大scale,令距离过渡更陡、更锐。
  • 负值:减小scale,令过渡更宽、更柔。

它调整距离场转换的斜率,Atlas 分辨率和纹理采样次数保持不变。


八、透视投影下的视角过滤

if (UNITY_MATRIX_P[3][3] == 0) { scale = lerp( abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot( UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert))))); }

1. 投影类型判断

在 Unity 常见投影矩阵约定下:

UNITY_MATRIX_P[3][3] == 0 → 透视投影 UNITY_MATRIX_P[3][3] != 0 → 通常为正交投影

因此视角过滤只在透视投影分支执行。

2. 法线与视线点积

viewDot = abs(dot(worldNormal, normalizedViewDirection));

这里两者都在世界空间。其方向关系为:

正视文本平面:|N · V| 接近 1 掠射或侧视: |N · V| 接近 0

abs()用于忽略文本正反面法线方向的符号。

3._PerspectiveFilter如何影响结果

P = _PerspectiveFilterT = |N · V|,则:

scale' = lerp(scale × (1 - P), scale, T)
  • 正视时T ≈ 1:结果接近原始scale
  • 侧视时T ≈ 0:结果接近scale × (1 - P)
  • P = 0:两端相同,等于关闭视角修正。
  • P越大:掠射角下的scale降低得越多,SDF 过渡更宽,可减少倾斜文本的闪烁和过锐锯齿。

侧视时该公式会减小距离斜率,实施更强的透视过滤。


九、数据关系总结

对象空间顶点 └─ _VertexOffsetX/Y └─ UnityObjectToClipPos() ├─ vPosition → SV_POSITION └─ vPosition.w └─ 投影矩阵 × 屏幕尺寸 × _ScaleX/Y └─ pixelSize(每像素局部尺度估计) └─ 1 / length(pixelSize) └─ × |texcoord1.y| × _GradientScale × (_Sharpness + 1) └─ scale └─ 透视视角过滤

最终的scale会通过output.param.y传给片元着色器,用于把 Atlas 中的归一化距离转换为近似屏幕像素尺度的距离。


十、源码索引

  • Assets/TextMesh Pro/Shaders/TMP_SDF.shaderVertShader()、Shader Properties。
  • Scripts/Runtime/TMPro_Private.cs:3D 文本写入texcoord1.y的路径。
  • Scripts/Runtime/TMPro_UGUI_Private.cs:UGUI 写入texcoord1.y的路径。
  • Scripts/Runtime/TMP_FontAsset.cs:字体材质_GradientScale的初始化。
  • Scripts/Runtime/TMP_ShaderUtilities.cs:Shader 属性 ID 与比例参数计算。
http://www.jsqmd.com/news/1400356/

相关文章:

  • KWDB Playground v1.0.0 正式发布:四种安装方式、三种终端与容器环境管理升级
  • 动手写 Linux 内核模块与驱动:从 hello 到 platform 总线(srvD)
  • InvesTool 部署与配置实战:四关点亮一套投资分析工具,附避坑清单
  • 跨省救护车出租怎么预约 北京长途转院车辆收费标准与卧床患者转运指南 - 产品推荐官
  • 旧款 Mac 免费升级 macOS 实测手记:OpenCore Legacy Patcher 让我那台“退休“电脑重新上岗
  • OpenMontage零密钥入门:把AI编码助手变成你的免费视频制作工作室
  • 终极API管理平台DreamFactory完整上手指南:5分钟把数据库变成REST API
  • Vue Query Builder高级技巧:提升查询构建体验的7个方法
  • Canta主题开发指南:如何为GTK应用贡献自定义样式
  • 终极内存管理方案:arena1/arena单文件C库完全指南
  • Taste-Skill 实用指南:让 AI 界面设计彻底告别模板化的终极方案
  • c语言中函数传递二级指针,修改指针指向
  • 如何使用Receptionist实现Swift分布式集群服务发现
  • DeepStream视频分割 pipeline:基于Portrait-Segmentation的多摄像头实时处理方案
  • Unity许可证验证绕过完整指南:用UniHacker快速解锁全平台开发
  • ncmdump 快速上手:3 关解锁网易云 NCM 文件转换
  • CDDIS 天顶总延迟(ZTD)数据下载教程
  • ComfyUI中文工作流快速上手指南:零基础玩转AI绘图不再难
  • 群星全DLC免费解锁实操:Stellaris DLC Unlocker 在 Windows 和 Linux 上的完整记录
  • 如何用Nail-Qwen3.6-35B-A3B-GGUF实现多轮对话?解锁262k tokens超长上下文能力
  • 掌握tumblr.js核心功能:用户信息、博客数据与内容发布全攻略
  • COSMO-RS 实操:从量子力学到热力学性质的“无参”预测之路
  • combo实战:5分钟快速上手模型组合,让你的分类器性能飙升
  • Ghidra逆向工程框架完全指南:从安装到实战,把“看不懂的二进制“变成“读得懂的代码“
  • 从Vim新手到高效开发者:一年Neovim实战配置与效率提升指南
  • C++输入输出、字符串分割
  • SOFARegistry集群部署教程:Docker Compose与Kubernetes方案
  • 30分钟上手Huginn:零基础打造自己的自动化工作流
  • PhotoGIMP实操指南:把免费GIMP 3改造成熟悉的Photoshop界面,告别高价订阅
  • 深入理解RAT-retrieval-augmented-thinking架构:ModelChain类与双客户端设计详解