Come attraversare un grafo di scena 3D in Python

Come attraversare un grafo di scena 3D in Python

Il grafo della scena in Aspose.3D FOSS è un albero di Node oggetti radicati in scene.root_node. Ogni file 3D, sia caricato da OBJ, glTF, STL, COLLADA o 3MF, produce la stessa struttura ad albero. Conoscere come attraversarlo ti permette di ispezionare la geometria, contare i poligoni, filtrare gli oggetti per tipo e processare parti specifiche di una scena complessa.

Guida passo-passo

Passo 1: Installa e Importa

Installa Aspose.3D FOSS da PyPI:

pip install aspose-3d-foss

Importa le classi di cui hai bisogno:

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

Tutte le classi pubbliche risiedono sotto aspose.threed o nei suoi sotto‑pacchetti (aspose.threed.entities, aspose.threed.utilities).


Passo 2: Carica una scena da un file

Usa il metodo statico Scene.from_file() per aprire qualsiasi formato supportato. Il formato viene rilevato automaticamente dall’estensione del file:

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

Puoi anche aprire con opzioni di caricamento esplicite:

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

options = ObjLoadOptions()
options.enable_materials = True

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

Dopo il caricamento, scene.root_node è la radice dell’albero. Tutti i nodi importati sono figli o discendenti di questo nodo.


Passo 3: Scrivi una funzione di attraversamento ricorsivo

L’attraversamento più semplice visita ogni nodo in ordine depth‑first:

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)

Esempio di output:

[-]
  [-] 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: Il nodo radice ha un nome vuoto (""), quindi la prima riga mostra [-] senza nome successivo.

node.child_nodes restituisce i figli nell’ordine di inserimento (l’ordine in cui l’importatore o l’utente li ha aggiunti).


Passo 4: Accedi alle proprietà dell’entità su ogni nodo

Usa node.entity per ottenere la prima entità collegata a un nodo, o node.entities per iterare tutti loro. Controlla il tipo prima di accedere alle proprietà specifiche 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, e node.transform.scaling forniscono la trasformazione nello spazio locale. node.global_transform fornisce il risultato valutato nello spazio globale (with global_transform.scale per la scala nello spazio mondiale).


Passo 5: Filtrare i nodi per tipo di entità

Per operare solo su tipi di entità specifici, aggiungi un isinstance guardia all’interno del 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}")

Questo schema è utile per operazioni di massa: applicare una trasformazione a ogni nodo mesh, sostituire i materiali o esportare sotto‑alberi.


Passo 6: Raccogliere tutte le mesh e stampare il conteggio dei vertici

Aggrega le statistiche delle mesh in un unico passaggio:

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")

Problemi comuni

ProblemaRisoluzione
AttributeError: 'NoneType' object has no attribute 'polygons'Guardia con if node.entity is not None oppure isinstance(node.entity, Mesh) prima di accedere alle proprietà della mesh. I nodi senza entità restituiscono None.
Il traversal si interrompe prematuramenteAssicurati che la ricorsione raggiunga node.child_nodes. Se iteri solo scene.root_node.child_nodes (non ricorsivamente), perdi tutti i discendenti.
Mesh mancante nei risultati raccoltiVerifica che il formato file preservi la gerarchia. OBJ appiattisce tutta la geometria in un unico livello di nodo. Usa glTF o COLLADA per gerarchie profonde.
node.entity restituisce solo la prima entitàUsa node.entities (l’elenco completo) quando un nodo contiene più entità. node.entity è una forma abbreviata di node.entities[0] quando entities non è vuoto.
collect_meshes restituisce 0 risultati per un STL caricatoI file STL tipicamente producono un singolo nodo piatto con una entità mesh direttamente sotto root_node. Controlla root_node.child_nodes[0].entity direttamente.
I nomi dei nodi sono stringhe vuoteAlcuni formati (binary STL, alcuni file OBJ) non memorizzano i nomi degli oggetti. I nodi avranno stringhe vuote name stringhe; usa l’indice per l’identificazione invece.

Domande frequenti

Quanto può essere profondo un grafo di scena?

Non esiste un limite rigido. Il limite di ricorsione predefinito di Python (1000 fotogrammi) si applica alle funzioni di attraversamento ricorsivo. Per gerarchie molto profonde, converti la ricorsione in uno stack esplicito:

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

Posso modificare l’albero durante l’attraversamento?

Non aggiungere o rimuovere nodi da child_nodes mentre lo iteri. Raccogli i nodi da modificare in un primo passaggio, poi applica le modifiche in un secondo passaggio.

Come trovo un nodo specifico per nome?

Usa node.get_child(name) per trovare un figlio diretto per nome. Per una ricerca profonda, attraversa l’albero con un filtro per nome:

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")

Restituisce node.entity sempre una Mesh?

No. Un nodo può contenere qualsiasi tipo di entità: Mesh, Camera, Light, o entità personalizzate. Controlla sempre con isinstance(node.entity, Mesh) prima di utilizzare le proprietà specifiche della mesh.

Come ottengo la posizione in spazio mondo di un nodo?

Leggi node.global_transform.translation. Questa è la posizione valutata nello spazio mondiale, tenendo conto di tutte le trasformazioni dei genitori. È di sola lettura; modifica node.transform.translation per riposizionare il nodo.

Posso contare il numero totale di poligoni in una scena senza scrivere un traversal?

Non direttamente tramite l’API; non c’è scene.total_polygon_count proprietà. Usa collect_meshes e somma mesh.polygon_count sui risultati, come mostrato al Passo 6.

 Italiano