vary渲染图没了_在 WebGL 中渲染文字

dc401bd65e20eaefcd477671aa33d2ae.png

在上篇文章最后,我们提到希望在聚合后的圆形上标注出点的数量:

潘与其:使用 k-d tree 实现点聚合图​zhuanlan.zhihu.com
d78f1da07e529dbee4e461586769944d.png

本文会围绕“如何在 WebGL 中渲染文字“这个问题展开。

  • 首先我们会介绍 WebGL 中几种常见的渲染文字方式。
  • 然后我们将着重介绍基于有向距离场的实现思路、存在的问题(在线性时间生成、圆角问题)以及对应的解法。
  • 最后使用 Mapbox 的 tiny-sdf 实现文字标注效果,完善之前的点聚合图。

876cdf3be8d79aa94a83204b948ea010.png

几种渲染文字的方式

在介绍这些方法之前,必须要推荐 StackOverflow 上的这个总结性的回答,其中对比了各种方式的适用场景、优缺点,十分详细。因此本文这部分就尽量精简了:

Better Quality Text in WebGL​stackoverflow.com
84320f5f5e0fb234895d80498ce09425.png

使用 DOM API 添加 HTML 元素是最直接的方式,「WebGL Programing Guide」中也介绍了这种 HUD 方案,在展示静态文本、且数量不大的场景中是很适合的:

WebGL Text - HTML​webglfundamentals.org
6e038f0f602cfafebf6860b95184b72e.png

而 Bitmap Fonts 方案类似 Web 中的 Sprite,如果需要生成动态的文字,可以一次上传包含全部字符的纹理,并记录下每个字符的位置:

https://webglfundamentals.org/webgl/lessons/webgl-text-glyphs.html​webglfundamentals.org

3350b6448a920311c936f0483a4901f3.png
Bitmap Fonts Atlas

但是这种方式只需要适用有限字符的场景中,中文、日文这样的非拉丁文就不太适合了。另外,一张位图也不可能包含所有字号,在放大时必然出现人工痕迹,下图中左1、左2:

6fa44e33d95aec5eade74a7b85eeeaf6.png
https://github.com/libgdx/libgdx/wiki/Distance-field-fonts

在 3D 文字展示场景中,可以使用 TextGeometry。但即使只有少量文本,生成顶点数量也很大。

在艺术字效果(glitch、扭曲、粒子等)展示场景中,https://blotter.js.org/ 是一个不错的选择:

20422464b94b1da6ff8249c843a3784d.png
Blotter 艺术字效果

最后,还有一些直接在 GPU 中渲染字体的探索性方案:

Font Rendering is Getting Interesting · Aras' website​aras-p.info
0ba945d47126f195cf86ec048b1db8c9.png
  • 矢量纹理:GPU text rendering with vector textures
  • GLyphy:不同于其他使用纹理存储 SDF 的方案
  • PathFinder:使用 Rust 编写,需要 OpenGL ES 3.0+

但是,在地理信息展示的场景中,文本量很大,并且也需要展示中文这样的字符,因此以上方案都不太适用。

有向距离场

@拳四郎 的这篇文章介绍了使用有向距离场绘制基础图形、对 Valve 的论文:「Improved Alpha-Tested Magnification for Vector Textures and Special Effects」 的解读和实践,强烈推荐阅读:

拳四郎:Signed Distance Field​zhuanlan.zhihu.com
9ee2dab71efb03a05da646ea079a87b3.png

下面引用的图片来自「Shape Decomposition for Multi-channel Distance Fields」这篇论文,89 页但是干货满满,包含了 msdf 的巧妙思路,后面我们也会介绍。

距离场存储了矢量信息,例如下图 16 x 16 的网格中,每个格子都存储了到黑色边缘的距离,其中红色表示处于形状内部,蓝色表示处于外部:

7d7ee476ce12dca5b572a79baccc2919.png
有向距离场存储的信息

这样在使用距离场重建原始图形时,每个网格只需要以存储的距离为半径画圆,就能逼近原始图形(黑色部分)了:

6182f92ece34c7b5b3a28a77fc79c478.png
根据距离场重建

即使在低分辨率的距离场中,利用二次线性插值也能得到平滑的效果。例如下图中只有白色网格点处存储了原始的距离信息,其余都是通过插值得到的:

bd263b426b7aef17d520d0b1f004b430.png

生成了距离场之后,在 Shader 中使用也十分简单,至于 Outlining、Glows 等效果可以参考上面那篇文章,这里就不介绍了:

// https://github.com/Jam3/three-bmfont-text/blob/master/shaders/sdf.js

但是这种方法存在两个问题。首先在地理信息展示场景中,需要在运行时生成包含中文的文字,因此必须在线性时间内生成距离场。另外,在使用低分辨率的距离场重建时,字符的拐角处过于平滑不能保持原有的尖锐效果。下面我们来看针对这两个问题的解决方案。

线性时间生成

