Як обійти 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.