阅读 704

浅谈「我爱掘金」3D文字技术方案

这是我参与8月更文挑战的第5天,活动详情查看: 8月更文挑战

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

THREE.JS如何去展示中文字体

首先three.js原生有个textGeometry, 原生是支持的,但是你如果想支持各种中文字体,首先你需要一个下载字体的ttf文件。然后你就去一个网站叫做, 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 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? 我可以绘制一个烟花🎇动画

所以我将这些数组信息,去都转成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

生成几何体

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

几何体

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

交互层的思考🤔

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

  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:

吸附

总结

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

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

\

文章分类
前端
文章标签