之前在群里一直有粉丝对我做的3d文字感兴趣,今天它来了,我是如何去做的。本篇文章可能不会讲太多代码层面的东西,主要是一个技术方案从选型到最终实现中的遇到的一些问题。 主要是结合自己项目做的一些思考。希望能对你有所帮助,或者是开阔眼界。

# 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视图展示,所以就是一套数据分别在3D2D都有对应的表达。看下面两张图:

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.

其实他的特性总结下来有下面:

  1. 非常高效
  2. 支持跑在浏览器和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
  }
}

然后输入任何文字会产生,一些SVGpath 信息。我们看下2 这个svgpath信息。然后你可以看下:

信息

M其实对应的就是画布移动, L 就是画直线, C就是三阶贝塞尔曲线, Z 就是闭合path。 svg的path 信息有了, 这里第一个难点出来了

# 贝塞尔曲线的离散

因为我们2d 可以用贝塞尔曲线去表达,但是我们3D的dataModel 中是没有这个数据去表示的,所以说什么呢,我得想好一个替代方案, 这里其实就设计到一个离散, 就是我将贝塞尔曲线,离散成多个点, 然后用直线去表达。这里不清楚的话,可以看我之前的一篇文章, 我里面对贝塞尔曲线做了详情讲解: 面试官问我会canvas? 我可以绘制一个烟花🎇动画 (opens new window)

所以我将这些数组信息,去都转成2d点,去存储, 然后到这里很多人以为结束了,然后把这些2D线段去转成3D线段,你以为这样就结束了?

# 单一文字分组

我也以为事情就这么简单,直到我打了个 e,才发现事情并没有辣么简单。我们看下他的svg信息。

复杂信息

好家伙不仔细一看,原来有两个闭合路径,为什么会有这样呢? 我这里给大家画个图 就知道了。

e字母

蓝色的其实对应的是第一个path 我们称作Outer, 红色其实对应的是内部。然后我就自然而然去思考了, 我去对数组进行分类。 主要是根据闭合曲线的Z 去分组, 也就是一个字分成多个数据。

# 射线检测法

这里的话很多人以为结束了,但是其实并没有。这里涉及到射线检测法。 算出一个文字每一个对应的order ,大概是由【true, false..】组成的数组。 false 表示逆时针, true表示 顺时针。 射线检测法的目的, 其实去判断这个path 和其他path 有没有交点, 交点为奇数其实就是逆时针, 为偶数其实就是顺时针。

射线检测法: 其实就是取每个path 的第一个点在X轴方向上发出射线,然后算出与其他path 的交点个数,这里我不细讲了, 感兴趣的可以看我这篇文章 canvas 实现事件系统 (opens new window)

至于为什么要去判断顺序, 与我们用的算法库clipper 有关系。有外轮廓和内轮廓之分, 内轮廓我们一般叫做洞也就是hole, 为了让大家有简单的概念, 我还是画图去表示:我就以这个字举例子:

首先回这个字是也就是有两个path, 第二个path 肯定是内轮廓 也就是顺序肯定是【false,true】

我们先看下正确✅的图形:

正确

注意方向:外轮廓是逆时针, 内轮廓是顺时针

看下都是顺序是【true,true】的图形是这样的:

错误图形

顺序错误会导致,区域都会填充。 所以为什么要有顺序了相信你也就明白了。 看下一个复杂的字吧感受下中国文字的博大精深。圗 和国

show

# 生成几何体

我们现在其实只是一个平面图形,文字肯定是个立方体, 这里 其实主要是生成顶面和侧面, 顶面的话其实就是通过底面上的点, 在底面的法向量延长一定距离。侧面的话,其实还是底面的点和顶面对应的点连起来的一条直线, 然后形成侧面。 我还是画图:

几何体

每一个侧面大概是这样的一个过程。虚线就是对应点的连线,然后形成侧面。这个过程看着十分简单,其实在去写的时候还是十分复杂的。

# 交互层的思考🤔

交互层面的思考主要是三维空间中矩阵的应用。我们主要讲下这几点:

  1. 2d 坐标转换到3d坐标
  2. 垂直、水平、偏移、缩放
  3. 吸附

# 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轴。

# 吸附

吸附这东西其实没有啥悬乎的东西:

  1. 面对照相机📷
  2. 算旋转矩阵

总结下来就这两个东西。 这里因为文字默认加载到的是相对于 世界坐标系的原点的, 比如你想吸附三维空间中的任意平面。 所以说你可以基于这个平面建立一个局部坐标系,其实本质上就是世界坐标系 —— 局部坐标系的转换, 吸附到任意平面本质上,你可以只可以获得一个平面的法向量, 至少2个轴去确定一个局部坐标系, 这里默认选取X轴的正方向, 这样。这里 用到了three.js 的一个方法叫做lookat, 其实也就是模拟相机去算出这个矩阵。

参数就是个vector

vector - 一个表示世界空间中位置的向量。

也可以使用世界空间中x、y和z的位置分量。

旋转物体使其在世界空间中面朝一个点。

由于还要让文字始终面对照相机📷 ,所以要计算照相机的方向 和平面的法向量去做点乘,来判断其他轴是否反向。大概就是这样:

我们看下gif:

吸附

# 总结

本期的分享到此结束,如果你举得我哪里有写的不对的地方,欢迎评论区交流指正,如果想试玩的话, 可以百度搜索🔍红星设计云,https://www.mshejiyun.com/ 里面有很多好玩的工具。我是喜欢图形的Fly,我们下次再见👋拉, 如果有收获,别忘了点赞收藏加关注。

关注我的公众号 前端图形,获取更多好玩与有趣的图形知识。如果你也一样对技术热爱,喜欢图形和数据可视化📚并且为之着迷,欢迎加我个人微信(wzf582344150),将会邀请你加入我们的可视化交流学习群一起面向快乐编程~ 🦄。 我是Fly,在这个互联网技术疯狂快速迭代的时代中,很高兴能和你一起变强!😉

上一次更新时间: 2021/12/26 下午6:37:38