Three.js 对象分组
概念与原理
在 Three.js 中,所有可见或不可见的 3D 对象都继承自 Object3D
。换句话说,Mesh
、Light
、Camera
、Scene
等等,都是基于 Object3D
做了不同功能的扩展。Group
也是这样一个基于 Object3D
的类,但它的主要角色是当作“容器”,用来批量管理多个 3D 对象。
Group
与 Object3D
、Scene
的关系
Object3D
这是 Three.js 中最基础的 3D 对象类型,它提供位置 (position
)、旋转 (rotation
)、缩放 (scale
) 等属性,以及管理子节点的能力。Scene
也是继承自Object3D
的一种特殊对象,一般作为我们 3D 世界的根节点。它除了具备Object3D
的常用属性外,还可以管理背景、雾效、全局灯光等全局场景设置。Group
和Scene
一样,Group
同样继承自Object3D
。不同的是,Group
主要用来把一批对象放在一起,方便统一移动、旋转、缩放等操作。它本身并没有像Scene
那样的场景全局配置功能,更像一个“可移动的小容器”。
为什么需要 Group
?
- 分组管理
当你在场景里需要同时控制多个对象(例如移动一整个房屋组、一个机器人各个部件、或者建筑群),就可以先把它们都加进同一个Group
,然后只需要操作这个Group
,就能让所有子对象一起移动或旋转。 - 层级结构清晰
通过Group
做层级嵌套,可以让场景结构更有条理,查找或管理也更方便。想隐藏一整组对象或做动画时,只要对Group
操作即可,无需一个个处理。 - 减少代码耦合
如果没有Group
,你可能得分别移动场景中的每个单独对象,或者把相关逻辑写在不同地方。用Group
统一管理后,可以将逻辑收拢到一个“容器”对象里,简化后期维护。
简而言之,把若干对象归纳到同一个父节点,然后可以通过操作这个父节点来批量管理。
常见方法与属性
add(object)
与 remove(object)
add(object)
:将任意Object3D
子类实例(Mesh
、Light
、Camera
、Group
等)添加到当前Group
。remove(object)
:从当前Group
中移除已经存在的子对象。
// 创建一个Group和一个简单的网格对象
const group = new THREE.Group();
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const boxMesh = new THREE.Mesh(geometry, material);
// 使用 add() 将网格添加到 group 中
group.add(boxMesh);
console.log(group.children.length); // 输出 1
// 使用 remove() 将网格从 group 中移除
group.remove(boxMesh);
console.log(group.children.length); // 输出 0
通过 add()
可以批量挂载多个子对象;remove()
则适用于动态场景中对对象进行移除操作。如果频繁增删子对象,需要注意性能问题,必要时可以使用隐藏(visible = false
)代替移除操作。
children
Group
(或 Object3D
)下所有子对象的只读数组,常用于遍历和批量管理。
// 假设 group 中已添加多个网格对象
group.children.forEach((child, index) => {
// 可以判断 child 的类型,或统一修改属性
if (child instanceof THREE.Mesh) {
child.material.color.set(0x00ff00); // 全部改为绿色
console.log(`子对象索引:${index}`, child);
}
});
children
数组可以让你批量处理所有子对象,例如统一修改材质、可见性等。也可用于查找特定子对象进行后续操作
position
、rotation
、scale
position
:表示Group
在三维空间的坐标(Vector3
类型),影响子对象的整体位置。rotation
:表示Group
的旋转角度(Euler
类型),父节点的旋转会叠加到子节点的世界坐标中。scale
:表示Group
的缩放系数(Vector3
类型),对子节点的显示尺寸也有直接影响。
// 创建一个 Group 并添加多个子对象
const groupTransform = new THREE.Group();
scene.add(groupTransform);
const sphere = new THREE.Mesh(
new THREE.SphereGeometry(0.5, 32, 32),
new THREE.MeshStandardMaterial({ color: 0x0000ff })
);
const cone = new THREE.Mesh(new THREE.ConeGeometry(0.5, 1, 16), new THREE.MeshStandardMaterial({ color: 0xffff00 }));
// 加入 Group
groupTransform.add(sphere);
groupTransform.add(cone);
// 统一设置位置:向 X 方向平移 3 个单位
groupTransform.position.set(3, 0, 0);
// 统一旋转:绕 Z 轴旋转 45 度(π/4)
groupTransform.rotation.z = Math.PI / 4;
// 统一缩放:在 X、Y、Z 方向均放大 1.5 倍
groupTransform.scale.set(1.5, 1.5, 1.5);
- 修改
groupTransform.position
会带动所有子对象同步移动; - 修改
groupTransform.rotation
会整体旋转整个组(包括所有子对象); scale
的数值越大,子对象被放大的比例越高;通常1
为原始大小;在动画中还可结合缓动函数实现平滑缩放效果。
例子
最后,我们在通过一个完整的例子,来看下 group 效果。
我们在创建一个简单的场景,包含立方体和圆柱体,然后将它们放在一个 Group
中,再将这个 Group
放置在场景中心偏左的区域。
import * as THREE from "three";
import "./style.css";
function initScene() {
// 1. 创建场景、相机、渲染器
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
60, // FOV: 视场角度
window.innerWidth / window.innerHeight, // 宽高比
0.1, // 近截面
1000 // 远截面
);
camera.position.set(0, 0, 20); // 将相机放在稍微高一点,往后一点的位置
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// 2. 添加一个基础光源
const ambientLight = new THREE.AmbientLight(0xffffff, 0.4);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 20, 10);
scene.add(directionalLight);
// 3. 创建一些简单对象,用以模拟建筑和树木
const geometryHouse = new THREE.BoxGeometry(4, 4, 4);
const materialHouse = new THREE.MeshStandardMaterial({ color: 0x8b4513 });
const house = new THREE.Mesh(geometryHouse, materialHouse);
house.position.set(0, 2, 0); // 让房子底部对准地面
const geometryTree = new THREE.CylinderGeometry(0.5, 0.5, 3, 8);
const materialTree = new THREE.MeshStandardMaterial({ color: 0x006400 });
const tree = new THREE.Mesh(geometryTree, materialTree);
tree.position.set(-3, 1.5, 3);
// 4. 创建 Group,并将这些对象加入其中
const group = new THREE.Group();
group.add(house);
group.add(tree);
// // 将 group 整体放置在场景中心偏左的区域
// group.position.set(-5, 0, 0);
// 5. 添加到场景
scene.add(group);
// 6. 动画循环
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
}
initScene();
通过上面的代码,我们可以在画面中间看到一个立方体和圆柱, 他们现在在画面中心。
现在,我们将上面的代码中的 group.position.set(-5, 0, 0);
注释掉,然后再次运行代码,我们可以看到,立方体和圆柱体现在在画面中心偏左的区域。
注意事项
- 滥用嵌套层级
虽然通过Group
可以轻松实现层级嵌套,但如果层级过深,会让场景树结构变得复杂,也会增加计算开销(因为每一层都会有自己的世界矩阵计算)。建议保持合理的层级深度。 - 频繁增删子对象
如果在运行时非常频繁地调用add()
和remove()
来增删子对象,可能会带来性能抖动。此时可考虑对象池等机制,在不需要渲染某对象时将其隐藏或移出摄像机视野,而不是直接移除。 - 批量操作
如果需要对一组子对象执行相同的操作(如更改材质、切换可见性),可以先把它们组织到一个Group
里,再统一改Group
的属性,或者遍历Group.children
进行批量处理。避免单独处理每个对象,降低代码复杂度。
总结
Group
是在 Three.js 中对多个对象进行统一管理、变换和渲染的强大工具,也是场景层级树结构的重要一环。通过使用 Group
:
- 可以对一批对象进行批量操作(整体移动、旋转、缩放、可见性切换等);
- 可以通过嵌套结构来实现更加复杂、灵活的分层动画或交互逻辑;
- 可以在大型项目中清晰地拆分和管理场景模块,提升可维护性和可扩展性。
在更为高级的场景中,Group
也常与以下功能配合使用:
- 骨骼动画:在蒙皮网格(
SkinnedMesh
)的骨骼系统中,每个骨骼节点本质上也类似一个层级结构,Group
思想与之相通。 - 粒子系统:可将粒子发射器或粒子集合归类到一个
Group
中,便于整体启用或禁用。 - 复杂交互:在做多人协同或多视角切换时,可能需要对不同子树进行可见性管理或者位置同步,这些也离不开
Group
带来的层级与批量优势。
代码
github
https://github.com/calmound/threejs-demo/tree/main/group (opens in a new tab)
gitee
https://gitee.com/calmound/threejs-demo/tree/main/group (opens in a new tab)