กราฟฉาก

scene graph เป็นโมเดลข้อมูลพื้นฐานของ Aspose.3D FOSS สำหรับ Python. ทุกไฟล์ 3D ไม่ว่าจะโหลดจากดิสก์หรือสร้างในหน่วยความจำ จะถูกแทนด้วยต้นไม้ของวัตถุ Node ที่มีรากเป็น Scene.root_node. แต่ละโหนดสามารถมีโหนดลูกและหนึ่งหรือหลายวัตถุ Entity (เมช, กล้อง, แสง). การทำความเข้าใจกราฟฉากทำให้คุณเข้าถึงเรขาคณิต, วัสดุ, และการแปลงเชิงพื้นที่ของทุกวัตถุในฉากโดยตรง.

การติดตั้งและการตั้งค่า

ติดตั้งไลบรารีจาก PyPI:

pip install aspose-3d-foss

ไม่จำเป็นต้องมีส่วนขยายพื้นฐาน, คอมไพเลอร์ หรือแพ็กเกจระบบเพิ่มเติมใดๆ สำหรับคำแนะนำการติดตั้งเต็มรูปแบบ ดูที่ Installation Guide.


ภาพรวม: แนวคิดกราฟฉาก

กราฟฉากใน Aspose.3D FOSS ปฏิบัติตามลำดับชั้นการบรรจุที่ตรงไปตรงมา:

Scene
└── root_node  (Node)
    ├── child_node_A  (Node)
    │   ├── entity: Mesh
    │   └── transform: translation, rotation, scale
    ├── child_node_B  (Node)
    │   └── child_node_C  (Node)
    │       └── entity: Mesh
    └── ...
ObjectRole
Sceneคอนเทนเนอร์ระดับบนสุด มี root_node, asset_info, animation_clips และ sub_scenes.
Nodeโหนดต้นไม้ที่มีชื่อ มีพาเรนท์, ศูนย์หรือมากกว่าลูกโหนด, ศูนย์หรือมากกว่าหน่วยข้อมูล, และ Transform ในระดับท้องถิ่น.
Entityเรขาคณิตหรือวัตถุฉากที่แนบกับโหนด ประเภทหน่วยข้อมูลทั่วไป: Mesh, Camera, Light.
Transformตำแหน่ง, การหมุนและสเกลในพื้นที่ท้องถิ่นของโหนด ผลลัพธ์ในพื้นที่โลกอ่านจาก global_transform.

ขั้นตอนต่อขั้น: การสร้างกราฟซีนโดยโปรแกรม

ขั้นตอนที่ 1: สร้างฉาก

Scene ใหม่จะเริ่มต้นเสมอด้วย root_node ว่าง:

from aspose.threed import Scene

scene = Scene()
print(scene.root_node.name)   # "" (empty string — root node has no name by default)
print(len(scene.root_node.child_nodes))  # 0

Scene เป็นจุดเริ่มต้นสำหรับทุกอย่าง: การโหลดไฟล์, การบันทึกไฟล์, การสร้างคลิปแอนิเมชัน, และการเข้าถึงโครงสร้างโหนด.


ขั้นตอนที่ 2: สร้างโหนดลูก

ใช้ create_child_node(name) เพื่อเพิ่มโหนดที่มีชื่อเข้าไปในต้นไม้:

from aspose.threed import Scene

scene = Scene()

parent = scene.root_node.create_child_node("parent")
child  = parent.create_child_node("child")

print(parent.name)                          # "parent"
print(child.parent_node.name)               # "parent"
print(len(scene.root_node.child_nodes))     # 1

หรืออีกทางเลือกหนึ่ง ให้สร้าง Node แบบสแตนด์อโลนและแนบอย่างชัดเจน:

from aspose.threed import Scene, Node

scene = Scene()
node = Node("standalone")
scene.root_node.add_child_node(node)

ทั้งสองวิธีให้ผลลัพธ์เดียวกัน create_child_node มีความกระชับมากกว่าในการสร้างแบบอินไลน์.


ขั้นตอนที่ 3: สร้าง Mesh Entity และแนบเข้ากับมัน

