Unity TMP_SDF 分析(三)顶点着色器1

顶点着色器源码

其中暂时省略关于宏的部分

csharp 复制代码
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) scale = lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))));

	float weight = lerp(_WeightNormal, _WeightBold, bold) / 4.0;
	weight = (weight + _FaceDilate) * _ScaleRatioA * 0.5;

	float bias =(.5 - weight) + (.5 / scale);

	float alphaClip = (1.0 - _OutlineWidth * _ScaleRatioA - _OutlineSoftness * _ScaleRatioA);

#if GLOW_ON
	//暂时省略
#endif

	alphaClip = alphaClip / 2.0 - ( .5 / scale) - weight;

#if (UNDERLAY_ON || UNDERLAY_INNER)
	//暂时省略
#endif

	// Generate UV for the Masking Texture
	float4 clampedRect = clamp(_ClipRect, -2e10, 2e10);
	float2 maskUV = (vert.xy - clampedRect.xy) / (clampedRect.zw - clampedRect.xy);

	// Support for texture tiling and offset
	float2 textureUV = UnpackUV(input.texcoord1.x);
	float2 faceUV = TRANSFORM_TEX(textureUV, _FaceTex);
	float2 outlineUV = TRANSFORM_TEX(textureUV, _OutlineTex);


	output.position = vPosition;
	output.color = input.color;
	output.atlas =	input.texcoord0;
	output.param =	float4(alphaClip, scale, bias, weight);
	output.mask = half4(vert.xy * 2 - clampedRect.xy - clampedRect.zw, 0.25 / (0.25 * half2(_MaskSoftnessX, _MaskSoftnessY) + pixelSize.xy));
	output.viewDir =	mul((float3x3)_EnvMatrix, _WorldSpaceCameraPos.xyz - mul(unity_ObjectToWorld, vert).xyz);
	#if (UNDERLAY_ON || UNDERLAY_INNER)
	//暂时省略
	#endif
	
	output.textures = float4(faceUV, outlineUV);

	return output;
}

第1部分解析

csharp 复制代码
 // 判断是否为粗体字(通过  的符号判断)
float bold = step(input.texcoord1.y, 0);

由前面的文章分析可知,texcoord1.y的符号代表是否加粗,负号为粗体

第2部分解析

csharp 复制代码
float4 vert = input.position;
vert.x += _VertexOffsetX;
vert.y += _VertexOffsetY;

获取顶点位置,并应用用户定义的偏移

第4部分解析

csharp 复制代码
float4 vPosition = UnityObjectToClipPos(vert);
// 计算当前像素在屏幕上的大小(用于后续的缩放计算)
float2 pixelSize = vPosition.w;

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

_ScaleX_ScaleY如图设置

1. float2 pixelSize = vPosition.w;
  • vPosition : 这是通过 UnityObjectToClipPos(vert) 计算出的裁剪空间 (Clip Space) 坐标。
    • vPosition.xyz 的范围通常是 [-w, w]
    • vPosition.w 是裁剪空间的 w 分量。
  • vPosition.w 的含义 :
    • 透视投影 中,vPosition.w 通常等于顶点的世界空间 Z 坐标(或与之成正比)。它代表了顶点到摄像机的距离。
    • 正交投影 中,vPosition.w 通常是一个常数(例如 1.0),因为它不随距离变化。
  • 为什么用 vPosition.w 作为 pixelSize 的初始值?
    • 这是一个非常关键的技巧vPosition.w 隐含了深度信息 。在透视投影中,距离越远,w 越大,物体看起来越小(像素越少);距离越近,w 越小,物体看起来越大(像素越多)。vPosition.w 是计算屏幕像素大小的一个基础因子
2. pixelSize /= float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy));

这行代码是归一化 (Normalization) 过程,将 vPosition.w 转换为真正的屏幕像素大小。我们来分解右边的分母:

分母:float2(_ScaleX, _ScaleY) * abs(mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy))

