three.js如何去展示中文字体
首先「three.js」原生有个「textGeometry」, 原生是支持的,但是你如果想支持各种中文字体,首先你需要一个下载字体的ttf文件。然后你就去一个网站叫做,
http://gero3.github.io/facetype.js/ 。你把你的「ttf」文件上传,然后将这些字体转成「json」, 再用three.js 自带的「fontLoader」 去解析这个json, 配合「textGeometry」 你就可以实现了。我这里做了一个简单的实现:
const loader = new THREE.FontLoader() loader.load('../json/alibaba.json', (font) => { const geometry = new THREE.TextGeometry('我爱掘金', { font: font, size: 20, height: 5, curveSegments: 12, bevelEnabled: false, bevelThickness: 10, bevelSize: 8, bevelOffset: 0, bevelSegments: 5, }) const material = new THREE.MeshBasicMaterial({ color: 0x50ff22 }) const mesh = new THREE.Mesh(geometry, material) this.scene.add(mesh) })
给大家看下gif效果图:
3d文字字体加载
其实不同的字体,对应不同的加载「json」,至于字体加粗,其实就是看字体有没有加粗的类型,如果有加粗的类型, 你就去展示就好了,其实还是不同的「json」, 我们这次的3D文字其实是没有采用这个「three」 这一套的。
3d文字技术选型
首先第一点不满足的就是我们的造型, 我们是做家居的,我们不光有3D视图展示,还有2D视图展示,所以就是一套数据分别在「3D」和「2D」都有对应的表达。看下面两张图:
3D视图2D视图
对吧,所以这是我当时去做技术评估不去考虑的最重要问题, 我们2D所有的数据都是用「SVG」去展示。所以说当时第一时间思考🤔,有没有一个库是可以支持解析字体文件转成「svg」的,功夫不负有心人哇,终于找到去「npm」找到了一个叫opentype.js 我们看下这个库的介绍:
❝opentype.js is a JavaScript parser and writer for TrueType and OpenType fonts.
It gives you access to the 「letterforms」 of text from the browser or Node.js. See https://opentype.js.org/ for a live demo.
❞
其实他的特性总结下来有下面:
- 非常高效
- 支持跑在浏览器和nodejs 中
其实当时我找到了很多社区方案, 有一个叫「text-to-svg」这个库, 看名字好像很满足我们的要求, 但是本着学习的本质,我只喜欢看源码,看看他到底用了啥,结果发现他是基于上面「opentype.js」 这个库去做了封装,那我肯定不用它了。我只需要字体被转换出来的「svg」信息,其实选用opentype.js 这个库还有两个原因哈**,第一支持ts ,第二的话他的周下载量是十分高的,至少说明他是稳定的。**
2d
有了「opentype.js」的加成,我们可以把输入的文字变成了转成「svg」的信息,这里主要用的一个api就是loadFont,然后就可以根据我们输入的文字,然后生成对应的「svg」, 我下面写一些伪代码:
async function make() { const font = await opentype.load( 'https://backend-public-asset-alpha.oss-cn-shanghai.aliyuncs.com/resources/website/font/11c302dd8c50619e4131da5d645fb422.otf' ) const map = new Map() return function (text) { // 防止重复添加 for (let i = 0; i < text.length - 1; i++) { const parseFont = font.getPath(text[i], 0, 150, 72) const char = text[i] console.log(text[i], '999') if (!map.has(char)) { map.set(text[i], parseFont.commands) } } return map } }
然后输入任何文字会产生,一些「SVG」path 信息。我们看下「2」 这个「svg」path信息。然后你可以看下:
信息
M其实对应的就是画布移动, L 就是画直线, C就是三阶贝塞尔曲线, Z 就是闭合path。svg的path 信息有了, 这里第一个难点出来了
贝塞尔曲线的离散
因为我们2d 可以用贝塞尔曲线去表达,但是我们3D的dataModel 中是没有这个数据去表示的,所以说什么呢,我得想好一个替代方案, 这里其实就设计到一个离散, 就是我将贝塞尔曲线,离散成多个点, 然后用直线去表达。这里不清楚的话,可以看我之前的一篇文章, 我里面对贝塞尔曲线做了详情讲解: 面试官问我会canvas? 我可以绘制一个烟花🎇动画
所以我将这些数组信息,去都转成2d点,去存储, 然后到这里很多人以为结束了,然后把这些2D线段去转成3D线段,你以为这样就结束了?
单一文字分组
我也以为事情就这么简单,直到我打了个 「e」,才发现事情并没有辣么简单。我们看下他的「svg」信息。
复杂信息
好家伙不仔细一看,原来有两个闭合路径,为什么会有这样呢?我这里给大家画个图 就知道了。
e字母
蓝色的其实对应的是「第一个path」 我们称作「Outer」, 红色其实对应的是内部。然后我就自然而然去思考了, 我去对数组进行分类。主要是根据闭合曲线的「Z」 去分组, 也就是一个字分成多个数据。
射线检测法
这里的话很多人以为结束了,但是其实并没有。这里涉及到射线检测法。算出一个文字每一个对应的「order」 ,大概是由【true, false..】组成的数组。false 表示逆时针, true表示 顺时针。 射线检测法的目的, 其实去判断这个path 和其他path 有没有交点, 交点为奇数其实就是逆时针, 为偶数其实就是顺时针。
「射线检测法」:其实就是取每个path 的第一个点在X轴方向上发出射线,然后算出与其他path 的交点个数,这里我不细讲了, 感兴趣的可以看我这篇文章 canvas 实现事件系统
至于为什么要去判断顺序, 与我们用的算法库「clipper」 有关系。有外轮廓和内轮廓之分, 内轮廓我们一般叫做洞也就是「hole」, 为了让大家有简单的概念, 我还是画图去表示:我就以「回」这个字举例子:
首先回这个字是也就是有两个path, 第二个path 肯定是内轮廓 也就是顺序肯定是【false,true】
我们先看下正确✅的图形:
正确
注意方向:外轮廓是「逆时针」, 内轮廓是「顺时针」
看下都是顺序是【true,true】的图形是这样的:
错误图形
顺序错误会导致,区域都会填充。所以为什么要有顺序了相信你也就明白了。 看下一个复杂的字吧感受下中国文字的博大精深。圗 和国
show
生成几何体
我们现在其实只是一个平面图形,文字肯定是个立方体, 这里 其实主要是生成顶面和侧面, 顶面的话其实就是通过底面上的点, 在底面的法向量延长一定距离。侧面的话,其实还是底面的点和顶面对应的点连起来的一条直线, 然后形成侧面。我还是画图:
几何体
每一个侧面大概是这样的一个过程。虚线就是对应点的连线,然后形成侧面。这个过程看着十分简单,其实在去写的时候还是十分复杂的。
交互层的思考🤔
交互层面的思考主要是三维空间中矩阵的应用。我们主要讲下这几点:
- 2d 坐标转换到3d坐标
- 垂直、水平、偏移、缩放
- 吸附
2d——3d
这里的话是这样的生成的「svg」 信息比如说他的开始点, 并不是在原点,但是我转到3d的世界坐标系,肯定默认是在原点的。所以的话,这里算出输入的字体的所有2d的信息,都要做一个「偏移Matrix」,因为在画布中移动,也就是文字跟着鼠标的点移动, 鼠标在哪里然后文字就在那里。这时候的「移动Matrix」 是相对世界原点的。所以这一层转换是非常重要的,而且还有一个「非常值得注意的点」是:svg 和canvas 的坐标系是在左上角的,也就是转到3d下来Y轴是要取反。我还是画图表示下哈:
2d-3d
垂直、水平、偏移、缩放
其实是这样的, 当你输入一行字默认是水平的,但是有需求我想把他搞成垂直的。这里就是对应的就是在X轴偏移和 Y轴偏移的问题。openType 默认是 可以批量解析字体的,但是呢我们不采用, 我还是一个个文字去处理,做到可控制。问题来了,每一个文字之间的间距, 怎么确保他们不相交呢?其实这里又涉及到计算每一个文字的「boundingBox」, 算出boundingBox之后呢,然后做一个距离叠加, 类似于reduce。因为输入的字有很多越往后面, 距离越大呗。缩放的话,其实是这样的,根据现有字体的大小 除上 基础字体大小 比如是20 算出一个scale, scale 可以算出缩放矩阵。物体字体大小变大, 然后✖️ 缩放矩阵。那么「bounding box」 自然也变化了。
整个一流程就是这样的:
变化
虚线框可以想象成每个矩形的bouding, 就是每个字, 每个字变化了, 矩形变化,想在 X轴 就在X轴,想在Y轴 就在Y轴。
吸附
吸附这东西其实没有啥悬乎的东西:
- 面对照相机📷
- 算旋转矩阵
总结下来就这两个东西。这里因为文字默认加载到的是相对于 世界坐标系的原点的, 比如你想吸附三维空间中的任意平面。所以说你可以基于这个平面建立一个局部坐标系,其实本质上就是「世界坐标系 —— 局部坐标系」的转换, 吸附到任意平面本质上,你可以只可以获得一个平面的法向量, 至少2个轴去确定一个局部坐标系, 这里默认选取X轴的正方向, 这样。这里 用到了three.js 的一个方法叫做「lookat」, 其实也就是模拟相机去算出这个矩阵。
参数就是个vector
❝vector - 一个表示世界空间中位置的向量。
也可以使用世界空间中x、y和z的位置分量。
旋转物体使其在世界空间中面朝一个点。
❞
由于还要让文字始终面对照相机📷 ,所以要计算照相机的方向 和平面的法向量去做点乘,来判断其他轴是否反向。大概就是这样:
我们看下gif: