3D 文本
本教程共 40 篇 · 第 30 篇 · 更新于 2026-08-14 · 约 8 分钟阅读
本节目标:学会在场景里加文字——用 FontLoader 加载 typeface.json 字体、用 TextGeometry 生成真正的 3D 立体文字,并了解 Sprite 贴图文字、CSS2DRenderer 等轻量替代方案。
场景加文字的三条路线
- TextGeometry:把文字变成真正的 3D 几何体。能旋转、受光照、投影,是”3D 感”最足的方案。代价是顶点多,中文需要自备字体。
- Canvas 画字 + Sprite:用 2D Canvas 把文字画成贴图,贴到永远面向相机的 Sprite 上。轻量,适合名字、标签。
- CSS2DRenderer:把 HTML 元素叠加在 3D 场景上(
three/addons/renderers/CSS2DRenderer.js)。最省性能,但元素不是场景的一部分,不能参与 3D 光照。
按需选择:标题用 TextGeometry,角色头顶名字用 Sprite,大量 UI 文字用 CSS2DRenderer。
三种方案也可以混用:场景标题用 TextGeometry 撑场面,角色名字用 Sprite,操作提示用 CSS2DRenderer,各干各的活。
FontLoader:加载 typeface.json 字体
three.js 不直接用系统字体,而是用 typeface.json——一种把字体轮廓转成 JSON 的格式,可以用 facetype.js 在线工具把 TTF 字体转换生成。官方仓库的 examples/fonts/ 目录自带几套:helvetiker、optimer、gentilis 等,分 regular 和 bold 两种字重。
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
const loader = new FontLoader();
// 回调式
loader.load('helvetiker_regular.typeface.json', (font) => {
// 拿到 Font 对象才能建 TextGeometry
});
// 或 Promise 式
const font = await loader.loadAsync('helvetiker_regular.typeface.json');
字体加载是异步的。在字体就绪之前不能创建 TextGeometry,这是新手最常见的报错来源。
Note官方字体只包含拉丁字符(英文、数字、标点)。直接写中文会缺字形,需要先用 facetype.js 把中文字体转成 typeface.json,而且转换后的文件可能很大,最好只转用到的字(子集化)。
字体加载失败多半是路径问题:typeface.json 要放在支持跨域的路径下(本地服务器或 CDN),直接双击 HTML 会被浏览器拦截;另外确认 URL 没写错,404 时 onError 回调会告诉你。
TextGeometry:生成立体文字
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
const geometry = new TextGeometry('Hello Three.js!', {
font: font, // 必填:FontLoader 加载好的字体
size: 1, // 字号
depth: 0.2, // 挤出深度(旧教程里叫 height)
curveSegments: 12, // 曲线分段数,越大越圆滑
bevelEnabled: true, // 是否加倒角
bevelThickness: 0.05,
bevelSize: 0.05,
bevelSegments: 3,
});
常用参数一览:
| 参数 | 默认值 | 作用 |
|---|---|---|
| font | 无(必填) | 字体对象 |
| size | 100 | 字号,注意默认很大 |
| depth | 50 | 文字厚度(旧版叫 height) |
| curveSegments | 12 | 曲线分段数,越大越圆滑 |
| bevelEnabled | false | 是否开启倒角 |
| bevelThickness / bevelSize | 10 / 8 | 倒角的深度和大小 |
| bevelSegments | 3 | 倒角分层数 |
| steps | 1 | 挤出方向分段数 |
| direction | ’ltr’ | 文字方向 |
倒角和 curveSegments 都会大幅增加顶点数。小标题无所谓,长文本、手机端要节制,否则卡顿。
调参经验:size 和 depth 要配合相机距离一起定。相机在 5 个单位外看文字,size 用 1 左右合适;depth 太薄文字像纸片,太厚又显得笨重,先试 0.1~0.3。bevelEnabled 打开后文字边缘有高光,视觉质感提升明显,代价是顶点数成倍上涨。
材质与居中
文字几何体可以指定一个材质,正反面统一;也可以给材质数组,正面和侧面分开:
const material = [
new THREE.MeshPhongMaterial({ color: 0x4dabf7 }), // 顶盖与底盖(正面和背面)
new THREE.MeshPhongMaterial({ color: 0x1c7ed6 }), // 侧面
];
const mesh = new THREE.Mesh(geometry, material);
(旧教程里的 MeshFaceMaterial 早就移除了,直接传数组即可。)
TextGeometry 的原点在文字左下角,直接摆放会偏。居中处理是标配:
geometry.computeBoundingBox();
geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
把包围盒中心取负号设到网格位置,文字就绕自己的中心旋转了。
注意:侧面材质要用受光的材质(MeshPhongMaterial、MeshStandardMaterial),立体感才出得来。用 MeshBasicMaterial 的话不受光,侧面和正面一个亮度,3D 效果直接归零。
Sprite 文字:轻量替代
需要”永远面向相机”的文字(角色名字、地标标签),Sprite 最合适:
// 1. 在 Canvas 上画字
const canvas = document.createElement('canvas');
canvas.width = 256;
canvas.height = 128;
const ctx = canvas.getContext('2d');
ctx.fillStyle = '#fff';
ctx.font = 'bold 48px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText('你好', 128, 64);
// 2. 转成纹理
const texture = new THREE.CanvasTexture(canvas);
// 3. 贴到 Sprite 上
const sprite = new THREE.Sprite(
new THREE.SpriteMaterial({ map: texture })
);
sprite.scale.set(2, 1, 1); // Sprite 用 scale 控制大小
scene.add(sprite);
Sprite 永远正对相机,没有透视变形,绘制成本几乎为零。想要中文?Canvas 用系统字体就行,没有 typeface 的限制。这是 Sprite 方案最实用的地方。
Sprite 文字的内容是画在 Canvas 上的静态图。想改文字(比如血量从 100 变到 80),重画 Canvas 后要设 texture.needsUpdate = true,纹理才会重新上传 GPU。每次重画都重建纹理对象是浪费,更新同一张纹理就行。
CSS2DRenderer:HTML 文字(了解)
CSS2DRenderer 把 HTML 元素当作”标签”叠在 3D 场景上。文字用普通 HTML/CSS 排版,支持任意字体、中文、富文本,开销最小,适合大量 UI 文字:
import { CSS2DRenderer, CSS2DObject } from 'three/addons/renderers/CSS2DRenderer.js';
const labelRenderer = new CSS2DRenderer();
labelRenderer.setSize(innerWidth, innerHeight);
labelRenderer.domElement.style.position = 'fixed';
labelRenderer.domElement.style.pointerEvents = 'none'; // 不挡鼠标
document.body.append(labelRenderer.domElement);
const div = document.createElement('div');
div.className = 'label';
div.textContent = '标签文字';
const label = new CSS2DObject(div);
label.position.set(0, 2, 0);
scene.add(label); // 元素跟随 3D 位置,但始终面向屏幕
// 每帧:renderer.render(scene, camera) 之后
// labelRenderer.render(scene, camera);
注意 CSS2DRenderer 是独立于 WebGLRenderer 的第二个渲染器,两个都要渲染。它适合”界面文字”,做不了立体字那种 3D 效果。
可运行示例:立体标题
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>30. 3D 文本</title>
<style>
* { margin: 0; padding: 0; }
body { overflow: hidden; background: #1a1a2e; }
canvas { display: block; }
#info {
position: fixed; left: 12px; top: 12px;
color: #fff; font: 14px/1.6 sans-serif;
background: rgba(0, 0, 0, 0.45); padding: 8px 12px; border-radius: 6px;
}
</style>
</head>
<body>
<div id="info">TextGeometry 立体文字 · 正面蓝色、侧面深蓝 · 绕中心旋转</div>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three@0.185.0/build/three.module.js",
"three/addons/": "https://cdn.jsdelivr.net/npm/three@0.185.0/examples/jsm/"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { FontLoader } from 'three/addons/loaders/FontLoader.js';
import { TextGeometry } from 'three/addons/geometries/TextGeometry.js';
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
scene.add(new THREE.AmbientLight(0xffffff, 1));
const dirLight = new THREE.DirectionalLight(0xffffff, 2);
dirLight.position.set(2, 4, 3);
scene.add(dirLight);
const camera = new THREE.PerspectiveCamera(50, innerWidth / innerHeight, 0.1, 100);
camera.position.set(0, 0, 5);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(innerWidth, innerHeight);
document.body.append(renderer.domElement);
const loader = new FontLoader();
loader.load(
'https://cdn.jsdelivr.net/gh/mrdoob/three.js@r185/examples/fonts/helvetiker_regular.typeface.json',
(font) => {
const geometry = new TextGeometry('Hello Three.js!', {
font: font,
size: 1,
depth: 0.2,
curveSegments: 12,
bevelEnabled: true,
bevelThickness: 0.05,
bevelSize: 0.05,
bevelSegments: 3,
});
// 正面浅蓝、侧面深蓝
const mesh = new THREE.Mesh(geometry, [
new THREE.MeshPhongMaterial({ color: 0x4dabf7 }),
new THREE.MeshPhongMaterial({ color: 0x1c7ed6 }),
]);
// 居中:让文字绕自己的中心旋转
geometry.computeBoundingBox();
geometry.boundingBox.getCenter(mesh.position).multiplyScalar(-1);
scene.add(mesh);
}
);
const clock = new THREE.Clock();
function animate() {
requestAnimationFrame(animate);
const t = clock.getElapsedTime();
const text = scene.children.find((o) => o.isMesh);
if (text) {
text.rotation.y = t * 0.5;
text.position.y = Math.sin(t) * 0.2;
}
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>
运行后文字缓缓旋转。把 bevelEnabled 改成 false 对比一下,边缘会变”锐利”;调大 curveSegments 可以看到曲线更圆滑(顶点也更多)。
字体文件只加载一次,多个 TextGeometry 可以共用同一个 Font 对象;配合 THREE.Cache.enabled = true,同一 URL 只请求一次(第 31 章会讲缓存)。中文字体尤其大,重复下载很浪费。
顶点预算:一段 20 个字母的文字,加上倒角后轻松超过一万个顶点,手机端长文本会明显掉帧。长段落优先 Sprite 或 CSS2DRenderer;实在要 3D,拆成多行、减小 curveSegments、关掉倒角,都能压住顶点数。
经验
TextGeometry 是静态几何,创建后别反复重建——改文字内容要先 geometry.dispose() 释放旧的再建新的。中文字体文件大,加载阶段配合第 31 章的 LoadingManager 做进度提示,体验会好很多。