第一个场景:Hello Cube
本教程共 40 篇 · 第 4 篇 · 更新于 2026-08-14 · 约 7 分钟阅读
本节目标:用场景、相机、渲染器三要素,写出第一个可运行的 three.js 程序——一个 3D 立方体。学完你能看懂任何 three.js 程序的最小结构。
完整代码
先把完整代码跑起来,再逐段讲解。保存为 index.html,用本地服务器打开:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>Hello Cube</title>
<style>
* { margin: 0; padding: 0; }
html, body { height: 100%; }
canvas { display: block; }
</style>
<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>
</head>
<body>
<script type="module">
import * as THREE from 'three';
// 1. 场景
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x1a1a2e);
// 2. 相机
const camera = new THREE.PerspectiveCamera(
75, window.innerWidth / window.innerHeight, 0.1, 100
);
camera.position.set(0, 0, 5);
// 3. 立方体:几何体 + 材质 = 网格
const geometry = new THREE.BoxGeometry(2, 2, 2);
const material = new THREE.MeshBasicMaterial({ color: 0x00d4ff });
const cube = new THREE.Mesh(geometry, material);
cube.rotation.y = 0.5; // 转一点角度,立体感就出来了
scene.add(cube);
// 4. 渲染器
const renderer = new THREE.WebGLRenderer();
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
document.body.appendChild(renderer.domElement);
// 5. 渲染一帧
renderer.render(scene, camera);
</script>
</body>
</html>
页面中央出现一个天蓝色立方体就成功了。接下来拆开讲。
三个核心对象
整个程序围绕三个对象展开,缺一不可:
| 对象 | 类 | 作用 |
|---|---|---|
| 场景 | Scene | 装所有 3D 物体 |
| 相机 | PerspectiveCamera | 决定从哪看 |
| 渲染器 | WebGLRenderer | 负责画出来 |
把场景比作小宇宙,相机是望远镜,渲染器是照着望远镜画面作画的画家。三个对象本身都不可见,可见的是被加入场景的物体。这套结构不是 three.js 独创的,几乎所有 3D 图形系统都是这个套路,学会一次,到处通用。第 3 章的 importmap、这里的场景相机渲染器,构成了后面所有章节示例的固定开头,往后会反复复用这套骨架。
场景 Scene
Scene 是所有物体的容器,内部是一棵场景树(scene graph):物体可以挂在场景下,也可以挂在其他物体下,形成父子关系。现在只需要知道最基本的用法:
const scene = new THREE.Scene();
scene.add(cube); // 添加物体
scene.remove(cube); // 移除物体
物体默认放在原点 (0, 0, 0)。新创建的物体如果不移动位置,全都堆在原点。
scene.background 设置背景色,接受 Color 对象:
scene.background = new THREE.Color(0x1a1a2e);
不设置的话默认黑色。想用图片当背景也可以,给 scene.background 赋一张纹理贴图就行,第 15 章会讲。
相机 PerspectiveCamera
透视相机模拟人眼效果:近大远小。构造函数有四个参数:
- fov:视野角度(度数),越大看到的东西越多,变形也越明显
- aspect:宽高比,画面宽度 ÷ 高度
- near:近裁剪面,比它更靠近相机的东西不画
- far:远裁剪面,比它更远的东西不画
const camera = new THREE.PerspectiveCamera(
75, window.innerWidth / window.innerHeight, 0.1, 100
);
near 和 far 之间围成的区域叫视锥体(frustum),只有视锥体内的物体才被渲染。物体部分在视锥体外时,露在外面的部分会被裁掉。
fov 取多少合适?常见范围是 45~75。75 视野宽、透视感强,适合游戏;45 接近人眼专注范围、变形小,适合展示产品。同一个场景,fov 越大,物体在画面里显得越小。实际使用中,near 别设太小、far 别设太大,否则深度精度会出问题,远处物体可能出现闪烁。
相机默认在原点 (0, 0, 0),朝向 -z 方向,和立方体重叠,所以要把相机往后移:
camera.position.set(0, 0, 5);
相机摆好位置后,可以用 lookAt 让它看向某个点(第 6 章详讲):
camera.lookAt(0, 0, 0); // 看向原点
网格 Mesh:几何体 + 材质
3D 物体里最常用的是网格(Mesh),它由两部分组成:几何体定义形状,材质定义外观。
const geometry = new THREE.BoxGeometry(2, 2, 2); // 2×2×2 的立方体形状
const material = new THREE.MeshBasicMaterial({ color: 0x00d4ff }); // 天蓝色外观
const cube = new THREE.Mesh(geometry, material);
- BoxGeometry 是内置几何体,三个参数是宽、高、深
- MeshBasicMaterial 是最基础的材质,不受光照影响,所以这个例子不需要加灯光
Noter125 之前 BoxGeometry 叫 BoxBufferGeometry,现在已经没有 BoxBufferGeometry 了。看到老教程这么写,记得去掉 Buffer。
因为正对着立方体的一面,它看起来是个正方形。加一行旋转就有立体感:
cube.rotation.y = 0.5; // 绕 y 轴转 0.5 弧度
想让立方体换个位置,设置 position:
cube.position.x = 1; // 单独设置一个轴
cube.position.set(1, 0, 0); // 或者三个轴一起设置
试着再加一个立方体,把下面几行插进「3. 立方体」之后:
const cube2 = new THREE.Mesh(geometry, material); // 可以复用同一个几何体和材质
cube2.position.set(3, 0, 0);
scene.add(cube2);
一个几何体、一个材质,可以创建任意多个网格,内存只占一份。这是 three.js 常用的省内存技巧。
渲染器 WebGLRenderer
渲染器负责把场景画出来,需要三步配置:
const renderer = new THREE.WebGLRenderer(); // 1. 创建
renderer.setSize(window.innerWidth, window.innerHeight); // 2. 画布尺寸
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); // 3. 高清屏适配
renderer.domElement 是渲染器自动创建的 canvas 元素,需要手动放进页面:
document.body.appendChild(renderer.domElement);
不调 setSize 的话,canvas 默认是 300×150 的小画布。页面样式里写 canvas { display: block; },是为了去掉 canvas 默认的内联样式空隙,避免页面底部多出一条白边。
Tip不调 setPixelRatio 的话,高分屏(Retina)上画面会模糊。先照抄这个写法,第 5 章详解。
渲染一帧
最后一行是真正出图的一步:
renderer.render(scene, camera);
它把场景从相机的视角「拍一张照片」画到 canvas 上。注意:这只渲染一帧静态画面。场景有任何变化(物体动了、相机动了),都要重新调用 render 才能看到新画面。
让立方体转起来(渲染循环预告)
把第 5 步的 render 换成下面的循环,立方体就会持续旋转:
function animate() {
requestAnimationFrame(animate); // 请求下一帧,约每秒 60 次
cube.rotation.y += 0.01; // 每帧转一点
renderer.render(scene, camera); // 重新画一帧
}
animate();
requestAnimationFrame 是浏览器提供的动画循环接口,它会在每次屏幕刷新前调用回调,通常每秒 60 次,跟屏幕刷新率同步。用 setInterval 也能实现循环,但会被标签页切后台、帧率波动影响,效果远不如 requestAnimationFrame。
循环的原理、Clock 计时、帧率控制,第 7 章专门讲,这里先跑起来感受一下。
一帧大约 16.7 毫秒(1000 ÷ 60)。如果一帧的渲染耗时超过这个值,帧率就会掉到 30fps 甚至更低,画面开始卡顿。记住这个数字,后面做性能优化时用得上。
常见问题
- 页面空白:先开控制台看报错,最常见是 file:// 打开导致模块加载失败
- 黑屏:场景里没加物体,或者相机在物体内部
- 画面被拉伸:aspect 和 setSize 的宽高比不一致
- 只看到一片颜色:相机离物体太远或太近,调整 camera.position.z
- 立方体看不到但也没报错:检查相机朝向,默认看向 -z,把物体放 -z 方向或移动相机
- 旋转了但看不出变化:rotation 的单位是弧度,不是角度,转 90° 要写 Math.PI / 2
- 只有背景色没有物体:确认 scene.add(cube) 写在了 renderer.render 之前
下一节深入渲染器,看看它还有哪些重要配置。