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

Trong hai bài trước, chúng ta đã tiếp cận tổng quan WebGL API và các phép biến đổi ma trận phức tạp. Đến đây, bạn đã biết Shaders là linh hồn của đồ họa 3D hiện đại. Bài học này sẽ đi sâu khảo sát cú pháp ngôn ngữ GLSL ES (OpenGL Shading Language) và cách quản lý luồng dữ liệu truyền tải giữa CPU và GPU.

1. Hệ thống giao tiếp dữ liệu: Attribute, Uniform và Varying

GPU là bộ xử lý song song khép kín. Để vẽ một đối tượng, CPU phải cấp phát và phân loại dữ liệu truyền vào theo 3 kênh giao tiếp đặc thù của GLSL:

  • Attributes (Chỉ có trong Vertex Shader): Nhận dữ liệu thay đổi theo từng đỉnh một từ các Vertex Buffer (VBO) gửi lên (ví dụ: tọa độ đỉnh, vector pháp tuyến, tọa độ UV).
  • Uniforms (Dùng cho cả Vertex & Fragment Shader): Nhận các biến số mang tính toàn cục, giữ nguyên giá trị cho toàn bộ các đỉnh và pixel trong cùng một lệnh vẽ (draw call). Ví dụ: Ma trận biến đổi MVP, thời gian `u_time`, độ phân giải màn hình `u_resolution`, vị trí nguồn sáng.
  • Varyings (Truyền từ Vertex sang Fragment Shader): Dùng để chia sẻ dữ liệu giữa hai shader. Vertex Shader ghi dữ liệu vào biến varying, bộ Rasterizer tự động nội suy giá trị này trên toàn bề mặt đa giác (qua hệ tọa độ Barycentric), và Fragment Shader sẽ nhận giá trị đã nội suy tương ứng tại pixel đó.

2. Cú pháp GLSL ES cơ bản cần nắm vững

GLSL có cú pháp tương tự ngôn ngữ C nhưng được tích hợp sâu các kiểu dữ liệu và toán tử hình học tối ưu cho đồ họa:

  • Kiểu dữ liệu Vector: vec2, vec3, vec4 đại diện cho tập hợp 2, 3, 4 số thực. Rất tiện lợi cho việc biểu diễn tọa độ (x, y, z, w), màu sắc (r, g, b, a), hoặc tọa độ texture (s, t).
  • Swizzling (Tráo đổi thành phần): Cho phép truy cập và tráo đổi nhanh các thành phần của vector. Ví dụ:
    vec4 color = vec4(1.0, 0.5, 0.0, 1.0);
    vec3 rgb = color.rgb; // Lấy (1.0, 0.5, 0.0)
    vec2 custom = color.gr; // Tạo vector mới (0.5, 1.0)
    vec4 bgr = color.bgra; // Hoán đổi thành (0.0, 0.5, 1.0, 1.0)
  • Các hàm toán học đồ họa cốt lõi:
    • `step(edge, x)`: Trả về 0.0 nếu x < edge, ngược lại trả về 1.0. Dùng để tạo các đường phân cách sắc nét.
    • clamp(x, minVal, maxVal): Giới hạn giá trị x nằm trong khoảng [minVal, maxVal].
    • `mix(x, y, a)`: Nội suy tuyến tính giữa x và y theo tỉ lệ a (tương đương công thức `x * (1 - a) + y * a`). Dùng để pha trộn màu.
    • `smoothstep(edge0, edge1, x)`: Nội suy mịn kiểu Hermite, trả về giá trị chuyển tiếp mềm mại từ 0.0 đến 1.0.

Dưới đây là một Vertex Shader tối giản nhưng đầy đủ: nhận tọa độ đỉnh qua attribute, biến đổi bằng ma trận uniform và ghi kết quả ra gl_Position.

vertex.glsl
// Tọa độ đỉnh, mỗi đỉnh một giá trị khác nhau (từ VBO)
attribute vec4 a_position;

// Ma trận biến đổi MVP, chung cho mọi đỉnh trong draw call
uniform mat4 u_matrix;

