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-fossImport the classes you need:
from aspose.threed import Scene
from aspose.threed.entities import MeshAll 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
| Issue | Resolution |
|---|---|
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 early | Ensure 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 results | Check 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 entity | Use 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 STL | STL 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 strings | Some 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.