这个分母代表了"从裁剪空间到屏幕像素空间的总缩放因子"。

  • float2(_ScaleX, _ScaleY):

    • 这是 TMP 的自定义缩放参数
    • 它通常由 TextMeshPro 组件根据 lossyScale 和字符的 scale 属性计算得出,并通过材质属性传递给着色器。
    • 它代表了对象在 X 和 Y 轴上的相对缩放 。例如,如果 _ScaleX = 2.0,表示文本在 X 方向上被拉伸了 2 倍。
  • _ScreenParams.xy:

    • 这是 Unity 内置的变量,代表屏幕的宽度和高度(以像素为单位)。
    • 例如,在 1920x1080 的屏幕上,_ScreenParams.xy = float2(1920, 1080)
    • 它是从裁剪空间 [-1, 1] 映射到屏幕像素空间 [0, width][0, height] 的基础比例因子
  • UNITY_MATRIX_P:

    • 这是 Unity 的投影矩阵 (Projection Matrix)
    • 它定义了如何将视图空间 (View Space) 的坐标转换为裁剪空间 (Clip Space) 的坐标。
    • 它包含了摄像机的视野 (FOV)宽高比 (Aspect Ratio)近裁剪面 (Near)远裁剪面 (Far) 等信息。
  • (float2x2)UNITY_MATRIX_P:

    • 这里将 4x4 的 UNITY_MATRIX_P 强制转换为 2x2 矩阵。

    • 这个 2x2 矩阵包含了投影矩阵中影响 X 和 Y 坐标缩放的部分。

    • 具体来说,它通常是:

      复制代码
      [ Pxx, Pxy ]
      [ Pyx, Pyy ]

      其中 PxxPyy 与 FOV 和宽高比有关。

  • mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy):

    • 这个乘法操作是核心
    • 它计算了在当前投影矩阵下,从裁剪空间 [-1, 1] 映射到屏幕像素空间 [0, width] 所需的缩放比例
    • 简化理解:
      • 裁剪空间的 X 范围是 [-1, 1] (宽度为 2)。
      • 屏幕空间的 X 范围是 [0, _ScreenParams.x]
      • 所以,从裁剪空间 X 到屏幕像素 X 的缩放因子大约是 _ScreenParams.x / 2
      • 但是,UNITY_MATRIX_P 会修改这个映射关系(尤其是在透视投影中,边缘会被压缩)。
      • mul((float2x2)UNITY_MATRIX_P, _ScreenParams.xy) 这个操作,精确地计算了在当前投影下,裁剪空间单位长度在屏幕像素空间中对应的实际长度
    • 结果 :得到一个 float2,代表了 X 和 Y 方向上的总投影缩放因子
  • abs(...):

    • 取绝对值,确保缩放因子为正数。这主要是为了处理某些特殊投影或矩阵配置。
  • float2(_ScaleX, _ScaleY) * abs(mul(...)):

    • 对象的自定义缩放投影+屏幕的总缩放相乘。
    • 得到最终的从裁剪空间到屏幕像素空间的总缩放因子

3.最终的 pixelSize

pixelSize = vPosition.w / (总缩放因子)

  • 物理意义pixelSize 现在代表了当前顶点位置的一个"世界单位"在屏幕空间中占据的像素数量
    • 例如,pixelSize.x = 4.0 意味着在 X 方向上,1 个单位长度对应 4 个像素。

第5部分解析

csharp 复制代码
float scale = rsqrt(dot(pixelSize, pixelSize));
scale *= abs(input.texcoord1.y) * _GradientScale * (_Sharpness + 1);

其中_Sharpness 在如下位置调整,范围在-1到1之间

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

