在使用TextMeshPro的时候遇到了字体黑底的问题,类似下图这样
当字体较大的时候表现正常,当缩小到一定程度就会出现黑底。这个情况让人第一时间就是怀疑SDF计算缩放的时候存在问题。在我们重新导出字体,调整图集字体大小以及Padding后,让SP/PD值差不多大于3%就能解决这个问题
这个SP/PD就是Padding值/Samping Point Size
Also, note the SP/PD ratio. The generator calculates this by dividing the Sampling Point Size by the Padding Value. For best results, this value should be under 10%. If it’s above 10%, either increase the Atlas Resolution or decrease the Padding value, then regenerate the font until the value is below 10%.
上面说到SP/PD值应该低于10%,这个是从节省图集大小来考虑的,当SP/PD过大,每个字符间隙很大,对于我们描边也没意义,10%就是差不多很够用的一个值。但是并没有说到需要一个下限,如果低于这个下限会出什么问题,文档没有说明,很多博客也没说到过。没错,如果过低就会出现上面看到的黑底问题,具体是为什么我们稍后讨论,这里先讲讲如何解决这个问题。
解决方案
我们的目标是控制SP/PD值在一个区间值内,低于某个值会出现黑底问题,高于会存在浪费空间的问题。10%为一个比较合理的上限,那个这个下限是多少呢?官方没有规定,那也只有我们自己看着合适就好,这充分贯彻了那句话”计算机图形学的第一定律:如果它看起来是对的,那么它就是对的“
在我调整Padding将SP/PD控制在4%以上之后,肉眼看过去基本就感觉不出来了。
想要调整SP/PD值的方法如下:
- 调整Padding
- 调整采样字体大小Samping Point Size
-
调整字体图集分辨率
了解更多
了解了如何解决这个问题,但是这是为什么呢?我脑子里面冒出了下面几个问题
- 为什么只有在缩放的时候或者字体很小的时候会出现黑底?
- 是不是只有使用outlie的时候才会出现黑底?
- 不使用也出现黑底,那个这个黑底出现的原因是什么?
浅浅的了解SDF
要想解决上面的问题,我们首先要浅浅的了解下什么是SDF,为什么SDF可以无限放大。
SDF(Signed Distance Field)是一种用于将矢量图形转换为比特图的技术,我们生成的图集其实就是一张单通道(Alpha)的纹理,这个alpha值就是存储的距离或者说是梯度。
通过这张纹理,我们可以知道字体的边界在哪里,有点类似边缘检测的卷积,也是知道边界梯度,我们可以拿这这个值无限放大字体,也可以在边界添加各种效果,其中当然就包括我们今天讨论的描边。
当然,纹理存储的信息是 [0,1]范围的值,在计算中需要-0.5映射到[-0.5,0.5]区间,0表示字体边界,大于表示在字体边界内,小于0表示在字体边界外。我们可以从TMP_SDF-Mobile.shader这个文件中窥探一二
float bias = (0.5 - weight) * scale - 0.5;
寻找线索
那个值是关键?
知道了解决问题的关键在于shader中,我们就需要研究下一个字体的渲染过程,我们测试发现,只有在缩放或字体极小的时候会出现黑底,那这个问题的关键肯定于缩放相关,Distance Field中很关键的scale值就与缩放相关,下面我们先看看代码
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);
这个scale计算的是当前分辨率下,顶点的物理尺寸,当我们缩放字体的时候scale会变得很小,这个时候就出现了黑底。
接下来我们就是需要找到为什么scale值小的时候会出现黑底?
为什么scale值变小后会出问题?
half d = tex2D(_MainTex, input.texcoord0.xy).a * input.param.x;
half4 c = input.faceColor * saturate(d - input.param.w);
理论上c的值在边界外a通道应该小于裁剪阈值0.01,但是没有,就可以推断上面代码d是大于input.param.w(bias),可以推断下面几种可能性
- d计算错误
- 采样得到alpha通道的梯度值错误
- 计算所得scale值错误
- bias计算错误
其中bias如果错误也可以转换到scale计算的错误。现在把范围缩小为到底是scale计算错误还是采样错误。由于我的目前比较菜,也只能猜测。我认为scale由于采用的是变换矩阵计算而得,是不会出现问题,如果出问题,都出问题了,能出问题的一定是我们导出的设置相关引起的问题。那必然就是我们导出的纹理有问题
我们通过上面图对比,相同PointSize与TextureSize下,Padding更大的那个纹理,alpha通道表示得到的梯度更加的光滑,这个就可以解释文本在缩小的时候导致物理尺寸变小,导致纹理采样的时候不能准确计算边界导致了黑底的存在。
以上都是自己根据看现成代码推理而得,个人不针对推理负责。不过解决方案你了解即可。