threejs渲染3D字体介绍
# 概述
本文主要介绍如何通过 Three.js 生成 3D 文本。
# 效果展示
# 代码分析
核心代码部分就是通过 Three.js 中的 FontLoader
和 TextGeometry
来加载字体并创建 3D 文本。
核心代码如下:
const loader = new FontLoader();
loader.load(textFamily.value, function (font) {
const geometry = new TextGeometry(text.value, {
font: font, //字体
size: 120, //文本字体大小
height: 10, //文本厚度
curveSegments: 4, //定义曲线细节的段数
bevelEnabled: true, //启用斜面效果
bevelThickness: 10, //控制斜面的厚度
bevelSize: 8, //控制斜面的大小
bevelSegments: 5, //控制斜面的段数
});
geometry.computeBoundingBox();
const xOffset = (geometry.boundingBox.max.x - geometry.boundingBox.min.x) / 2;
textMesh = new THREE.Mesh(geometry, materials);
textMesh.position.set(-xOffset, 0, 0);
scene.add(textMesh);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
逐行分析如下:
- FontLoader 实例化:
const loader = new FontLoader(); //创建了一个 FontLoader 的实例,用于加载字体文件。
- 加载字体:
loader.load(textFamily.value, function (font) {
使用 loader.load()
方法加载字体,textFamily.value
是字体文件的路径。当字体加载完成后,会调用回调函数,参数font
是加载的字体对象。
- 创建文本几何体:
使用 TextGeometry
创建一个几何体。text.value
是要显示的文本内容,通过选项定义了文本的样式,各选项说明如上。
- 计算边界框:
geometry.computeBoundingBox(); //计算文本几何体的边界框,以便后续使用。
- 计算 X 偏移量:
const xOffset = (geometry.boundingBox.max.x - geometry.boundingBox.min.x) / 2;
计算文本的 X
轴偏移量,使文本在场景中居中。boundingBox.max.x
和 boundingBox.min.x
分别代表边界框的最大和最小 X
坐标。
- 创建网格并设置位置
textMesh = new THREE.Mesh(geometry, materials);
textMesh.position.set(-xOffset, 0, 0);
2
创建一个 Mesh
对象,将文本几何体和材料应用到该网格上。然后将网格的位置设置为 (-xOffset, 0, 0)
,使其在 X
轴上居中。
- 将网格添加到场景:
scene.add(textMesh);
将 textMesh
添加到 scene
场景中,使其在渲染中可见。
- 移除字体
如果需要移除字体,可以调用
scene.remove(textMesh)
# FontLoader
FontLoader
字体加载器并不是 Three.js 的核心实现,示例中的FontLoader
引用如下
"three/addons/loaders/FontLoader.js"
其代码如下:
import { FileLoader, Loader, ShapePath } from "three";
class FontLoader extends Loader {
constructor(manager) {
super(manager);
}
load(url, onLoad, onProgress, onError) {
const scope = this;
const loader = new FileLoader(this.manager);
loader.setPath(this.path);
loader.setRequestHeader(this.requestHeader);
loader.setWithCredentials(this.withCredentials);
loader.load(
url,
function (text) {
const font = scope.parse(JSON.parse(text));
if (onLoad) onLoad(font);
},
onProgress,
onError
);
}
parse(json) {
return new Font(json);
}
}
//
class Font {
constructor(data) {
this.isFont = true;
this.type = "Font";
this.data = data;
}
generateShapes(text, size = 100) {
const shapes = [];
const paths = createPaths(text, size, this.data);
for (let p = 0, pl = paths.length; p < pl; p++) {
shapes.push(...paths[p].toShapes());
}
return shapes;
}
}
function createPaths(text, size, data) {
const chars = Array.from(text);
const scale = size / data.resolution;
const line_height =
(data.boundingBox.yMax - data.boundingBox.yMin + data.underlineThickness) *
scale;
const paths = [];
let offsetX = 0,
offsetY = 0;
for (let i = 0; i < chars.length; i++) {
const char = chars[i];
if (char === "\n") {
offsetX = 0;
offsetY -= line_height;
} else {
const ret = createPath(char, scale, offsetX, offsetY, data);
offsetX += ret.offsetX;
paths.push(ret.path);
}
}
return paths;
}
function createPath(char, scale, offsetX, offsetY, data) {
const glyph = data.glyphs[char] || data.glyphs["?"];
if (!glyph) {
console.error(
'THREE.Font: character "' +
char +
'" does not exists in font family ' +
data.familyName +
"."
);
return;
}
const path = new ShapePath();
let x, y, cpx, cpy, cpx1, cpy1, cpx2, cpy2;
if (glyph.o) {
const outline =
glyph._cachedOutline || (glyph._cachedOutline = glyph.o.split(" "));
for (let i = 0, l = outline.length; i < l; ) {
const action = outline[i++];
switch (action) {
case "m": // moveTo
x = outline[i++] * scale + offsetX;
y = outline[i++] * scale + offsetY;
path.moveTo(x, y);
break;
case "l": // lineTo
x = outline[i++] * scale + offsetX;
y = outline[i++] * scale + offsetY;
path.lineTo(x, y);
break;
case "q": // quadraticCurveTo
cpx = outline[i++] * scale + offsetX;
cpy = outline[i++] * scale + offsetY;
cpx1 = outline[i++] * scale + offsetX;
cpy1 = outline[i++] * scale + offsetY;
path.quadraticCurveTo(cpx1, cpy1, cpx, cpy);
break;
case "b": // bezierCurveTo
cpx = outline[i++] * scale + offsetX;
cpy = outline[i++] * scale + offsetY;
cpx1 = outline[i++] * scale + offsetX;
cpy1 = outline[i++] * scale + offsetY;
cpx2 = outline[i++] * scale + offsetX;
cpy2 = outline[i++] * scale + offsetY;
path.bezierCurveTo(cpx1, cpy1, cpx2, cpy2, cpx, cpy);
break;
}
}
}
return { offsetX: glyph.ha * scale, path: path };
}
export { FontLoader, Font };
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
由上可知,FontLoader
就是一个继承自Loader
类,在其load
方法中实例化 Three.js 的FileLoader
类去加载字体文件,然后调用parse
方法去解析字体文件。
pares
方法中也就是实列化Font
类,在Font
类中定义了一个generateShapes
方法,用于绘制字体,该方法会在TextGeometry
中去调用。
# TextGeometry
TextGeometry
位于three/examples/jsm/geometries/TextGeometry.js
,其实现如下:
class TextGeometry extends ExtrudeGeometry {
constructor(text, parameters = {}) {
const font = parameters.font;
if (font === undefined) {
super();
} else {
const shapes = font.generateShapes(text, parameters.size);
if (parameters.depth === undefined && parameters.height !== undefined) {
console.warn(
"THREE.TextGeometry: .height is now depreciated. Please use .depth instead"
); // @deprecated, r163
}
parameters.depth =
parameters.depth !== undefined
? parameters.depth
: parameters.height !== undefined
? parameters.height
: 50;
// defaults
if (parameters.bevelThickness === undefined)
parameters.bevelThickness = 10;
if (parameters.bevelSize === undefined) parameters.bevelSize = 8;
if (parameters.bevelEnabled === undefined)
parameters.bevelEnabled = false;
super(shapes, parameters);
}
this.type = "TextGeometry";
}
}
export { TextGeometry };
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
TextGeometry
就是继承了ExtrudeGeometry
类,该类是 Three.js 中用于创建三维几何体的类,主要通过对二维形状进行挤出(extrusion)来生成三维对象。而TextGeometry
则就是调用Font
类的generateShapes
方法生成THREE.Shape
对象。
# ExtrudeGeometry
主要特点
基础形状:
ExtrudeGeometry
通过一个二维的形状(Shape
对象)作为基础形状进行挤出。挤出方向:可以指定挤出的深度和方向,允许生成具有不同高度的三维对象。
轮廓调整:可以在挤出过程中应用各种配置选项,比如切角、法线方向、凹凸等,使得生成的几何体更加丰富和复杂。
曲面细分:支持细分,可以通过设置细分参数来增加几何体的细节。
构造函数 ExtrudeGeometry
的构造函数通常如下:
const geometry = new THREE.ExtrudeGeometry(shapes, options);
- 参数:
shapes
: 一个或多个THREE.Shape
对象,定义了挤出的二维形状。options
: 一个对象,包含以下可选属性:depth
: 挤出的深度。bevelEnabled
: 是否启用斜角。bevelThickness
: 斜角的厚度。bevelSize
: 斜角的大小。bevelSegments
: 斜角的细分数。