วิชาการโดย milo อ่าน 11 นาทีเรียบเรียงโดยมี AI ช่วย

จากจุดพิกัดสู่โลก 3D: เรื่องเล่าระบบพิกัดและ Transformation ที่นักพัฒนาเกมต้องรู้

เคยสงสัยไหมว่าทำไมตัวละครในเกมของคุณถึงเดินทะลุพื้น หรือกล้องหมุนแล้วมึนงง? คำตอบอาจซ่อนอยู่ในระบบพิกัดและการแปลงพิกัดที่คุณยังไม่เข้าใจจริง

สารบัญ

เมื่อตัวละครเดินทะลุพื้น: เรื่องราวเริ่มต้นจากจุดพิกัด

เมื่อสามปีก่อน ผมนั่งแก้บั๊กอยู่หน้าจอทั้งคืน ตัวละครในเกม 3D ของผมเดินไปข้างหน้าแล้วลอยขึ้นฟ้า กล้องหมุนไปรอบๆ แล้วภาพกระตุก ราวกับว่าโลกในเกมกำลังแกล้งผม

สาเหตุจริงๆ ไม่ใช่บั๊กของ engine แต่เป็นเพราะผมไม่เข้าใจ game math 3D coordinate system อย่างแท้จริง

บทความนี้จะพาคุณเดินทะลุผ่านระบบพิกัดต่างๆ ที่ใช้ในเกม 3D ทำความเข้าใจ transformation matrix และเห็นภาพว่าโลกในเกมถูกสร้างขึ้นจากตัวเลขอย่างไร


ระบบพิกัดในเกม 3D: มากกว่าแค่ X Y Z

Cartesian Coordinate System: รากฐานของทุกอย่าง

ระบบพิกัดคาร์ทีเซียนคือสิ่งที่นักพัฒนาเกมทุกคนคุ้นเคย มีแกน X (ซ้าย-ขวา), Y (บน-ล่าง), Z (หน้า-หลัง) แต่สิ่งที่หลายคนมองข้ามคือ มือต้นแบบของระบบ

  • Left-handed system (เช่น Unity, DirectX): แกน Z ชี้เข้าหาจอ
  • Right-handed system (เช่น OpenGL, Godot): แกน Z ชี้ออกจากจอ

ความต่างนี้สำคัญมาก เพราะถ้าคุณย้ายโค้ดจาก Unity ไป Godot โดยไม่แปลงพิกัด ทุกอย่างจะกลับด้าน

# ตัวอย่างการสร้างจุดในระบบ Cartesian
point_3d = {
    'x': 5.0,   # ขวา
    'y': 2.0,   # บน
    'z': -3.0   # ถอยหลัง (Unity left-handed)
}

Polar Coordinate System: เหมาะกับการหมุนใน 2D

ระบบพิกัดเชิงขั้วใช้ระยะทาง (r) และมุม (θ) ในเกม 3D เราใช้ในกรณีจำกัด เช่น

  • การคำนวณตำแหน่งศัตรูรอบผู้เล่น
  • การสร้างวงกลมเลือด (health ring)
  • การกระจายอนุภาค (particle) เป็นวง
import math

# แปลงจาก Polar เป็น Cartesian
r = 10.0
theta = math.radians(45)  # 45 องศา

x = r * math.cos(theta)
y = r * math.sin(theta)

print(f"ตำแหน่ง: ({x:.2f}, {y:.2f})")
# ตำแหน่ง: (7.07, 7.07)

Spherical Coordinate System: โลกกลมในมือคุณ

ระบบพิกัดทรงกลมใช้สามค่า: r (รัศมี), θ (theta - มุมแนวนอน), φ (phi - มุมแนวตั้ง)

นี่คือหัวใจของ:

  • การวางกล้องรอบตัวละคร (orbit camera)
  • การสร้างท้องฟ้าและดวงดาว
  • การคำนวณเสียง 3D (spatial audio)
import math

# แปลงจาก Spherical เป็น Cartesian
r = 15.0
theta = math.radians(60)  # มุมแนวนอน
phi = math.radians(30)    # มุมแนวตั้ง

