This programming guide is only available in Vietnamese. Switch the language toggle to Vietnamese to read the full article.

Khi xây dựng các mô hình 3D phức tạp chứa hàng chục ngàn đỉnh (vertices), hiệu năng truyền tải dữ liệu giữa CPU và GPU trở thành nút thắt cổ chai lớn nhất. Bài học này sẽ giúp bạn hiểu sâu về kỹ thuật vẽ chỉ mục (Indexed Drawing), cấu trúc sắp đặt bộ đệm đỉnh tối ưu và cách đóng gói trạng thái thông qua Vertex Array Objects (VAO) của WebGL 2.

1. Vẽ tuần tự vs Vẽ chỉ mục (drawArrays vs drawElements)

Thông thường để vẽ một mô hình 3D, chúng ta có 2 phương pháp:

  • Vẽ tuần tự (gl.drawArrays): GPU đọc lần lượt từng đỉnh từ VBO và vẽ chúng. Đối với một khối lập phương gồm 6 mặt (12 tam giác), chúng ta cần vẽ 36 đỉnh. Tuy nhiên, khối lập phương thực chất chỉ có 8 đỉnh vật lý duy nhất. Điều này có nghĩa là chúng ta đang gửi lặp đi lặp lại rất nhiều dữ liệu đỉnh trùng nhau lên GPU, gây lãng phí bộ nhớ băng thông cực lớn.
  • Vẽ chỉ mục (gl.drawElements): Chúng ta nạp 8 đỉnh vật lý duy nhất vào bộ đệm đỉnh (Vertex Buffer) và tạo một bộ đệm phụ gọi là Index Buffer (Element Array Buffer) chứa các chỉ mục trỏ đến các đỉnh đó. Ví dụ: hình tam giác thứ nhất cấu thành từ đỉnh chỉ mục `[0, 1, 2]`. Nhờ đó, dữ liệu đỉnh nặng (gồm tọa độ, màu sắc, UV, pháp tuyến) chỉ cần nạp 1 lần duy nhất lên GPU.

Trước hết, ta tạo một Vertex Buffer Object (VBO) trong ARRAY_BUFFER chứa dữ liệu đỉnh xếp xen kẽ (interleaved) — mỗi đỉnh gồm tọa độ và màu nằm liền nhau:

create-vbo.js
// Dữ liệu interleaved: mỗi đỉnh = [x, y, z, r, g, b]
const vertices = new Float32Array([
  -0.5, -0.5, 0.0,  1.0, 0.0, 0.0, // đỉnh 0: đỏ
   0.5, -0.5, 0.0,  0.0, 1.0, 0.0, // đỉnh 1: xanh lá
   0.5,  0.5, 0.0,  0.0, 0.0, 1.0, // đỉnh 2: xanh dương
  -0.5,  0.5, 0.0,  1.0, 1.0, 0.0, // đỉnh 3: vàng
]);

// 1. Tạo một buffer rỗng trên GPU
const vbo = gl.createBuffer();

// 2. Bind nó vào điểm gắn ARRAY_BUFFER
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);

// 3. Nạp dữ liệu lên GPU; STATIC_DRAW = ít thay đổi
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

Tiếp theo, thay vì lặp lại các đỉnh trùng nhau, ta dùng một Index Buffer trong ELEMENT_ARRAY_BUFFER chứa các chỉ mục Uint16Array và vẽ bằng gl.drawElements. Nhờ tái sử dụng đỉnh, 4 đỉnh vật lý đủ để dựng 2 tam giác (hình vuông):

index-buffer.js
// Chỉ mục trỏ tới các đỉnh trong VBO.
// Hai tam giác dùng chung đỉnh 0 và 2 -> tái sử dụng đỉnh.
const indices = new Uint16Array([
  0, 1, 2, // tam giác 1
  0, 2, 3, // tam giác 2
]);

// Tạo và nạp Index Buffer (IBO)
const ibo = gl.createBuffer();
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);
gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, indices, gl.STATIC_DRAW);

// Vẽ theo chỉ mục: GPU đọc 6 chỉ số trỏ về 4 đỉnh.
// So với gl.drawArrays(gl.TRIANGLES, 0, 6) phải nạp 6 đỉnh
// (lặp đỉnh 0 và 2), drawElements chỉ cần 4 đỉnh trong VBO.
gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0);

2. Tổ chức cấu trúc VBO: Interleaved vs Separate Buffers

