How to Traverse a 3D Scene Graph in TypeScript
The scene graph in Aspose.3D FOSS for TypeScript is a tree of Node objects rooted at scene.rootNode. Traversal is recursive: each node exposes a childNodes iterable and an optional entity property. This guide shows how to walk the entire tree, identify entity types, and collect mesh statistics.
Prerequisites
- Node.js 16 or later
- TypeScript 5.0 or later
@aspose/3dinstalled
Step-by-Step Guide
Step 1: Install and Import
Install the package:
npm install @aspose/3dImport the classes used in this guide:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';Scene and Mesh are the core classes. ObjLoadOptions is used in the load example; substitute the matching options class for other formats.
Step 2: Load a Scene from a File
Create a Scene and call scene.open() with a file path. Format detection is automatic from binary magic numbers, so you do not need to specify the format for GLB, STL, or 3MF files:
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}`);You can also load from a Buffer in memory using scene.openFromBuffer(buffer, options); useful in serverless pipelines where disk I/O is not available.
Step 3: Write a Recursive Traversal Function
Recursion over childNodes is the standard pattern. The function visits each node depth-first:
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);For a scene with one mesh named Cube, the output will look like:
[-] 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 is null for group nodes, bones, and locators. The constructor.name check works for any entity type: Mesh, Camera, Light, etc.
Step 4: Access the Entity Type on Each Node
To take action based on entity type, use an instanceof check after the null guard:
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 is the safest way to confirm the entity is a polygon mesh before accessing controlPoints, polygonCount, or vertex elements.
Step 5: Filter Nodes by Entity Type
To collect only mesh-bearing nodes without printing the full tree, use a recursive accumulator:
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)`);The function accepts an optional results array so callers can pre-populate it for merging results across multiple subtrees.
Step 6: Collect All Meshes and Print Vertex Counts
Extend the collector to print per-mesh statistics:
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`);
}Example output for a two-mesh scene:
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>
Tips and Best Practices
- Always null-check
node.entitybefore accessing entity-specific properties. Many nodes are pure group nodes that carry no entity. - Use
instanceofoverconstructor.namefor type checks in logic paths.instanceofis refactor-safe; string comparison onconstructor.namebreaks with minification. - Traverse via
for...ofoverchildNodes: the iterable handles all array sizes safely. Avoid numeric indexing for forward compatibility. - Avoid mutating the tree during traversal: do not add or remove nodes inside the recursive call. Collect results first, then modify.
- Pass a results array as a parameter: this avoids allocating a new array on every recursive call and makes it easy to merge subtree results.
Common Issues
| Symptom | Cause | Fix |
|---|---|---|
childNodes has zero length on rootNode | Model not loaded | Ensure scene.open() completed without error before traversing |
node.entity instanceof Mesh never true | Wrong Mesh import path | Import Mesh from @aspose/3d/entities, not from @aspose/3d root |
| Traversal misses nested meshes | Not recursing into all children | Ensure the recursive call covers every element in node.childNodes |
mesh.controlPoints.length is 0 | Mesh loaded but contains no geometry | Check OBJ source for empty groups; use mesh.polygonCount as a secondary check |
| Stack overflow on deep hierarchies | Very deep scene tree (hundreds of levels) | Replace recursion with an explicit stack using Array.push / Array.pop |
Frequently Asked Questions
Does scene.rootNode itself carry an entity?
No. The root node is a container created automatically by the library. It has no entity. Your geometry and other scene objects live on child nodes one or more levels below rootNode.
What is the difference between node.entity and node.entities?
node.entity holds the single primary entity (the common case). Some older FBX and COLLADA files may produce nodes with multiple attached entities; in that case node.entities (plural) provides the full list.
Can I traverse in breadth-first order instead of depth-first?
Yes. Use a queue instead of a recursive call: push scene.rootNode into an array, then shift and process nodes while pushing each node’s childNodes into the queue tail.
Is scene.open() synchronous?
Yes. scene.open() and scene.openFromBuffer() both block the calling thread until the file is fully parsed. Wrap them in a worker thread if you need to keep the event loop responsive.
How do I get world-space positions from a node?
Read node.globalTransform; it returns a read-only GlobalTransform with the world-space matrix, composed from all ancestor transforms. For explicit matrix math, call node.evaluateGlobalTransform(false).
What entity types are possible besides Mesh?
Camera, Light, and custom skeleton/bone entities. Check node.entity.constructor.name or use instanceof with the specific class imported from @aspose/3d.