(一)场景(Scene)对象
在 Three.js 中,THREE.Scene是整个 3D 场景的容器,它是场景层级结构的根节点。可以将场景想象成一个空的舞台,所有的 3D 元素(如几何体、光源、相机等)都要被添加到这个舞台上才能在最终渲染中呈现。
创建一个场景非常简单,只需如下代码:
js
const scene = new THREE.Scene();
这个scene对象将作为所有后续添加到场景中的对象的父级。
(二)对象 3D(Object3D)基类
THREE.Object3D是 Three.js 中几乎所有对象的基类,包括THREE.Scene、THREE.Mesh(网格,用于表示 3D 物体)、THREE.Group(组,用于将多个对象组合在一起)、THREE.Light(光源)等。这意味着这些对象都继承了Object3D的属性和方法,其中与场景层级密切相关的是add、remove方法以及children属性。
- add 方法:用于将一个或多个子对象添加到当前对象中。例如,如果我们有一个场景对象scene和一个网格对象mesh,可以通过以下方式将mesh添加到scene中:
js
const mesh = new THREE.Mesh(geometry, material);
scene.add(mesh);
- remove 方法:与add方法相反,用于从当前对象中移除一个或多个子对象。假设我们要从scene中移除刚才添加的mesh,可以这样做:
js
scene.remove(mesh);
- children 属性:该属性是一个数组,包含了当前对象的所有直接子对象。通过访问children属性,可以遍历和操作当前对象的子对象。例如,要遍历scene中的所有子对象,可以使用以下代码:
js
scene.children.forEach((child) => {
// 在这里对每个子对象进行操作
console.log(child);
});
(三)组(Group)对象
THREE.Group对象是Object3D的一个子类,它的主要作用是将多个对象组合成一个逻辑单元。通过将相关的对象组合到一个组中,可以方便地对这些对象进行统一的变换(如移动、旋转、缩放)和管理。
例如,我们要创建一个由多个部分组成的复杂物体(如一辆汽车,包含车身、车轮等),可以将每个部分作为一个独立的Mesh对象,然后将这些Mesh对象添加到一个Group对象中。
js
// 创建车身网格
const bodyGeometry = new THREE.BoxGeometry(1, 0.5, 2);
const bodyMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const bodyMesh = new THREE.Mesh(bodyGeometry, bodyMaterial);
// 创建车轮网格
const wheelGeometry = new THREE.CylinderGeometry(0.1, 0.1, 0.2, 32);
const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x000000 });
const wheelMesh1 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheelMesh2 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheelMesh3 = new THREE.Mesh(wheelGeometry, wheelMaterial);
const wheelMesh4 = new THREE.Mesh(wheelGeometry, wheelMaterial);
// 将车轮网格放置在合适位置
wheelMesh1.position.set(-0.8, -0.2, 0.8);
wheelMesh2.position.set(0.8, -0.2, 0.8);
wheelMesh3.position.set(-0.8, -0.2, -0.8);
wheelMesh4.position.set(0.8, -0.2, -0.8);
// 创建组对象并添加车身和车轮
const carGroup = new THREE.Group();
carGroup.add(bodyMesh);
carGroup.add(wheelMesh1);
carGroup.add(wheelMesh2);
carGroup.add(wheelMesh3);
carGroup.add(wheelMesh4);
// 最后将组对象添加到场景中
scene.add(carGroup);
这样,当我们对carGroup进行移动、旋转或缩放操作时,组内的所有对象(车身和车轮)都会相应地发生变化。
二、场景层级中的坐标系统
(一)世界坐标(World Coordinates)
世界坐标是整个场景的全局坐标系统。场景中的每个对象都有一个相对于世界坐标系原点(0, 0, 0)的位置。当对象直接添加到Scene对象下时,它的位置就是以世界坐标来表示的。
例如,创建一个简单的立方体并将其添加到场景中:
js
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cubeMesh = new THREE.Mesh(cubeGeometry, cubeMaterial);
cubeMesh.position.set(2, 1, 3); // 在世界坐标中设置位置
scene.add(cubeMesh);
这里cubeMesh的位置(2, 1, 3)就是相对于世界坐标系原点的位置。
(二)局部坐标(Local Coordinates)
局部坐标是相对于对象自身的坐标系统。每个对象都有自己的局部坐标系,其原点位于对象自身的中心(对于大多数几何体而言)。当一个对象作为另一个对象的子对象时,它的位置、旋转和缩放都是相对于其父对象的局部坐标系来定义的。
回到前面汽车的例子,车轮作为carGroup的子对象,它们的位置(如wheelMesh1.position.set(-0.8, -0.2, 0.8))就是相对于carGroup的局部坐标系的。这意味着当carGroup在世界坐标系中移动、旋转时,车轮会基于它们在carGroup局部坐标系中的位置相应地移动和旋转,而无需重新计算它们在世界坐标系中的绝对位置。
理解世界坐标和局部坐标的转换关系非常重要。Three.js 提供了一些方法来进行坐标转换,例如localToWorld
方法可以将局部坐标转换为世界坐标,worldToLocal
方法则相反。假设我们有一个对象obj,要将其局部坐标(1, 1, 1)转换为世界坐标,可以这样做:
js
const localVector = new THREE.Vector3(1, 1, 1);
const worldVector = localVector.clone();
obj.localToWorld(worldVector);
console.log(worldVector); // 输出转换后的世界坐标
三、场景层级与渲染
(一)渲染顺序
Three.js 在渲染场景时,会从场景层级结构的根节点(即Scene对象)开始,递归地遍历所有子对象,并按照它们在层级结构中的顺序进行渲染。这意味着先添加到场景中的对象会先被渲染,后添加的对象会在上面进行渲染。
例如,如果我们先添加一个蓝色的立方体,然后在相同位置添加一个红色的球体,那么球体将会覆盖在立方体之上显示,因为球体是后被渲染的。
js
// 先添加蓝色立方体
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshStandardMaterial({ color: 0x0000ff });
const cubeMesh = new THREE.Mesh(cubeGeometry, cubeMaterial);
scene.add(cubeMesh);
// 后添加红色球体
const sphereGeometry = new THREE.SphereGeometry(0.5, 32, 32);
const sphereMaterial = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const sphereMesh = new THREE.Mesh(sphereGeometry, sphereMaterial);
sphereMesh.position.set(0, 0, 0);
scene.add(sphereMesh);
在这个例子中,球体由于后添加到场景中,所以会显示在立方体之上。
(二)可见性与剔除
场景层级结构对于控制对象的可见性和剔除(culling)非常有用。每个Object3D对象都有一个visible属性,通过设置该属性可以控制对象是否在渲染中显示。当一个对象的visible属性设置为false时,它及其所有子对象都不会被渲染。
js
const group = new THREE.Group();
const mesh1 = new THREE.Mesh(geometry1, material1);
const mesh2 = new THREE.Mesh(geometry2, material2);
group.add(mesh1);
group.add(mesh2);
scene.add(group);
// 设置组不可见,组内的mesh1和mesh2都不会被渲染
group.visible = false;
此外,Three.js 的渲染器会自动进行一些剔除操作,以提高渲染效率。例如,对于背对摄像机的面(默认情况下,Three.js 使用背面剔除),渲染器不会对其进行渲染。在复杂场景中,合理利用场景层级结构和对象的可见性设置,可以进一步优化渲染性能,减少不必要的渲染计算。
四、场景层级的遍历与查找
(一)遍历场景层级
在开发过程中,有时需要遍历整个场景层级结构,对每个对象进行特定的操作。由于场景层级是一个树状结构,我们可以使用递归算法来实现遍历。
下面是一个简单的递归函数,用于遍历场景中的所有对象并打印它们的名称(假设每个对象都有name属性):
js
function traverseScene(node) {
console.log(node.name);
node.children.forEach((child) => {
traverseScene(child);
});
}
// 从场景根节点开始遍历
traverseScene(scene);
这个函数首先打印当前节点的名称,然后递归地调用自身来遍历当前节点的所有子节点。通过这种方式,可以访问到场景中的每一个对象。
(二)查找特定对象
在复杂的场景中,可能需要根据特定的条件查找某个对象。例如,根据对象的名称查找对象。我们可以在遍历场景层级的基础上实现对象查找功能。
js
function findObjectByName(node, name) {
if (node.name === name) {
return node;
}
for (let i = 0; i < node.children.length; i++) {
const found = findObjectByName(node.children[i], name);
if (found) {
return found;
}
}
return null;
}
// 查找名称为"specificMesh"的对象
const specificMesh = findObjectByName(scene, "specificMesh");
if (specificMesh) {
// 对找到的对象进行操作
specificMesh.material.color.set(0xff00ff);
}
这个函数在遍历场景层级的过程中,检查每个对象的名称是否与目标名称匹配。如果匹配,则返回该对象;如果在当前节点及其子节点中都没有找到匹配的对象,则返回null。
通过对 Three.js 场景层级的深入理解,包括场景层级的基础构成、坐标系统、渲染相关以及遍历查找等方面,开发者能够更加高效地构建、管理和优化复杂的 3D 场景,实现丰富的 3D 交互体验。