Jak przeglądać graf sceny 3D w TypeScript
Graf scen w Aspose.3D FOSS dla TypeScript jest drzewem obiektów Node, którego korzeniem jest scene.rootNode. Przeglądanie jest rekurencyjne: każdy węzeł udostępnia iterowalny childNodes oraz opcjonalną właściwość entity. Ten przewodnik pokazuje, jak przejść całe drzewo, zidentyfikować typy encji i zebrać statystyki siatek.
Wymagania wstępne
- Node.js 18 lub nowszy
- TypeScript 5.0 lub nowszy
@aspose/3dzainstalowany
Przewodnik krok po kroku
Krok 1: Instalacja i import
Zainstaluj pakiet:
npm install @aspose/3dZaimportuj klasy użyte w tym przewodniku:
import { Scene } from '@aspose/3d';
import { ObjLoadOptions } from '@aspose/3d/formats/obj';
import { Mesh } from '@aspose/3d/entities';Scene i Mesh są klasami podstawowymi. ObjLoadOptions jest używany w przykładzie ładowania; zamień na odpowiednią klasę opcji dla innych formatów.
Krok 2: Załaduj scenę z pliku
Utwórz Scene i wywołaj scene.open() z ścieżką do pliku. Wykrywanie formatu jest automatyczne na podstawie binarnych liczb magicznych, więc nie musisz podawać formatu dla plików GLB, STL lub 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}`);Możesz również wczytać z Buffer w pamięci przy użyciu scene.openFromBuffer(buffer, options); przydatne w bezserwerowych pipeline’ach, w których nie jest dostępny disk I/O.
Krok 3: Napisz rekurencyjną funkcję przeglądania
Rekurencja nad childNodes jest standardowym wzorcem. Funkcja odwiedza każdy węzeł w kolejności przeglądania wgłąb:
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);Dla sceny z jedną siatką o nazwie Cube, wynik będzie wyglądał następująco:
[-] 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 jest null dla węzłów grupowych, kości i lokalizatorów. Sprawdzenie constructor.name działa dla dowolnego typu jednostki: Mesh, Camera, Light, itp.
Krok 4: Uzyskaj dostęp do typu jednostki na każdym węźle
Aby podjąć działanie w zależności od typu encji, użyj sprawdzenia instanceof po null guard:
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 jest najbezpieczniejszym sposobem potwierdzenia, że jednostka jest siatką wielokątową, przed dostępem do controlPoints, polygonCount lub elementów wierzchołków.
Krok 5: Filtruj węzły według typu encji
Aby zebrać tylko węzły zawierające siatkę bez drukowania całego drzewa, użyj rekurencyjnego akumulatora:
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)`);Funkcja przyjmuje opcjonalną tablicę results, aby wywołujący mógł ją wstępnie wypełnić w celu scalania wyników z wielu poddrzew.
Krok 6: Zbierz wszystkie siatki i wypisz liczbę wierzchołków
Rozszerz kolektor, aby wyświetlał statystyki dla każdej siatki:
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`);
}Przykładowy wynik dla sceny z dwoma siatkami:
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>
Wskazówki i najlepsze praktyki
- Zawsze sprawdzaj null
node.entityprzed dostępem do właściwości specyficznych dla encji. Wiele węzłów to czyste węzły grupowe, które nie zawierają encji. - Używaj
instanceofzamiastconstructor.nameprzy sprawdzaniu typów w ścieżkach logiki.instanceofjest bezpieczny przy refaktoryzacji; porównywanie łańcuchów naconstructor.namepsuje się po minifikacji. - Przechodź przez
for...ofzamiastchildNodes: iterowalny obiekt obsługuje wszystkie rozmiary tablic bezpiecznie. Unikaj indeksowania numerycznego dla przyszłej kompatybilności. - Unikaj mutacji drzewa podczas przechodzenia: nie dodawaj ani nie usuwaj węzłów wewnątrz wywołania rekurencyjnego. Najpierw zbierz wyniki, a potem modyfikuj.
- Przekazuj tablicę wyników jako parametr: to unika alokacji nowej tablicy przy każdym wywołaniu rekurencyjnym i ułatwia scalanie wyników poddrzewa.
Typowe problemy
| Objaw | Przyczyna | Rozwiązanie |
|---|---|---|
childNodes ma zerową długość na rootNode | Model nie został załadowany | Upewnij się, że scene.open() zakończyło się bez błędów przed przeglądaniem |
node.entity instanceof Mesh nigdy nie jest prawdziwe | Nieprawidłowa ścieżka importu Mesh | Importuj Mesh z @aspose/3d/entities, a nie z rootu @aspose/3d |
| Traversowanie pomija zagnieżdżone siatki | Brak rekurencji we wszystkich dzieciach | Upewnij się, że wywołanie rekurencyjne obejmuje każdy element w node.childNodes |
mesh.controlPoints.length wynosi 0 | Siatka załadowana, ale nie zawiera geometrii | Sprawdź źródło OBJ pod kątem pustych grup; użyj mesh.polygonCount jako dodatkowego sprawdzenia |
| Przepełnienie stosu przy głębokich hierarchiach | Bardzo głębokie drzewo sceny (setki poziomów) | Zastąp rekurencję jawnym stosem przy użyciu Array.push / Array.pop |
Najczęściej zadawane pytania
Czy scene.rootNode sam posiada encję?
Nie. Węzeł główny jest kontenerem tworzonym automatycznie przez bibliotekę. Nie posiada encji. Twoja geometria i inne obiekty sceny znajdują się w węzłach potomnych jeden lub kilka poziomów poniżej rootNode.
Jaka jest różnica między node.entity a node.entities?node.entity przechowuje pojedynczy podmiot główny (typowy przypadek). Niektóre starsze pliki FBX i COLLADA mogą generować węzły z wieloma podłączonymi podmiotami; w takim przypadku node.entities (liczba mnoga) zapewnia pełną listę.
Czy mogę przeglądać w kolejności wszerz zamiast wgłąb?
Tak. Użyj kolejki zamiast wywołania rekurencyjnego: wstaw scene.rootNode do tablicy, a następnie przesuń i przetwarzaj węzły, jednocześnie wstawiając childNodes każdego węzła do ogona kolejki.
Czy scene.open() jest synchroniczne?
Tak. scene.open() i scene.openFromBuffer() blokują wątek wywołujący, dopóki plik nie zostanie w pełni przetworzony. Umieść je w wątku roboczym, jeśli musisz utrzymać responsywność pętli zdarzeń.
Jak uzyskać pozycje w przestrzeni świata z węzła?
Odczytaj node.globalTransform; zwraca ona macierz w przestrzeni świata w trybie tylko do odczytu GlobalTransform, złożoną ze wszystkich transformacji przodków. Aby wykonać explicite obliczenia macierzy, wywołaj node.evaluateGlobalTransform(false).
Jakie typy encji są możliwe oprócz Mesh?Camera, Light oraz własne encje szkieletu/kości. Sprawdź node.entity.constructor.name lub użyj instanceof z konkretną klasą zaimportowaną z @aspose/3d.