Як обійти 3D граф сцени у Python

Як обійти 3D граф сцени у Python

Граф сцени у Aspose.3D FOSS є деревом Node об’єктів, коренем яких є scene.root_node. Кожен 3D‑файл, незалежно від того, чи завантажений з OBJ, glTF, STL, COLLADA або 3MF, створює одну й ту ж структуру дерева. Знання того, як його обходити, дозволяє вам досліджувати геометрію, підраховувати полігони, фільтрувати об’єкти за типом і обробляти конкретні частини складної сцени.

Покроковий посібник

Крок 1: Встановлення та імпорт

Встановіть Aspose.3D FOSS з PyPI:

pip install aspose-3d-foss

Імпортуйте потрібні класи:

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

Усі публічні класи розташовані в aspose.threed або в його підпакетах (aspose.threed.entities, aspose.threed.utilities).


Крок 2: Завантаження сцени з файлу

Використовуйте статичний Scene.from_file() метод для відкриття будь‑якого підтримуваного формату. Формат визначається автоматично за розширенням файлу:

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

Ви також можете відкрити з явними параметрами завантаження:

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

options = ObjLoadOptions()
options.enable_materials = True

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

Після завантаження, scene.root_node є коренем дерева. Усі імпортовані вузли є дітьми або нащадками цього вузла.


Крок 3: Написання рекурсивної функції обходу

Найпростіший обхід відвідує кожен вузол у порядку глибини (depth‑first):

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)

Приклад виводу:

[-]
  [-] 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=“Скопіювати код”

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

Примітка: Кореневий вузол має порожню назву (""), тому перший рядок показує [-] без наступної назви.

node.child_nodes повертає дочірні елементи у порядку вставки (у порядку, в якому їх додав імпортер або користувач).


Крок 4: Доступ до властивостей сутності в кожному вузлі

Використовуйте node.entity для отримання першої сутності, прикріпленої до вузла, або node.entities щоб ітерувати їх усіх. Перевірте тип перед доступом до властивостей, специфічних для формату:

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, і node.transform.scaling надає трансформацію у локальному просторі. node.global_transform повертає обчислений результат у світовому просторі (з global_transform.scale для масштабування у світовому просторі).


Крок 5: Фільтрування вузлів за типом сутності

Щоб працювати лише з певними типами сутностей, додайте isinstance захисну перевірку всередині обходу:

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

Цей шаблон корисний для масових операцій: застосування трансформації до кожного вузла сітки, заміна матеріалів або експорт піддерев.


Крок 6: Зібрати всі сітки та вивести кількість вершин

Агрегуйте статистику сіток за один прохід:

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

Типові проблеми

ПроблемаРоздільна здатність
AttributeError: 'NoneType' object has no attribute 'polygons'Захистити за допомогою if node.entity is not None або isinstance(node.entity, Mesh) перед доступом до властивостей сітки. Вузли без сутностей повертають None.
Обхід зупиняється ранішеПереконайтеся, що рекурсія досягає node.child_nodes. Якщо ви ітеруєте лише scene.root_node.child_nodes (не рекурсивно), ви пропускаєте всіх нащадків.
Mesh відсутня у зібраних результатахПереконайтеся, що формат файлу зберігає ієрархію. OBJ сплющує всю геометрію до одного рівня вузлів. Використовуйте glTF або COLLADA для глибоких ієрархій.
node.entity повертає лише перший об’єктВикористовуйте node.entities (повний список) коли вузол містить кілька сутностей. node.entity є скороченням для node.entities[0] коли entities не порожнє.
collect_meshes повертає 0 результатів для завантаженого STLФайли STL зазвичай створюють один плоский вузол з однією сутністю сітки безпосередньо під root_node. Перевірте root_node.child_nodes[0].entity безпосередньо.
Імена вузлів – порожні рядкиДеякі формати (бінарний STL, деякі файли OBJ) не зберігають імена об’єктів. Вузли матимуть порожні name рядки; використовуйте індекс для ідентифікації замість цього.

Часті запитання

Наскільки глибоким може бути граф сцени?

Жорсткого обмеження немає. Python’s default recursion limit (1000 frames) applies to recursive traversal functions. Для дуже глибоких ієрархій перетворіть рекурсію на явний стек:

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

Чи можу я змінювати дерево під час його обходу?

Не додавайте та не видаляйте вузли з child_nodes під час ітерації. Зберіть вузли для модифікації під час першого проходу, а потім застосуйте зміни під час другого проходу.

Як знайти конкретний вузол за назвою?

Використовуйте node.get_child(name) для пошуку прямого дочірнього елемента за іменем. Для глибокого пошуку пройдіть дерево з фільтром за іменем:

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

Чи node.entity завжди повертає Mesh?

Ні. Вузол може містити будь‑який тип сутності: Mesh, Camera, Light, або користувацькі сутності. Завжди перевіряйте з isinstance(node.entity, Mesh) перед використанням властивостей, специфічних для mesh.

Як отримати позицію вузла у світовому просторі?

Читати node.global_transform.translation. Це обчислена позиція у світовому просторі, з урахуванням усіх трансформацій предків. Вона лише для читання; змініть node.transform.translation щоб перемістити вузол.

Чи можу я підрахувати загальну кількість полігонів у сцені без написання обходу?

Не можна безпосередньо через API; немає scene.total_polygon_count властивості. Використайте collect_meshes і підсумуйте mesh.polygon_count по результатах, як показано у кроці 6.

 Українська