Python에서 3D 씬 그래프를 탐색하는 방법

Python에서 3D 씬 그래프를 탐색하는 방법

Aspose.3D FOSS의 씬 그래프는 트리 구조입니다 Node 다음에 루트된 객체 scene.root_node. OBJ, glTF, STL, COLLADA, 또는 3MF에서 로드되든 모든 3D 파일은 동일한 트리 구조를 생성합니다. 이를 순회하는 방법을 알면 기하학을 검사하고, 폴리곤을 셀 수 있으며, 유형별로 객체를 필터링하고, 복잡한 씬의 특정 부분을 처리할 수 있습니다.

단계별 가이드

1단계: 설치 및 임포트

PyPI에서 Aspose.3D FOSS를 설치합니다:

pip install aspose-3d-foss

필요한 클래스를 임포트합니다:

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

모든 public 클래스는 aspose.threed 또는 그 하위 패키지 (aspose.threed.entities, aspose.threed.utilities).


2단계: 파일에서 씬 로드

static 메서드를 사용하세요 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 삽입 순서대로 자식을 반환합니다 (importer 또는 사용자가 추가한 순서).


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 평가된 월드 공간 결과를 제공합니다 (with 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 (재귀적으로 하지 않으면), 모든 하위 요소를 놓칩니다.
수집된 결과에서 메시가 누락됨파일 형식이 계층 구조를 보존하는지 확인하십시오. OBJ는 모든 기하학을 단일 노드 레벨로 평탄화합니다. 깊은 계층 구조를 위해서는 glTF 또는 COLLADA를 사용하십시오.
node.entity 첫 번째 엔티티만 반환합니다사용 node.entities (전체 목록) 노드가 여러 엔터티를 보유할 때. node.entity 은(는) 약식 표현이다 node.entities[0]entities 은 비어 있지 않다.
collect_meshes 로드된 STL에 대해 0개의 결과를 반환합니다STL 파일은 일반적으로 하나의 평면 노드와 그 바로 아래에 하나의 mesh 엔터티를 생성합니다 root_node. 확인 root_node.child_nodes[0].entity 직접.
노드 이름이 빈 문자열입니다일부 포맷(바이너리 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")

Does 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에 표시된 대로.

 한국어