点击关注公众号,“技术干货” 及时达!
前言
本文主要讲解怎么将ttf文字文件转成threejs可用的json文件,从而通过加载器将指定文字的路径和顶点信息获取到,并且兼容孔洞的处理,既然有路径和顶点信息,可做的效果有很多,比如你可以做一个立体字,做一个霓虹灯,等等等等...,代码中提供了两种字体,和两种字体对应的精简版,你也可以找UI要一个比较可爱的字体替换成原文的字体效果~
另:考虑到每个人的电脑性能不一样,所以演示代码中不加发光效果,可以在下载后自己运行(本地运行默认开启发光特效)
字体转json
众所周知,在网上下载的字体,都是ttf
或者woff
格式的,并不能直接给threejs用,所以就需要一个工具转成threejs的FontLoader
支持的json文件,这里介绍一个工具:ttf_to_json,内含html文件,打开即用,双击打开后需要上传一个文件,如果想转全量字体,文件上传完毕之后点击convert
按钮,等待下载,这样的话,文件比较大,如果是复杂的字体,文件能达到18MB,这时就需要选择,如果你只需要几个字而已,则可以通过下面的Restrict character set.
选项去限制字符,这样转换出来的文件就会小很多,示例提供了4种字体文件,两款不同的字体,和他们的限制版,下面是工具样式的展示:
加载字体
通过同居的转换,我们得到了一个json格式的文字数据,下面就来将字体加载到threejs中吧!
import { Font, FontLoader } from 'three/addons/loaders/FontLoader.js';
// 字体加载器
const loader = new FontLoader();
// 加载字体
const loadFont = (url: string): Promise<Font> => {
return new Promise((res) => {
loader.load(`${import.meta.env.VITE_ASSETS_URL}assets/font/${url}.json`, function (response: Font) {
res(response)
});
})
}
加载器的load方法支持4个回调,分别是url(文件路径),onLoad(加载完毕),onProgress(加载进度)和onError(加载失败),这些通过源码也可以了解到
load(
url: string,
onLoad?: (data: Font) => void,
onProgress?: (event: ProgressEvent) => void,
onError?: (err: unknown) => void,
): void;
字体文件结构
其中onLoad的回调接受一个参数Font
,这个是加载字体方法的实例,支持方法generateShapes
,可以通过这个方法得到字体文件的形状Shapes
,
generateShapes(text: string, size: number): Shape[];
第一个参数是字符,第二个参数是尺寸,就是加载后字体的尺寸,决定了形状的大小。
加载字体
所以根据Font提供的generateShapes
方法,我们可以将指定字符并获得这个字符的形状
const shapes = font.generateShapes(t, 4);
console.log('shapes',shapes);
看这结构眼熟不,加载svg文件也是得到的这些形状,接下来就是根据形状生成形状几何体ShapeGeometry
,我们需要加几行代码,用于绘制形状
const color = 0x006699;
const matDark = new THREE.LineBasicMaterial({
color: color,
side: THREE.DoubleSide
});
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i];
const points = shape.getPoints();
const geometry = new THREE.BufferGeometry().setFromPoints(points);
const lineMesh = new THREE.Line(geometry, matDark);
lineText.add(lineMesh);
}
第一步遍历形状内的线段,第二步通过getPoints
获取到线段的顶点信息,将顶点信息赋值给geometry
,第三步根据线条的材质和顶点信息生成一个线段Line,于是得到了以下的文字
镂空点位
发现问题了么,我加载的是“猫” 字,得到的只有外框,没有孔洞的形状,所以需要进一步加工一下,将孔洞加载出来,在前面打印shapes
时候是包含holes
孔洞信息的。
所以我们需要单独将这些孔洞遍历一下并应用到shapes
中,在遍历shapes
之前,我们需要先获取到孔洞信息
for (let i = 0; i < shapes.length; i++) {
const shape = shapes[i];
if (shape.holes && shape.holes.length > 0) {
for (let j = 0; j < shape.holes.length; j++) {
const hole = shape.holes[j];
holeShapes.push(hole);
}
}
}
// 将孔洞信息添加到shapes中
shapes.push.apply(shapes, holeShapes);
通过这样的遍历,shapes就包含了外框的顶点信息和孔洞的信息,再去遍历shapes
就会将孔洞的线也加载出来,看一下效果吧。
调整位置
当然,我们不能只加载一个文字,如果是多个字该怎么办?将generateShapes
的第一个参数写成多个字?
对!哈哈哈
那我们以猫啃什锦黑
这几个字作为示例搞一下
确实可以将多个字一起绘制成Line,那么还有一个问题,线条的原点是左下角,但是我想要的是居中的效果,这样比较好操作,什么?用position修改?也可以,那就需要通过box3来计算出线条的尺寸再用公式去改变position,相对我下面说的方法比较复杂,直接修改geometry
,在前面遍历shapes
用来生成线段的时候就进行修改,通过 geometry.translate
去修改形状的偏移量
首先需要获取偏移量的具体数值,主要通过BufferGeometry
中的boundingBox
属性去获取尺寸,因为需要获取文字整体的尺寸来计算偏移量,所以我们需要计算shapes
所有的顶点。在遍历它之前
// 根据形状获得几何体
const geometry = new THREE.ShapeGeometry(shapes);
// 更新几何体边界
geometry.computeBoundingBox();
// 获取几何体尺寸 目的是居中
const xMid = - 0.5 * ((geometry?.boundingBox?.max?.x || 0) - (geometry?.boundingBox?.min.x || 0));
const yMid = - 0.5 * ((geometry?.boundingBox?.max?.y || 0) - (geometry?.boundingBox?.min.y || 0));
调用computeBoundingBox
是必须的,不然的话geometry?.boundingBox
是默认值null,获取到shapes
的x和y轴的偏移量就要在遍历的时候去应用了:
for (let i = 0; i < shapes.length; i++) {
...
let offset = new THREE.Vector3(xMid, yMid, 0)
const geometry = new THREE.BufferGeometry().setFromPoints(points);
geometry.translate(offset.x, offset.y, offset.z);
...
}
这样的话,文字整体就在整个坐标系的正中间了。
调整相机
现在文字已经加载到坐标系的中心,但是还有一个问题,为了适配更多文字的显示,相机不能是固定的角度去观察视图,这样会导致文字太多会显示不下,文字太小或者太少,又显得视图很空,所以需要动态调整,又两种方式可以去修改相机的视角,代码里的方法是根据条件修改fov(摄像机视锥体垂直视野角度)
可以参考官网-常见问题-如何在窗口调整大小时保持场景比例不变?。这里给的方案是屏幕尺寸改变后怎么调整fov,同理咱们的内容修改了,也可以参照这个方法,只不过不是调整当前屏幕尺寸和修改前屏幕尺寸的比例,文中修改的是文字的宽度和上次文字宽度的比例,let lastHeight = 27.339999496936798
这个数字是在fov为45,文字单个尺寸为4时,固定猫啃什锦黑这几个文字的尺寸,在保证这个尺寸的文字能完全在视图中显示为准,做一个标准数据,下面文字不管多少,都按照这个统一尺寸去衡量,
camera.aspect = window.innerWidth / window.innerHeight;
camera.fov = ( 360 / Math.PI ) * Math.atan( tanFOV * ( (xMid * 2) / lastHeight ) );
camera.updateProjectionMatrix();
第一行:修改屏幕比例的代码,其实可以去掉
第二行:根据初始尺寸lastHeight和当前文字尺寸做的比例,tanFOV
是初始fov比例,默认为tanFOV = Math.tan(((Math.PI / 180) * camera.fov / 2));
,
第三行:更新相机配置
支持动态输入文字
现在我们来写一个input输入框,在点击按钮的时候获取到输入的内容并绘制到canvas里,
<div class="input-btn">
<form action="#">
<input type="text" id="text" /><br />
<input type="submit" value="提交" />
</form>
</div>
var form = document.querySelector('form');
if(form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
var text =( document.getElementById('text') as any)?.value;
// alert("你输入的内容是:" + text);
createText(text)
});
}
通过input获得到text后通过封装好的createText
方法进行绘制,大概如下效果
在初始文字后重新渲染了一个文字,文字也绘制出来了,相机fov也调整了,那么问题来了,前面的文字没有清空,lineText.removeFromParent()
添加这行代码,就将原有的组清空掉,再新建一个组添加到scene中,lineText是用来存放绘制好的文字线段的,直接删除就可以了。
轮廓飞线
飞线的代码在源码中src/utils/fly.ts
文件中,具体讲解在之前的文章threejs开发可视化数字城市效果,源码在gitee上,可以自行下载克隆
let Fly = new TFly()
实例化构造函数后,需要在render方法中调用update
方法,Fly.upDate()
,使用也是老少皆宜,调用setFly
方法,传入相应参数即可
const createFly = (points: THREE.Vector3[]) => {
const flyGroup = Fly.setFly({
index: 0,
num: Math.floor(points.length * .2),
points: points,
spaced: 1000, // 要将曲线划分为的分段数。默认是 5
starColor: new THREE.Color(color),
endColor: new THREE.Color(color),
size: 0.2
})
flyLineGroup.add(flyGroup)
}
参数讲解
interface SetFly {
index: number, // 截取起点
num: number, // 截取长度 // 要小于length
points: Vector3[],
spaced: number // 控制速度,要将曲线划分为的分段数。默认是 5
starColor: Color,
endColor: Color,
size: number, // 飞线顶端顶点尺寸,后面会依次变小
}
发光体
参照[threejs渲染高级感可视化涡轮模型](https://juejin.cn/post/7301486808236130345)一文,内部详细讲解。
发光效果
点击关注公众号,“技术干货” 及时达!