这行代码将之前计算的 pixelSize 向量转换为一个标量缩放因子

  • pixelSize : 如前所述,pixelSize 是一个 float2,代表了当前顶点位置在 X 和 Y 方向上,一个"世界单位"对应的屏幕像素数量
    • 例如,pixelSize = float2(4.0, 4.0) 表示 1 个单位 = 4 个像素。
  • dot(pixelSize, pixelSize) :
    • 计算向量的点积,即 pixelSize.x * pixelSize.x + pixelSize.y * pixelSize.y
    • 这等于 pixelSize 向量长度的平方 (|pixelSize|²)。
  • rsqrt(...) :
    • rsqrtReciprocal Square Root 的缩写,即 1 / sqrt(x)
    • rsqrt(dot(pixelSize, pixelSize)) 等价于 1 / sqrt(|pixelSize|²) = 1 / |pixelSize|
  • 物理意义
    • |pixelSize| 是一个综合的"每单位像素数"。
    • 1 / |pixelSize| 就是"每个像素对应的世界单位长度"。
    • 这个值越小,说明像素越"密集"(高分辨率、放大状态),我们需要更精细的 SDF 采样。
    • 这个值越大,说明像素越"稀疏"(低分辨率、缩小状态),我们可以使用更粗糙的 SDF 采样。

结论scale 的初始值是一个基础缩放因子,它反比于屏幕空间的像素密度。它确保了 SDF 采样能适应摄像机和缩放的变化。


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

这行代码对基础 scale 进行最终调整,使其适应具体的字体样式和渲染需求。

  • abs(input.texcoord1.y):

    • 这是最关键的调整项
    • Unity TMP_SDF 分析(一)数据来源1 中,详细讨论的,input.texcoord1.y (即 uv2.y) 存储了SDF 缩放因子
    • abs(...) 确保我们取其绝对值,因为负号可能用于标记"程序化加粗",而我们只关心其大小。
    • 来源 :这个值通常由 TextMeshPro 组件根据 lossyScale、字符 scalem_charWidthAdjDelta 计算得出,并通过 PackUV 方法写入 uv2.y
    • 作用 :它代表了该字符或文本对象的"内在"缩放级别。例如,一个大号字体需要比小号字体更高的 SDF 精度。
  • _GradientScale:

    • 这是一个材质属性 (Material Property)
    • 它是一个全局的、可由用户调整的缩放因子
    • 用途
      • 艺术调整:美术人员可以根据需要微调所有 TMP 文本的锐利程度。
      • 适配不同纹理 :如果 SDF 纹理的分辨率或生成参数不同,可以通过 _GradientScale 来补偿。
      • 性能/质量平衡 :减小 _GradientScale 可以让边缘更模糊(性能更好),增大则让边缘更锐利(质量更高)。

第6部分解析

csharp 复制代码
if (UNITY_MATRIX_P[3][3] == 0) 

scale = lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))));

其中_PerspectiveFilter在如下图设置,范围在0到1之间



1. if (UNITY_MATRIX_P[3][3] == 0)
  • UNITY_MATRIX_P : Unity 的投影矩阵 (Projection Matrix)
  • UNITY_MATRIX_P[3][3] :
    • 透视投影 矩阵中,[3][3] 位置的值通常是 0
    • 正交投影 矩阵中,[3][3] 位置的值通常是 1
  • 作用 :这是一个高效的条件判断 ,用于区分当前摄像机是透视模式 还是正交模式
    • 如果 == 0,说明是透视投影,需要进行额外的视角校正。
    • 如果 != 0 (通常是 1),说明是正交投影 ,跳过此校正(使用之前计算的 scale)。
2. dot(UnityObjectToWorldNormal(input.normal.xyz), normalize(WorldSpaceViewDir(vert)))

