TypeScript에서 3D 씬 그래프 순회하는 방법
Aspose.3D FOSS for TypeScript의 씬 그래프는 scene.rootNode를 루트로 하는 Node 객체 트리입니다. 순회는 재귀적으로 이루어지며, 각 노드는 childNodes 이터러블과 선택적 entity 속성을 노출합니다. 이 가이드는 전체 트리를 탐색하고, 엔티티 유형을 식별하며, 메시 통계를 수집하는 방법을 보여줍니다.
사전 요구 사항
- Node.js 18 이상
- TypeScript 5.0 이상
@aspose/3d설치됨
단계별 가이드
1단계: 설치 및 가져오기
패키지를 설치하세요:
npm install @aspose/3d이 가이드에서 사용되는 클래스를 가져오세요:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';Scene과 Mesh는 핵심 클래스입니다. ObjLoadOptions는 로드 예제에서 사용됩니다. 다른 형식에는 해당하는 옵션 클래스를 사용하세요.
2단계: 파일에서 씬 로드하기
Scene을 생성하고 파일 경로와 함께 scene.open()을 호출합니다. 형식 감지는 바이너리 매직 넘버에서 자동으로 이루어지므로 GLB, STL 또는 3MF 파일의 형식을 지정할 필요가 없습니다:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
const scene = new Scene();
scene.open('model.obj', new ObjLoadOptions());
console.log(`Root node: "${scene.rootNode.name}"`);
console.log(`Top-level children: ${scene.rootNode.childNodes.length}`);scene.openFromBuffer(buffer, options)를 사용하여 메모리의 Buffer에서도 로드할 수 있습니다. 디스크 I/O를 사용할 수 없는 서버리스 파이프라인에서 유용합니다.
3단계: 재귀 순회 함수 작성하기
childNodes에 대한 재귀는 표준 패턴입니다. 이 함수는 각 노드를 깊이 우선 방식으로 방문합니다:
function traverse(node: any, depth = 0): void {
const indent = ' '.repeat(depth);
const entityType = node.entity ? node.entity.constructor.name : '-';
console.log(`${indent}[${entityType}] ${node.name}`);
for (const child of node.childNodes) {
traverse(child, depth + 1);
}
}
traverse(scene.rootNode);Cube라는 이름의 메시가 하나인 씬에서 출력은 다음과 같습니다:
[-] RootNode
[Mesh] Cube<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.entity는 그룹 노드, 본, 로케이터의 경우 null입니다. constructor.name 확인은 모든 엔티티 유형에 작동합니다: Mesh, Camera, Light 등.
4단계: 각 노드에서 엔티티 유형 접근하기
엔티티 유형에 따라 작업을 수행하려면 null 가드 이후에 instanceof 확인을 사용하세요:
import { Mesh } from '@aspose/3d/entities';
function visitWithTypeCheck(node: any, depth = 0): void {
const indent = ' '.repeat(depth);
if (node.entity instanceof Mesh) {
const mesh = node.entity as Mesh;
console.log(`${indent}MESH "${node.name}": ${mesh.controlPoints.length} vertices`);
} else if (node.entity) {
console.log(`${indent}${node.entity.constructor.name} "${node.name}"`);
} else {
console.log(`${indent}GROUP "${node.name}"`);
}
for (const child of node.childNodes) {
visitWithTypeCheck(child, depth + 1);
}
}
visitWithTypeCheck(scene.rootNode);instanceof Mesh는 controlPoints, polygonCount 또는 정점 요소에 접근하기 전에 엔티티가 폴리곤 메시임을 확인하는 가장 안전한 방법입니다.
5단계: 엔티티 유형별 노드 필터링하기
전체 트리를 출력하지 않고 메시가 있는 노드만 수집하려면 재귀 누적기를 사용하세요:
import { Mesh } from '@aspose/3d/entities';
function collectMeshes(
node: any,
results: Array<{ name: string; mesh: Mesh }> = []
): Array<{ name: string; mesh: Mesh }> {
if (node.entity instanceof Mesh) {
results.push({ name: node.name, mesh: node.entity as Mesh });
}
for (const child of node.childNodes) {
collectMeshes(child, results);
}
return results;
}
const meshNodes = collectMeshes(scene.rootNode);
console.log(`Found ${meshNodes.length} mesh node(s)`);이 함수는 선택적 results 배열을 받으므로 호출자가 여러 서브트리에 걸쳐 결과를 병합하기 위해 미리 채울 수 있습니다.
6단계: 모든 메시 수집 및 정점 수 출력하기
수집기를 확장하여 메시별 통계를 출력합니다:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';
function collectMeshes(node: any, results: Array<{name: string, mesh: Mesh}> = []) {
if (node.entity instanceof Mesh) {
results.push({ name: node.name, mesh: node.entity as Mesh });
}
for (const child of node.childNodes) {
collectMeshes(child, results);
}
return results;
}
const scene = new Scene();
scene.open('model.obj', new ObjLoadOptions());
const meshes = collectMeshes(scene.rootNode);
for (const { name, mesh } of meshes) {
console.log(`${name}: ${mesh.controlPoints.length} vertices, ${mesh.polygonCount} polygons`);
}두 개의 메시가 있는 씬의 예시 출력:
Cube: 8 vertices, 6 polygons
Sphere: 482 vertices, 480 polygons<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.entity의 null을 확인하세요. 엔티티별 속성에 접근하기 전에 반드시 확인해야 합니다. 많은 노드는 엔티티를 가지지 않는 순수한 그룹 노드입니다. - 타입 확인에는
constructor.name대신instanceof를 사용하세요.instanceof는 리팩터링에 안전합니다.constructor.name의 문자열 비교는 코드 축소(minification) 시 깨질 수 있습니다. childNodes에서for...of로 순회하세요. 이터러블은 모든 배열 크기를 안전하게 처리합니다. 앞으로의 호환성을 위해 숫자 인덱싱은 피하세요.- 순회 중 트리를 변경하지 마세요. 재귀 호출 내부에서 노드를 추가하거나 제거하지 마세요. 먼저 결과를 수집하고 나서 수정하세요.
- 결과 배열을 매개변수로 전달하세요. 이렇게 하면 재귀 호출마다 새로운 배열을 할당하는 것을 방지하고 서브트리 결과를 쉽게 병합할 수 있습니다.
일반적인 문제
| 증상 | 원인 | 해결책 |
|---|---|---|
rootNode에서 childNodes의 길이가 0입니다 | 모델이 로드되지 않음 | 순회하기 전에 scene.open()이 오류 없이 완료되었는지 확인하세요 |
node.entity instanceof Mesh가 항상 false입니다 | 잘못된 Mesh 가져오기 경로 | Mesh를 @aspose/3d 루트가 아닌 @aspose/3d/entities에서 가져오세요 |
| 순회가 중첩된 메시를 놓칩니다 | 모든 자식에 재귀하지 않음 | 재귀 호출이 node.childNodes의 모든 요소를 커버하는지 확인하세요 |
mesh.controlPoints.length가 0입니다 | 메시는 로드되었지만 지오메트리가 없음 | 빈 그룹에 대해 OBJ 소스를 확인하고 mesh.polygonCount를 보조 확인으로 사용하세요 |
| 깊은 계층에서 스택 오버플로 | 매우 깊은 씬 트리 (수백 레벨) | Array.push / Array.pop을 사용한 명시적 스택으로 재귀를 대체하세요 |
자주 묻는 질문
scene.rootNode 자체가 엔티티를 가지나요?
아니요. 루트 노드는 라이브러리가 자동으로 생성하는 컨테이너입니다. 엔티티를 가지지 않습니다. 지오메트리와 다른 씬 객체들은 rootNode 아래 하나 이상의 레벨에 있는 자식 노드에 있습니다.
node.entity와 node.entities의 차이점은 무엇인가요?
node.entity는 단일 기본 엔티티를 보유합니다(일반적인 경우). 일부 오래된 FBX 및 COLLADA 파일은 여러 연결된 엔티티가 있는 노드를 생성할 수 있습니다. 이 경우 node.entities(복수형)가 전체 목록을 제공합니다.
깊이 우선 대신 너비 우선 순서로 순회할 수 있나요?
네. 재귀 호출 대신 큐를 사용하세요. scene.rootNode를 배열에 push하고, 각 노드의 childNodes를 큐의 끝에 push하면서 노드를 shift하고 처리하세요.
scene.open()은 동기식인가요?
네. scene.open()과 scene.openFromBuffer()는 파일이 완전히 파싱될 때까지 호출 스레드를 차단합니다. 이벤트 루프를 반응형으로 유지해야 한다면 워커 스레드로 감싸세요.
노드에서 월드 공간 위치를 얻으려면 어떻게 하나요?
node.globalTransform을 읽으세요. 모든 조상 변환으로 구성된 월드 공간 행렬을 가진 읽기 전용 GlobalTransform을 반환합니다. 명시적인 행렬 연산을 위해서는 node.evaluateGlobalTransform(false)를 호출하세요.
Mesh 외에 가능한 엔티티 유형은 무엇인가요?
Camera, Light, 그리고 사용자 정의 스켈레톤/본 엔티티입니다. node.entity.constructor.name을 확인하거나 @aspose/3d에서 가져온 특정 클래스와 함께 instanceof를 사용하세요.