Three.js 场景完全入门指南:让你的 3D 场景不在乱成一团

场景图到底是什么?一句话说清楚

场景图 = 你 3D 世界里的「家族族谱」

你在 Three.js 里创建的每一个物体------立方体、球体、灯光、相机------它们不是孤立存在的,而是像家族成员一样,有爸爸、有儿子、有孙子,形成一个树状的层级结构。

这个结构,就叫场景图。

想象一下你在玩乐高:

  • 你先搭了一个车身(父节点)
  • 然后在车身上装了 4 个轮子(子节点)
  • 每个轮子上又装了轮毂装饰(孙节点)

当你拿起整个车身移动时,轮子和轮毂会自动跟着动。你不需要一个一个去移动它们。

这就是场景图的核心逻辑:父节点动,子节点自动跟着动。

为什么需要场景图?

假设你要做一个太阳系模型:

  • 太阳在中心
  • 地球绕着太阳转
  • 月球绕着地球转

如果没有场景图,你得这么写:

javascript 复制代码
// 每一帧都要手动计算位置
function animate() {
  // 地球绕太阳转
  earth.position.x = Math.cos(time) * 10;
  earth.position.z = Math.sin(time) * 10;

  // 月球绕地球转,还要加上地球的位置
  moon.position.x = earth.position.x + Math.cos(time * 2) * 2;
  moon.position.z = earth.position.z + Math.sin(time * 2) * 2;

  // 如果再加个火星、木星、土星...
  // 你的代码会变成一坨屎
}

有了场景图,你只需要:

javascript 复制代码
// 把月球设为地球的子节点
earth.add(moon);

// 把地球设为太阳的子节点
sun.add(earth);

// 每一帧只需要旋转父节点
function animate() {
  sun.rotation.y += 0.01;  // 太阳自转
  earth.rotation.y += 0.02; // 地球自转,月球自动跟着转
}

场景图让你从「手动计算每个物体的绝对位置」,变成「只管理父子关系,让系统自动计算」。


场景图的三大核心规则

规则 1:每个物体都有自己的「局部坐标系」

这是最容易搞混的地方。

在 Three.js 里,每个物体的 positionrotationscale 都是相对于它的父节点的,不是相对于整个世界的。

举个例子:

javascript 复制代码
const car = new THREE.Group(); // 汽车
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial); // 轮子

wheel.position.x = 2; // 轮子在汽车坐标系里,向右偏移 2 个单位
car.add(wheel);

car.position.x = 10; // 汽车在世界坐标系里,向右移动 10 个单位

此时,轮子在世界坐标系里的实际位置是 10 + 2 = 12

但你在代码里看到的 wheel.position.x 还是 2,因为它记录的是相对于父节点(汽车)的位置

这就像你在高铁上走动:

  • 你相对于车厢的位置是「第 5 排座位」(局部坐标)
  • 但你相对于地球的位置,是「第 5 排座位 + 高铁的位置」(世界坐标)

规则 2:父节点的变换会「传递」给所有子节点

这是场景图最强大的地方。

当你旋转、缩放、移动一个父节点时,它的所有子节点、孙节点、曾孙节点......都会跟着变。

javascript 复制代码
const robot = new THREE.Group();
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
const leftArm = new THREE.Mesh(armGeometry, armMaterial);
const rightArm = new THREE.Mesh(armGeometry, armMaterial);

robot.add(body);
body.add(leftArm);
body.add(rightArm);

// 旋转机器人,整个机器人(包括身体和手臂)都会转
robot.rotation.y = Math.PI / 4;

// 旋转身体,手臂会跟着转,但机器人的腿不会动
body.rotation.x = Math.PI / 6;

这就像你转身:

  • 你的头、手、脚都会跟着转(子节点跟随父节点)
  • 但你手上拿的手机屏幕方向不会变(子节点保持自己的局部旋转)

规则 3:Scene 是所有节点的「根节点」

在 Three.js 里,Scene 就是那个最顶层的「祖宗节点」。

所有你想渲染出来的东西,都必须直接或间接地添加到 Scene 里。

javascript 复制代码
const scene = new THREE.Scene();

// 方式 1:直接添加到场景
scene.add(cube);

// 方式 2:添加到某个组,再把组添加到场景
const group = new THREE.Group();
group.add(cube);
scene.add(group);

Scene 就像一个舞台:

  • 只有站在舞台上的演员(或演员团队)才能被观众(相机)看到
  • 你在后台准备的道具(没 add 到 scene 的物体),观众看不见

···

真实场景:用场景图管理一辆汽车

假设你要做一个可交互的汽车模型:

  • 汽车可以前进、后退、转弯
  • 4 个轮子要跟着车身动
  • 轮子转弯时要旋转
  • 车门可以单独打开

没有场景图的噩梦写法:

javascript 复制代码
// 每次移动汽车,你要手动更新 5 个物体的位置
function moveCar(distance) {
  carBody.position.z += distance;
  wheel1.position.z += distance;
  wheel2.position.z += distance;
  wheel3.position.z += distance;
  wheel4.position.z += distance;
  door.position.z += distance;
}

// 转弯时,你要手动计算每个轮子的新位置
function turnCar(angle) {
  // 这里要写一堆三角函数...
  // 而且很容易算错
}

用场景图的优雅写法:

javascript 复制代码
// 1. 创建层级结构
const car = new THREE.Group(); // 汽车根节点
const body = new THREE.Mesh(bodyGeometry, bodyMaterial); // 车身
const door = new THREE.Mesh(doorGeometry, doorMaterial); // 车门

const wheel1 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheel2 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheel3 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheel4 = new THREE.Mesh(wheelGeometry, wheelMaterial);

