Understand WGSL syntax, struct alignment, Vertex-to-Fragment shader location bindings, system builtins, and how to use Uniform Buffers to animate colors dynamically over time.

This lesson is currently only available in Vietnamese. Please switch the language toggle in the menu to Vietnamese to read the full guide and take the interactive quiz.

Ở bài học trước, chúng ta đã cấu hình thành công Logical Device và vẽ một hình tam giác thô. Tuy nhiên, mã nguồn Shader WGSL vẫn còn rất sơ sài. Bài học này sẽ giúp bạn hiểu sâu về ngôn ngữ WGSL (WebGPU Shading Language) và cách truyền dữ liệu động để tạo ra các hiệu ứng biến đổi màu sắc chuyển động mượt mà (Dynamic Gradients).

1. Khái quát về cú pháp WGSL & Kiểu dữ liệu cơ bản

WGSL là ngôn ngữ lập trình shader chính thức của WebGPU. Cú pháp của WGSL có nhiều nét tương đồng với Rust và TypeScript, chú trọng tính an toàn kiểu dữ liệu chặt chẽ (strictly typed). Các kiểu dữ liệu cơ bản bao gồm:

  • Kiểu vô hướng: f32 (số thực 32-bit), i32 (số nguyên), u32 (số nguyên không dấu).
  • Vector: vec2<f32>, vec3<f32>, vec4<f32> (hoặc viết tắt là vec2f, vec3f, vec4f trong WebGPU mới).
  • Ma trận: mat2x2<f32>, mat4x4<f32> (dành cho biến đổi 3D).
types.wgsl
// Số nguyên & thực
var<private> a: i32 = 42;
var<private> b: u32 = 7u;
var<private> c: f32 = 3.14;

// Vector — nhóm 2/3/4 số thành một đơn vị
var<private> pos: vec2<f32> = vec2<f32>(0.5, -0.3);
var<private> color: vec4<f32> = vec4<f32>(1.0, 0.0, 0.0, 1.0); // RGBA đỏ

// Ma trận 2×2 đơn vị (identity)
var<private> identity: mat2x2<f32> = mat2x2<f32>(
  1.0, 0.0,
  0.0, 1.0
);
💡 Mẹo: Viết tắt kiểu vector trong WGSL mới
Từ phiên bản WGSL gần đây, bạn có thể viết vec2f thay cho vec2<f32>, vec4u thay cho vec4<u32>. Tuy nhiên dạng đầy đủ vẫn phổ biến hơn trong tài liệu và ví dụ hiện tại.

2. Luồng truyền dữ liệu: Vertex Shader sang Fragment Shader

Mọi pipeline dựng hình luôn bắt đầu ở Vertex Shader (tính tọa độ các đỉnh) và kết thúc ở Fragment Shader (tô màu cho từng điểm ảnh). Để chuyển dữ liệu từ đỉnh sang pixel, ta sử dụng cấu trúc location bindings:

bindings.wgsl
struct VertexOutput {
  @builtin(position) pos: vec4<f32>,
  @location(0) color: vec4<f32>, // Truyền sang Fragment ở location 0
  @location(1) uv: vec2<f32>,    // Truyền tọa độ UV ở location 1
};

3. Cơ chế Nội suy Đồ họa (Rasterization & Interpolation)

Khi dữ liệu đi từ Vertex Shader ra ngoài, GPU sẽ thực hiện bước Rasterization (Số hóa lưới). Trong bước này, các thuộc tính đỉnh (như màu sắc, UV) sẽ được nội suy tuyến tính (Linear Interpolation) qua bề mặt tam giác.

Ví dụ: Nếu đỉnh $A$ có màu đỏ ($1.0, 0.0, 0.0$) và đỉnh $B$ có màu xanh dương ($0.0, 0.0, 1.0$), điểm ảnh nằm chính giữa cạnh $AB$ sẽ được GPU tự động tính toán nội suy ra màu tím ($0.5, 0.0, 0.5$).

interpolation.wgsl
@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
  // Gán màu khác nhau cho 3 đỉnh tam giác
  var colors = array<vec4<f32>, 3>(
    vec4<f32>(1.0, 0.0, 0.0, 1.0), // đỉnh 0: đỏ
    vec4<f32>(0.0, 1.0, 0.0, 1.0), // đỉnh 1: xanh lá
    vec4<f32>(0.0, 0.0, 1.0, 1.0), // đỉnh 2: xanh dương
  );
  var out: VertexOutput;
  out.pos = vec4<f32>(positions[idx], 0.0, 1.0);
  out.color = colors[idx]; // GPU tự nội suy giữa các đỉnh
  return out;
}

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
  return in.color; // mỗi pixel nhận màu nội suy tuyến tính
}
ℹ️ Lưu ý: Nội suy phối cảnh (Perspective-Correct Interpolation)
Mặc định GPU thực hiện nội suy hiệu chỉnh phối cảnh — không phải chia đều khoảng cách trên màn hình mà tính theo chiều sâu thật trong không gian 3D. Nếu muốn nội suy phẳng (không hiệu chỉnh), dùng chỉ thị @interpolate(linear, center) hoặc @interpolate(flat) để tắt nội suy hoàn toàn (mọi pixel dùng giá trị của đỉnh đầu tiên).

