How to Traverse a 3D Scene Graph in Python

How to Traverse a 3D Scene Graph in Python

The scene graph in Aspose.3D FOSS is a tree of Node objects rooted at scene.root_node. Every 3D file, whether loaded from OBJ, glTF, STL, COLLADA, or 3MF, produces the same tree structure. Knowing how to traverse it lets you inspect geometry, count polygons, filter objects by type, and process specific parts of a complex scene.

Step-by-Step Guide

Step 1: Install and Import

Install Aspose.3D FOSS from PyPI:

pip install aspose-3d-foss

Import the classes you need:

from aspose.threed import Scene
from aspose.threed.entities import Mesh

All public classes live under aspose.threed or its sub-packages (aspose.threed.entities, aspose.threed.utilities).


Step 2: Load a Scene from a File

Use the static Scene.from_file() method to open any supported format. The format is detected automatically from the file extension:

scene = Scene.from_file("model.gltf")

You can also open with explicit load options:

from aspose.threed import Scene
from aspose.threed.formats import ObjLoadOptions

options = ObjLoadOptions()
options.enable_materials = True

scene = Scene()
scene.open("model.obj", options)

After loading, scene.root_node is the root of the tree. All imported nodes are children or descendants of this node.


Step 3: Write a Recursive Traversal Function

The simplest traversal visits every node in depth-first order:

from aspose.threed import Scene
from aspose.threed.entities import Mesh

def traverse(node, depth=0):
    prefix = "  " * depth
    entity_name = type(node.entity).__name__ if node.entity else "-"
    print(f"{prefix}[{entity_name}] {node.name}")
    for child in node.child_nodes:
        traverse(child, depth + 1)

scene = Scene.from_file("model.gltf")
traverse(scene.root_node)

Example output:

[-] RootNode
  [-] Armature
    [Mesh] Body
    [Mesh] Eyes
  [-] Ground
    [Mesh] Plane

<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.child_nodes returns children in insertion order (the order in which the importer or the user added them).


Step 4: Access Entity Properties on Each Node

Use node.entity to get the first entity attached to a node, or node.entities to iterate all of them. Check the type before accessing format-specific properties:

from aspose.threed import Scene
from aspose.threed.entities import Mesh

def print_entity_details(node, depth=0):
    indent = "  " * depth
    for entity in node.entities:
        if isinstance(entity, Mesh):
            mesh = entity
            print(f"{indent}Mesh '{node.name}':")
            print(f"{indent}  vertices  : {len(mesh.control_points)}")
            print(f"{indent}  polygons  : {mesh.polygon_count}")
            print(f"{indent}  cast_shadows   : {mesh.cast_shadows}")
            print(f"{indent}  receive_shadows: {mesh.receive_shadows}")
    for child in node.child_nodes:
        print_entity_details(child, depth + 1)

scene = Scene.from_file("model.gltf")
print_entity_details(scene.root_node)

node.transform.translation, node.transform.rotation, and node.transform.scaling give the local-space transform. node.global_transform gives the evaluated world-space result (with global_transform.scale for world-space scale).


Step 5: Filter Nodes by Entity Type

To operate only on specific entity types, add an isinstance guard inside the traversal:

from aspose.threed import Scene
from aspose.threed.entities import Mesh

def find_mesh_nodes(node, results=None):
    if results is None:
        results = []
    for entity in node.entities:
        if isinstance(entity, Mesh):
            results.append(node)
            break   # One match per node is enough
    for child in node.child_nodes:
        find_mesh_nodes(child, results)
    return results

scene = Scene.from_file("model.gltf")
mesh_nodes = find_mesh_nodes(scene.root_node)
print(f"Found {len(mesh_nodes)} mesh node(s)")
for n in mesh_nodes:
    print(f"  {n.name}")

This pattern is useful for bulk operations: applying a transform to every mesh node, replacing materials, or exporting sub-trees.


Step 6: Collect All Meshes and Print Vertex Counts

Aggregate mesh statistics in a single pass:

from aspose.threed import Scene
from aspose.threed.entities import Mesh

def collect_meshes(node, results=None):
    if results is None:
        results = []
    if isinstance(node.entity, Mesh):
        results.append((node.name, node.entity))
    for child in node.child_nodes:
        collect_meshes(child, results)
    return results

scene = Scene.from_file("model.gltf")
meshes = collect_meshes(scene.root_node)

print(f"Total meshes: {len(meshes)}")
total_verts = 0
total_polys = 0

for name, mesh in meshes:
    verts = len(mesh.control_points)
    polys = mesh.polygon_count
    total_verts += verts
    total_polys += polys
    print(f"  {name}: {verts} vertices, {polys} polygons")

print(f"Scene totals: {total_verts} vertices, {total_polys} polygons")

Common Issues

IssueResolution
AttributeError: 'NoneType' object has no attribute 'polygons'Guard with if node.entity is not None or isinstance(node.entity, Mesh) before accessing mesh properties. Nodes without entities return None.
Traversal stops earlyEnsure the recursion reaches into node.child_nodes. If you iterate only scene.root_node.child_nodes (not recursively), you miss all descendants.
Mesh missing from collected resultsCheck that the file format preserves the hierarchy. OBJ flattens all geometry into a single node level. Use glTF or COLLADA for deep hierarchies.
node.entity returns only the first entityUse node.entities (the full list) when a node carries multiple entities. node.entity is a shorthand for node.entities[0] when entities is non-empty.
collect_meshes returns 0 results for a loaded STLSTL files typically produce a single flat node with one mesh entity directly under root_node. Check root_node.child_nodes[0].entity directly.
Node names are empty stringsSome formats (binary STL, some OBJ files) do not store object names. Nodes will have empty name strings; use the index for identification instead.

Frequently Asked Questions

How deep can a scene graph be?

There is no hard limit. Python’s default recursion limit (1000 frames) applies to recursive traversal functions. For very deep hierarchies, convert the recursion to an explicit stack:

from collections import deque
from aspose.threed import Scene

scene = Scene.from_file("deep.gltf")
queue = deque([(scene.root_node, 0)])

while queue:
    node, depth = queue.popleft()
    print("  " * depth + node.name)
    for child in node.child_nodes:
        queue.append((child, depth + 1))

Can I modify the tree while traversing it?

Do not add or remove nodes from child_nodes while iterating it. Collect the nodes to modify in a first pass, then apply changes in a second pass.

How do I find a specific node by name?

Use node.get_child(name) to find a direct child by name. For a deep search, traverse the tree with a name filter:

def find_by_name(root, name):
    if root.name == name:
        return root
    for child in root.child_nodes:
        result = find_by_name(child, name)
        if result:
            return result
    return None

target = find_by_name(scene.root_node, "Wheel_FL")

Does node.entity always return a Mesh?

No. A node can hold any entity type: Mesh, Camera, Light, or custom entities. Always check with isinstance(node.entity, Mesh) before using mesh-specific properties.

How do I get the world-space position of a node?

Read node.global_transform.translation. This is the evaluated position in world space, accounting for all ancestor transforms. It is read-only; modify node.transform.translation to reposition the node.

Can I count total polygons in a scene without writing a traversal?

Not directly through the API; there is no scene.total_polygon_count property. Use collect_meshes and sum mesh.polygon_count across the results, as shown in Step 6.