Cómo recorrer un grafo de escena 3D en Python
El grafo de escena en Aspose.3D FOSS es un árbol de Node objetos con raíz en scene.root_node. Cada archivo 3D, ya sea cargado desde OBJ, glTF, STL, COLLADA o 3MF, produce la misma estructura de árbol. Saber cómo recorrerlo le permite inspeccionar la geometría, contar polígonos, filtrar objetos por tipo y procesar partes específicas de una escena compleja.
Guía paso a paso
Paso 1: Instalar e Importar
Instale Aspose.3D FOSS desde PyPI:
pip install aspose-3d-fossImporte las clases que necesite:
from aspose.threed import Scene
from aspose.threed.entities import MeshTodas las clases públicas se encuentran bajo aspose.threed o sus subpaquetes (aspose.threed.entities, aspose.threed.utilities).
Paso 2: Cargar una Escena desde un Archivo
Utilice el estático Scene.from_file() método para abrir cualquier formato compatible. El formato se detecta automáticamente a partir de la extensión del archivo:
scene = Scene.from_file("model.gltf")También puede abrir con opciones de carga explícitas:
from aspose.threed import Scene
from aspose.threed.formats import ObjLoadOptions
options = ObjLoadOptions()
options.enable_materials = True
scene = Scene()
scene.open("model.obj", options)Después de cargar, scene.root_node es la raíz del árbol. Todos los nodos importados son hijos o descendientes de este nodo.
Paso 3: Escribir una Función de Recorrido Recursivo
El recorrido más simple visita cada nodo en orden de profundidad primero:
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)Salida de ejemplo:
[-]
[-] 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>
Nota: El nodo raíz tiene un nombre vacío (""), así que la primera línea muestra [-] sin nombre a continuación.
node.child_nodes devuelve los hijos en orden de inserción (el orden en que el importador o el usuario los añadió).
Paso 4: Acceder a las Propiedades de la Entidad en Cada Nodo
Usar node.entity para obtener la primera entidad adjunta a un nodo, o node.entities para iterar todas ellas. Verifique el tipo antes de acceder a propiedades específicas del formato:
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, y node.transform.scaling dan la transformación en espacio local. node.global_transform devuelve el resultado evaluado en espacio mundial (con global_transform.scale para escala en espacio mundial).
Paso 5: Filtrar nodos por tipo de entidad
Para operar solo con tipos de entidad específicos, añada un isinstance guardia dentro del recorrido:
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}")Este patrón es útil para operaciones en bloque: aplicar una transformación a cada nodo de malla, reemplazar materiales o exportar subárboles.
Paso 6: Recopilar todas las mallas e imprimir el recuento de vértices
Agrega estadísticas de mallas en una sola pasada:
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")Problemas comunes
| Problema | Resolución |
|---|---|
AttributeError: 'NoneType' object has no attribute 'polygons' | Guardia con if node.entity is not None o isinstance(node.entity, Mesh) antes de acceder a las propiedades de la malla. Los nodos sin entidades devuelven None. |
| El recorrido se detiene temprano | Asegúrate de que la recursión llegue a node.child_nodes. Si solo iteras scene.root_node.child_nodes (no recursivamente), pierdes todos los descendientes. |
| Malla ausente de los resultados recopilados | Comprueba que el formato de archivo preserve la jerarquía. OBJ aplana toda la geometría en un único nivel de nodo. Usa glTF o COLLADA para jerarquías profundas. |
node.entity devuelve solo la primera entidad | Usa node.entities (la lista completa) cuando un nodo lleva múltiples entidades. node.entity es una abreviatura de node.entities[0] cuando entities no está vacío. |
collect_meshes devuelve 0 resultados para un STL cargado | Los archivos STL suelen producir un único nodo plano con una entidad de malla directamente bajo root_node. Comprueba root_node.child_nodes[0].entity directamente. |
| Los nombres de los nodos son cadenas vacías | Algunos formatos (binary STL, algunos archivos OBJ) no almacenan nombres de objetos. Los nodos tendrán cadenas vacías name cadenas; use el índice para identificación en su lugar. |
Preguntas frecuentes
¿Qué tan profundo puede ser un grafo de escena?
No hay un límite estricto. El límite de recursión predeterminado de Python (1000 fotogramas) se aplica a las funciones de recorrido recursivo. Para jerarquías muy profundas, convierte la recursión en una pila explícita:
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))¿Puedo modificar el árbol mientras lo recorro?
No añada ni elimine nodos de child_nodes mientras lo itera. Recoja los nodos a modificar en una primera pasada, luego aplique los cambios en una segunda pasada.
¿Cómo encuentro un nodo específico por nombre?
Use node.get_child(name) para encontrar un hijo directo por nombre. Para una búsqueda profunda, recorra el árbol con un filtro de nombre:
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")¿Devuelve node.entity siempre un Mesh?
No. Un nodo puede contener cualquier tipo de entidad: Mesh, Camera, Light, o entidades personalizadas. Siempre verifique con isinstance(node.entity, Mesh) antes de usar propiedades específicas de la malla.
¿Cómo obtengo la posición en espacio mundial de un nodo?
Leer node.global_transform.translation. Esta es la posición evaluada en el espacio mundial, teniendo en cuenta todas las transformaciones de los ancestros. Es de solo lectura; modifica node.transform.translation para reposicionar el nodo.
¿Puedo contar los polígonos totales en una escena sin escribir un recorrido?
No directamente a través de la API; no hay scene.total_polygon_count propiedad. Usa collect_meshes y suma mesh.polygon_count a través de los resultados, como se muestra en el Paso 6.