Как обходить 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: Написание рекурсивной функции обхода

Самый простой обход посещает каждый узел в порядке обхода в глубину:

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.

 Русский