x = r * math.sin(phi) * math.cos(theta)
y = r * math.cos(phi)
z = r * math.sin(phi) * math.sin(theta)

print(f"ตำแหน่ง 3D: ({x:.2f}, {y:.2f}, {z:.2f})")

Transformation Matrix: ภาษาลับของโลก 3D

ทุกการเคลื่อนไหวในเกม 3D ล้วนเป็นการคูณเมทริกซ์ ไม่ว่าจะเดิน หมุน หรือย่อ-ขยาย

Translate: การเคลื่อนย้าย

การเคลื่อนย้ายคือการบวกค่าเข้าไปในพิกัด แต่ในโลกเมทริกซ์ เราใช้ homogeneous coordinate เพิ่มแกนที่สี่เข้าไป

Translation Matrix (4x4):
| 1  0  0  Tx |
| 0  1  0  Ty |
| 0  0  1  Tz |
| 0  0  0  1  |

Rotate: การหมุนรอบแกน

การหมุนมีสามแกนหลัก แต่ละแกนมีเมทริกซ์เฉพาะ

import numpy as np

# เมทริกซ์หมุนรอบแกน Y (มุมในหน่วย radian)
def rotation_y(angle):
    c, s = np.cos(angle), np.sin(angle)
    return np.array([
        [ c, 0, s, 0],
        [ 0, 1, 0, 0],
        [-s, 0, c, 0],
        [ 0, 0, 0, 1]
    ])

# หมุนจุด (1, 0, 0) ไป 90 องศา
point = np.array([1, 0, 0, 1])
rotated = rotation_y(np.radians(90)) @ point
print(f"หลังหมุน: {rotated[:3]}")
# หลังหมุน: [0, 0, 1]  (ในระบบ right-handed)

Scale: การย่อ-ขยาย

# เมทริกซ์ย่อ-ขยาย
def scale_matrix(sx, sy, sz):
    return np.array([
        [sx, 0,  0,  0],
        [0,  sy, 0,  0],
        [0,  0,  sz, 0],
        [0,  0,  0,  1]
    ])

ลำดับการคูณสำคัญ!

ในการประกอบ transformation ลำดับการคูณเปลี่ยนผลลัพธ์

# กรณี A: หมุนก่อนแล้วค่อยเคลื่อนย้าย
result_A = translate_matrix @ rotate_matrix @ point

# กรณี B: เคลื่อนย้ายก่อนแล้วค่อยหมุน
result_B = rotate_matrix @ translate_matrix @ point

กรณี A ตัวละครหมุนในที่แล้วเดินไปทิศที่หัน กรณี B ตัวละครเดินไปก่อนแล้ววงกลมหมุนรอบจุดกำเนิด

ความเข้าใจนี้คือสิ่งที่ทำให้ตัวละครเดินไปข้างหน้าถูกทิศ ไม่ใช่เดินทะลุพื้น


World Space กับ Local Space: สองโลกในหนึ่งเกม

World Space: พิกัดสัมบัติของโลกเกม

จุด (0,0,0) ใน world space คือจุดกำเนิดของฉาก ทุก object มีพิกัดสัมบัติที่ไม่เปลี่ยนแปลงไม่ว่ากล้องจะอยู่ที่ไหน

Local Space: พิกัดสัมพัทธ์ของวัตถุ

แขนของตัวละครมี local space ของมันเอง เมื่อตัวละครหมุน แขนก็หมุนตาม

การแปลงระหว่างสองโลก

// Unity: แปลงจาก local space เป็น world space
Vector3 worldPos = transform.TransformPoint(localPos);

// Unity: แปลงจาก world space เป็น local space
Vector3 localPos = transform.InverseTransformPoint(worldPos);

ตัวอย่างจริง: สมมติคุณมีปืนติดอยู่ที่มือตัวละคร ปืนมี local position (0.3, 1.2, 0.5) เมื่อตัวละครหมุน ปืนต้องยังอยู่ที่มือ ไม่ใช่ลอยไปกลางอากาศ

