Explore primitive topologies, depth stencil configuration, the causes and solutions for z-fighting, and multisample anti-aliasing (MSAA) setup in WebGPU pipelines.

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.

Trong lập trình đồ họa 3D, việc định hình cách các mảnh đa giác được hiển thị trên màn hình 2D phụ thuộc chặt chẽ vào Pipeline State (Trạng thái Pipeline). Bài học này sẽ mổ xẻ cơ chế hoạt động của Rasterizer, giải quyết triệt để lỗi chồng lấn hình ảnh bằng Depth Testing (Kiểm thử độ sâu) và làm mượt các đường viền bằng MSAA (Khử răng cưa đa mẫu).

1. Rasterizer State & Primitive Topology

Giai đoạn Rasterizer trên GPU nhận nhiệm vụ biến các đỉnh toán học của Vertex Shader thành các mảnh điểm ảnh (Fragments). Có hai thuộc tính cốt lõi điều khiển quá trình này:

  • Primitive Topology: Xác định cách GPU liên kết các đỉnh với nhau. Các loại hình cơ bản bao gồm 'triangle-list' (mỗi 3 đỉnh tạo 1 tam giác độc lập), 'triangle-strip' (đỉnh tiếp theo ghép với 2 đỉnh trước), 'line-list', và 'point-list'.
  • Cull Mode: Cơ chế loại bỏ các mặt không hướng về phía camera để tối ưu hiệu năng. Thường dùng 'back' (loại bỏ mặt sau) kết hợp với thứ tự đỉnh ngược chiều kim đồng hồ (Counter-Clockwise) làm mặt trước.

2. Bộ đệm độ sâu (Depth Buffer / Z-Buffer) & Z-Fighting

Mặc định, GPU sẽ vẽ các tam giác theo thứ tự lệnh gọi trong mã nguồn. Nếu đối tượng ở xa được vẽ sau đối tượng ở gần, nó sẽ đè lên đối tượng gần, tạo ra lỗi hiển thị sai lệch nghiêm trọng.

Để xử lý vấn đề này, ta sử dụng Depth Buffer. Đây là một kết cấu texture phụ trợ lưu trữ giá trị độ sâu $Z$ chạy từ $0.0$ (gần camera nhất) đến $1.0$ (xa camera nhất).

\[Z_{\text{normalized}} = \frac{f + n}{f - n} + \frac{2fn}{d \cdot (n - f)}\]

Trong đó $n$ là Near plane, $f$ là Far plane, và $d$ là khoảng cách thực tế từ đỉnh đến camera. Trước khi vẽ một pixel, GPU sẽ so sánh độ sâu hiện tại của nó với giá trị đang lưu trong Z-Buffer. Nếu độ sâu mới nhỏ hơn (tức là gần camera hơn), pixel đó được vẽ và Z-Buffer được cập nhật.

⚠️ Hiện tượng Z-Fighting
Khi hai đa giác nằm quá gần nhau, do độ chính xác giới hạn của bộ nhớ đệm (ví dụ 24-bit float), GPU không thể phân biệt đa giác nào nằm trước. Kết quả là hình ảnh bị nhấp nháy, xước vân chéo liên tục. Để khắc phục, hãy giữ khoảng cách giữa các đa giác hợp lý, hoặc kéo gần tỉ lệ giữa Near plane và Far plane.

3. Khử răng cưa đa mẫu (MSAA - Multisample Anti-Aliasing)

Do màn hình cấu tạo từ mạng lưới ô vuông (pixels), các đường viền nghiêng của vật thể 3D sẽ xuất hiện răng cưa (aliasing). MSAA giải quyết vấn đề này bằng cách lấy mẫu tại nhiều điểm phụ (sub-pixels) bên trong mỗi pixel:

  1. Ta tạo một render target đa mẫu (ví dụ 4x MSAA texture).
  2. GPU vẽ hình và tính toán màu sắc khử răng cưa trên texture đa mẫu này.
  3. Tại bước kết thúc render pass, ta thực hiện thao tác Resolve để gộp mẫu màu sắc về texture đơn mẫu (Canvas hiển thị).
