Как да обходим 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=“Copy code”

<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 guard вътре в обхода:

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 резултата за зареден STLSTL файловете обикновено създават един плосък възел с една mesh entity директно под 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.

 Български