4. Cập nhật Gradient động theo thời gian qua Uniform Buffer

Để tạo ra hoạt ảnh chuyển động, ta cần gửi một biến thời gian trôi qua (elapsed time) từ JavaScript lên Fragment Shader mỗi frame. Ta thiết lập một Uniform Buffer nhỏ 4-bytes chứa 1 số thực đơn lẻ:

renderer.js
// 1. Khởi tạo Uniform Buffer (dung lượng 4 bytes cho 1 float32)
const timeBuffer = device.createBuffer({
  size: 4,
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST
});

// 2. Ghi và gửi dữ liệu mỗi khung hình
function render(currentTime) {
  const timeSeconds = currentTime / 1000.0;
  device.queue.writeBuffer(timeBuffer, 0, new Float32Array([timeSeconds]));
  
  // Thực hiện draw calls...
  requestAnimationFrame(render);
}

Sân chơi tương tác: WGSL Shader Live Editor

Chào mừng bạn đến với trình soạn thảo Shader thời gian thực. Phía dưới là mã nguồn của Fragment Shader nhận dữ liệu tọa độ UV đã nội suy (@location(0) uv) và thời gian trôi qua (u_time.time). Hãy thay đổi công thức toán học tính màu sắc trực tiếp trong trình soạn thảo bên dưới và bấm Biên dịch để cập nhật tức thì:

🎨 WGSL Shader Live Editor

Trình soạn Fragment Shader WGSL

Shader đã biên dịch thành công.

Kết quả Render (WebGPU Preview)

Trình duyệt của bạn chưa hỗ trợ WebGPU để kích hoạt Live Shader Preview.

vertex-shader.wgsl
struct VertexOutput {
  @builtin(position) pos: vec4<f32>,
  @location(0) uv: vec2<f32>,
};

@vertex
fn vs_main(@builtin(vertex_index) idx: u32) -> VertexOutput {
  // Screen-space Quad: 2 tam giác phủ toàn màn hình
  var pos = array<vec2<f32>, 6>(
    vec2<f32>(-1.0, -1.0), vec2<f32>(1.0, -1.0), vec2<f32>(-1.0, 1.0),
    vec2<f32>(-1.0,  1.0), vec2<f32>(1.0, -1.0), vec2<f32>( 1.0, 1.0)
  );
  var uv = array<vec2<f32>, 6>(
    vec2<f32>(0.0, 0.0), vec2<f32>(1.0, 0.0), vec2<f32>(0.0, 1.0),
    vec2<f32>(0.0, 1.0), vec2<f32>(1.0, 0.0), vec2<f32>(1.0, 1.0)
  );

  var out: VertexOutput;
  out.pos = vec4<f32>(pos[idx], 0.0, 1.0);
  out.uv = uv[idx];  // tọa độ UV nội suy sang fragment
  return out;
}
shader-setup.js
// 1. Khởi tạo WebGPU
const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const ctx = canvas.getContext('webgpu');
const format = navigator.gpu.getPreferredCanvasFormat();
ctx.configure({ device, format, alphaMode: 'premultiplied' });

// 2. Uniform Buffer cho biến thời gian (4 bytes = 1 float32)
const timeBuffer = device.createBuffer({
  size: 16, // bội 16 (alignment rule!)
  usage: GPUBufferUsage.UNIFORM | GPUBufferUsage.COPY_DST,
});

// 3. Biên dịch shader từ textarea editor
const vsModule = device.createShaderModule({ code: vertexShaderSource });
const fsModule = device.createShaderModule({ code: editor.value });

// 4. Render loop: ghi thời gian vào buffer mỗi frame
function render() {
  const elapsed = (performance.now() - startTime) / 1000;
  device.queue.writeBuffer(timeBuffer, 0, new Float32Array([elapsed]));
  // ... begin render pass, draw, end ...
  requestAnimationFrame(render);
}
render();

Trắc nghiệm ôn tập

Câu 1: Ký hiệu @location(n) trong cấu trúc struct của WGSL có nhiệm vụ gì?

Trắc nghiệm ôn tập

Câu 2: Cơ chế nội suy tuyến tính (Linear Interpolation) hoạt động ở giai đoạn nào của pipeline đồ họa?

📖 Tài liệu tham khảo / References

Related Articles

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

Lesson 1: GPU Architecture & WebGPU Context Setup Bài 1: Kiến trúc GPU & WebGPU Setup — Vẽ Tam Giác Đầu Tiên Lesson 3: Uniform & Storage Buffers — Struct Alignment Bài 3: Uniform & Storage Buffers — Quy Tắc Căn Chỉnh Dữ Liệu Back to WebGPU Course Overview Quay lại Lộ trình Series WebGPU