经纬志
首页 / three.js / 教程

场景图与坐标

先看结论

场景里所有对象都挂在一棵树上,子对象的位置是相对父对象表达的。 理解这棵树,才知道为什么改了坐标却没动、移动了组却带动了一片。

最后验证 2026-11-03 · three.js 0.160.0 · 难度:入门 · 核验 2026-10-29 · 引用本文

Object3D 是一棵树

Scene、Group、Mesh、Camera 都继承自 Object3D。每个节点有 parent 与 children,scene.add(obj) 把对象挂到场景根节点。

const cityBlock = new THREE.Group();
cityBlock.position.set(120, 0, -80);

const building = new THREE.Mesh(boxGeometry, wallMaterial);
building.position.set(10, 15, 6);        // 相对 cityBlock
cityBlock.add(building);
scene.add(cityBlock);

console.log(building.parent === cityBlock);       // true
console.log(building.getWorldPosition(new THREE.Vector3()).x);  // 130

cityBlock.traverse(function (obj) {
  if (obj.isMesh) obj.castShadow = true;
});

building.position.x 是 10,但世界坐标是 130。 面板上看到的永远是局部坐标,调试位置问题时先确认对象挂在哪一层。

属性 / 方法含义注意
position相对父节点的位移单位自定,GIS 场景通常用米
rotation欧拉角,弧度顺序 XYZ,万向锁时改用四元数
scale缩放,1 为原尺寸会影响子节点与包围盒
getWorldPosition()世界坐标要求矩阵已更新
localToWorld()局部点转世界会就地修改传入向量
attach()换父节点并保持世界位置add() 不保持

变换的顺序

一个节点的局部矩阵按「缩放 → 旋转 → 平移」组合,世界矩阵是父矩阵乘以局部矩阵。 因此旋转子对象时,它绕的是自己的原点,不是父对象的中心。

const pivot = new THREE.Group();
pivot.position.set(200, 0, 0);

const arm = new THREE.Mesh(new THREE.BoxGeometry(60, 2, 2), metalMaterial);
arm.position.set(30, 0, 0);      // 伸出 30 米
pivot.add(arm);

// 绕支点旋转,而不是绕臂的中心
pivot.rotation.y = THREE.MathUtils.degToRad(35);

// 手动更新后读取世界坐标
pivot.updateMatrixWorld(true);
const endPoint = new THREE.Vector3(60, 0, 0);
arm.localToWorld(endPoint);

localToWorld 会直接修改传入的向量并返回它。 需要保留原值时先 clone(),否则原始坐标会被覆盖。

旋转用弧度。rotation.y = 35 表示 35 弧度, 不是 35 度。用 THREE.MathUtils.degToRad() 转换,或直接写角度常量时加上单位注释。

GIS 场景的原点与轴向

three.js 默认 Y 轴向上,与 GIS 常用的 Z 轴向上不同。把投影坐标搬进场景时, 需要一个明确的映射约定:通常取片区的左下角作为原点, x 向东、z 向南(或向北,取决于视角)、y 为高程。

const ORIGIN = { x: 440000, y: 4420000 };   // 投影坐标原点,米
const METERS_TO_UNITS = 1;

function toScene(easting, northing, height) {
  return new THREE.Vector3(
    (easting - ORIGIN.x) * METERS_TO_UNITS,
    height,
    -(northing - ORIGIN.y) * METERS_TO_UNITS
  );
}

const schools = [
  { easting: 440210, northing: 4420410, height: 42 },
  { easting: 440560, northing: 4420180, height: 38 }
];

schools.forEach(function (p) {
  const pos = toScene(p.easting, p.northing, p.height);
  const marker = new THREE.Mesh(sphereGeometry, schoolMaterial);
  marker.position.copy(pos);
  scene.add(marker);
});

坐标原点必须靠近数据范围,否则几百万量级的投影坐标转成浮点后精度不够, 远处的模型会出现抖动。toScene 这类函数只写一次,全部数据统一入口。

更新矩阵的时机

渲染器每帧会自动更新世界矩阵。但如果在 render 之前读取世界坐标, 必须手动调用 updateMatrixWorld(true),否则拿到的是上一帧的矩阵。 频繁改位置时,关闭自动更新反而更快:

group.matrixAutoUpdate = false;
group.position.set(10, 0, 0);
group.rotation.y = 0.5;
group.updateMatrix();          // 手动合成局部矩阵
group.updateMatrixWorld(true); // 同步到子节点

注意事项

  • add 会保留局部坐标、改变世界位置;attach 保留世界位置、改变局部坐标。
  • 对象只能有一个父节点,重复 add 会自动从原父节点移除。
  • visible = false 会跳过该节点及其所有子节点的渲染,但不会释放显存。
  • 删除对象后要调用 geometry.dispose() 与 material.dispose(),否则资源不会回收。
  • 大量静态对象可以合并成一个 Mesh,减少场景图遍历与绘制调用。
引用本文

经纬志(2026)。《场景图与坐标》。https://www.jingweizhi.com/three/tutorial/scene-graph.html(访问日期 2026-11-03)。