Skip to content

19. 场景遍历和世界坐标

Published:

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

image.png

可以通过 Group 来添加子对象,这些对象之间构成一棵树。

那一个网格模型 Mesh,直接添加到 Scene 中,和添加到 Group 下再添加到 Scene 中,有什么区别呢?

我们试一下就知道了。

创建项目:

mkdir scene-group
cd scene-group
npm init -y

image.png

安装 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

image.png

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

image.png

mesh.position.x = 200;
mesh.translateZ(200);

比如我们把它 position.x 设置 200,然后再 translateZ 200

image.png

它会移动到这个为止。

那如果我们把它放到 Group 里再移动呢?

image.png

const group = new THREE.Group();
group.add(mesh);
scene.add(group);
group.position.x = 200;
group.translateZ(200);

把它 clone 一份放到 Group 里,然后移动 Group。

image.png

这样位置是一样的。

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

image.png

mesh.position.x = 200;

image.png

可以看到,它现在位置和原来的不一样了,x 是 400

也就是说添加到 Group 之后,它的绝对坐标是 group 的 position 加上它的 position,这个叫做世界坐标

而它在 Group 内部的 position 叫做局部坐标

那随便给一个 Scene 中的物体,如何计算它的世界坐标呢?

用 getWorldPosition 这个 API:

image.png

const pos = new THREE.Vector3();
mesh.getWorldPosition(pos);
console.log(pos);
console.log(group.position);
console.log(mesh.position);

创建一个 Vector3 三维向量对象,然后调用 getWorldPosition 方法,传入它,这样就可以从 vector3 对象里拿到位置了。

image.png

第一个就是 mesh 的世界坐标,可以看到是 group 的 position 加上了 mesh 的 position

而下面打印的 position 就是 mesh 的局部坐标。

我们可以加一个 AxesHelper 让 group 的局部坐标系展示出来:

image.png

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

image.png

我们打印下现在的 scene:

image.png

image.png

可以看到,它有 Group、DirectionLight、AxesHelper 这三个子对象。

而 Group 的 children 也有两个:

image.png

那遍历 Scene 中的所有对象就是几重循环的事情了。

不过不用自己实现,Three.js 提供了这个 API:

image.png

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

image.png

还可以做下过滤,比如改下所有 Mesh 的材质颜色

scene.traverse((obj) => {
    if(obj.isMesh) {
        obj.material.color = new THREE.Color('pink');
    }
});

image.png

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

image.png

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

image.png

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

image.png

getObjectById 则是根据 id 找,用法一样。

这样,我们就可以遍历场景中的所有对象,然后找到 Mesh 或者特定的对象了。

案例代码上传了小册仓库

总结

Scene 中保存的是一个对象树,包含 Mesh、Group、Light 等各种对象。

Mesh 如果添加到 Group 中,那它的 position 就是相对于 Group 的,叫做局部坐标,而它相对于坐标原点的,叫做世界坐标,可以通过 obj.getWorldPosition 来拿到。

遍历这颗对象树,用 traverse 的 API,还可以通过 isMesh、isPoints 等来区分具体的类型,或者通过 getObjectByName、getObjectById 来查找特定对象。

复杂的场景基本都是一个很大的对象树,后面会经常需要遍历 scene、查找某个具体的对象。

评论