如何在 TypeScript 中遍历 3D 场景图
在 Aspose.3D FOSS for TypeScript 中,场景图是一个树形结构,包含 Node 根对象 scene.rootNode. 遍历是递归的:每个节点公开一个 childNodes iterable 和一个可选的 entity 属性。本文档展示了如何遍历整棵树、识别实体类型并收集 mesh 统计信息。.
先决条件
- Node.js 18 或更高版本
- TypeScript 5.0 或更高版本
@aspose/3d已安装
分步指南
步骤 1:安装并导入
安装该包::
npm install @aspose/3d导入本指南中使用的类::
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';Scene 和 Mesh 是核心类。. ObjLoadOptions 在加载示例中使用;对于其他格式,请替换为相应的 options 类。.
步骤 2:从文件加载场景
创建一个 Scene 并调用 scene.open() 并提供文件路径。格式检测会自动根据二进制魔数进行,因此对于 GLB、STL 或 3MF 文件,无需手动指定格式::
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
const scene = new Scene();
scene.open('model.obj', new ObjLoadOptions());
console.log(`Root node: "${scene.rootNode.name}"`);
console.log(`Top-level children: ${scene.rootNode.childNodes.length}`);您也可以从一个 Buffer 在内存中使用 scene.openFromBuffer(buffer, options);;在磁盘 I/O 不可用的无服务器流水线中很有用。.
步骤 3:编写递归遍历函数
对…进行递归 childNodes 是标准模式。该函数以深度优先方式访问每个节点::
function traverse(node: any, depth = 0): void {
const indent = ' '.repeat(depth);
const entityType = node.entity ? node.entity.constructor.name : '-';
console.log(`${indent}[${entityType}] ${node.name}`);
for (const child of node.childNodes) {
traverse(child, depth + 1);
}
}
traverse(scene.rootNode);对于一个包含名为 Cube,,输出将类似于::
[-] RootNode
[Mesh] Cube<button class=“hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50” title=“Copy code”
<div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div>
<div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div>
node.entity 是 null 用于组节点、骨骼和定位器。该 constructor.name 检查适用于任何实体类型:: Mesh, Camera, Light,,等等。.
步骤 4:访问每个节点的实体类型
要根据实体类型采取操作,请使用一个 instanceof 在空值防护后检查::
import { Mesh } from '@aspose/3d/entities';
function visitWithTypeCheck(node: any, depth = 0): void {
const indent = ' '.repeat(depth);
if (node.entity instanceof Mesh) {
const mesh = node.entity as Mesh;
console.log(`${indent}MESH "${node.name}": ${mesh.controlPoints.length} vertices`);
} else if (node.entity) {
console.log(`${indent}${node.entity.constructor.name} "${node.name}"`);
} else {
console.log(`${indent}GROUP "${node.name}"`);
}
for (const child of node.childNodes) {
visitWithTypeCheck(child, depth + 1);
}
}
visitWithTypeCheck(scene.rootNode);instanceof Mesh 是确认实体在访问前为多边形网格的最安全方式 controlPoints, polygonCount, 或者顶点元素。.
步骤 5:按实体类型过滤节点
若只收集包含网格的节点而不打印完整树,可使用递归累加器::
import { Mesh } from '@aspose/3d/entities';
function collectMeshes(
node: any,
results: Array<{ name: string; mesh: Mesh }> = []
): Array<{ name: string; mesh: Mesh }> {
if (node.entity instanceof Mesh) {
results.push({ name: node.name, mesh: node.entity as Mesh });
}
for (const child of node.childNodes) {
collectMeshes(child, results);
}
return results;
}
const meshNodes = collectMeshes(scene.rootNode);
console.log(`Found ${meshNodes.length} mesh node(s)`);该函数接受一个可选的 results 数组,以便调用者可以预先填充它,用于合并跨多个子树的结果。.
步骤 6:收集所有网格并打印顶点计数
扩展收集器以打印每个网格的统计信息::
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';
function collectMeshes(node: any, results: Array<{name: string, mesh: Mesh}> = []) {
if (node.entity instanceof Mesh) {
results.push({ name: node.name, mesh: node.entity as Mesh });
}
for (const child of node.childNodes) {
collectMeshes(child, results);
}
return results;
}
const scene = new Scene();
scene.open('model.obj', new ObjLoadOptions());
const meshes = collectMeshes(scene.rootNode);
for (const { name, mesh } of meshes) {
console.log(`${name}: ${mesh.controlPoints.length} vertices, ${mesh.polygonCount} polygons`);
}两网格场景的示例输出::
Cube: 8 vertices, 6 polygons
Sphere: 482 vertices, 480 polygons<button class=“hextra-code-copy-btn hx-group/copybtn hx-transition-all active:hx-opacity-50 hx-bg-primary-700/5 hx-border hx-border-black/5 hx-text-gray-600 hover:hx-text-gray-900 hx-rounded-md hx-p-1.5 dark:hx-bg-primary-300/10 dark:hx-border-white/10 dark:hx-text-gray-400 dark:hover:hx-text-gray-50” title=“Copy code”
<div class="copy-icon group-[.copied]/copybtn:hx-hidden hx-pointer-events-none hx-h-4 hx-w-4"></div>
<div class="success-icon hx-hidden group-[.copied]/copybtn:hx-block hx-pointer-events-none hx-h-4 hx-w-4"></div>
提示与最佳实践
- 始终进行空值检查
node.entity在访问特定实体属性之前。许多节点是纯组节点,不携带任何实体。. - 使用
instanceof相较于constructor.name用于逻辑路径中的类型检查。.instanceof是重构安全的;对…进行字符串比较constructor.name在压缩时会出错。. - 遍历方式
for...of遍历childNodes::可迭代对象安全地处理所有数组大小。避免使用数值索引以保持向前兼容性。. - 遍历过程中避免修改树结构::不要在递归调用中添加或删除节点。先收集结果,再进行修改。.
- 将结果数组作为参数传递::这可以避免在每次递归调用时分配新数组,并且便于合并子树结果。.
常见问题
| 症状 | 原因 | 解决方案 |
|---|---|---|
childNodes 在…上长度为零 rootNode | 模型未加载 | 确保 scene.open() 在遍历之前确保已完成且无错误 |
node.entity instanceof Mesh 永不为真 | 错误 Mesh 导入路径 | 导入 Mesh 来自 @aspose/3d/entities,,而不是来自 @aspose/3d 根 |
| 遍历遗漏了嵌套网格 | 未递归所有子节点 | 确保递归调用覆盖其中的每个元素 node.childNodes |
mesh.controlPoints.length 是 0 | 网格已加载,但不包含几何体 | 检查 OBJ 源文件中的空组;使用 mesh.polygonCount 作为二次检查 |
| 深层层次结构导致栈溢出 | 非常深的场景树(数百层) | 使用显式栈替代递归,使用 Array.push / Array.pop |
常见问答
是否 scene.rootNode 本身携带实体吗?? 不。根节点是库自动创建的容器。它没有实体。你的几何体和其他场景对象位于子节点上,至少在下面一层或多层 rootNode.
之间有什么区别 node.entity 和 node.entities? node.entity 保存单一的主要实体(常见情况)。某些较旧的 FBX 和 COLLADA 文件可能会生成具有多个附加实体的节点;在这种情况下 node.entities (复数)提供完整列表。.
我可以使用广度优先而不是深度优先遍历吗?? 是的。使用队列而不是递归调用:push scene.rootNode 到数组中,然后 shift 并在处理节点的同时将每个节点的 childNodes 推入队列尾部。.
是 scene.open() 同步吗?? 是的。. scene.open() 以及 scene.openFromBuffer() 两者都会阻塞调用线程,直到文件完全解析完毕。如果需要保持事件循环响应,请将它们包装在工作线程中。.
如何从节点获取世界空间位置?? 读取 node.globalTransform; 它返回只读的 GlobalTransform 使用世界空间矩阵,由所有祖先变换组合而成。若需显式矩阵运算,请调用 node.evaluateGlobalTransform(false).
除了…之外,还可能有哪些实体类型 Mesh? Camera, Light, 和自定义骨架/骨骼实体。检查 node.entity.constructor.name 或使用 instanceof 使用从…导入的特定类 @aspose/3d.