Three.js:在浏览器中构建 3D 世界
认识 WebGL 与 Three.js
为什么需要 WebGL?
在现代浏览器中,我们通常使用 HTML、CSS 和 JavaScript 来构建二维界面。但当需求进入三维领域——如数据可视化、产品展示、游戏、AR/VR 体验时,仅靠 DOM 和 Canvas 2D 已经无法实现高质量的实时渲染。这时,WebGL(Web Graphics Library)便成为浏览器与 GPU 之间沟通的桥梁。
WebGL 是一套基于 OpenGL ES 的底层 JavaScript API,允许你通过 <canvas> 元素直接调用图形硬件,绘制复杂的 3D 图形。它的优势在于:
- 高性能:直接访问 GPU,支持并行处理。
- 跨平台:主流浏览器以及移动设备都提供支持。
- 无需插件:纯网页技术,用户端零安装。
但 WebGL 的原始接口非常底层。仅仅绘制一个旋转的彩色立方体,就需要编写上百行的着色器代码、缓冲区和矩阵操作。对初学者而言,这既是强大的能力,也是陡峭的学习曲线。
Three.js 的使命
Three.js 是一个轻量、易用的 JavaScript 3D 库,它对 WebGL 进行了高度抽象和封装。它不仅大幅降低了入门门槛,还提供了丰富的功能模块,让你能够像搭积木一样在浏览器中构建 3D 世界。
学习 Three.js,意味着你不再需要从零开始处理顶点着色器、矩阵变换、光照模型等底层细节,而是专注于三样核心事物:
- 场景(Scene):你构建的世界。
- 相机(Camera):观察世界的视角。
- 渲染器(Renderer):将画面输出到屏幕。
此外,Three.js 还内置了大量几何体、材质、光源、动画系统以及加载器,并能通过插件扩展到物理引擎、后期特效等领域。
快速起步:搭建你的第一个 3D 场景
要开始使用 Three.js,你可以通过 CDN 或者 npm 安装引入。本教程采用最简单的 CDN 方式。
准备 HTML 文件
创建一个基本的 HTML 文件,引入 Three.js 的 CDN 链接(使用 importmap 方式或直接使用 ESM 导入)。推荐使用现代 ES 模块导入,确保使用最新版本:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Three.js 入门</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.168.0/build/three.module.js"
}
}
</script>
<script type="module">
// 我们的代码将写在这里
</script>
</body>
</html>
初始化场景、相机与渲染器
每一个 Three.js 项目都包含这三个核心组件。我们在 <script type="module"> 中添加:
import * as THREE from 'three';
// 1. 场景 (Scene) —— 虚拟的 3D 空间
const scene = new THREE.Scene();
// 设置场景背景颜色 (深灰色)
scene.background = new THREE.Color(0x202030);
// 2. 相机 (Camera) —— 透视相机模拟人眼观察
const camera = new THREE.PerspectiveCamera(
45, // 视野角度 (FOV)
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近裁剪面
1000 // 远裁剪面
);
// 把相机放在 Z 轴正方向,往里看
camera.position.set(3, 2, 5);
camera.lookAt(0, 0, 0);
// 3. 渲染器 (Renderer) —— 负责将场景通过相机输出到画布
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(window.devicePixelRatio);
// 将 canvas 添加到 body
document.body.appendChild(renderer.domElement);
此时页面会显示一个深灰色背景的空场景,因为我们还没有添加任何物体。
添加发光几何体:一个立方体
任何可见的 3D 物体都由几何体(形状)和材质(外观)组合而成,称为网格(Mesh)。
// 几何体:边长为 1 的立方体
const geometry = new THREE.BoxGeometry(1, 1, 1);
// 材质:一个带有蓝色光泽的标准材质
const material = new THREE.MeshStandardMaterial({
color: 0x3377ff,
roughness: 0.3,
metalness: 0.1
});
// 组合为网格
const cube = new THREE.Mesh(geometry, material);
// 添加到场景中
scene.add(cube);
但现在立方体还看不到,因为场景是黑的,没有光源。
为世界点亮光源
Three.js 提供了多种光源。我们添加一个环境光(AmbientLight)来提供基础亮度,再加一个方向光(DirectionalLight)以产生立体感。
// 环境光:均匀照亮一切,避免死黑
const ambientLight = new THREE.AmbientLight(0x404080); // 微弱的蓝紫色环境光
scene.add(ambientLight);
// 方向光:模拟太阳光,产生明暗效果
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2);
directionalLight.position.set(5, 10, 7);
scene.add(directionalLight);
现在重新加载页面,你会看到一个带有明暗面的蓝色立方体,但它还是静止的。
创建动画循环
要让立方体动起来,我们需要一个持续运行的渲染循环。使用 requestAnimationFrame 递归调用渲染函数,并在每一帧更新物体的旋转。
function animate() {
requestAnimationFrame(animate);
// 绕 Y 轴旋转立方体
cube.rotation.y += 0.01;
// 轻微绕 X 轴旋转,产生动态效果
cube.rotation.x += 0.005;
// 渲染场景
renderer.render(scene, camera);
}
// 启动动画
animate();
保存并刷新浏览器,你就能看到一个旋转的、带有真实光照的 3D 立方体。这就是用 Three.js 构建的第一个完整场景。
深入核心概念
场景图与父子关系
场景中的对象可以形成树形层级。一个对象可以成为另一个对象的子对象,子对象的移动、旋转和缩放会相对于父对象进行。
// 创建一个组作为父级
const group = new THREE.Group();
// 创建两个立方体并放入组
const cubeA = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xff6666 }));
const cubeB = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0x66ff66 }));
cubeA.position.x = -1.5;
cubeB.position.x = 1.5;
group.add(cubeA);
group.add(cubeB);
scene.add(group);
// 在动画循环中旋转整个组
group.rotation.y += 0.01;
这样,两个立方体会围绕组的中心公转,同时保持相对位置不变。
常用几何体
Three.js 内置了大量基础几何形状,无需自己定义顶点:
BoxGeometry(width, height, depth):长方体/立方体SphereGeometry(radius, widthSegments, heightSegments):球体CylinderGeometry(radiusTop, radiusBottom, height, segments):圆柱/圆锥/棱柱PlaneGeometry(width, depth):平面(默认面向 XY)TorusGeometry(radius, tube, radialSegments, tubularSegments):圆环体DodecahedronGeometry(radius):十二面体 等
你可以通过添加段数参数实现更复杂的形状,例如增加球体的段数使其更平滑。
材质与贴图
材质定义了物体表面如何在光照下呈现。除了标准材质 MeshStandardMaterial,还有:
MeshBasicMaterial:不受光照影响,颜色均衡,常用于线框或无光照风格。MeshLambertMaterial:基于朗伯反射,性能好,但高光效果简单。MeshPhongMaterial:支持镜面高光,适合塑料质感。MeshToonMaterial:卡通渲染风格,产生色阶阴影。
也可以通过 map 属性添加纹理贴图:
const textureLoader = new THREE.TextureLoader();
const texture = textureLoader.load('path/to/texture.jpg');
const material = new THREE.MeshStandardMaterial({ map: texture });
相机控制
手动设置相机位置不够灵活。Three.js 提供了多种控制器插件。最常用的是 OrbitControls,允许鼠标/触摸旋转、缩放、平移视角。
需要在项目中引入该控制器。使用 CDN 导入时:
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
然后在初始化后创建:
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; // 启用惯性
controls.dampingFactor = 0.05;
controls.target.set(0, 0, 0); // 看向原点
controls.update();
在动画循环中每帧更新控制器:
controls.update();
这样即可通过鼠标拖拽自由观察场景。
响应式设计
当窗口大小改变时,需要更新相机和渲染器,否则视图会变形或出现空白。
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
构建真实世界的元素
添加阴影
阴影是 3D 场景真实感的关键。开启阴影需要三个步骤:
- 渲染器启用阴影映射:
renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; // 柔和阴影 - 光源投射阴影。以方向光为例:
directionalLight.castShadow = true; directionalLight.shadow.mapSize.width = 1024; directionalLight.shadow.mapSize.height = 1024; directionalLight.shadow.camera.near = 0.5; directionalLight.shadow.camera.far = 50; - 物体接收和投射阴影:
cube.castShadow = true; // 立方体投射阴影 cube.receiveShadow = true; // 立方体也可以接收阴影(通常自遮挡不常见,但可以用在地面上) // 一个地面平面来显示阴影 const planeGeometry = new THREE.PlaneGeometry(10, 10); const planeMaterial = new THREE.MeshStandardMaterial({ color: 0xcccccc, roughness: 0.8 }); const plane = new THREE.Mesh(planeGeometry, planeMaterial); plane.rotation.x = -Math.PI / 2; plane.position.y = -1.5; plane.receiveShadow = true; // 地面接收阴影 scene.add(plane);
加载外部 3D 模型
Three.js 支持 glTF(GL Transmission Format)等现代格式,推荐使用 GLTFLoader。
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
const loader = new GLTFLoader();
loader.load(
'path/to/model.glb',
(gltf) => {
// 加载完成,将模型添加到场景
scene.add(gltf.scene);
},
(progress) => {
console.log(`加载进度: ${(progress.loaded / progress.total * 100).toFixed(2)}%`);
},
(error) => {
console.error('模型加载失败', error);
}
);
注意:由于浏览器安全策略,直接打开本地 HTML 文件时可能无法加载外部模型,需要启动本地服务器(例如使用 Live Server 插件或 npx serve)。
粒子系统与特效
有时我们需要星空、雨雪等效果,可以使用 Points 配合 BufferGeometry。
const particlesGeometry = new THREE.BufferGeometry();
const particlesCount = 1000;
const positions = new Float32Array(particlesCount * 3); // 每个粒子 x,y,z
for (let i = 0; i < particlesCount * 3; i += 3) {
positions[i] = (Math.random() - 0.5) * 20; // x
positions[i+1] = (Math.random() - 0.5) * 20; // y
positions[i+2] = (Math.random() - 0.5) * 20; // z
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const particlesMaterial = new THREE.PointsMaterial({
size: 0.02,
color: 0xffffff,
blending: THREE.AdditiveBlending
});
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
scene.add(particles);
然后在动画循环中旋转粒子系统即可产生星空流动效果。
实战建议与最佳实践
性能优化
- 减少 Draw Call:合并静态几何体(使用
BufferGeometryUtils.mergeGeometries)或使用 InstancedMesh 绘制大量相同物体。 - 合理使用纹理:压缩纹理格式,避免过大分辨率,使用 power-of-two 尺寸。
- 控制阴影精度:阴影贴图分辨率不宜过大,根据视角距离调整阴影相机的范围。
- 复用材质和几何体:相同物体共享材质和几何体实例。
- 使用 requestAnimationFrame 节流:对于不需要高频更新的交互,可以适当降低更新频率。
调试工具
- 浏览器开发者工具:可以查看 DOM 中的
<canvas>元素。 stats.js性能监视器:显示实时帧率。dat.gui或lil-gui:快速创建调节 UI,动态改变材质颜色、光源强度等。- Three.js 内置的
WebGLRenderer.info:提供渲染调用次数、内存占用等数据。
从学习到创造
- 阅读官方文档和示例(threejs.org),逐行理解每个示例的代码。
- 拆分复杂项目为小模块:场景设置、物体管理、动画控制器、资源加载等。
- 使用现代 JavaScript 特性(模块化、类)组织代码,避免全局变量污染。
- 持续关注 Three.js 社区和更新日志,库迭代很快,新特性会不断简化开发。
总结
Three.js 将 WebGL 的复杂度封装成了一套直观的 3D 构建工具。通过场景、相机、渲染器三要素,结合几何体、材质、光源,你就能开始在浏览器中创造三维视觉作品。随着对动画循环、交互控件、阴影、模型加载和性能优化的掌握,你能够搭建从简单产品展示到复杂沉浸式体验的各类应用。
WebGL 的底层能力依然是 Three.js 坚实的基础,但站在 Three.js 的肩膀上,你可以更快速地实现创意,而不必每一次都从着色器代码开始。现在,打开你的代码编辑器,创造一个属于你的 3D 世界吧。