如何在 TypeScript 中遍历 3D 场景图

如何在 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';

SceneMesh 是核心类。. 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.entitynull 用于组节点、骨骼和定位器。该 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.entitynode.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.

另请参阅

 中文