msaa-setup.js
// Khai báo pipeline hỗ trợ 4x MSAA
const pipeline = device.createRenderPipeline({
  layout: 'auto',
  multisample: {
    count: 4, // Bật 4 mẫu cho mỗi pixel
  },
  // ...
});

Sân chơi tương tác: Mô phỏng Rubik 3D

Bên dưới là mô phỏng khối Rubik 3D chuyển động cấu tạo từ 27 khối sub-cubes nhỏ. Hãy thử tương tác bật/tắt chức năng Depth TestingMSAA để trực quan hóa sự khác biệt vô cùng rõ rệt:

🎨 Khối Rubik 3D — Pipeline State Demo

Cấu hình Pipeline

Trình duyệt chưa hỗ trợ WebGPU. Hãy sử dụng phiên bản Chrome/Edge mới nhất.

rubik-shader.wgsl
struct Uniforms { mvp: mat4x4<f32> };
@group(0) @binding(0) var<uniform> u: Uniforms;

struct VSIn {
  @location(0) pos: vec3<f32>,
  @location(1) normal: vec3<f32>,
  @location(2) color: vec3<f32>,
};

struct VSOut {
  @builtin(position) pos: vec4<f32>,
  @location(0) color: vec3<f32>,
};

@vertex
fn vs(in: VSIn) -> VSOut {
  var o: VSOut;
  o.pos = u.mvp * vec4<f32>(in.pos, 1.0);
  
  let lightDir = normalize(vec3<f32>(0.5, 1.0, 2.0));
  let diffuse = max(dot(in.normal, lightDir), 0.0) * 0.4 + 0.6;
  o.color = in.color * diffuse;
  return o;
}

@fragment
fn fs(in: VSOut) -> @location(0) vec4<f32> {
  return vec4<f32>(in.color, 1.0);
}
rubik-pipeline.js
// 1. Khai báo Render Pipeline hỗ trợ Depth Stencil và MSAA
const pipeline = device.createRenderPipeline({
  layout: device.createPipelineLayout({ bindGroupLayouts: [bgl] }),
  vertex: {
    module,
    entryPoint: 'vs',
    buffers: [{
      arrayStride: 36,
      attributes: [
        { format: 'float32x3', offset: 0, shaderLocation: 0 },
        { format: 'float32x3', offset: 12, shaderLocation: 1 },
        { format: 'float32x3', offset: 24, shaderLocation: 2 },
      ]
    }]
  },
  fragment: { module, entryPoint: 'fs', targets: [{ format }] },
  primitive: { topology: 'triangle-list', cullMode: 'none' },
  // Cấu hình kiểm thử chiều sâu Depth Stencil
  depthStencil: useDepth ? {
    format: 'depth24plus',
    depthWriteEnabled: true,
    depthCompare: 'less',
  } : undefined,
  // Cấu hình khử răng cưa đa mẫu (MSAA 4x)
  multisample: { count: 4 }
});

Trắc nghiệm ôn tập

Câu 1: Hiện tượng Z-fighting xảy ra khi nào?

Trắc nghiệm ôn tập

Câu 2: Thao tác "Resolve" trong cấu hình Render Pipeline đa mẫu MSAA thực hiện nhiệm vụ gì?

🔬 Tài liệu tham khảo học thuật (Academic References)
Để tìm hiểu kỹ hơn về toán học Z-Buffer và các cấu hình pipeline nâng cao:

Related Articles

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

Lesson 3: Uniform & Storage Buffers — Struct Alignment Bài 3: Uniform & Storage Buffers — Quy Tắc Căn Chỉnh Dữ Liệu Lesson 5: Phong Lighting & Shadow Maps — Real-time Shadows Bài 5: Phong Lighting & Shadow Maps — Bóng Đổ Động Thời Gian Thực Back to WebGPU Course Overview Quay lại Lộ trình Series WebGPU