Chapter 01

Three.js 简介与基础场景

从 WebGL 底层到 Three.js 抽象层,掌握 Scene / Camera / Renderer 三要素,渲染第一个旋转的 3D 立方体

1. WebGL 与 Three.js 的关系

WebGL(Web Graphics Library) 是浏览器内置的底层图形 API,基于 OpenGL ES 2.0/3.0,允许 JavaScript 直接调用 GPU 进行 3D 渲染。然而,原生 WebGL 编程极其繁琐——绘制一个三角形需要写数百行代码,涉及缓冲区对象、着色器编译、顶点数组对象等底层概念。

Three.js 是建立在 WebGL 之上的高级抽象库,它把复杂的 WebGL 操作封装成直观的 JavaScript 对象(场景、相机、几何体、材质……),让开发者可以用几十行代码渲染出复杂的 3D 场景。

原生 WebGL — 绘制一个三角形

// 需要手动:创建缓冲区、编写
// GLSL 顶点/片段着色器、
// 编译链接程序、绑定属性...
// 约 120+ 行代码
const gl = canvas.getContext('webgl2');
const buf = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
// ... 100+ 行更多设置

Three.js — 绘制一个立方体

import * as THREE from 'three';
const geo = new THREE.BoxGeometry();
const mat = new THREE.MeshStandardMaterial();
const cube = new THREE.Mesh(geo, mat);
scene.add(cube);
// 完成!

2. 安装方式:npm / CDN / Vite

推荐:Vite + npm(现代工程化方式)

# 创建 Vite 项目
npm create vite@latest my-3d-app -- --template vanilla
cd my-3d-app

# 安装 Three.js
npm install three

# 如果使用 TypeScript,还需要类型定义
npm install --save-dev @types/three

# 启动开发服务器
npm run dev

CDN 方式(快速原型)

<!-- 使用 importmap 导入 Three.js 模块 -->
<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.168.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.168.0/examples/jsm/"
  }
}
</script>
<script type="module">
  import * as THREE from 'three';
  // 你的代码...
</script>
ℹ️

关于版本:Three.js 使用 r + 数字的版本命名(如 r168)。每个版本都可能有破坏性变更,建议固定版本号。本教程基于 r168(2024年)。

3. 三要素:Scene / Camera / Renderer

任何 Three.js 应用都由三个核心对象构成,缺一不可:

4. 第一个旋转立方体(完整代码)

下面是一个完整可运行的 Three.js 基础场景,包含立方体、相机、渲染器和动画循环:

JS// main.js
import * as THREE from 'three';

// ── 1. 创建场景 ──────────────────────────────────
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x060912); // 深蓝背景

// ── 2. 创建透视相机 ───────────────────────────────
// PerspectiveCamera(fov, aspect, near, far)
// fov: 垂直视野角度(75°)
// aspect: 画面宽高比
// near/far: 近/远裁剪面(只渲染此范围内的物体)
const camera = new THREE.PerspectiveCamera(
  75,
  window.innerWidth / window.innerHeight,
  0.1,
  1000
);
camera.position.set(0, 0, 3); // 相机位置:z 轴向后退 3 个单位

// ── 3. 创建渲染器 ───────────────────────────────
const renderer = new THREE.WebGLRenderer({
  antialias: true,     // 开启抗锯齿
  alpha: false         // 不透明背景
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 适配高清屏,限制最大 2x
document.body.appendChild(renderer.domElement); // 将 canvas 插入 DOM

// ── 4. 创建立方体 ───────────────────────────────
const geometry = new THREE.BoxGeometry(1, 1, 1); // 宽、高、深各 1 单位
const material = new THREE.MeshStandardMaterial({
  color: 0x049EF4,     // Three.js 蓝
  roughness: 0.3,
  metalness: 0.7
});
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);

// ── 5. 添加光源(MeshStandardMaterial 需要光照)──
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);

const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5);
directionalLight.position.set(5, 5, 5);
scene.add(directionalLight);

// ── 6. 动画循环 ─────────────────────────────────
function animate() {
  requestAnimationFrame(animate);  // 请求下一帧

  // 每帧旋转立方体
  cube.rotation.x += 0.005;
  cube.rotation.y += 0.01;

  renderer.render(scene, camera); // 渲染本帧
}
animate();

// ── 7. 响应式:监听窗口大小变化 ────────────────────
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix(); // 必须调用此方法让改变生效
  renderer.setSize(window.innerWidth, window.innerHeight);
});

为什么要 updateProjectionMatrix():相机的投影矩阵在构造时计算。修改 fovaspectnearfar 后,必须手动调用 updateProjectionMatrix() 才能让变化生效,否则画面不会更新。

5. requestAnimationFrame 动画循环详解

requestAnimationFrame(callback)(简称 rAF)是浏览器提供的原生 API,它会在浏览器准备好绘制下一帧时调用回调函数,通常与屏幕刷新率同步(60fps 显示器上约每 16.7ms 调用一次)。

// 基础模式——递归调用,形成循环
function animate() {
  requestAnimationFrame(animate); // 把自己注册为下一帧回调
  renderer.render(scene, camera);
}
animate(); // 启动循环

// 基于时间的动画(帧率无关)
const clock = new THREE.Clock();

function animate() {
  requestAnimationFrame(animate);
  const delta = clock.getDelta();   // 上一帧到这一帧的时间差(秒)
  const elapsed = clock.getElapsedTime(); // 从开始到现在的总时间

  // 无论 60fps 还是 120fps,旋转速度相同
  cube.rotation.y = elapsed * 0.5; // 每秒旋转 0.5 弧度

  renderer.render(scene, camera);
}
animate();
⚠️

帧率无关动画:如果直接写 cube.rotation.y += 0.01,在 60fps 设备上速度与 120fps 设备上不一样。正确做法是乘以 delta(帧时间差)或使用 clock.getElapsedTime() 绑定到绝对时间轴。

6. 画布尺寸响应式

Three.js 的 canvas 默认不会自动跟随容器大小变化,需要手动监听窗口 resize 事件:

// 完整的响应式处理
function onResize() {
  const w = window.innerWidth;
  const h = window.innerHeight;

  camera.aspect = w / h;
  camera.updateProjectionMatrix();

  renderer.setSize(w, h);
  renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
}

window.addEventListener('resize', onResize);

// 如果 Three.js 渲染到特定容器而非全屏
const container = document.getElementById('canvas-wrapper');
const resizeObserver = new ResizeObserver(() => {
  renderer.setSize(container.clientWidth, container.clientHeight);
  camera.aspect = container.clientWidth / container.clientHeight;
  camera.updateProjectionMatrix();
});
resizeObserver.observe(container);

7. 名词解释

本章小结:Three.js 的核心工作流是:创建 Scene → 添加 Camera → 配置 Renderer → 往场景添加 Mesh(几何体+材质)→ 在动画循环中调用 render。接下来第2章我们深入探索 Three.js 内置的各种几何体。