// Truyền dữ liệu xuống Fragment Shader (sẽ được nội suy)
varying vec3 v_color;

void main() {
  // Biến đổi đỉnh từ không gian model sang clip space
  gl_Position = u_matrix * a_position;
  // Tạm gán màu theo vị trí để minh họa varying
  v_color = a_position.xyz * 0.5 + 0.5;
}

Tương ứng, Fragment Shader dưới đây nhận biến varying đã được Rasterizer nội suy và dùng hàm dựng sẵn mix() để pha trộn màu mượt mà:

fragment.glsl
precision mediump float;

// Nhận màu đã được nội suy tuyến tính từ Vertex Shader
varying vec3 v_color;

void main() {
  vec3 colorA = v_color;
  vec3 colorB = vec3(0.1, 0.2, 0.8);

  // smoothstep tạo hệ số chuyển tiếp mềm theo độ sáng
  float t = smoothstep(0.0, 1.0, v_color.r);

  // mix pha trộn tuyến tính hai màu theo tỉ lệ t
  vec3 finalColor = mix(colorA, colorB, t);

  gl_FragColor = vec4(finalColor, 1.0);
}

Phía JavaScript (CPU), ta phải lấy vị trí (location) của uniform và attribute rồi nạp dữ liệu lên GPU. Lưu ý uniform dùng uniformMatrix4fv, còn attribute cần mô tả cách đọc buffer qua vertexAttribPointer và bật bằng enableVertexAttribArray:

setup-data.js
// --- Nạp UNIFORM (ma trận MVP) ---
const matrixLoc = gl.getUniformLocation(program, 'u_matrix');
gl.useProgram(program);
// false = không transpose; mvpMatrix là Float32Array(16)
gl.uniformMatrix4fv(matrixLoc, false, mvpMatrix);

// --- Nạp ATTRIBUTE (tọa độ đỉnh từ VBO) ---
const positionLoc = gl.getAttribLocation(program, 'a_position');

// Bind buffer chứa dữ liệu đỉnh đã tạo trước đó
gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);

// Bật attribute slot này
gl.enableVertexAttribArray(positionLoc);

// Mô tả cách đọc buffer:
// 3 thành phần (x,y,z), kiểu FLOAT, không normalize,
// stride = 0 (xếp khít), offset = 0 (bắt đầu từ đầu buffer)
gl.vertexAttribPointer(positionLoc, 3, gl.FLOAT, false, 0, 0);

3. Thực hành viết Shaders với GLSL Fragment Editor

Hãy thử nghiệm trực tiếp sức mạnh của Fragment Shader bằng bộ công cụ soạn thảo dưới đây. Chương trình Vertex Shader đã được cấu hình vẽ một hình vuông bao phủ toàn bộ màn hình (Full-screen Quad). Nhiệm vụ của bạn là tùy biến nội dung Fragment Shader bằng GLSL.

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

Trắc nghiệm 1: Phân biệt Attribute vs Uniform

Phát biểu nào sau đây mô tả đúng nhất về sự khác biệt giữa biến attribute và uniform?

Trắc nghiệm 2: Swizzling trong GLSL

Cho biến `vec4 color = vec4(1.0, 0.0, 0.0, 1.0);`. Biểu thức swizzling nào dưới đây không hợp lệ?

Trắc nghiệm 3: Vai trò của Varying

Biến varying trong GLSL ES 1.0 đóng vai trò gì trong luồng dữ liệu shader?

Mã nguồn JavaScript khởi tạo môi trường Shader Editor hoàn chỉnh:

Tải mã nguồn Shader editor WebGL

Related Articles

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

Lesson 4: Buffers Management & Vertex Array Objects (VAO) Bài 4: Quản Lý Buffers & Vertex Array Objects (VAO) Lesson 2: 3D Mathematics, Transformation & MVP Matrices Bài 2: Toán Học Đồ Họa 3D — Vector, Matrix & Phép Biến Đổi Back to WebGL Course Overview Quay lại Lộ trình WebGL Series