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

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-foss

Importe las clases que necesite:

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

Todas 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

ProblemaResolució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 tempranoAsegú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 recopiladosComprueba 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 entidadUsa 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 cargadoLos 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íasAlgunos 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.

 Español