最暴力的遍历方法 O(n^2) 肯定是不能接受的,一张 300K 的图片就意味着需要 900 亿次对距离的运算,我们需要高效的 O(n) 的算法才能在运行时完成。

对于二维网格,距离场中的“距离”为欧式距离,因此 EDT(Euclidean Distance Transform)定义如下。其中 (x',y') 为构成形状的点集

,而 f(x, y) 为 sampled function。在网格中如果
则 f(x, y) = 0,否则为

4c6093c8f898d578fa3bf58aa9fc533a.png

其中第一部分与 y‘ 无关,可以展开成两趟一维的 DT 计算,其中第一趟固定 x':

4e3e925684a00ee0a76a1f486b217456.png

因此我们只需要考虑一维的距离平方:

07c7e76f19b803ce3e273ce6dc563e8a.png

如果从几何角度来理解上述一维距离平方场计算公式,其实是一组抛物线,每个抛物线最低点为 (q, f(q)):

5b102feb715ef72e32af14384b1786d4.png

因此这组抛物线的下界,即下图中的实线部分就代表了 EDT 的计算结果:

9426a012d75a62bfa7bcf63b18d0eef8.png

为了找出这个下界,我们需要计算任意两个抛物线的交点横坐标,例如对于(x=r, x=q)这两根抛物线,交点横坐标 s 为:

474c73c0f33e35f1d4bfc247472915c9.png
抛物线求交

现在我们有了计算 EDT 1D 的预备知识,按照从左往右的顺序,将下界最右侧的抛物线序号保存在 v[] 中。下界中每一段抛物线的边界保存在 z[] 中。这样计算下一段抛物线 x=q 时,只需要与 v[k] 抛物线求交,交点横坐标与 z[k] 的位置关系只有如下两种:

abd886421a956cc110816e1e79cb33f3.png
两个抛物线交点与当前最右交点的位置关系

完整算法如下:

23dda6243aec728fd756271a63cbb1b8.png

Mapbox 的 tiny-sdf 实现了上述算法(EDT 1D),连变量名都是一致的。我们后续的 DEMO 将直接使用它在运行时生成 SDF 纹理。

mapbox/tiny-sdf​github.com
43d68108364da7c30a64618938f08e64.png

对于 2D EDT 的计算正如我们本节开头介绍的,分解成两趟 1D 距离平方,最后开方得到结果。这里也能直接看出对于 height * width 尺寸的网格,复杂度为 O(n):

function 

最后,除了欧式距离(

),如果要计算 city block(
) 或者 Chessboard(
) ,同样也可以分解成两趟实现 O(n) 复杂度。详细算法可以参考这份 Slide:「CS664 Computer Vision 7. Distance Transforms」 。

f9b155b24f82df6db3a2d906fe50a823.png
原始图、Euclidean distance(L2)、City block(L1) 和 Chessboard 距离场

圆角问题

虽然在地图场景中,对于放大字体时呈现的平滑转角是可以容忍的,但是对于这个问题的解法还是有必要了解一下。

21f4af4d5ccea431c0bd689964c35a84.png
左侧为 SDF 重建效果,右侧为 MSDF

距离场是可以进行集合运算的。例如下图中,我们将两个距离场分别存储在位图的两个分量(R、G)中,在重建时,虽然这两个距离场转角是平滑的,但是进行求交就能得到锐利的还原效果:

2ed09d8165b4acd03b8b1c1c4fc72a6a.png
使用分解后的两个距离场求交重建

分解算法可以参考原论文「Shape Decomposition for Multi-channel Distance Fields」 中4.4 节:Direct multi-channel distance field construction。

在实际使用时,作者提供了 MSDF 生成工具 https://github.com/Chlumsky/msdfgen,可以看出 MSDF 在低分辨率效果明显更好,甚至优于更高分辨率的 SDF:

7ab5bf255f6dfd174bb82433b5c34be7.png
MSDF 与 SDF 效果对比

在重建时使用 median:

// https://github.com/Jam3/three-bmfont-text/blob/master/shaders/msdf.js

DEMO 效果

为我们之前的点聚合图加上标注吧:

f47a5402545e4881be2c2eae25d2c80b.png
https://www.zhihu.com/video/1110879349514989568

使用 tiny-sdf 生成 SDF 十分简单,当然更好的改进方式是对当前出现的所有字符生成一张 atlas,每次有新字符出现再更新它:

import 

DEMO 地址:https://xiaoiver.github.io/custom-mapbox-layer/VectorTileClusterLayer.html

GitHub 地址:https://github.com/xiaoiver/custom-mapbox-layer/blob/master/src/layers/VectorTileClusterLayer.ts

参考资料

  • 「Shape Decomposition for Multi-channel Distance Fields」
  • 「Distance Transforms of Sampled Functions」
  • 「A GENERAL ALGORITHM FOR COMPUTING DISTANCE TRANSFORMS IN LINEAR TIME」
  • 「CS664 Computer Vision 7. Distance Transforms」
  • Better Quality Text in WebGL
  • Font Rendering is Getting Interesting · Aras' website
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值