Граф сцены

scene graph — фундаментальная модель данных Aspose.3D FOSS for Python. Каждый 3D‑файл, независимо от того, загружен ли он с диска или создан в памяти, представляется в виде дерева объектов Node с корнем Scene.root_node. Каждый узел может содержать дочерние узлы и один или несколько объектов Entity (меши, камеры, источники света). Понимание scene graph даёт прямой доступ к геометрии, материалам и пространственным преобразованиям каждого объекта в сцене.

Установка и настройка

Установите библиотеку из PyPI:

pip install aspose-3d-foss

Не требуются нативные расширения, компиляторы или дополнительные системные пакеты. Для полного руководства по установке см. Installation Guide.


Обзор: Концепции графа сцены

Граф сцены в Aspose.3D FOSS следует простой иерархии вложения:

Scene
└── root_node  (Node)
    ├── child_node_A  (Node)
    │   ├── entity: Mesh
    │   └── transform: translation, rotation, scale
    ├── child_node_B  (Node)
    │   └── child_node_C  (Node)
    │       └── entity: Mesh
    └── ...
ObjectRole
SceneКонтейнер верхнего уровня. Содержит root_node, asset_info, animation_clips и sub_scenes.
NodeИменованный узел дерева. Имеет родителя, ноль или более дочерних узлов, ноль или более сущностей и локальный Transform.
EntityГеометрия или объект сцены, прикреплённый к узлу. Общие типы сущностей: Mesh, Camera, Light.
TransformПозиция, вращение и масштаб в локальном пространстве для узла. Результат в мировом пространстве читается из global_transform.

Пошаговое руководство: построение графа сцены программно

Шаг 1: Создать сцену

Новый Scene всегда начинается с пустого 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))  # 0

Scene является точкой входа для всего: загрузка файлов, сохранение файлов, создание анимационных клипов и доступ к дереву узлов.


Шаг 2: Создание дочерних узлов

Используйте create_child_node(name) для добавления именованных узлов в дерево:

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

В качестве альтернативы создайте отдельный Node и явно прикрепите его:

from aspose.threed import Scene, Node

scene = Scene()
node = Node("standalone")
scene.root_node.add_child_node(node)

Оба подхода дают одинаковый результат. create_child_node более лаконичен для встроенного построения.


Шаг 3: Создать Mesh Entity и присоединить её

Mesh хранит данные вершин (control_points) и топологию граней (polygons). Создайте один, добавьте геометрию, затем присоедините его к узлу:

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) представляет однородную координату. Используйте w=1 для обычных позиций точек.

create_polygon(*indices) принимает индексы вершин и регистрирует одну грань в списке полигонов. Передайте три индекса для треугольника, четыре — для четырёхугольника.


Шаг 4: Установить преобразования узлов

У каждого узла есть Transform, который контролирует его позицию, ориентацию и размер в локальном пространстве:

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)

Трансформации являются кумулятивными: позиция дочернего узла в мировом пространстве — это композиция его собственной трансформации со всеми трансформациями предков. Прочитайте вычисленный результат в мировом пространстве из node.global_transform (неизменяемый, только для чтения).


Шаг 5: Рекурсивный обход графа сцены

Обойдите всё дерево, рекурсивно проходя через 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)

Пример вывода для построенной выше сцены (имя корневого узла — пустая строка):

 [None]
  parent [None]
    child [Mesh]

Для сцен с несколькими сущностями на узел используйте итерацию node.entities вместо 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)

Шаг 6: Сохранить сцену

Передайте путь к файлу в scene.save(). Формат определяется по расширению файла:

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

Для параметров, специфичных для формата, передайте объект save-options в качестве второго аргумента:

from aspose.threed.formats import GltfSaveOptions

opts = GltfSaveOptions()
scene.save("scene.gltf", opts)

Советы и лучшие практики

  • Name every node. Giving nodes meaningful names makes debugging traversals far easier and ensures names are preserved in the exported file.
  • One mesh per node. Keeping entities 1:1 with nodes simplifies transforms and collision queries.
  • Use create_child_node over manual attachment. It sets the parent reference automatically and is less error-prone.
  • Read global_transform after building the hierarchy. The world-space result is only stable once all ancestor transforms are set.
  • Do not mutate the tree during traversal. Adding or removing child nodes while iterating child_nodes will produce unpredictable behaviour. Collect nodes first, then modify.
  • Control points use Vector4, not Vector3. Always pass w=1 for ordinary vertex positions; w=0 represents a direction vector (not a point).
  • mesh.control_points returns a copy. The control_points property returns list(self._control_points) — appending to the returned list does not modify the mesh. Always append to mesh._control_points directly when building geometry programmatically. This is a known library limitation; a public mutation API does not yet exist.

Общие проблемы

IssueResolution
AttributeError: 'NoneType' object has no attribute 'polygons'Защитите с помощью if node.entity is not None перед доступом к свойствам сущностей. Узел без сущностей имеет entity = None.
Сетка появляется в начале координат, несмотря на установку translationtransform.translation применяет локальное смещение. Если у родительского узла сам по себе трансформация не является единичной, мировая позиция может отличаться. Проверьте global_transform.
Дочерние узлы отсутствуют после scene.save() / перезагрузкиНекоторые форматы (OBJ) уплощают иерархию. Используйте glTF или COLLADA, чтобы сохранить полное дерево узлов.
polygon_count равно 0 после mesh.create_polygon(...)polygon_count равно 0 после mesh.create_polygon(...). Убедитесь, что индексы вершин, переданные в create_polygon, находятся в диапазоне (0 до len(control_points) - 1).
Node.get_child(name) возвращает NoneNode.get_child(name) возвращает None. Имя чувствительно к регистру. Подтвердите точную строку имени, использованную при создании.
Обход посещает узлы в неожиданном порядкеchild_nodes возвращает дочерние элементы в порядке вставки (в порядке, в котором вызывались add_child_node / create_child_node).
 Русский