Three.js 模型树结构与节点查询学习笔记
1. 场景图(Scene Graph)概念
什么是场景图?
- Three.js使用树形结构来组织3D对象
- Scene是根节点,包含所有子对象
- 对象可以嵌套形成父子关系
javascript
Scene (根节点)
├── Group1 (组节点)
│ ├── Mesh1 (网格对象)
│ ├── Mesh2
│ └── Group1-1
└── Group2
├── Mesh3
└── Mesh4
2. 递归遍历方法
2.1 scene.traverse() - 递归遍历所有后代
javascript
scene.traverse((object) => {
// 这个回调函数会对每个对象执行
console.log('遍历到的对象:', object.name);
console.log('对象类型:', object.type);
console.log('对象ID:', object.id);
});
2.2 筛选特定类型的对象
javascript
scene.traverse((object) => {
// 只处理网格对象
if (object.isMesh) {
console.log("网格对象:", object.name);
// 可以修改材质、位置等属性
object.material.color.set(0xff0000);
}
// 只处理灯光对象
if (object.isLight) {
console.log("灯光对象:", object.name);
}
// 只处理组对象
if (object.isGroup) {
console.log("组对象:", object.name);
}
});
3. 节点查询方法
3.1 通过名称查询
javascript
// 获取单个对象(返回第一个匹配的)
const building = scene.getObjectByName("1号楼");
if (building) {
building.material.color.set(0xffffff);
}
// 获取所有同名对象
const allBuildings = [];
scene.traverse(obj => {
if (obj.name.includes("号楼")) {
allBuildings.push(obj);
}
});
3.2 通过ID查询
javascript
// 每个Three.js对象都有唯一ID
const objectById = scene.getObjectById(12345);
3.3 通过属性查询
javascript
// 查找所有特定材质的对象
const redBuildings = [];
scene.traverse(obj => {
if (obj.isMesh && obj.material.color.getHex() === 0xff0000) {
redBuildings.push(obj);
}
});
4. 实用查询函数示例
4.1 查找所有后代网格
javascript
function findAllMeshes(scene) {
const meshes = [];
scene.traverse(obj => {
if (obj.isMesh) {
meshes.push(obj);
}
});
return meshes;
}
const allMeshes = findAllMeshes(scene);
console.log("场景中所有网格:", allMeshes);
4.2 按名称模式查找
javascript
function findObjectsByNamePattern(scene, pattern) {
const results = [];
scene.traverse(obj => {
if (obj.name && obj.name.match(pattern)) {
results.push(obj);
}
});
return results;
}
// 查找所有以"号楼"结尾的对象
const buildings = findObjectsByNamePattern(scene, /号楼$/);
4.3 查找对象的父级链
javascript
function getParentChain(object) {
const chain = [];
let current = object;
while (current.parent) {
chain.unshift(current.parent.name || '无名对象');
current = current.parent;
}
return chain;
}
const obj = scene.getObjectByName("1号楼");
console.log("父级链:", getParentChain(obj));
// 输出: ["一号楼群", "Scene"]
5. 实际应用场景
5.1 批量修改材质
javascript
// 批量修改所有楼的材质
function changeAllBuildingMaterials(scene, color) {
scene.traverse(obj => {
if (obj.isMesh && obj.name.includes("号楼")) {
obj.material.color.set(color);
}
});
}
changeAllBuildingMaterials(scene, 0x00ff00); // 全部变绿色
5.2 选择性隐藏对象
javascript
// 隐藏特定名称的对象
function hideObjectsByName(scene, namePattern) {
scene.traverse(obj => {
if (obj.name && obj.name.match(namePattern)) {
obj.visible = false;
}
});
}
hideObjectsByName(scene, /^[0-3]号楼/); // 隐藏0-3号楼
5.3 统计场景信息
javascript
function analyzeScene(scene) {
const stats = {
totalObjects: 0,
meshes: 0,
lights: 0,
groups: 0,
materials: new Set()
};
scene.traverse(obj => {
stats.totalObjects++;
if (obj.isMesh) {
stats.meshes++;
if (obj.material) {
stats.materials.add(obj.material);
}
}
if (obj.isLight) stats.lights++;
if (obj.isGroup) stats.groups++;
});
return {
...stats,
uniqueMaterials: stats.materials.size
};
}
console.log("场景分析:", analyzeScene(scene));
6. 调试技巧
6.1 打印完整场景结构
javascript
function printSceneStructure(scene, indent = 0) {
const spaces = ' '.repeat(indent);
scene.children.forEach(child => {
console.log(`${spaces}${child.type}: ${child.name || '无名'} (ID: ${child.id})`);
if (child.children && child.children.length > 0) {
printSceneStructure(child, indent + 1);
}
});
}
printSceneStructure(scene);
7. 注意事项
- 性能考虑:遍历大型场景时注意性能影响
- 内存管理:及时清理不再需要的对象引用
- 名称唯一性:尽量使用唯一名称便于查询
- 错误处理:查询时检查对象是否存在
8. 总结
scene.traverse()是强大的递归遍历工具getObjectByName()适合精确查询单个对象- 组合使用这些方法可以实现复杂的场景管理
- 理解场景树结构是掌握Three.js的关键
这些技能在游戏开发、3D编辑器、数据可视化等场景中都非常有用!
javascript
<template>
<div class="container" ref="containerRef"></div>
</template>
<script setup>
import { onMounted, ref } from "vue";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls";
const containerRef = ref(null);
// 创建3D场景对象Scene
const scene = new THREE.Scene();
// 实例化一个透视投影相机对象
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// 实例化一个WebGL渲染器
const renderer = new THREE.WebGLRenderer({ antialias: true });
// 设置Canvas画布尺寸
renderer.setSize(window.innerWidth, window.innerHeight);
// 创建第一组楼群
const group1 = new THREE.Group();
group1.name = '一号楼群';
for (let i = 0; i < 5; i++) {
const geometry = new THREE.BoxGeometry(0.3, 0.8, 0.3); // 宽度, 高度, 深度
const material = new THREE.MeshLambertMaterial({
color: Math.random() * 0xffffff,
});
const building = new THREE.Mesh(geometry, material);
building.position.x = i * 0.6; // X轴方向排列
building.position.y = 0.4; // 将楼放在地面上(高度的一半)
building.name = i + "号楼";
group1.add(building);
}
// 创建第二组楼群
const group2 = new THREE.Group();
group2.name = '二号楼群';
for (let i = 0; i < 5; i++) {
const geometry = new THREE.BoxGeometry(0.3, 1.0, 0.3);
const material = new THREE.MeshLambertMaterial({
color: Math.random() * 0xffffff,
});
const building = new THREE.Mesh(geometry, material);
building.position.x = i * 0.6;
building.position.z = -0.8; // Z轴方向排列
building.position.y = 0.5; // 将楼放在地面上(高度的一半)
building.name = i + "号楼";
group2.add(building);
}
scene.add(group1, group2);
// 环境光
const ambientLight = new THREE.AmbientLight(0xffffff, 0.6);
scene.add(ambientLight);
// 方向光
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(10, 10, 5);
scene.add(directionalLight);
// 遍历场景中的所有对象
scene.traverse((obj) => {
// 判断对象是否为网格模型对象
if (obj.isMesh) {
console.log("对象名称:" + obj.name);
//修改模型颜色
// obj.material.color.set(0xff0000);
}
});
// 通过名称获取特定对象并修改颜色
const obj = scene.getObjectByName("1号楼");
if (obj) {
obj.material.color.set(0xffffff); // 将1号楼设置为白色
}
// 坐标系辅助
const axesHelper = new THREE.AxesHelper(5);
scene.add(axesHelper);
// 设置相机位置
camera.position.set(3, 3, 3);
camera.lookAt(0, 0, 0);
function animate() {
requestAnimationFrame(animate);
renderer.render(scene, camera);
}
animate();
onMounted(() => {
const controls = new OrbitControls(camera, containerRef.value);
controls.enableDamping = true;
containerRef.value.appendChild(renderer.domElement);
});
</script>