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: ,

    13 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的字体渲染是有问题的, 我就遇到了下面这个问题

    11. QT 如何支持高清屏,视网膜屏幕 – iNfc

    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).

    This site uses Akismet to reduce spam. Learn how your comment data is processed.