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