Граф сцены

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

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

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

asposefoss/3d is not yet published — build from source until it ships. See the project README for build instructions.

Не требуются нативные расширения, компиляторы или дополнительные системные пакеты. Для полного руководства по установке см. 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).
 Русский