Grafo de escena
Un grafo de escena es el modelo de datos fundamental de Aspose.3D FOSS para Python. Cada archivo 3D, ya sea cargado desde el disco o construido en memoria, se representa como un árbol de Node objetos con raíz en Scene.root_node. Cada nodo puede contener nodos hijos y uno o más Entity objetos (mallas, cámaras, luces). Comprender el grafo de escena le brinda acceso directo a la geometría, los materiales y las transformaciones espaciales de cada objeto en una escena.
Instalación y configuración
Instale la biblioteca desde PyPI:
pip install aspose-3d-fossNo se requieren extensiones nativas, compiladores ni paquetes de sistema adicionales. Para obtener instrucciones completas de instalación, consulte el Guía de instalación.
Resumen: Conceptos del Grafo de Escena
El grafo de escena en Aspose.3D FOSS sigue una jerarquía de contención sencilla:
Scene
└── root_node (Node)
├── child_node_A (Node)
│ ├── entity: Mesh
│ └── transform: translation, rotation, scale
├── child_node_B (Node)
│ └── child_node_C (Node)
│ └── entity: Mesh
└── ...| Objeto | Rol |
|---|---|
Scene | Contenedor de nivel superior. Contiene root_node, asset_info, animation_clips, y sub_scenes. |
Node | Nodo de árbol con nombre. Tiene un padre, cero o más hijos, cero o más entidades, y un local Transform. |
Entity | Geometría u objeto de escena adjunto a un nodo. Tipos de entidad comunes: Mesh, Camera, Light. |
Transform | Posición, rotación y escala en espacio local para un nodo. El resultado en espacio mundial se lee de global_transform. |
Paso a Paso: Construyendo un Grafo de Escena Programáticamente
Paso 1: Crear una Escena
Un nuevo Scene siempre comienza con un root_node:
from aspose.threed import Scene
scene = Scene()
print(scene.root_node.name) # "" (empty string — root node has no name by default)
print(len(scene.root_node.child_nodes)) # 0Scene es el punto de entrada para todo: cargar archivos, guardar archivos, crear clips de animación y acceder al árbol de nodos.
Paso 2: Crear Nodos Hijos
Usa create_child_node(name) para agregar nodos con nombre al árbol:
from aspose.threed import Scene
scene = Scene()
parent = scene.root_node.create_child_node("parent")
child = parent.create_child_node("child")
print(parent.name) # "parent"
print(child.parent_node.name) # "parent"
print(len(scene.root_node.child_nodes)) # 1Alternativamente, crea un Node y adjúntalo explícitamente:
from aspose.threed import Scene, Node
scene = Scene()
node = Node("standalone")
scene.root_node.add_child_node(node)Ambos enfoques producen el mismo resultado. create_child_node es más conciso para la construcción en línea.
Paso 3: Crear una Entidad Mesh y Adjuntarla
A Mesh almacena datos de vértices (control_points) y topología de caras (polygons). Crea uno, agrega geometría y luego adjúntalo a un nodo:
from aspose.threed import Scene, Node
from aspose.threed.entities import Mesh
from aspose.threed.utilities import Vector3, Vector4
scene = Scene()
parent = scene.root_node.create_child_node("parent")
child = parent.create_child_node("child")
##Create a quad mesh (four vertices, one polygon)
# Note: control_points returns a copy of the internal list; append to
# _control_points directly to actually add vertices. This is a known
# library limitation — a public add_control_point() API is not yet available.
mesh = Mesh("cube")
mesh._control_points.append(Vector4(0, 0, 0, 1))
mesh._control_points.append(Vector4(1, 0, 0, 1))
mesh._control_points.append(Vector4(1, 1, 0, 1))
mesh._control_points.append(Vector4(0, 1, 0, 1))
mesh.create_polygon(0, 1, 2, 3)
child.add_entity(mesh)
print(f"Mesh name: {mesh.name}")
print(f"Vertex count: {len(mesh.control_points)}")
print(f"Polygon count: {mesh.polygon_count}")Vector4(x, y, z, w) representa una coordenada homogénea. Usa w=1 para posiciones de puntos regulares.
create_polygon(*indices) acepta índices de vértices y registra una cara en la lista de polígonos. Pasa tres índices para un triángulo, cuatro para un cuádruplo.
Paso 4: Configurar transformaciones de nodo
Cada nodo tiene un Transform que controla su posición, orientación y tamaño en el espacio local:
from aspose.threed.utilities import Vector3, Quaternion
##Translate the node 2 units along the X axis
child.transform.translation = Vector3(2.0, 0.0, 0.0)
##Scale the node to half its natural size
child.transform.scaling = Vector3(0.5, 0.5, 0.5)
##Rotate 45 degrees around the Y axis using Euler angles
child.transform.euler_angles = Vector3(0.0, 45.0, 0.0)Las transformaciones son acumulativas: la posición en espacio mundial de un nodo hijo es la composición de su propia transformación con todas las transformaciones de los ancestros. Lea el resultado evaluado en espacio mundial de node.global_transform (immutable, read-only).
Paso 5: Recorrer el grafo de escena recursivamente
Recorra todo el árbol recursivamente a través de node.child_nodes:
def traverse(node, depth=0):
indent = " " * depth
entity_type = type(node.entity).__name__ if node.entity else "None"
print(f"{indent}{node.name} [{entity_type}]")
for child in node.child_nodes:
traverse(child, depth + 1)
traverse(scene.root_node)Ejemplo de salida para la escena construida arriba (el nombre del nodo raíz es una cadena vacía):
[None]
parent [None]
child [Mesh]Para escenas con múltiples entidades por nodo, itere node.entities en lugar de node.entity:
def traverse_full(node, depth=0):
indent = " " * depth
entity_names = [type(e).__name__ for e in node.entities] or ["None"]
print(f"{indent}{node.name} [{', '.join(entity_names)}]")
for child in node.child_nodes:
traverse_full(child, depth + 1)
traverse_full(scene.root_node)Paso 6: Guardar la escena
Pase una ruta de archivo a scene.save(). El formato se infiere a partir de la extensión del archivo:
scene.save("scene.gltf") # JSON glTF 2.0
scene.save("scene.glb") # Binary GLB container
scene.save("scene.obj") # Wavefront OBJ
scene.save("scene.stl") # STLPara opciones específicas de formato, pase un objeto de opciones de guardado como segundo argumento:
from aspose.threed.formats import GltfSaveOptions
opts = GltfSaveOptions()
scene.save("scene.gltf", opts)Consejos y mejores prácticas
- Nombre a cada nodo. Asignar nombres significativos a los nodos facilita mucho la depuración de recorridos y garantiza que los nombres se conserven en el archivo exportado.
- Una malla por nodo. Mantener una relación 1:1 entre entidades y nodos simplifica las transformaciones y las consultas de colisión.
- Usar
create_child_nodeen lugar de la unión manual. Establece la referencia al padre automáticamente y es menos propenso a errores. - Leer
global_transformdespués de construir la jerarquía. El resultado en espacio mundial solo es estable una vez que se establecen todas las transformaciones de los ancestros. - No modifiques el árbol durante el recorrido. Agregar o eliminar nodos hijos mientras se itera
child_nodesproducirá un comportamiento impredecible. Recoge los nodos primero, luego modifícalos. - Los puntos de control usan
Vector4, noVector3. Siempre pasarw=1para posiciones de vértices ordinarias;w=0representa un vector de dirección (no un punto). mesh.control_pointsdevuelve una copia. Elcontrol_pointspropiedad devuelvelist(self._control_points)— agregar a la lista devuelta no modifica la malla. Siempre agrega amesh._control_pointsdirectamente al crear geometría programáticamente. Esta es una limitación conocida de la biblioteca; aún no existe una API pública de mutación.
Problemas comunes
| Problema | Resolución |
|---|---|
AttributeError: 'NoneType' object has no attribute 'polygons' | Proteger con if node.entity is not None antes de acceder a las propiedades de la entidad. Un nodo sin entidades tiene entity = None. |
Mesh aparece en el origen a pesar de establecer translation | transform.translation aplica un desplazamiento local. Si el nodo padre mismo tiene una transformación que no es la identidad, la posición mundial puede diferir. Verifique global_transform. |
Nodos hijos faltantes después de scene.save() / recargar | Algunos formatos (OBJ) aplanan la jerarquía. Use glTF o COLLADA para preservar el árbol completo de nodos. |
polygon_count es 0 después de mesh.create_polygon(...) | Verifique que los índices de vértice pasados a create_polygon están dentro del rango (0 a len(control_points) - 1). |
Node.get_child(name) devuelve None | El nombre distingue entre mayúsculas y minúsculas. Confirme la cadena de nombre exacta utilizada en el momento de la creación. |
| El recorrido visita los nodos en un orden inesperado | child_nodes devuelve los hijos en el orden de inserción (el orden add_child_node / create_child_node fue llamado). |