Skip to content
three.js 核心对象:场景、相机与渲染器
WebGL 直接把顶点、着色器、缓冲区抛给你,能精确控制每一块 GPU 内存,但代价是费劲。Three.js 在这些细节上封装了一层,把“画什么”变成场景(Scene)、“从哪看”变成相机(Camera)、“怎么画出来”变成渲染器(Renderer)。这三样东西是任何一个 Three.js 程序都必须有的,缺一个都看不到东西。
场景
Scene 是 Object3D 的子类,同时充当整棵场景图的根节点。Object3D 是所有可放置对象的基类,维护一个子节点列表,支持位移、旋转、缩放和父子层级关系。Scene 本身没有增加太多额外方法,主要职责是作为容器——把物体、光源甚至相机挂上来,渲染器只看这个容器里的东西。
创建只需要:
js
const scene = new THREE.Scene();所有可渲染对象都必须通过 scene.add(obj) 挂上去,否则渲染器在遍历场景图时看不到它们。add 做的事就是把传入对象注册为当前节点的子节点,子节点会继承父节点的变换矩阵。
js
// 只创建一个空的场景不会有任何可视结果
// 必须往里加东西
scene.add(cube);
scene.add(light);最终场景图结构类似 [Scene] -> [Mesh, Light, Camera],Mesh 本身又挂载着 Geometry 和 Material。这种层级关系在复杂场景中很有用——比如把一堆小部件挂到同一个父级下,移动父级就能整体移动。
相机
相机定义了观察者的位置以及从 3D 场景到 2D 屏幕的投影规则。最常用的是 PerspectiveCamera,模拟人眼的近大远小透视效果。
构造函数需要四个参数:
js
const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000);- fov — 视场角,单位是度。值越大,视野越广,但远端物体会显得更小。
- aspect — 宽高比,一般直接取画布宽度除以高度。
- near — 近裁剪面距离。比这个距离更靠近相机的物体不会被渲染。
- far — 远裁剪面距离。超出这个距离的物体也会被裁剪掉。
这四个参数共同定义了一个视锥体(frustum)。视锥体是一个 3D 截头金字塔,只有落在它里面的物体才会被投影到屏幕上。fov 控制锥体的张开角度,near 和 far 控制锥体沿视线方向的长度。理解视锥体对调试“物体明明加了但渲染不出来”这类问题很有用——最常见的原因就是物体落在了 near 前面或者 far 后面。
相机本身需要放到场景中的某个位置,通常放在 z 轴正方向:
js
camera.position.z = 5;Three.js 默认使用右手坐标系:x 向右,y 向上,z 指向屏幕外(即观察者的方向)。所以把相机放在 z=5 的位置,意味着它在原点前方 5 个单位处,朝 -z 方向看。距离单位没有具体物理含义,只要场景里所有对象统一就好。
相机也可以执行 scene.add(camera),但这不会影响渲染结果,只是方便把相机的变换纳入场景图的继承链。实际调用 renderer.render(scene, camera) 时,渲染器直接用传入的相机实例计算视图和投影矩阵,跟它是否在场景图里无关。
渲染器
渲染器负责把相机视角下的场景画到 <canvas> 元素上。Three.js 提供了多种渲染器(WebGL、SVG、CSS 等),WebGLRenderer 是默认且最常用的。
创建并配置一个渲染器的典型代码:
js
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xdddddd, 1);
document.body.appendChild(renderer.domElement);{ antialias: true }— 启用多采样抗锯齿,物体边缘会更平滑。代价是占用更多显存,在移动设备上可能导致帧率明显下降,需要根据场景复杂度决定是否开启。setSize— 设置画布的像素尺寸。这里直接取了窗口大小。宽高比必须与相机的aspect一致,否则会出现拉伸。窗口大小改变时需要同时更新renderer.setSize和camera.aspect(并调用camera.updateProjectionMatrix())。setClearColor— 每帧清除画布时填充的颜色。这里用了浅灰色 (0xdddddd)。renderer.domElement— 就是渲染器创建的<canvas>节点,不挂到 DOM 里就不会显示在页面上。
渲染调用链
三个对象都准备好以后,渲染的核心只有一行:
js
renderer.render(scene, camera);这一句做的事情:遍历场景图中的所有网格对象,对每个 Mesh 执行顶点变换和片段着色,生成当前相机视角下的 2D 图像并画到 canvas 上。这行代码触发的是单次绘制。
最小示例
下面是一个完整的 HTML 文件,会在灰色背景上显示一个缓慢旋转的蓝色立方体。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>body { margin: 0; } canvas { display: block; }</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.150.0/build/three.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
45,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 5;
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setClearColor(0xdddddd, 1);
document.body.appendChild(renderer.domElement);
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshBasicMaterial({ color: 0x0095dd });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
function animate() {
requestAnimationFrame(animate);
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
}
animate();
</script>
</body>
</html>这里用了 MeshBasicMaterial,它不参与光照计算,直接按 color 指定的颜色填充表面。因此即使场景里没有光源,也能看到立方体。如果用 MeshStandardMaterial 或者 MeshPhongMaterial,在没有光源的情况下物体会是纯黑色。
渲染循环
示例中的 animate 函数通过 requestAnimationFrame 不断调用自己,并在每次调用时更新旋转角度然后执行渲染。requestAnimationFrame 在浏览器下一次重绘前触发回调,动画能自动跟随屏幕刷新率(通常是 60 Hz),而且当页面切到后台时会被暂停,不会浪费资源。
如果场景完全静态,没有动画也不需要响应窗口大小变化,可以只调用一次 renderer.render(scene, camera),跳过循环。但一旦用户缩放窗口导致尺寸变化,就需要手动重绘,否则画面会错位。
注意点
- 画布尺寸与相机宽高比必须同步:初始化时用
window.innerWidth / window.innerHeight作为相机aspect并用同样尺寸设置渲染器。窗口大小改变时,须同步更新两者并调用camera.updateProjectionMatrix(),否则物体会拉伸。 - 右手坐标系的方向:Three.js 默认 y 轴向上,z 轴指向屏幕外。放置相机时一般把相机放在 z > 0 的位置并指向 -z 方向。如果物体位置和相机位置没有正确放置,场景可能一片空白。
- 材质选择与光照:在引入光照之前,验证渲染管线时优先使用
MeshBasicMaterial或MeshNormalMaterial,它们不依赖光照,能保证肉眼可见。一旦换了带光照的材质但没加光源,画面会全黑,这是常见问题。 - antialias 的性能代价:桌面端一般无所谓,但在移动端
antialias: true可能让帧率从 60 掉到 30 甚至更低。对于性能敏感的移动端页面,可以考虑关闭并用后期抗锯齿(如 FXAA)替代。