// โครงสร้าง hierarchy ใน Unity
// Player (root)
//   └── Arm
//        └── Gun

// ปืนอยู่ใน local space ของแขน
// เมื่อแขนหมุน ปืนตามไปอัตโนมัติ
transform.localPosition = new Vector3(0.3f, 0f, 0.5f);

ตัวอย่างโค้ด: การเคลื่อนที่ในโลก 3D

Python: สร้างระบบเคลื่อนที่พื้นฐาน

import numpy as np

class GameObject3D:
    def __init__(self, name, position=(0,0,0), rotation=(0,0,0), scale=(1,1,1)):
        self.name = name
        self.position = np.array(position, dtype=float)
        self.rotation = np.array(rotation, dtype=float)  # องศา
        self.scale = np.array(scale, dtype=float)
    
    def get_world_matrix(self):
        """สร้าง model matrix สำหรับแปลง local space เป็น world space"""
        T = self._translation_matrix()
        R = self._rotation_matrix()
        S = self._scale_matrix()
        return T @ R @ S  # Scale ก่อน หมุน แล้วค่อยเคลื่อนย้าย
    
    def _translation_matrix(self):
        x, y, z = self.position
        return np.array([
            [1, 0, 0, x],
            [0, 1, 0, y],
            [0, 0, 1, z],
            [0, 0, 0, 1]
        ])
    
    def _rotation_matrix(self):
        rx, ry, rz = np.radians(self.rotation)
        # หมุนรอบแกน Y ก่อน (มาตรฐานในเกม)
        cy, sy = np.cos(ry), np.sin(ry)
        cx, sx = np.cos(rx), np.sin(rx)
        cz, sz = np.cos(rz), np.sin(rz)
        
        rot_y = np.array([
            [ cy, 0, sy, 0],
            [  0, 1,  0, 0],
            [-sy, 0, cy, 0],
            [  0, 0,  0, 1]
        ])
        rot_x = np.array([
            [1,  0,  0, 0],
            [0, cx,-sx, 0],
            [0, sx, cx, 0],
            [0,  0,  0, 1]
        ])
        rot_z = np.array([
            [cz,-sz, 0, 0],
            [sz, cz, 0, 0],
            [ 0,  0, 1, 0],
            [ 0,  0, 0, 1]
        ])
        return rot_y @ rot_x @ rot_z
    
    def _scale_matrix(self):
        sx, sy, sz = self.scale
        return np.array([
            [sx, 0,  0,  0],
            [0,  sy, 0,  0],
            [0,  0,  sz, 0],
            [0,  0,  0,  1]
        ])
    
    def move_forward(self, distance):
        """เดินไปข้างหน้าตามทิศที่หัน"""
        ry = np.radians(self.rotation[1])
        forward = np.array([np.sin(ry), 0, np.cos(ry)])
        self.position += forward * distance

# ใช้งาน
player = GameObject3D("Player", position=(0, 0, 0), rotation=(0, 45, 0))
player.move_forward(10.0)
print(f"ตำแหน่งหลังเดิน: {player.position}")
# ตำแหน่งหลังเดิน: [7.07, 0, 7.07]

Unity C#: การเคลื่อนที่แบบมืออาชีพ

using UnityEngine;

public class PlayerMovement3D : MonoBehaviour
{
    public float moveSpeed = 5f;
    public float rotationSpeed = 180f;
    
    void Update()
    {
        // รับค่าจากผู้เล่น
        float horizontal = Input.GetAxis("Horizontal");
        float vertical = Input.GetAxis("Vertical");
        
        // สร้างทิศทางใน local space
        Vector3 moveDirection = new Vector3(horizontal, 0f, vertical).normalized;
        
        // แปลงเป็น world space โดยอ้างอิงการหันของตัวละคร
        Vector3 worldMoveDirection = transform.TransformDirection(moveDirection);
        
        // เคลื่อนย้ายใน world space
        transform.position += worldMoveDirection * moveSpeed * Time.deltaTime;
        
        // หมุนตัวละครด้วย mouse
        float mouseX = Input.GetAxis("Mouse X");
        transform.Rotate(0f, mouseX * rotationSpeed * Time.deltaTime, 0f);
    }
}

