Как обходить 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: Написание рекурсивной функции обхода
Самый простой обход посещает каждый узел в порядке обхода в глубину:
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 непосредственно. |
| Имена узлов — пустые строки | Некоторые форматы (binary STL, некоторые файлы OBJ) не сохраняют имена объектов. Узлы будут иметь пустые name строки; используйте индекс для идентификации вместо этого. |
Часто задаваемые вопросы
Насколько глубокой может быть граф сцены?
Жёсткого ограничения нет. Значение по умолчанию Python для предела рекурсии (1000 кадров) применяется к рекурсивным функциям обхода. Для очень глубоких иерархий преобразуйте рекурсию в явный стек:
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.