Cómo recorrer un grafo de escena 3D en TypeScript

Cómo recorrer un grafo de escena 3D en TypeScript

El grafo de escena en Aspose.3D FOSS for TypeScript es un árbol de Node objetos con raíz en scene.rootNode. El recorrido es recursivo: cada nodo expone un iterable childNodes y una propiedad opcional entity. Esta guía muestra cómo recorrer todo el árbol, identificar los tipos de entidad y recopilar estadísticas de mallas.

Requisitos

  • Node.js 18 o posterior
  • TypeScript 5.0 o posterior
  • @aspose/3d instalado

Guía paso a paso

Paso 1: Instalar e Importar

Instala el paquete:

npm install @aspose/3d

Importe las clases utilizadas en esta guía:

import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';

Scene y Mesh son las clases principales. ObjLoadOptions se usa en el ejemplo de carga; sustituya la clase de opciones correspondiente para otros formatos.


Paso 2: Cargar una escena desde un archivo

Cree un Scene y llame a scene.open() con una ruta de archivo. La detección de formato es automática a partir de los números mágicos binarios, por lo que no necesita especificar el formato para archivos GLB, STL o 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}`);

También puedes cargar desde un Buffer en memoria usando scene.openFromBuffer(buffer, options); útil en canalizaciones sin servidor donde el disk I/O no está disponible.


Paso 3: Escribe una función de recorrido recursivo

La recursión sobre childNodes es el patrón estándar. La función visita cada nodo en profundidad:

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);

Para una escena con una malla llamada Cube, la salida se verá así:

[-] 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 es null para nodos de grupo, huesos y localizadores. La comprobación constructor.name funciona para cualquier tipo de entidad: Mesh, Camera, Light, etc.


Paso 4: Acceder al Tipo de Entidad en Cada Nodo

Para tomar acción según el tipo de entidad, use una verificación instanceof después de la protección contra nulos:

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 es la forma más segura de confirmar que la entidad es una malla de polígonos antes de acceder a controlPoints, polygonCount o a los elementos de vértice.


Paso 5: Filtrar nodos por tipo de entidad

Para recopilar solo los nodos que contienen malla sin imprimir el árbol completo, use un acumulador recursivo:

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)`);

La función acepta una matriz opcional results para que los llamadores puedan pre‑poblarla al fusionar resultados a través de varios subárboles.


Paso 6: Recopilar todas las mallas y imprimir el recuento de vértices

Extienda el colector para imprimir estadísticas por malla:

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`);
}

Ejemplo de salida para una escena de dos mallas:

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>

Consejos y Mejores Prácticas

  • Siempre verifique nulos node.entity antes de acceder a propiedades específicas de la entidad. Muchos nodos son nodos de grupo puros que no llevan entidad.
  • Use instanceof en lugar de constructor.name para verificaciones de tipo en rutas lógicas. instanceof es seguro para refactorizaciones; la comparación de cadenas en constructor.name se rompe con la minificación.
  • Recorra mediante for...of en lugar de childNodes: el iterable maneja de forma segura todos los tamaños de arreglos. Evite el indexado numérico para compatibilidad futura.
  • Evite mutar el árbol durante el recorrido: no añada ni elimine nodos dentro de la llamada recursiva. Primero recopile los resultados y luego modifique.
  • Pase un arreglo de resultados como parámetro: esto evita asignar un nuevo arreglo en cada llamada recursiva y facilita la fusión de resultados de subárboles.

Problemas comunes

SíntomaCausaSolución
childNodes tiene longitud cero en rootNodeModelo no cargadoAsegúrese de que scene.open() se haya completado sin errores antes de recorrer
node.entity instanceof Mesh nunca es verdaderoRuta de importación de Mesh incorrectaImporte Mesh desde @aspose/3d/entities, no desde la raíz @aspose/3d
El recorrido omite mallas anidadasNo se recursiona en todos los hijosAsegúrese de que la llamada recursiva cubra cada elemento en node.childNodes
mesh.controlPoints.length es 0Malla cargada pero no contiene geometríaVerifique la fuente OBJ en busca de grupos vacíos; use mesh.polygonCount como verificación secundaria
Desbordamiento de pila en jerarquías profundasÁrbol de escena muy profundo (cientos de niveles)Reemplace la recursión con una pila explícita usando Array.push / Array.pop

Preguntas frecuentes

¿scene.rootNode lleva una entidad?
No. El nodo raíz es un contenedor creado automáticamente por la biblioteca. No tiene entidad. Tu geometría y otros objetos de escena viven en nodos hijos uno o más niveles por debajo de rootNode.

¿Cuál es la diferencia entre node.entity y node.entities?
node.entity contiene la única entidad primaria (el caso común). Algunos archivos FBX y COLLADA más antiguos pueden generar nodos con múltiples entidades adjuntas; en ese caso node.entities (plural) proporciona la lista completa.

¿Puedo recorrer en orden de amplitud en lugar de profundidad?
Sí. Use una cola en lugar de una llamada recursiva: push scene.rootNode en un arreglo, luego shift y procese los nodos mientras push childNodes de cada nodo al final de la cola.

¿Es scene.open() sincrónico?
Sí. scene.open() y scene.openFromBuffer() bloquean el hilo de llamada hasta que el archivo se haya analizado completamente. Envuélvalos en un hilo de trabajo si necesita mantener el bucle de eventos receptivo.

¿Cómo obtener posiciones en espacio mundial de un nodo?
Lea node.globalTransform; devuelve un GlobalTransform de solo lectura con la matriz en espacio mundial, compuesta a partir de todas las transformaciones de los ancestros. Para matemáticas de matrices explícitas, llame a node.evaluateGlobalTransform(false).

¿Qué tipos de entidad son posibles además de Mesh? Camera, Light y entidades personalizadas de esqueleto/hueso. Consulte node.entity.constructor.name o use instanceof con la clase específica importada de @aspose/3d.

Ver también

 Español