首页 / Three.js 入门教程 / 3D 文本

Three.js 入门教程

3D 文本

本教程共 40 篇 · 第 30 篇 · 更新于 2026-08-14 · 约 8 分钟阅读

Three.js文字TextGeometryFontLoaderSpriteCanvasTexturetypeface

本节目标:学会在场景里加文字——用 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无(必填)字体对象
size100字号,注意默认很大
depth50文字厚度(旧版叫 height)
curveSegments12曲线分段数,越大越圆滑
bevelEnabledfalse是否开启倒角
bevelThickness / bevelSize10 / 8倒角的深度和大小
bevelSegments3倒角分层数
steps1挤出方向分段数
direction’ltr’文字方向

倒角和 curveSegments 都会大幅增加顶点数。小标题无所谓,长文本、手机端要节制,否则卡顿。

调参经验:sizedepth 要配合相机距离一起定。相机在 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 做进度提示,体验会好很多。