สังเกตว่า Unity จัดการเมทริกซ์ให้อัตโนมัติ แต่ถ้าคุณเข้าใจหลักการ คุณจะ debug ได้เร็วขึ้นมาก


Gimbal Lock: ปัญหาที่หลายคนเจอแต่ไม่รู้ชื่อ

อาการของ Gimbal Lock

ลองนึกถึงกล้องในเกมที่หมุนได้รอบทิศ แต่เมื่อคุณมองขึ้นฟ้าจนสุดแล้วกลับลงมา ภาพกระตุก ทิศทางสับสน นี่คือ gimbal lock

สาเหตุคือเมื่อใช้ Euler angles (สามมุม: pitch, yaw, roll) ถ้ามุมหนึ่งถึง 90 องศา แกนอีกสองแกนจะทับซ้อนกัน ทำให้สูญเสียองศาอิสระไปหนึ่งแกน

Quaternion: ทางออกที่สะอาดที่สุด

Quaternion คือตัวเลขสี่ตัวที่แทนการหมุนใน 3D โดยไม่มีปัญหา gimbal lock

// Unity: ใช้ Quaternion แทน Euler angles

// หลีกเลี่ยงแบบนี้ (มี gimbal lock)
transform.rotation = Quaternion.Euler(90f, 0f, 0f);

// ใช้แบบนี้แทน
Quaternion targetRotation = Quaternion.LookRotation(direction);
transform.rotation = Quaternion.Slerp(
    transform.rotation, 
    targetRotation, 
    Time.deltaTime * 5f
);
# Python: สร้าง quaternion สำหรับหมุนรอบแกน
import numpy as np

def axis_angle_to_quaternion(axis, angle_degrees):
    """แปลง axis-angle เป็น quaternion"""
    angle = np.radians(angle_degrees)
    half_angle = angle / 2.0
    
    axis = np.array(axis, dtype=float)
    axis = axis / np.linalg.norm(axis)  # normalize
    
    w = np.cos(half_angle)
    x, y, z = axis * np.sin(half_angle)
    
    return np.array([w, x, y, z])

def quaternion_to_matrix(q):
    """แปลง quaternion เป็น rotation matrix 4x4"""
    w, x, y, z = q
    return np.array([
        [1-2*(y*y+z*z), 2*(x*y-w*z),   2*(x*z+w*y),   0],
        [2*(x*y+w*z),   1-2*(x*x+z*z), 2*(y*z-w*x),   0],
        [2*(x*z-w*y),   2*(y*z+w*x),   1-2*(x*x+y*y), 0],
        [0,             0,             0,             1]
    ])

# หมุน 90 องศารอบแกน Y
q = axis_angle_to_quaternion([0, 1, 0], 90)
rot_matrix = quaternion_to_matrix(q)
print("Rotation matrix from quaternion:")
print(rot_matrix)

ข้อดีของ Quaternion

  • ไม่มี gimbal lock
  • การ interpolate การหมุนราบรื่น (slerp)
  • ใช้พื้นที่น้อยกว่าเมทริกซ์ (4 ตัวเลข vs 9 ตัวเลข)
  • คูณ quaternion รวมการหมุนได้

การประยุกต์ใช้จริง: กรณีศึกษา

กรณีที่ 1: กล้องอิสระแบบ Orbit

// Unity: Orbit camera รอบตัวละคร
public class OrbitCamera : MonoBehaviour
{
    public Transform target;
    public float distance = 10f;
    public float sensitivity = 2f;
    
    private float yaw = 0f;
    private float pitch = 30f;
    
