Three.js 的 Scene 中可以添加很多对象:

可以通过 Group 来添加子对象,这些对象之间构成一棵树。
那一个网格模型 Mesh,直接添加到 Scene 中,和添加到 Group 下再添加到 Scene 中,有什么区别呢?
我们试一下就知道了。
创建项目:
mkdir scene-group
cd scene-group
npm init -y

安装 ts 类型:
npm install --save-dev @types/three
创建 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
body {
margin: 0;
}
</style>
</head>
<body>
<script type="importmap">
{
"imports": {
"three": "https://esm.sh/three@0.174.0/build/three.module.js",
"three/addons/": "https://esm.sh/three@0.174.0/examples/jsm/"
}
}
</script>
<script type="module" src="./index.js"></script>
</body>
</html>
index.js
import * as THREE from 'three';
import {
OrbitControls
} from 'three/addons/controls/OrbitControls.js';
import mesh from './mesh.js';
const scene = new THREE.Scene();
scene.add(mesh);
const light = new THREE.DirectionalLight(0xffffff);
light.position.set(3000, 2000, 1000);
scene.add(light);
const axesHelper = new THREE.AxesHelper(1000);
scene.add(axesHelper);
const width = window.innerWidth;
const height = window.innerHeight;
const camera = new THREE.PerspectiveCamera(60, width / height, 1, 10000);
camera.position.set(500, 500, 500);
camera.lookAt(0, 0, 0);
const renderer = new THREE.WebGLRenderer();
renderer.setSize(width, height)
function render() {
renderer.render(scene, camera);
requestAnimationFrame(render);
}
render();
document.body.append(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
引入 three.js,创建 Scene、Light、Camera、Renderer
然后写下 mesh.js
import * as THREE from 'three';
const geometry = new THREE.BoxGeometry(100, 100, 100);
const material = new THREE.MeshLambertMaterial({
color: new THREE.Color('orange')
});
const mesh = new THREE.Mesh(geometry, material);
export default mesh;
跑下看看:
npx live-server

我们设置 position 或者调用 translate 方法,它都会做一些偏移:

mesh.position.x = 200;
mesh.translateZ(200);
比如我们把它 position.x 设置 200,然后再 translateZ 200

它会移动到这个为止。
那如果我们把它放到 Group 里再移动呢?

const group = new THREE.Group();
group.add(mesh);
scene.add(group);
group.position.x = 200;
group.translateZ(200);
把它 clone 一份放到 Group 里,然后移动 Group。

这样位置是一样的。
那这时候如果我设置立方体的 position.x 为 200,那它应该在哪里呢?

mesh.position.x = 200;

可以看到,它现在位置和原来的不一样了,x 是 400
也就是说添加到 Group 之后,它的绝对坐标是 group 的 position 加上它的 position,这个叫做世界坐标。
而它在 Group 内部的 position 叫做局部坐标。
那随便给一个 Scene 中的物体,如何计算它的世界坐标呢?
用 getWorldPosition 这个 API:

const pos = new THREE.Vector3();
mesh.getWorldPosition(pos);
console.log(pos);
console.log(group.position);
console.log(mesh.position);
创建一个 Vector3 三维向量对象,然后调用 getWorldPosition 方法,传入它,这样就可以从 vector3 对象里拿到位置了。

第一个就是 mesh 的世界坐标,可以看到是 group 的 position 加上了 mesh 的 position
而下面打印的 position 就是 mesh 的局部坐标。
我们可以加一个 AxesHelper 让 group 的局部坐标系展示出来:

const axesHelper2 = new THREE.AxesHelper(200);
group.add(axesHelper2);

我们打印下现在的 scene:


可以看到,它有 Group、DirectionLight、AxesHelper 这三个子对象。
而 Group 的 children 也有两个:

那遍历 Scene 中的所有对象就是几重循环的事情了。
不过不用自己实现,Three.js 提供了这个 API:

scene.traverse((obj) => {
console.log(obj);
});

还可以做下过滤,比如改下所有 Mesh 的材质颜色
scene.traverse((obj) => {
if(obj.isMesh) {
obj.material.color = new THREE.Color('pink');
}
});

如果你想找特定的 Mesh,那可以给他一个 name

然后用 getObjectByName 的 api 来查找对象:

const cube = scene.getObjectByName('cube');
cube.material.color = new THREE.Color('lightgreen');

getObjectById 则是根据 id 找,用法一样。
这样,我们就可以遍历场景中的所有对象,然后找到 Mesh 或者特定的对象了。
案例代码上传了小册仓库。
总结
Scene 中保存的是一个对象树,包含 Mesh、Group、Light 等各种对象。
Mesh 如果添加到 Group 中,那它的 position 就是相对于 Group 的,叫做局部坐标,而它相对于坐标原点的,叫做世界坐标,可以通过 obj.getWorldPosition 来拿到。
遍历这颗对象树,用 traverse 的 API,还可以通过 isMesh、isPoints 等来区分具体的类型,或者通过 getObjectByName、getObjectById 来查找特定对象。
复杂的场景基本都是一个很大的对象树,后面会经常需要遍历 scene、查找某个具体的对象。