TypeScriptで3Dシーングラフをトラバースする方法
Aspose.3D FOSS for TypeScript のシーン グラフは、Node オブジェクトのツリーで、scene.rootNode をルートとしています。走査は再帰的に行われ、各ノードは 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}`);Buffer をメモリからロードすることも、scene.openFromBuffer(buffer, options) を使用して可能です; ディスク 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);1つのメッシュが 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>
ヒントとベストプラクティス
- Always null-check
node.entityの前にエンティティ固有のプロパティにアクセスしてください。多くのノードはエンティティを持たない純粋なグループノードです。 - Use
instanceofoverconstructor.nameをロジックパスの型チェックに使用してください。instanceofはリファクタリングに安全です;constructor.nameの文字列比較はミニフィケーションで壊れます。 - Traverse via
for...ofoverchildNodes:このイテラブルはすべての配列サイズを安全に処理します。将来の互換性のために数値インデックスは避けてください。 - Avoid mutating the tree during traversal:再帰呼び出し内でノードを追加または削除しないでください。まず結果を収集し、次に変更してください。
- Pass a results array as a parameter:これにより、各再帰呼び出しで新しい配列を割り当てるのを防ぎ、サブツリーの結果を簡単にマージできます。
共通の問題
| Symptom | Cause | Fix |
|---|---|---|
childNodes が rootNode 上で長さがゼロです | モデルがロードされていません | トラバースする前に scene.open() がエラーなく完了していることを確認してください |
node.entity instanceof Mesh が決して true にならない | 誤った Mesh のインポートパス | Mesh を @aspose/3d/entities からインポートし、@aspose/3d のルートからではなく |
| トラバースがネストされたメッシュを見逃す | すべての子要素に再帰していない | 再帰呼び出しが node.childNodes のすべての要素をカバーしていることを確認してください |
mesh.controlPoints.length が 0 です | メッシュはロードされたがジオメトリが含まれていない | OBJ ソースの空グループを確認し、mesh.polygonCount を二次チェックとして使用してください |
| 深い階層でスタックオーバーフロー | 非常に深いシーンツリー(数百レベル) | Array.push / Array.pop を使用した明示的なスタックに置き換えて再帰を廃止してください |
よくある質問
scene.rootNode自体はエンティティを持ちますか?
いいえ。ルートノードはライブラリによって自動的に作成されるコンテナです。エンティティはありません。あなたのジオメトリやその他のシーンオブジェクトは、rootNodeの1つ以上下の子ノードに存在します。
node.entity と node.entities の違いは何ですか?node.entity は単一の主要エンティティを保持します(一般的なケース)。
古い FBX および COLLADA ファイルの一部では、複数の添付エンティティを持つノードが生成されることがあります。その場合、node.entities(複数形)が完全なリストを提供します。
深さ優先ではなく幅優先で走査できますか?
はい。再帰呼び出しの代わりにキューを使用します:scene.rootNode を配列にプッシュし、次にシフトしてノードを処理しながら、各ノードの childNodes をキューの末尾にプッシュします。
scene.open() は同期ですか?
はい。scene.open() と scene.openFromBuffer() は、ファイルが完全に解析されるまで呼び出しスレッドをブロックします。イベントループを応答可能に保つ必要がある場合は、ワーカースレッドでラップしてください。
ノードからワールド空間の位置を取得するには?node.globalTransform を読み取ります; それはすべての先祖変換から構成されたワールド空間行列を持つ読み取り専用の GlobalTransform を返します。 明示的な行列計算には、node.evaluateGlobalTransform(false) を呼び出してください。
Mesh以外に可能なエンティティタイプは何ですか?Camera、Light、およびカスタムスケルトン/ボーンエンティティ。node.entity.constructor.nameを確認するか、@aspose/3dからインポートした特定のクラスとともにinstanceofを使用してください。