Mesh หนึ่งอันเก็บข้อมูลเวอร์เท็กซ์ (control_points) และโทโพโลยีของหน้า (polygons). สร้างหนึ่งอัน, เพิ่มเรขาคณิต, แล้วผูกเข้ากับโหนด:

from aspose.threed import Scene, Node
from aspose.threed.entities import Mesh
from aspose.threed.utilities import Vector3, Vector4

scene = Scene()
parent = scene.root_node.create_child_node("parent")
child  = parent.create_child_node("child")

##Create a quad mesh (four vertices, one polygon)
# Note: control_points returns a copy of the internal list; append to
# _control_points directly to actually add vertices. This is a known
# library limitation — a public add_control_point() API is not yet available.
mesh = Mesh("cube")
mesh._control_points.append(Vector4(0, 0, 0, 1))
mesh._control_points.append(Vector4(1, 0, 0, 1))
mesh._control_points.append(Vector4(1, 1, 0, 1))
mesh._control_points.append(Vector4(0, 1, 0, 1))
mesh.create_polygon(0, 1, 2, 3)

child.add_entity(mesh)

print(f"Mesh name:      {mesh.name}")
print(f"Vertex count:   {len(mesh.control_points)}")
print(f"Polygon count:  {mesh.polygon_count}")

Vector4(x, y, z, w) แสดงถึงพิกัดเชิงเนื้อเดียว. ใช้ w=1 สำหรับตำแหน่งจุดปกติ.

create_polygon(*indices) รับดัชนีเวอร์เท็กซ์และลงทะเบียนหนึ่งหน้าในรายการโพลิกอน ส่งดัชนีสามตัวสำหรับสามเหลี่ยม สี่ตัวสำหรับควอด.


ขั้นตอนที่ 4: ตั้งค่าการแปลงโหนด

แต่ละโหนดมี Transform ที่ควบคุมตำแหน่ง, การวางแนว, และขนาดในพื้นที่ท้องถิ่น:

from aspose.threed.utilities import Vector3, Quaternion

##Translate the node 2 units along the X axis
child.transform.translation = Vector3(2.0, 0.0, 0.0)

##Scale the node to half its natural size
child.transform.scaling = Vector3(0.5, 0.5, 0.5)

##Rotate 45 degrees around the Y axis using Euler angles
child.transform.euler_angles = Vector3(0.0, 45.0, 0.0)

การแปลงเป็นการสะสม: ตำแหน่งในพื้นที่โลกของโหนดลูกเป็นการผสมผสานของการแปลงของตนเองกับการแปลงของบรรพบุรุษทั้งหมด. อ่านผลลัพธ์ในพื้นที่โลกที่ประเมินจาก node.global_transform (immutable, read‑only).


ขั้นตอนที่ 5: สำรวจ Scene Graph แบบเรียกซ้ำ

เดินตามต้นไม้ทั้งหมดโดยทำการเรียกซ้ำผ่าน node.child_nodes:

def traverse(node, depth=0):
    indent = "  " * depth
    entity_type = type(node.entity).__name__ if node.entity else "None"
    print(f"{indent}{node.name} [{entity_type}]")
    for child in node.child_nodes:
        traverse(child, depth + 1)

traverse(scene.root_node)

ตัวอย่างผลลัพธ์สำหรับฉากที่สร้างขึ้นด้านบน (ชื่อโหนดรากเป็นสตริงว่าง):

 [None]
  parent [None]
    child [Mesh]

สำหรับฉากที่มีเอนทิตี้หลายรายการต่อโหนด ให้ทำการวนซ้ำ node.entities แทน node.entity:

def traverse_full(node, depth=0):
    indent = "  " * depth
    entity_names = [type(e).__name__ for e in node.entities] or ["None"]
    print(f"{indent}{node.name} [{', '.join(entity_names)}]")
    for child in node.child_nodes:
        traverse_full(child, depth + 1)

traverse_full(scene.root_node)

ขั้นตอน 6: บันทึกฉาก

ส่งเส้นทางไฟล์ไปยัง scene.save(). รูปแบบจะถูกสรุปจากนามสกุลไฟล์:

