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 应用都由三个核心对象构成,缺一不可:
- Scene(场景) 3D 世界的容器,所有可见对象(网格、灯光、粒子)都必须被添加到 Scene 中才能被渲染。类比于电影摄影棚:场景是摄影棚本身。
- Camera(相机) 定义"从哪里看"以及"看到什么范围"的视点。最常用是 PerspectiveCamera(透视投影,近大远小,模拟人眼)。相机本身也需要添加到场景中。
-
WebGLRenderer(渲染器)
将场景通过相机视角渲染到 HTML canvas 元素上的引擎。调用
renderer.render(scene, camera)生成一帧画面。
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():相机的投影矩阵在构造时计算。修改 fov、aspect、near 或 far 后,必须手动调用 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. 名词解释
-
WebGL 上下文
通过
canvas.getContext('webgl2')获取的对象,代表与 GPU 通信的接口。Three.js 的 WebGLRenderer 内部持有此上下文。每个 canvas 只能有一个活跃的 WebGL 上下文。 - NDC 坐标 归一化设备坐标(Normalized Device Coordinates)。WebGL 的坐标系中,x/y/z 都在 [-1, 1] 范围内,超出此范围的顶点被裁剪掉不渲染。Three.js 的相机投影矩阵负责把 3D 世界坐标转换为 NDC。
- 帧缓冲 GPU 中用于存储渲染结果的内存区域。默认渲染到屏幕的"前缓冲",Three.js 也支持渲染到纹理(WebGLRenderTarget),常用于后处理效果和反射镜。
-
抗锯齿(Antialias)
减少几何体边缘出现锯齿状像素的技术。WebGL 提供多重采样抗锯齿(MSAA),在
new WebGLRenderer({ antialias: true })时启用,以少量性能换取更平滑的边缘。 -
像素比(devicePixelRatio)
物理像素与 CSS 像素的比值。Retina/高清屏上通常是 2,意味着渲染分辨率是 CSS 分辨率的 2 倍。设置
renderer.setPixelRatio(dpr)可以让渲染更清晰,但 dpr > 2 时性能消耗很高,一般限制到最大 2。 - 视锥体(Frustum) 透视相机可见范围形成的四棱锥形体积,由 fov(视野角度)、aspect(宽高比)、near(近裁剪面)、far(远裁剪面)定义。只有位于视锥体内的物体才会被渲染——这就是"视锥体剔除"优化的基础。
本章小结:Three.js 的核心工作流是:创建 Scene → 添加 Camera → 配置 Renderer → 往场景添加 Mesh(几何体+材质)→ 在动画循环中调用 render。接下来第2章我们深入探索 Three.js 内置的各种几何体。