This programming guide is only available in Vietnamese. Switch the language toggle to Vietnamese to read the full article.
Trong đồ họa máy tính 3D, chúng ta không vẽ trực tiếp trên màn hình 2D raster. Thay vào đó, tất cả thực thể 3D phải được tính toán, biến đổi qua các không gian tọa độ khác nhau bằng các phép toán Vector và Ma trận (Matrix). Bài học này sẽ giúp bạn làm chủ toán học 3D từ các phép biến đổi hình học cơ bản đến ma trận phức tạp nhất trong đồ họa: Ma trận MVP.
1. Hệ tọa độ và Không gian Đồ Họa 3D
Để đưa một điểm từ file mô hình 3D lên màn hình hiển thị, WebGL phải biến đổi điểm đó qua 5 không gian tọa độ độc lập:
- Local Space (Không gian cục bộ): Hệ tọa độ riêng của mô hình 3D ban đầu (ví dụ: tâm của khối hộp nằm tại tọa độ (0,0,0)).
- World Space (Không gian thế giới): Các mô hình được sắp đặt vị trí trong một thế giới chung bằng cách nhân với ma trận Model Matrix.
- View Space (Không gian Camera): Toàn bộ thế giới được biến đổi sao cho Camera nằm tại gốc tọa độ (0,0,0) và nhìn thẳng theo hướng trục Z. Việc này được thực hiện bởi ma trận View Matrix.
- Clip Space (Không gian cắt): Đối tượng được chiếu về màn hình phẳng 2D. GPU sẽ loại bỏ (clip) tất cả các đỉnh nằm ngoài vùng nhìn của camera. Điều này được thực hiện bởi ma trận Projection Matrix.
-
Screen Space (Không gian màn hình): GPU chia tọa độ Clip Space cho thành phần
wđể có tọa độ NDC (Normalized Device Coordinates) từ-1đến1, sau đó ánh xạ về pixel thực trên màn hình (ví dụ: từ0đến1920pixel).
2. Ma trận phép biến đổi Affine (Affine Transformation)
Trong đồ họa 3D, chúng ta dùng ma trận kích thước 4x4 đại diện cho các phép biến đổi trong không gian thuần nhất (Homogeneous Coordinates) với vector dạng `(x, y, z, w)`. Thành phần `w` (thường bằng 1.0) cho phép chúng ta tích hợp phép tịnh tiến (Translation) vào ma trận nhân.
Ma trận Tịnh tiến (Translation Matrix) dịch chuyển điểm đi khoảng cách $(t_x, t_y, t_z)$:
\[T = \begin{bmatrix} 1 & 0 & 0 & t_x \\ 0 & 1 & 0 & t_y \\ 0 & 0 & 1 & t_z \\ 0 & 0 & 0 & 1 \end{bmatrix}\]Ma trận Quay quanh trục Y (Rotation Matrix Y) với góc quay $\theta$:
\[R_y = \begin{bmatrix} \cos\theta & 0 & \sin\theta & 0 \\ 0 & 1 & 0 & 0 \\ -\sin\theta & 0 & \cos\theta & 0 \\ 0 & 0 & 0 & 1 \end{bmatrix}\]Dưới đây là cách dựng ba ma trận Affine cơ bản trong JavaScript bằng Float32Array. Lưu ý WebGL lưu ma trận theo dạng cột trước (column-major): 16 phần tử được đọc theo từng cột, nên thành phần tịnh tiến (tx, ty, tz) nằm ở vị trí 12, 13, 14 của mảng.
// Tất cả ma trận 4x4 lưu theo column-major (đọc theo từng cột)
// 1. Ma trận Tịnh tiến — dịch điểm đi (tx, ty, tz)
function translation(tx, ty, tz) {
return new Float32Array([
1, 0, 0, 0, // cột 0
0, 1, 0, 0, // cột 1
0, 0, 1, 0, // cột 2
tx, ty, tz, 1 // cột 3: thành phần tịnh tiến
]);
}
// 2. Ma trận Tỉ lệ — phóng to/thu nhỏ theo từng trục
function scale(sx, sy, sz) {
return new Float32Array([
sx, 0, 0, 0,
0, sy, 0, 0,
0, 0, sz, 0,
0, 0, 0, 1
]);
}
// 3. Ma trận Quay quanh trục Z — góc rad (radian)
function rotationZ(rad) {
const c = Math.cos(rad), s = Math.sin(rad);
return new Float32Array([
c, s, 0, 0, // cột 0
-s, c, 0, 0, // cột 1
0, 0, 1, 0,
0, 0, 0, 1
]);
}
Để kết hợp nhiều phép biến đổi, ta cần nhân các ma trận với nhau. Hàm multiplyMat4(a, b) dưới đây thực hiện phép nhân hai ma trận 4x4 theo chuẩn column-major:
// Nhân hai ma trận 4x4 (column-major): out = a * b
function multiplyMat4(a, b) {
const out = new Float32Array(16);
for (let c = 0; c < 4; ++c) { // cột của kết quả
for (let r = 0; r < 4; ++r) { // hàng của kết quả
out[c * 4 + r] =
a[0 * 4 + r] * b[c * 4 + 0] +
a[1 * 4 + r] * b[c * 4 + 1] +
a[2 * 4 + r] * b[c * 4 + 2] +
a[3 * 4 + r] * b[c * 4 + 3];
}
}
return out;
}
3. Phép chiếu phối cảnh (Perspective Projection)
Khác với phép chiếu trực giao (Orthographic) giữ nguyên tỉ lệ kích thước bất kể khoảng cách, phép chiếu phối cảnh mô phỏng mắt người: đối tượng càng xa thì kích thước hiển thị càng nhỏ. Ma trận chiếu phối cảnh phụ thuộc vào góc nhìn dọc (Field of View - FOV), tỉ lệ màn hình (Aspect Ratio), và hai mặt phẳng giới hạn Near Plane (n) và Far Plane (f).
Công thức ma trận chiếu phối cảnh chuẩn của OpenGL:
\[P = \begin{bmatrix} \frac{1}{\text{aspect} \cdot \tan(\text{fov}/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan(\text{fov}/2)} & 0 & 0 \\ 0 & 0 & -\frac{f + n}{f - n} & -\frac{2fn}{f - n} \\ 0 & 0 & -1 & 0 \end{bmatrix}\]Cài đặt hàm perspective(fovy, aspect, near, far) trong JavaScript, trả về ma trận chiếu phối cảnh (column-major). Tham số fovy là góc nhìn dọc tính bằng radian:
// fovy: góc nhìn dọc (radian); aspect = width / height
function perspective(fovy, aspect, near, far) {
const f = 1.0 / Math.tan(fovy / 2); // hệ số phóng tiêu cự
const nf = 1.0 / (near - far);
return new Float32Array([
f / aspect, 0, 0, 0, // co giãn trục X theo aspect
0, f, 0, 0, // co giãn trục Y theo FOV
0, 0, (far + near) * nf, -1, // nén Z + báo GPU chia w
0, 0, 2 * far * near * nf, 0 // dịch Z, w = -z_view
]);
}
4. Tổng hợp Ma trận MVP (Model-View-Projection)
Để tối ưu hóa tính toán trên GPU, chúng ta không nhân lần lượt từng ma trận Model, View, Projection với từng đỉnh. Thay vào đó, CPU tính toán nhân ma trận trước:
M_mvp = P_projection * V_view * M_model
Chú ý thứ tự nhân ma trận từ phải qua trái. Ma trận Model được áp dụng trước, sau đó tới View, và cuối cùng là Projection. Sau đó, ma trận tổng hợp `u_MVP` này sẽ được truyền lên GPU dưới dạng một biến `uniform` duy nhất cho Vertex Shader.
Đoạn code sau lắp ráp ma trận MVP rồi truyền lên GPU. Vì WebGL dùng column-major và vector đỉnh nằm
bên phải, thứ tự nhân phải đảo ngược: projection × view × model để Model
được áp dụng lên đỉnh trước tiên:
// Thứ tự đảo ngược: Model áp dụng đầu tiên lên vector đỉnh bên phải
// gl_Position = (projection * view * model) * vec4(pos, 1.0)
const mv = multiplyMat4(view, model); // gộp View * Model
const mvp = multiplyMat4(projection, mv); // gộp Projection * (View * Model)
// Truyền lên uniform mat4 u_MVP của Vertex Shader.
// Tham số transpose = false vì mảng đã ở dạng column-major sẵn.
const mvpLoc = gl.getUniformLocation(program, 'u_MVP');
gl.uniformMatrix4fv(mvpLoc, false, mvp);
5. Câu hỏi trắc nghiệm ôn tập
Trắc nghiệm 1: Thứ tự nhân ma trận MVP
Tại sao trong Vertex Shader, phép nhân ma trận biến đổi đỉnh lại có thứ tự là Projection * View * Model mà không phải ngược lại?
Trắc nghiệm 2: Ý nghĩa của tọa độ thuần nhất w
Sau khi nhân với ma trận Projection, thành phần w trong tọa độ thuần nhất (x, y, z, w) của đỉnh đại diện cho cái gì?
Trắc nghiệm 3: Orthographic và Perspective Projection
Đâu là điểm khác biệt cốt lõi giữa phép chiếu trực giao (Orthographic) và phép chiếu phối cảnh (Perspective)?
Mã nguồn thư viện ma trận 3D tối giản viết bằng Vanilla JS:
Xem Code File mô phỏng Cube 3D & MVP Matrix
Comments
Bình luận