    void LateUpdate()
    {
        yaw += Input.GetAxis("Mouse X") * sensitivity;
        pitch -= Input.GetAxis("Mouse Y") * sensitivity;
        pitch = Mathf.Clamp(pitch, -80f, 80f);  // จำกัดมุมเพื่อไม่ให้ flip
        
        // ใช้ Spherical coordinate แปลงเป็น Cartesian
        Quaternion rotation = Quaternion.Euler(pitch, yaw, 0f);
        Vector3 offset = rotation * Vector3.forward * distance;
        
        transform.position = target.position - offset;
        transform.LookAt(target);
    }
}

กรณีที่ 2: การยิงกระสุนในทิศที่กล้องมอง

// Unity: ยิงจากกล้องไปยังจุดที่เล็ง
public class ShootFromCamera : MonoBehaviour
{
    public Camera playerCamera;
    public GameObject bulletPrefab;
    public float bulletSpeed = 50f;
    
    void Update()
    {
        if (Input.GetMouseButtonDown(0))
        {
            // ทิศทางจากกล้อง (world space)
            Vector3 shootDirection = playerCamera.transform.forward;
            
            // สร้างกระสุนที่ตำแหน่งกล้อง
            GameObject bullet = Instantiate(
                bulletPrefab, 
                playerCamera.transform.position, 
                Quaternion.LookRotation(shootDirection)
            );
            
            // ให้ความเร็วในทิศที่กล้องมอง
            Rigidbody rb = bullet.GetComponent<Rigidbody>();
            rb.velocity = shootDirection * bulletSpeed;
        }
    }
}

สรุป: ทำความเข้าใจให้ลึก แก้บั๊กได้เร็ว

การเข้าใจ game math 3D coordinate system ไม่ใช่แค่ทฤษฎีในตำรา แต่คือเครื่องมือที่ช่วยให้คุณ:

  • แก้บั๊กการเคลื่อนที่ได้เร็วขึ้น
  • เขียนระบบกล้องและการควบคุมได้สะอาดขึ้น
  • ย้ายโค้ดระหว่าง engine ได้โดยไม่สับสน
  • สื่อสารกับทีมได้ตรงประเด็น

จำไว้: ทุกอย่างในโลก 3D คือตัวเลขและการคูณเมทริกซ์ เมื่อคุณเข้าใจระบบพิกัดและ transformation คุณจะมองเห็นโครงสร้างที่ซ่อนอยู่ใต้ภาพกราฟิกสวยๆ


พร้อมลงมือทำหรือยัง?

ลองเปิดโปรเจกต์เกมของคุณแล้วตรวจสอบดูว่า:

  1. คุณใช้ระบบพิกัดแบบไหน (left-handed หรือ right-handed)
  2. มีจุดไหนที่ใช้ Euler angles แล้วควรเปลี่ยนเป็น quaternion
  3. ลำดับการคูณเมทริกซ์ถูกต้องหรือไม่

ถ้าคุณเพิ่งเริ่มต้น ลองสร้างวัตถุ 3D ตัวเล็กๆ แล้วเขียนโค้ดควบคุมการเคลื่อนที่ด้วยตัวเอง อย่าพึ่งพึ่ง engine ทุกอย่าง การเขียนเมทริกซ์ด้วยมือสักครั้งจะทำให้คุณเข้าใจระบบลึกซึ้งขึ้น

คุณเคยเจอปัญหา gimbal lock ในเกมของคุณไหม? แชร์ประสบการณ์มาคุยกันได้ในคอมเมนต์ หรือถ้ามีคำถามเรื่องระบบพิกัดและ transformation ฝากไว้ได้เลย ผมจะมาตอบให้ครับ

เนื้อหาที่จัดทำโดยมี AI ช่วยจะมีป้ายกำกับ "เรียบเรียงโดยมี AI ช่วย" เพื่อให้คุณทราบอย่างชัดเจน เราถือว่าความโปร่งใสเรื่องการใช้ AI เป็นสิ่งสำคัญต่อความไว้วางใจของผู้อ่าน

พบข้อมูลที่ไม่ถูกต้องหรือคลาดเคลื่อน?เข้าสู่ระบบเพื่อทักท้วง
บทความนี้เป็นอย่างไร?

ยังไม่มีความคิดเห็น — มาเป็นคนแรกกันเถอะ!