Граф сцени

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)

Поради та кращі практики

  • Назвіть кожен вузол. Надання вузлам змістовних імен значно спрощує налагодження обходів і забезпечує збереження імен у експортованому файлі.
  • Один меш на вузол. Підтримка співвідношення 1:1 між сутностями та вузлами спрощує трансформації та запити зіткнень.
  • Використовуйте create_child_node замість ручного приєднання. Він автоматично встановлює посилання на батька і менш схильний до помилок.
  • Зчитайте global_transform після побудови ієрархії. Результат у світовому просторі стабільний лише після встановлення всіх трансформацій предків.
  • Не змінюйте дерево під час обходу. Додавання або видалення дочірніх вузлів під час ітерації child_nodes призведе до непередбачуваної поведінки. Спочатку зберіть вузли, потім модифікуйте їх.
  • Контрольні точки використовуйте Vector4, а не Vector3. Завжди передавайте w=1 для звичайних позицій вершин; w=0 представляє вектор напрямку (не точку).
  • mesh.control_points повертає копію. Властивість control_points повертає list(self._control_points) — додавання елементів до повернутого списку не змінює меш. Завжди додавайте безпосередньо до mesh._control_points під час програмного створення геометрії. Це відома обмеженість бібліотеки; публічного API для мутації ще не існує.

Поширені проблеми

IssueResolution
AttributeError: 'NoneType' object has no attribute 'polygons'Захистіть за допомогою if node.entity is not None перед доступом до властивостей сутності. Вузол без сутностей має entity = None.
Mesh appears at the origin despite setting translationtransform.translation застосовує локальний зсув. Якщо батьківський вузол сам має трансформацію, відмінну від одиничної, світова позиція може бути іншою. Перевірте global_transform.
Child nodes missing after scene.save() / reloadДеякі формати (OBJ) сплющують ієрархію. Використовуйте glTF або COLLADA, щоб зберегти повне дерево вузлів.
polygon_count is 0 after mesh.create_polygon(...)Переконайтеся, що індекси вершин, передані до create_polygon, знаходяться в діапазоні (0 до len(control_points) - 1).
Node.get_child(name) returns NoneНазва чутлива до регістру. Підтвердіть точний рядок назви, використаний під час створення.
Traversal visits nodes in unexpected orderchild_nodes повертає дочірні елементи у порядку вставки (у порядку, у якому викликали add_child_node / create_child_node).
 Українська