Comment parcourir un graphe de scène 3D dans Python

Comment parcourir un graphe de scène 3D dans Python

Le graphe de scène dans Aspose.3D FOSS est un arbre de Node objets enracinés à scene.root_node. Chaque fichier 3D, qu’il soit chargé depuis OBJ, glTF, STL, COLLADA ou 3MF, produit la même structure d’arbre. Savoir comment le parcourir vous permet d’inspecter la géométrie, de compter les polygones, de filtrer les objets par type et de traiter des parties spécifiques d’une scène complexe.

Guide étape par étape

Étape 1 : Installer et importer

Installez Aspose.3D FOSS depuis PyPI :

pip install aspose-3d-foss

Importez les classes dont vous avez besoin :

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

Toutes les classes publiques se trouvent sous aspose.threed ou ses sous‑packages (aspose.threed.entities, aspose.threed.utilities).


Étape 2 : Charger une scène depuis un fichier

Utilisez le static Scene.from_file() méthode pour ouvrir n’importe quel format pris en charge. Le format est détecté automatiquement à partir de l’extension du fichier :

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

Vous pouvez également ouvrir avec des options de chargement explicites :

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

options = ObjLoadOptions()
options.enable_materials = True

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

Après le chargement, scene.root_node est la racine de l’arbre. Tous les nœuds importés sont des enfants ou des descendants de ce nœud.


Étape 3 : Écrire une fonction de traversée récursive

La traversée la plus simple visite chaque nœud en ordre profondeur d’abord :

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)

Exemple de sortie :

[-]
  [-] 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>

Remarque : Le nœud racine a un nom vide (""), donc la première ligne affiche [-] sans nom qui suit.

node.child_nodes renvoie les enfants dans l’ordre d’insertion (l’ordre dans lequel l’importateur ou l’utilisateur les a ajoutés).


Étape 4 : Accéder aux propriétés d’entité sur chaque nœud

Utiliser node.entity pour obtenir la première entité attachée à un nœud, ou node.entities pour parcourir toutes. Vérifiez le type avant d’accéder aux propriétés spécifiques au format :

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, et node.transform.scaling donnent la transformation en espace local. node.global_transform donne le résultat évalué en espace mondial (avec global_transform.scale pour l’échelle en espace mondial).


Étape 5 : Filtrer les nœuds par type d’entité

Pour n’opérer que sur des types d’entité spécifiques, ajoutez un isinstance garde dans le parcours :

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

Ce modèle est utile pour des opérations en masse : appliquer une transformation à chaque nœud de maillage, remplacer des matériaux, ou exporter des sous‑arbres.


Étape 6 : Collecter tous les maillages et afficher le nombre de sommets

Regrouper les statistiques des maillages en un seul passage :

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

Problèmes courants

ProblèmeRésolution
AttributeError: 'NoneType' object has no attribute 'polygons'Garde avec if node.entity is not None ou isinstance(node.entity, Mesh) avant d’accéder aux propriétés du maillage. Les nœuds sans entités renvoient None.
Le parcours s’arrête prématurémentAssurez-vous que la récursion pénètre dans node.child_nodes. Si vous itérez uniquement scene.root_node.child_nodes (pas de façon récursive), vous manquez tous les descendants.
Maillage manquant dans les résultats collectésVérifiez que le format de fichier préserve la hiérarchie. OBJ aplatit toute la géométrie en un seul niveau de nœud. Utilisez glTF ou COLLADA pour les hiérarchies profondes.
node.entity renvoie uniquement la première entitéUtilisez node.entities (la liste complète) lorsqu’un nœud porte plusieurs entités. node.entity est une abréviation de node.entities[0] lorsque entities n’est pas vide.
collect_meshes renvoie 0 résultats pour un STL chargéLes fichiers STL produisent généralement un seul nœud plat avec une entité mesh directement sous root_node. Vérifiez root_node.child_nodes[0].entity directement.
Les noms de nœuds sont des chaînes videsCertains formats (STL binaire, certains fichiers OBJ) ne stockent pas les noms d’objet. Les nœuds auront des chaînes vides name ; utilisez l’index pour l’identification à la place.

Foire aux questions

Quelle profondeur peut atteindre un graphe de scène ?

Il n’y a pas de limite stricte. La limite de récursion par défaut de Python (1000 appels) s’applique aux fonctions de parcours récursif. Pour des hiérarchies très profondes, convertissez la récursion en une pile explicite :

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

Puis-je modifier l’arbre pendant son parcours ?

N’ajoutez pas et ne supprimez pas de nœuds de child_nodes pendant son itération. Collectez les nœuds à modifier lors d’un premier passage, puis appliquez les changements lors d’un second passage.

Comment trouver un nœud spécifique par son nom ?

Utilisez node.get_child(name) pour trouver un enfant direct par son nom. Pour une recherche approfondie, parcourez l’arbre avec un filtre de nom :

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

Est‑ce que node.entity renvoie toujours un Mesh ?

Non. Un nœud peut contenir n’importe quel type d’entité : Mesh, Camera, Light, ou des entités personnalisées. Vérifiez toujours avec isinstance(node.entity, Mesh) avant d’utiliser les propriétés spécifiques au maillage.

Comment obtenir la position en espace mondial d’un nœud ?

Lire node.global_transform.translation. Il s’agit de la position évaluée dans l’espace mondial, en tenant compte de toutes les transformations des ancêtres. Elle est en lecture seule ; modifiez node.transform.translation pour repositionner le nœud.

Puis-je compter le nombre total de polygones dans une scène sans écrire de traversée ?

Pas directement via l’API ; il n’existe pas de scene.total_polygon_count propriété. Utilisez collect_meshes et additionnez mesh.polygon_count sur l’ensemble des résultats, comme indiqué à l’étape 6.

 Français