几天前,Gunnar向大家展示了 QML Scene Graph 技术。那篇文章里提到了一个新特性,那就是基于 “distance field” 的新的文本渲染技术。这个新技术能够释放 OpenGL 的全部能量,并带来以往 Qt 里所欠缺的文本渲染特性:可缩放的,次像素定位,以及次像素平滑……几乎没有性能上的损耗。
这里是一段视频,展示了新技术带来的改进: 原地址,搬运后地址。
Distance Field……好吃么?
现在,大家最想知道的可能是一件事情: “distance field” 是什么? Distance field 又叫做 distance transform 或 distance map ,衍生自图像处理技术,它会将每个像素到最近的字形轮廓边缘的“距离”,映射为一个 0.0 到 1.0 之间的值,在轮廓边缘上的像素,值为 0.5 , 轮廓外的像素的值趋于 0.0 ,而轮廓内的像素的值则趋于 1.0 。
- 一个常规字形
这个字形的 Distance field
红线标出了轮廓边缘
Distance field 可以用字形的高分辨率位图或是矢量图生成。常用的方法是 Brute-Force 算法。这方法很慢,很难一次生成数十个字形(或者上百个,在处理汉字时)的 distance field 。一些工具(比如这个)能够预渲染一组字形,并且保存起来在运行时加载。这个方案的灵活性太糟糕了,所以也被我们排除了。
我们选择了一种基于矢量字形的技术来生成 distance field ,简单的说,我们向内/向外缩放了字形的轮廓,然后用渐变填充两个轮廓间的空隙,然后用一个 64×64 像素的 8 位通道材质来保存结果。经过一些优化后(感谢 Kim ),在移动设备上生成一个 distance field 的花销不到 1 豪秒(平均速度),任何矢量字体都可以在运行时使用。
它是如何工作的
这项技术利用 GPU 的材质渲染管线来进行双线性插值计算,从像素到轮廓边缘的距离能够被精确地插值,在任何缩放条件下,字形都能被正确地重构出来。之后我们所需要做的,就是进行一次 aplha 测试:像素被显示还是被丢弃取决于一个阈值,在字形轮廓边缘上,这个阈值通常是 0.5 。经过处理后,字形便有了清晰锐利的轮廓。唯一的缺陷是字形的辺角会被切去一部分。但和未经处理直接缩放的字形相比,这个缺陷显得微不足道。
- 20 px 的字形在十倍缩放后,未使用 distance field 处理
20 px 的字形在十倍缩放后,经过 distance field 和 alpha 测试处理,使用一个同尺寸的材质
这项技术带来了视觉体验上的巨大改进,对运行效率也没有什么影响。因为对于图形硬件来说,这些处理几乎是“免费”的。
进一步的改进
高质量反锯齿
使用同一份 distance field 数据,我们还可以进行高质量的反锯齿处理。只要一行 shader 代码:
varying highp vec2 sampleCoord;
uniform sampler2D texture;
uniform lowp vec4 color;
uniform highp float distMin;
uniform highp float distMax;
void main() {
gl_FragColor = color * smoothstep(distMin, distMax, texture2D(texture, sampleCoord).a);
}
shader 用两个距离阈值(函数里的 distMin 和 distMax )来柔化字形的边缘。 Distance field 在两个阈值间被插值,以消除轮廓边缘的锯齿。当字形缩小时,柔化区域被放大,反之亦然。
除此以外,如果 GPU 足够强大(在桌面平台上),只需要在 shader 里增加几行代码,就可以实现次像素平滑。我们不再单纯地使用 distance field 数据来计算输出像素的 alpha 值,而是使用相邻像素的数据,来计算输出像素的各个颜色值。为此需要 5 张材质。计算红色的值时,需要将 distance field 的向左移 1/3 ,绿色居中,蓝色则向右移。这需要更多的计算,因此在移动平台上,次像素平滑默认是禁用的。不过,近来高分辨率显示器开始普及了,这使得次像素平滑没有以往那么重要了。
特效
除了反锯齿意外, distance field 也能用在其他地方。再写上几行 shader 代码,我们还能实现一些特效,例如勾线,模糊,或是阴影。通过这个技术,我们已经在 QML 里实现了三种风格的文本元素(勾线,阳文,阴文)。拿勾线来举例,我们只需计算两个距离上的 distance field ,然后用不同的颜色着色就可以了。这么做的效率很高,效果也不错,而且在不同的缩放等级上都有良好的表现(在不基于 Scene Graph 的 QtQuick 1.0 中,缩放一个特效文本的效果会很糟糕)。
一些细节
对于一个指定的字体,我们在一个 64×64 像素的材质上光栅化每一个字形(或是字形的 distance field 数据),并缓存起来。同一份 distance field 材质可以渲染任意尺寸的文本。与此相对的是, QPainter 会缓存不同尺寸的字形。显然,这降低了内存的消耗,提升了性能。
为了自由地缩放字形,我们需要禁用字体微调( hinting ),这样才能在缩放时,字形保持正确的位置,以及正确的次像素位置。
Hack It Your Way
如果你还没完全理解,那么去找 Qt 5 repositories ,看看 QtDeclarative 模块里的 qsgdistancefield 吧。
- 为了 debug ,你可以设置 QML_DISABLE_DISTANCEFIELD 环境变量来停止使用 distance field 渲染文本
- 在桌面平台上,次像素平滑默认是开启的。设置 qmlscene 的 text-gray-antialiasing 选项可以切换到标准的灰度抗锯齿上。
如果需要更多的技术细节,可以去读 Valve 的论文。
QT 如何支持高清屏,视网膜屏幕 – iNfc