Khi đỉnh có nhiều thuộc tính (Position, Normal, TexCoord), chúng ta có hai cách tổ chức bộ đệm VBO:

  1. Separate Buffers (Bộ đệm tách biệt): Tạo 3 VBO độc lập: 1 VBO cho tọa độ đỉnh, 1 VBO cho pháp tuyến, và 1 VBO cho tọa độ texture. Khi vẽ, chúng ta phải bind và cấu hình từng buffer riêng biệt.
  2. Interleaved Buffer (Bộ đệm gộp xen kẽ): Tạo 1 VBO duy nhất chứa tất cả thuộc tính của đỉnh xếp xen kẽ liên tục nhau. Ví dụ: `[x,y,z, nx,ny,nz, u,v, x,y,z, nx,ny,nz, u,v...]`. Đây là phương pháp được khuyên dùng vì giúp GPU đọc bộ nhớ liên tục (Sequential memory access), tối ưu hóa bộ nhớ đệm Cache của chip đồ họa một cách tối đa.

3. Vertex Array Objects (VAO) trong WebGL 2

Trong WebGL 1, mỗi lần vẽ một vật thể, CPU phải gửi rất nhiều hàm API thiết lập trạng thái lên GPU (như `gl.bindBuffer`, `gl.enableVertexAttribArray`, `gl.vertexAttribPointer` cho từng thuộc tính). Việc này gây ra quá tải giao tiếp CPU-GPU (API Overhead).

Vertex Array Object (VAO) trong WebGL 2 giải quyết triệt để việc này. VAO giống như một "hộp ghi nhớ". Nó lưu lại tất cả các liên kết giữa bộ đệm đỉnh (VBO), bộ đệm chỉ mục (IBO) và các mô tả thuộc tính đỉnh. Khi vẽ, chúng ta chỉ cần bind duy nhất 1 đối tượng VAO bằng 1 lệnh duy nhất:

// Khởi tạo và ghi nhận cấu hình vào VAO
const vao = gl.createVertexArray();
gl.bindVertexArray(vao);

// (Thực hiện bind các VBO, IBO và gọi gl.vertexAttribPointer...)

gl.bindVertexArray(null); // Tắt ghi nhận

// Khi vẽ, chỉ cần gọi:
gl.bindVertexArray(vao);
gl.drawElements(gl.TRIANGLES, count, gl.UNSIGNED_SHORT, 0);

Ví dụ đầy đủ dưới đây minh họa cách một VAO "ghi nhớ" toàn bộ trạng thái đỉnh trong giai đoạn thiết lập, để vòng lặp vẽ trở nên cực kỳ gọn gàng:

setup-vao.js
// === GIAI ĐOẠN THIẾT LẬP (chỉ chạy 1 lần) ===
const vao = gl.createVertexArray();
gl.bindVertexArray(vao); // bắt đầu "ghi" trạng thái

// Bind VBO + cấu hình attribute -> VAO ghi nhớ liên kết này
gl.bindBuffer(gl.ARRAY_BUFFER, vbo);
gl.enableVertexAttribArray(posLoc);
// stride = 24 bytes (6 float), offset 0 cho vị trí
gl.vertexAttribPointer(posLoc, 3, gl.FLOAT, false, 24, 0);
gl.enableVertexAttribArray(colorLoc);
// offset 12 bytes cho màu (sau 3 float vị trí)
gl.vertexAttribPointer(colorLoc, 3, gl.FLOAT, false, 24, 12);

// VAO cũng ghi nhớ cả Index Buffer đang được bind
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, ibo);

gl.bindVertexArray(null); // ngừng ghi

// === VÒNG LẶP VẼ (mỗi frame) ===
gl.bindVertexArray(vao); // khôi phục toàn bộ trạng thái bằng 1 lệnh
gl.drawElements(gl.TRIANGLES, 36, gl.UNSIGNED_SHORT, 0);
🎮 Demo tương tác: Indexed Drawing & VAO Performance Visualizer
Số đỉnh nạp trên GPU: 8
Bộ nhớ đỉnh chiếm dụng: 192 bytes

4. Câu hỏi trắc nghiệm ôn tập

Trắc nghiệm 1: Phép toán vẽ bằng Index Buffer

Khi vẽ một mô hình 3D phức tạp bằng Index Buffer qua hàm gl.drawElements, bộ nhớ GPU được tối ưu hóa như thế nào?

Trắc nghiệm 2: Vertex Array Objects (VAO)

Mục đích chính của việc giới thiệu Vertex Array Objects (VAO) trong WebGL 2/OpenGL là gì?

Trắc nghiệm 3: VAO lưu trữ những gì?

Khi gọi gl.bindVertexArray(vao) lúc vẽ, đối tượng VAO khôi phục lại trạng thái nào sau đây?

Mã nguồn mẫu quản lý VAO & Index Buffer bằng WebGL 2:

Tải mã nguồn VAO & Buffers

Related Articles

Bài viết liên quan trong series

Lesson 5: 3D Lighting Models - Phong & Blinn-Phong Bài 5: Mô Hình Chiếu Sáng Chuyên Sâu — Gouraud, Phong & Blinn-Phong Lesson 3: GLSL Shader Language & Data Pipeline Bài 3: Ngôn Ngữ Shaders GLSL & Giao Tiếp Dữ Liệu Back to WebGL Course Overview Quay lại Lộ trình WebGL Series