![dc401bd65e20eaefcd477671aa33d2ae.png](https://i-blog.csdnimg.cn/blog_migrate/dd579b511c2424f6823ddae2944f208d.jpeg)
在上篇文章最后,我们提到希望在聚合后的圆形上标注出点的数量:
潘与其:使用 k-d tree 实现点聚合图zhuanlan.zhihu.com![d78f1da07e529dbee4e461586769944d.png](https://i-blog.csdnimg.cn/blog_migrate/69be92d51df1072358be54c8f65aca34.jpeg)
本文会围绕“如何在 WebGL 中渲染文字“这个问题展开。
- 首先我们会介绍 WebGL 中几种常见的渲染文字方式。
- 然后我们将着重介绍基于有向距离场的实现思路、存在的问题(在线性时间生成、圆角问题)以及对应的解法。
- 最后使用 Mapbox 的 tiny-sdf 实现文字标注效果,完善之前的点聚合图。
![876cdf3be8d79aa94a83204b948ea010.png](https://i-blog.csdnimg.cn/blog_migrate/ea85f5b9c912a927efaa100562e992ca.jpeg)
几种渲染文字的方式
在介绍这些方法之前,必须要推荐 StackOverflow 上的这个总结性的回答,其中对比了各种方式的适用场景、优缺点,十分详细。因此本文这部分就尽量精简了:
Better Quality Text in WebGLstackoverflow.com![84320f5f5e0fb234895d80498ce09425.png](https://i-blog.csdnimg.cn/blog_migrate/7c1b7c566fd1385581bae5fdb70e3ff8.png)
使用 DOM API 添加 HTML 元素是最直接的方式,「WebGL Programing Guide」中也介绍了这种 HUD 方案,在展示静态文本、且数量不大的场景中是很适合的:
WebGL Text - HTMLwebglfundamentals.org![6e038f0f602cfafebf6860b95184b72e.png](https://i-blog.csdnimg.cn/blog_migrate/6f92190ce548f3ccecb77e0126ca29f3.jpeg)
而 Bitmap Fonts 方案类似 Web 中的 Sprite,如果需要生成动态的文字,可以一次上传包含全部字符的纹理,并记录下每个字符的位置:
https://webglfundamentals.org/webgl/lessons/webgl-text-glyphs.htmlwebglfundamentals.org![3350b6448a920311c936f0483a4901f3.png](https://i-blog.csdnimg.cn/blog_migrate/c28b433715723323e266dcd98abd6076.jpeg)
但是这种方式只需要适用有限字符的场景中,中文、日文这样的非拉丁文就不太适合了。另外,一张位图也不可能包含所有字号,在放大时必然出现人工痕迹,下图中左1、左2:
![6fa44e33d95aec5eade74a7b85eeeaf6.png](https://i-blog.csdnimg.cn/blog_migrate/53e6e097192fcaf6243fce1b70ace83f.jpeg)
在 3D 文字展示场景中,可以使用 TextGeometry。但即使只有少量文本,生成顶点数量也很大。
在艺术字效果(glitch、扭曲、粒子等)展示场景中,https://blotter.js.org/ 是一个不错的选择:
![20422464b94b1da6ff8249c843a3784d.png](https://i-blog.csdnimg.cn/blog_migrate/e8ad6cad2e653f63f7d57df44177bdb6.jpeg)
最后,还有一些直接在 GPU 中渲染字体的探索性方案:
Font Rendering is Getting Interesting · Aras' websitearas-p.info![0ba945d47126f195cf86ec048b1db8c9.png](https://i-blog.csdnimg.cn/blog_migrate/ec25b9227eb21d4ac446ba300a062bc1.jpeg)
- 矢量纹理: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 Fieldzhuanlan.zhihu.com![9ee2dab71efb03a05da646ea079a87b3.png](https://i-blog.csdnimg.cn/blog_migrate/39155810ba5181d9f8425a7b968e62fb.jpeg)
下面引用的图片来自「Shape Decomposition for Multi-channel Distance Fields」这篇论文,89 页但是干货满满,包含了 msdf 的巧妙思路,后面我们也会介绍。
距离场存储了矢量信息,例如下图 16 x 16 的网格中,每个格子都存储了到黑色边缘的距离,其中红色表示处于形状内部,蓝色表示处于外部:
![7d7ee476ce12dca5b572a79baccc2919.png](https://i-blog.csdnimg.cn/blog_migrate/7bd5a05ad2cd2f0b3c12ed334d16e840.jpeg)
这样在使用距离场重建原始图形时,每个网格只需要以存储的距离为半径画圆,就能逼近原始图形(黑色部分)了:
![6182f92ece34c7b5b3a28a77fc79c478.png](https://i-blog.csdnimg.cn/blog_migrate/9ecbda160e6ed610ee585463a4576044.jpeg)
即使在低分辨率的距离场中,利用二次线性插值也能得到平滑的效果。例如下图中只有白色网格点处存储了原始的距离信息,其余都是通过插值得到的:
![bd263b426b7aef17d520d0b1f004b430.png](https://i-blog.csdnimg.cn/blog_migrate/9ff27ad38c308cc0671359043b67056b.jpeg)
生成了距离场之后,在 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') 为构成形状的点集
![4c6093c8f898d578fa3bf58aa9fc533a.png](https://i-blog.csdnimg.cn/blog_migrate/df1ca68cbe3d1864a32972159653c0cc.png)
其中第一部分与 y‘ 无关,可以展开成两趟一维的 DT 计算,其中第一趟固定 x':
![4e3e925684a00ee0a76a1f486b217456.png](https://i-blog.csdnimg.cn/blog_migrate/37162b7321dd83af6d033b0c69fc0252.jpeg)
因此我们只需要考虑一维的距离平方:
![07c7e76f19b803ce3e273ce6dc563e8a.png](https://i-blog.csdnimg.cn/blog_migrate/2900f4a793550320ce868020e1aca02c.png)
如果从几何角度来理解上述一维距离平方场计算公式,其实是一组抛物线,每个抛物线最低点为 (q, f(q)):
![5b102feb715ef72e32af14384b1786d4.png](https://i-blog.csdnimg.cn/blog_migrate/b9b12989275ad8b1144028bf089d3227.jpeg)
因此这组抛物线的下界,即下图中的实线部分就代表了 EDT 的计算结果:
![9426a012d75a62bfa7bcf63b18d0eef8.png](https://i-blog.csdnimg.cn/blog_migrate/f0d87212777ca080865a9167aff3decb.jpeg)
为了找出这个下界,我们需要计算任意两个抛物线的交点横坐标,例如对于(x=r, x=q)这两根抛物线,交点横坐标 s 为:
![474c73c0f33e35f1d4bfc247472915c9.png](https://i-blog.csdnimg.cn/blog_migrate/630db6b707b9726c3a7f292fd0b08f0b.jpeg)
现在我们有了计算 EDT 1D 的预备知识,按照从左往右的顺序,将下界最右侧的抛物线序号保存在 v[] 中。下界中每一段抛物线的边界保存在 z[] 中。这样计算下一段抛物线 x=q 时,只需要与 v[k] 抛物线求交,交点横坐标与 z[k] 的位置关系只有如下两种:
![abd886421a956cc110816e1e79cb33f3.png](https://i-blog.csdnimg.cn/blog_migrate/2207546ce286ac1c7805a924d62d7276.jpeg)
完整算法如下:
![23dda6243aec728fd756271a63cbb1b8.png](https://i-blog.csdnimg.cn/blog_migrate/c85528cab62cf1e35cfa1b5ccc33ac8f.jpeg)
Mapbox 的 tiny-sdf 实现了上述算法(EDT 1D),连变量名都是一致的。我们后续的 DEMO 将直接使用它在运行时生成 SDF 纹理。
mapbox/tiny-sdfgithub.com![43d68108364da7c30a64618938f08e64.png](https://i-blog.csdnimg.cn/blog_migrate/a3d8c8c1773ae863d78ee5f1372b3b26.png)
对于 2D EDT 的计算正如我们本节开头介绍的,分解成两趟 1D 距离平方,最后开方得到结果。这里也能直接看出对于 height * width 尺寸的网格,复杂度为 O(n):
function
最后,除了欧式距离(
![f9b155b24f82df6db3a2d906fe50a823.png](https://i-blog.csdnimg.cn/blog_migrate/80cf7e29b9df2492bf6efc9d3e3bbaa8.jpeg)
圆角问题
虽然在地图场景中,对于放大字体时呈现的平滑转角是可以容忍的,但是对于这个问题的解法还是有必要了解一下。
![21f4af4d5ccea431c0bd689964c35a84.png](https://i-blog.csdnimg.cn/blog_migrate/45b55f17a5de81049c3d8665702dc986.jpeg)
距离场是可以进行集合运算的。例如下图中,我们将两个距离场分别存储在位图的两个分量(R、G)中,在重建时,虽然这两个距离场转角是平滑的,但是进行求交就能得到锐利的还原效果:
![2ed09d8165b4acd03b8b1c1c4fc72a6a.png](https://i-blog.csdnimg.cn/blog_migrate/67b23e08f608813fb4523dd98d3e0a99.jpeg)
分解算法可以参考原论文「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](https://i-blog.csdnimg.cn/blog_migrate/b3d0145642b6b865eca861fb3091b7d8.jpeg)
在重建时使用 median:
// https://github.com/Jam3/three-bmfont-text/blob/master/shaders/msdf.js
DEMO 效果
为我们之前的点聚合图加上标注吧:
![f47a5402545e4881be2c2eae25d2c80b.png](https://i-blog.csdnimg.cn/blog_migrate/8aa15e9b389089436567bc3b76e6c7f8.jpeg)
使用 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