这是计算视角校正因子的核心。

  • input.normal.xyz : 顶点的法线向量 。对于一个标准的文本网格(平面),这通常是 (0, 0, 1)(0, 0, -1),指向 Z 轴正方向或负方向。
  • UnityObjectToWorldNormal(...) : 将法线从模型空间 (Object Space) 转换到世界空间 (World Space)。这考虑了对象的旋转。
  • WorldSpaceViewDir(vert) : 计算从顶点位置 指向摄像机位置视线向量 (View Direction) ,结果在世界空间
  • normalize(...): 将视线向量归一化为单位向量。
  • dot(...) :
    • 计算世界空间法线归一化视线向量点积 (Dot Product)
    • 点积的结果等于 |A||B|cosθ,其中 θ 是两个向量之间的夹角。
    • 因为两个向量都是单位向量(或接近单位向量),所以 dot 的结果就是 cosθ
  • abs(...) :
    • 取绝对值。因为法线可能指向 (0,0,1)(0,0,-1),我们关心的是夹角的大小 ,而不关心方向。abs(cosθ) 确保结果在 [0, 1] 范围内。
  • 物理意义
    • 视线垂直于文本平面 时(正视),θ = 90°cosθ = 0
    • 视线平行于文本平面 时(侧视),θ = 0°cosθ = 1
    • 所以,abs(dot(...)) 的值越大,表示视角越倾斜,文本平面在屏幕上的"投影面积"越小,有效像素密度越低,越容易模糊。
3. lerp(abs(scale) * (1 - _PerspectiveFilter), scale, abs(dot(...)))

这是一个线性插值 (Linear Interpolation) ,用于根据视角倾斜程度动态调整 scale

  • lerp(A, B, T) : 当 T=0 时返回 A;当 T=1 时返回 BT[0,1] 之间时返回 AB 的插值。
  • T = abs(dot(...)) : 视角倾斜程度,0 (正视) 到 1 (侧视)。
  • B = scale : 这是未校正 的原始 scale 值(基于距离和内在缩放)。
  • A = abs(scale) * (1 - _PerspectiveFilter) :
    • abs(scale): 确保 scale 为正。
    • (1 - _PerspectiveFilter): 这是校正强度的控制因子。
    • _PerspectiveFilter: 这是一个材质属性 (Material Property) ,通常范围是 [0, 1]
      • _PerspectiveFilter = 0: 完全禁用校正。A = abs(scale) * 1 = abs(scale)lerpabs(scale)scale 之间插值,效果很弱。
      • _PerspectiveFilter = 1: 最大校正。A = abs(scale) * 0 = 0lerp0scale 之间插值。
4.结论如下

在正交投影 (Orthographic) 下,物体的大小不随距离变化,scale 的计算相对简单且一致。

但在透视投影下:

  • 近处 的文本看起来更大,像素更密集,需要更小的 scale 值来保持边缘锐利。
  • 远处 的文本看起来更小,像素更稀疏,需要更大的 scale 值来避免锯齿。

然而,还有一个更隐蔽的问题:倾斜角度 (Viewing Angle)

  • 当你从一个倾斜的角度看一个文本平面时(比如抬头看高处的招牌),文本平面与视线的夹角会变小。
  • 这会导致文本在屏幕上被"压扁",其有效像素密度降低,即使距离不远,边缘也可能变得模糊。

这段代码就是为了解决这个视角倾斜带来的模糊问题。

相关推荐
mxwin2 小时前
Unity Shader 使用 Noise 图 制作Shader 溶解效果
unity·游戏引擎
mxwin4 小时前
Unity Shader 用 Ramp 贴图实现薄膜干涉效果
unity·游戏引擎·贴图·shader·uv
魔士于安5 小时前
Unity星球资源,八大星球,带fps显示
游戏·unity·游戏引擎·贴图·模型
张老师带你学6 小时前
unity资源,深空陨石,适合太空背景的游戏开发
游戏·unity·模型
鹿野素材屋8 小时前
Unity动画幅度太大怎么办
unity·游戏引擎
垂葛酒肝汤9 小时前
Unity Sprite Rect 越界问题笔记
笔记·unity·游戏引擎
平行云9 小时前
数字孪生信创云渲染系列(一):混合信创与全国产化架构
unity·ue5·3dsmax·webgl·gpu算力·实时云渲染·像素流送
废嘉在线抓狂.10 小时前
Unity拓展关于阵列物品生成以及物品替换
unity·游戏引擎
电子云与长程纠缠11 小时前
Godot学习01 - HelloWorld
学习·游戏引擎·godot