QML Scene Graph 的文本渲染

foolegg | 2011/07/30

几天前,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

红线标出了轮廓边缘 红线标出了轮廓边缘

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 处理

20 px 的字形在十倍缩放后,经过 distance field 和 alpha 测试处理,使用一个同尺寸的材质 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 的论文

原文链接:Text Rendering in the QML Scene Graph

    Tags: ,

    12 FEEDBACKS

    1. uu

      谁能告诉我为什么qt程序有时候中文字体为什么变那么粗壮?

    2. @uu 应该是伪粗体补丁的bug……

    3. our1944

      阀值?阈值?

    4. @our1944 已经修改,感谢指出。

    5. adaptee

      看演示视频的样子,很不错啊

    6. foolegg

      @our1944 现在才知道这个是别字OJZ…

    7. uu

      @csslayer 请问有没有办法解决?看着憋屈……

    8. Qt下的字体渲染终于也要由图形处理其来解决了…… 禁用hinting……在一些低分辨率的显示器上,为了笔画能看清楚,hinting还是相当重要的。

      另外其实还有错别字……豪秒->毫秒

    9. stecue

      还要禁用hinting啊,那那个kword字体破碎的bug永远解决不了了么。

    10. Evince

      目前QtQuick2的字体渲染是有问题的, 我就遇到了下面这个问题

    Leave a Reply

    Your email address will not be published. Required fields are marked *

    Note: Commenter is allowed to use '@User+blank' to automatically notify your reply to other commenter. e.g, if ABC is one of commenter of this post, then write '@ABC '(exclude ') will automatically send your comment to ABC. Using '@all ' to notify all previous commenters. Be sure that the value of User should exactly match with commenter's name (case sensitive).