这个特效拿去表白,CL都免了~

2024-11-19 08:30   重庆  

点击关注公众号,“技术干货” 及时达!

前言

本文主要讲解怎么将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)一文,内部详细讲解。

发光效果

点击关注公众号,“技术干货” 及时达!

稀土掘金技术社区
掘金,一个帮助开发者成长的技术社区
 最新文章