// 2. 建立父子关系
car.add(body);
body.add(door); // 车门是车身的子节点
body.add(wheel1);
body.add(wheel2);
body.add(wheel3);
body.add(wheel4);

scene.add(car); // 整辆车添加到场景

// 3. 设置轮子的局部位置(相对于车身)
wheel1.position.set(-1, -0.5, 1.5);  // 左前轮
wheel2.position.set(1, -0.5, 1.5);   // 右前轮
wheel3.position.set(-1, -0.5, -1.5); // 左后轮
wheel4.position.set(1, -0.5, -1.5);  // 右后轮

// 4. 移动汽车,只需要操作根节点
function moveCar(distance) {
  car.position.z += distance; // 一行代码,整辆车都动了
}

// 5. 转弯,也只需要操作根节点
function turnCar(angle) {
  car.rotation.y += angle; // 一行代码,整辆车都转了
}

// 6. 打开车门,只操作车门节点
function openDoor() {
  door.rotation.y = Math.PI / 3; // 车门绕自己的轴旋转
}

// 7. 轮子转动,只操作轮子节点
function rotateWheels(speed) {
  wheel1.rotation.x += speed;
  wheel2.rotation.x += speed;
  wheel3.rotation.x += speed;
  wheel4.rotation.x += speed;
}

场景图让你的代码从「管理 100 个物体的绝对位置」,变成「管理 10 个父子关系」。

代码量少了 90%,bug 也少了 90%。

···

进阶技巧:Group 是你最好的朋友

Three.js 提供了一个专门用来组织场景图的工具:THREE.Group()

它就是一个「空节点」,自己不渲染任何东西,但可以作为其他物体的容器。

什么时候用 Group?

  1. 逻辑分组:把相关的物体放在一起

    javascript 复制代码
    const furniture = new THREE.Group();
    furniture.add(table);
    furniture.add(chair);
    furniture.add(lamp);
    
    // 一次性移动所有家具
    furniture.position.x = 5;
  2. 动画控制:需要整体旋转或移动时

    javascript 复制代码
    const solarSystem = new THREE.Group();
    solarSystem.add(sun);
    solarSystem.add(earth);
    solarSystem.add(mars);
    
    // 整个太阳系旋转
    solarSystem.rotation.y += 0.01;
  3. 坐标系转换:需要改变物体的旋转中心时

    javascript 复制代码
    // 默认情况下,物体绕自己的中心旋转
    // 如果你想让它绕另一个点旋转,可以用 Group
    
    const pivot = new THREE.Group();
    pivot.add(cube);
    cube.position.x = 5; // 立方体偏离 pivot 中心
    
    pivot.rotation.y += 0.01; // 立方体绕 pivot 中心旋转(公转)
    cube.rotation.y += 0.02;  // 立方体绕自己中心旋转(自转)

Group 就像乐高的底板:

  • 你可以在底板上搭建复杂的结构
  • 然后拿起整个底板移动,所有东西都跟着动
  • 底板本身不占空间,只是一个「组织工具」

···

常见坑点:为什么我的物体位置不对?

坑点 1:忘记父节点的变换会累积

javascript 复制代码
const parent = new THREE.Group();
parent.scale.set(2, 2, 2); // 父节点放大 2 倍

const child = new THREE.Mesh(geometry, material);
child.scale.set(2, 2, 2); // 子节点也放大 2 倍
parent.add(child);

// 结果:子节点实际被放大了 2 × 2 = 4 倍!

解决方法:

  • 要么只在父节点设置缩放
  • 要么在子节点用 1 / parent.scale.x 来抵消

坑点 2:直接修改 world position 不生效

javascript 复制代码
const child = new THREE.Mesh(geometry, material);
parent.add(child);

// ❌ 错误:直接设置世界坐标不会生效
child.position.set(10, 0, 0); // 这是局部坐标!

// ✅ 正确:如果要设置世界坐标,需要先转换
const worldPos = new THREE.Vector3(10, 0, 0);
child.parent.worldToLocal(worldPos);
child.position.copy(worldPos);

坑点 3:移除节点时忘记清理引用

javascript 复制代码
// ❌ 错误:只从场景移除,但父子关系还在
scene.remove(child);

// ✅ 正确:从父节点移除
parent.remove(child);

// ✅ 更好:彻底清理
parent.remove(child);
child.geometry.dispose();
child.material.dispose();

核心代码与完整示例: my-three-app

总结

如果你喜欢本教程,记得点赞+收藏!关注我获取更多Three.js开发干货

相关推荐
EliseL1 天前
SuperMap iClient3D for WebGL 如何实时汇报相机位置天气情况
javascript·3d·html·webgl
ct9781 天前
Cesium的时间与时钟系统
gis·webgl·cesium
Jay-r1 天前
樱花雨特效 WebGL实现 短视频同款浪漫视觉效果(附源码下载)
开发语言·javascript·ecmascript·编程·webgl·代码·樱花雨
棋鬼王1 天前
Cesium(八) 三峡大坝水淹分析,江、湖、水库、大坝水淹决堤分析
3d·信息可视化·智慧城市·webgl·cesium
平行云PVT2 天前
数字孪生信创云渲染技术解析:从混合信创到全国产化架构
linux·unity·云原生·ue5·图形渲染·webgl·gpu算力
ai超级个体3 天前
前端下午茶:这 3 个网页特效建议收藏(送源码)
前端·three.js·threejs·网页设计·vibe coding·网页灵感·网页分享
qq_283720053 天前
WebGL基础教程(十四):投影矩阵深度解析——正交 vs 透视,从公式推导到实战
线性代数·矩阵·webgl·正交·投影
该怎么办呢5 天前
cesium核心代码学习-01项目目录及其基本作用
前端·3d·源码·webgl·cesium·webgis
codingWhat5 天前
用 three.js 实现 3D 地图
three.js