scene.save("scene.gltf")   # JSON glTF 2.0
scene.save("scene.glb")    # Binary GLB container
scene.save("scene.obj")    # Wavefront OBJ
scene.save("scene.stl")    # STL

สำหรับตัวเลือกที่เฉพาะเจาะจงต่อรูปแบบ ให้ส่งอ็อบเจ็กต์ save-options เป็นอาร์กิวเมนต์ที่สอง:

from aspose.threed.formats import GltfSaveOptions

opts = GltfSaveOptions()
scene.save("scene.gltf", opts)

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

  • ตั้งชื่อทุกโหนด. การตั้งชื่อโหนดให้มีความหมายทำให้การดีบักการเดินทางง่ายขึ้นมากและทำให้ชื่อคงอยู่ในไฟล์ที่ส่งออก
  • หนึ่งเมชต่อโหนด. การรักษาเอนทิตี 1:1 กับโหนดทำให้การแปลงและการสอบถามการชนง่ายขึ้น
  • ใช้ create_child_node แทนการแนบด้วยตนเอง. มันตั้งค่าการอ้างอิงพาเรนต์โดยอัตโนมัติและมีความเสี่ยงต่อข้อผิดพลาดน้อยกว่า
  • อ่าน global_transform หลังจากสร้างลำดับชั้น. ผลลัพธ์ในเวิลด์สเปซจะคงที่ก็ต่อเมื่อการแปลงของบรรพบุรุษทั้งหมดถูกตั้งค่าแล้ว
  • ห้ามเปลี่ยนแปลงต้นไม้ระหว่างการเดิน. การเพิ่มหรือเอาโหนดลูกออกขณะวนซ้ำ child_nodes จะทำให้พฤติกรรมไม่คาดคิด เก็บโหนดก่อนแล้วจึงแก้ไข
  • จุดควบคุมใช้ Vector4 ไม่ใช่ Vector3. ควรส่ง w=1 สำหรับตำแหน่งเวอร์เท็กซ์ทั่วไปเสมอ; w=0 แทนเวกเตอร์ทิศทาง (ไม่ใช่จุด)
  • mesh.control_points คืนค่ากลับเป็นสำเนา. คุณสมบัติ control_points คืนค่า list(self._control_points) — การเพิ่มรายการในลิสต์ที่คืนมาจะไม่แก้ไขเมช ควรเพิ่มลงใน mesh._control_points โดยตรงเสมอเมื่อสร้างเรขาคณิตด้วยโปรแกรม นี่เป็นข้อจำกัดที่ทราบของไลบรารี; API การเปลี่ยนแปลงสาธารณะยังไม่มี

ปัญหาทั่วไป

IssueResolution
AttributeError: 'NoneType' object has no attribute 'polygons'ปกป้องด้วย if node.entity is not None ก่อนเข้าถึงคุณสมบัติของเอนทิตี้. โหนดที่ไม่มีเอนทิตี้จะมี entity = None.
Mesh appears at the origin despite setting translationtransform.translation ใช้การชดเชยในระดับท้องถิ่น. หากโหนดพาเรนท์เองมีการแปลงที่ไม่เป็นเอกลักษณ์ ตำแหน่งโลกอาจแตกต่างกัน. ตรวจสอบ global_transform.
Child nodes missing after scene.save() / reloadรูปแบบบางอย่าง (OBJ) ทำให้โครงสร้างแบนลง. ใช้ glTF หรือ COLLADA เพื่อรักษาโครงสร้างโหนดทั้งหมด.
polygon_count is 0 after mesh.create_polygon(...)ตรวจสอบว่าดัชนีเวอร์เท็กซ์ที่ส่งไปยัง create_polygon อยู่ในช่วง (0 ถึง len(control_points) - 1).
Node.get_child(name) returns Noneชื่อเป็นตัวพิมพ์ใหญ่-เล็กแตกต่างกัน. ยืนยันสตริงชื่อที่ใช้ในขณะสร้าง.
Traversal visits nodes in unexpected orderchild_nodes คืนค่าลูกในลำดับการแทรก (ลำดับที่ add_child_node / create_child_node ถูกเรียก).
 ภาษาไทย