วิธีการเดินทางผ่านกราฟฉาก 3D ใน TypeScript

วิธีการเดินทางผ่านกราฟฉาก 3D ใน TypeScript

กราฟฉากใน Aspose.3D FOSS สำหรับ TypeScript เป็นต้นไม้ของ Node อ็อบเจ็กต์ที่มีรากที่ scene.rootNode. การท่องผ่านเป็นแบบเรียกซ้ำ: แต่ละโหนดเปิดเผย childNodes อ็อบเจ็กต์ที่สามารถวนซ้ำได้และตัวเลือก entity property. คู่มือนี้แสดงวิธีการเดินผ่านต้นไม้ทั้งหมด, ระบุประเภทเอนทิตี้, และรวบรวมสถิติเมช.

Prerequisites

  • 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 ใช้ในตัวอย่างการโหลด; แทนที่ด้วยคลาส options ที่ตรงกันสำหรับฟอร์แมตอื่น.


ขั้นตอนที่ 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); มีประโยชน์ใน pipeline แบบ serverless ที่ไม่มีการเข้าถึง 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 สำหรับโหนดกลุ่ม, กระดูก, และตำแหน่ง. The constructor.name การตรวจสอบทำงานสำหรับประเภทเอนทิตีใดก็ได้: Mesh, Camera, Light, เป็นต้น.


ขั้นตอนที่ 4: เข้าถึงประเภทเอนทิตี้บนแต่ละโหนด

เพื่อดำเนินการตามประเภทเอนทิตี, ใช้ an instanceof ตรวจสอบหลังจากการตรวจสอบค่า null:

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 อาร์เรย์เพื่อให้ผู้เรียกสามารถเติมค่าไว้ล่วงหน้าเพื่อรวมผลลัพธ์ข้ามหลาย subtree.


ขั้นตอนที่ 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>

เคล็ดลับและแนวปฏิบัติที่ดีที่สุด

  • ตรวจสอบค่า null เสมอ node.entity ก่อนเข้าถึงคุณสมบัติที่เฉพาะของเอนทิตี. โหนดหลายตัวเป็นโหนดกลุ่มบริสุทธิ์ที่ไม่มีเอนทิตี.
  • ใช้ instanceof เหนือ constructor.name สำหรับการตรวจสอบประเภทในเส้นทางตรรกะ. instanceof ปลอดภัยต่อการรีแฟคเตอร์; การเปรียบเทียบสตริงบน constructor.name จะพังเมื่อทำการย่อขนาด (minification).
  • ท่องผ่าน for...of บน childNodes: ตัววนซ้ำจัดการขนาดอาร์เรย์ทั้งหมดอย่างปลอดภัย. หลีกเลี่ยงการใช้ดัชนีเชิงตัวเลขเพื่อความเข้ากันได้ในอนาคต.
  • หลีกเลี่ยงการเปลี่ยนแปลงต้นไม้ระหว่างการท่องผ่าน: อย่าเพิ่มหรือลบโหนดภายในการเรียกแบบเรียกซ้ำ. รวบรวมผลลัพธ์ก่อน, แล้วจึงทำการแก้ไข.
  • ส่งอาร์เรย์ผลลัพธ์เป็นพารามิเตอร์: สิ่งนี้ช่วยหลีกเลี่ยงการจัดสรรอาร์เรย์ใหม่ในแต่ละการเรียกแบบเรียกซ้ำและทำให้การรวมผลลัพธ์ของโครงย่อยเป็นเรื่องง่าย.

ปัญหาทั่วไป

อาการสาเหตุวิธีแก้
childNodes มีความยาวเป็นศูนย์บน rootNodeโมเดลไม่ได้โหลดตรวจสอบให้แน่ใจ scene.open() เสร็จสมบูรณ์โดยไม่มีข้อผิดพลาดก่อนการเดินผ่าน
node.entity instanceof Mesh ไม่เคยเป็นจริงผิด Mesh เส้นทางการนำเข้านำเข้า Mesh จาก @aspose/3d/entities, ไม่ใช่จาก @aspose/3d รูท
การเดินผ่านพลาดเมชที่ซ้อนกันไม่ได้ทำการเรียกซ้ำไปยังลูกทั้งหมดตรวจสอบให้แน่ใจว่าการเรียกแบบเรียกซ้ำครอบคลุมทุกองค์ประกอบใน node.childNodes
mesh.controlPoints.length เป็น 0Mesh ถูกโหลดแล้วแต่ไม่มีเรขาคณิตตรวจสอบแหล่ง OBJ สำหรับกลุ่มที่ว่างเปล่า; ใช้ mesh.polygonCount เป็นการตรวจสอบรอง
Stack overflow บนลำดับชั้นที่ลึกต้นไม้ซีนลึกมาก (หลายร้อยระดับ)แทนที่การเรียกซ้ำด้วยสแตกแบบชัดเจนโดยใช้ Array.push / Array.pop

คำถามที่พบบ่อย

ทำ scene.rootNode ตัวมันเองมีเอนทิตี้หรือไม่? ไม่. โหนดรากเป็นคอนเทนเนอร์ที่สร้างโดยอัตโนมัติโดยไลบรารี. มันไม่มีเอนทิตี้. เรขาคณิตและวัตถุซีนอื่น ๆ ของคุณอยู่บนโหนดลูกหนึ่งระดับหรือหลายระดับด้านล่าง rootNode.

ความแตกต่างระหว่าง node.entity และ node.entities? node.entity ถือเอนทิตีหลักเดียว (กรณีทั่วไป) ไฟล์ FBX และ COLLADA รุ่นเก่าบางไฟล์อาจสร้างโหนดที่มีเอนทิตีแนบหลายตัว; ในกรณีนั้น node.entities (พหูพจน์) ให้รายการเต็ม.

ฉันสามารถเดินทางแบบกว้างก่อน (breadth‑first) แทนแบบลึกก่อน (depth‑first) ได้ไหม? ได้เลย ใช้คิวแทนการเรียกแบบเรียกซ้ำ: ผลัก scene.rootNode เข้าไปในอาร์เรย์ จากนั้นชิฟท์และประมวลผลโหนดขณะผลักแต่ละโหนดของ childNodes เข้าไปที่ท้ายคิว.

เป็น scene.open() synchronous หรือไม่? ใช่. scene.open() และ scene.openFromBuffer() ทั้งสองจะบล็อกเธรดที่เรียกจนกว่าไฟล์จะถูกแยกวิเคราะห์อย่างสมบูรณ์ หากคุณต้องการให้ event loop ตอบสนองได้ ให้ห่อหุ้มพวกมันใน worker thread.

ฉันจะดึงตำแหน่งใน world-space จากโหนดได้อย่างไร? อ่าน node.globalTransform; มันจะคืนค่าแบบอ่าน‑อย่างเดียว GlobalTransform พร้อมเมทริกซ์เวิลด์สเปซที่ประกอบจากการแปลงของบรรพบุรุษทั้งหมด สำหรับการคำนวณเมทริกซ์โดยตรง ให้เรียก node.evaluateGlobalTransform(false).

ประเภทเอนทิตีที่เป็นไปได้นอกจาก Mesh? Camera, Light, และเอนทิตีโครงกระดูก/กระดูกที่กำหนดเอง ตรวจสอบ node.entity.constructor.name หรือใช้ instanceof พร้อมคลาสเฉพาะที่นำเข้าจาก @aspose/3d.

ดูเพิ่มเติม

 ภาษาไทย