Learn the math and implementation behind the Phong lighting model, world normal matrices transformation, and two-pass shadow mapping technique using depth textures in WebGPU.

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.

Một cảnh 3D không thể chân thực nếu thiếu đi ánh sáng và bóng đổ. Trong bài học này, chúng ta sẽ cùng nghiên cứu công thức toán học của Mô hình chiếu sáng Phong (Phong Reflection Model) và kỹ thuật kết xuất đổ bóng động thời gian thực bằng thuật toán Shadow Mapping hai lượt (Two-Pass) thông qua WebGPU.

1. Mô hình phản chiếu ánh sáng Phong (Phong Reflection Model)

Mô hình Phong chia ánh sáng phản chiếu trên bề mặt vật thể thành 3 thành phần độc lập:

\[I = I_{\text{ambient}} + I_{\text{diffuse}} + I_{\text{specular}}\]
  • Ambient (Ánh sáng môi trường): Ánh sáng nền tán xạ mọi nơi trong không gian, có giá trị không đổi: \(I_{\text{ambient}} = K_a \cdot L_a\).
  • Diffuse (Ánh sáng tán xạ): Ánh sáng phản chiếu đều theo mọi hướng từ bề mặt nhám, tuân theo Định luật Cosine của Lambert: \(I_{\text{diffuse}} = K_d \cdot L_d \cdot \max(\vec{N} \cdot \vec{L}, 0)\), với \(\vec{N}\) là vector pháp tuyến bề mặt và \(\vec{L}\) là vector hướng sáng.
  • Specular (Ánh sáng phản chiếu gương): Tạo ra đốm sáng bóng loáng trên bề mặt kim loại/nhẵn, phụ thuộc góc nhìn của camera \(\vec{V}\) và vector phản xạ ánh sáng \(\vec{R}\): \(I_{\text{specular}} = K_s \cdot L_s \cdot \max(\vec{R} \cdot \vec{V}, 0)^p\), với \(p\) là hệ số bóng loáng (shininess).

2. Biến đổi pháp tuyến và Ma trận Pháp tuyến (Normal Matrix)

Để tính toán hướng phản xạ chính xác, vector pháp tuyến của mô hình phải được chuyển từ Không gian Đối tượng (Object Space) sang Không gian Thế giới (World Space).

Tuy nhiên, nếu ta sử dụng ma trận biến đổi mô hình (Model Matrix) chứa phép tỷ lệ không đồng đều (non-uniform scale), vector pháp tuyến sẽ không còn vuông góc với bề mặt. Để khắc phục điều này, ma trận pháp tuyến thế giới được định nghĩa là Ma trận nghịch đảo chuyển vị (Transpose of the Inverse) của ma trận mô hình:

\[N_{\text{world}} = (M_{\text{model}}^{-1})^T\]

3. Thuật toán Shadow Mapping hai lượt (Two-Pass)

Kỹ thuật đổ bóng Shadow Map hoạt động theo hai lượt kết xuất riêng biệt trên GPU:

  1. Lượt 1 (Shadow Pass): Đóng vai trò camera đặt tại vị trí của nguồn sáng, kết xuất cảnh vật vào một bộ đệm chiều sâu Depth Texture (gọi là Shadow Map). Lượt này chỉ ghi nhận khoảng cách gần nhất từ nguồn sáng đến vật thể che chắn đầu tiên.
  2. Lượt 2 (Forward Render Pass): Vẽ cảnh vật bình thường từ góc nhìn của camera chính. Với mỗi mảnh điểm ảnh (fragment), ta chiếu tọa độ 3D của nó sang không gian ánh sáng của nguồn sáng để tìm tọa độ UV tương ứng trên Shadow Map. So sánh độ sâu thực tế của điểm ảnh với giá trị lấy ra từ Shadow Map: nếu điểm ảnh xa hơn, nó nằm trong bóng tối.

Sân chơi tương tác: Phong Lighting & Shadow Map

Dưới đây là một mô phỏng tương tác kết xuất một hình cầu và một khối hộp đặt trên một tấm sàn phẳng. Nguồn sáng (quả cầu nhỏ màu vàng) tự động xoay tròn trên quỹ đạo phía trên. Hãy tương tác thay đổi các thông số phản xạ Phong và bật/tắt bóng đổ:

🎨 Phong Lighting & Shadow Maps Demo

Cấu hình ánh sáng & Bóng

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

phong-shadow.wgsl
struct Uniforms {
  mvp: mat4x4<f32>,
  model: mat4x4<f32>,
  lightMvp: mat4x4<f32>,
};
struct LightingData {
  lightPos: vec4<f32>,
  cameraPos: vec4<f32>,
  ambient: f32,
  diffuse: f32,
  specular: f32,
  shininess: f32,
};

@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<uniform> uLight: LightingData;
@group(0) @binding(2) var shadowMap: texture_depth_2d;
@group(0) @binding(3) var shadowSampler: sampler_comparison;

@fragment
fn fs(in: VSOut) -> @location(0) vec4<f32> {
  let N = normalize(in.normal);
  let L = normalize(uLight.lightPos.xyz - in.worldPos);
  let V = normalize(uLight.cameraPos.xyz - in.worldPos);
  let H = normalize(L + V);

  let ambient = uLight.ambient * in.color;
  let diffuse = uLight.diffuse * max(dot(N, L), 0.0) * in.color;
  let specular = uLight.specular * pow(max(dot(N, H), 0.0), uLight.shininess) * vec3<f32>(1.0, 1.0, 0.9);

  // So sánh chiều sâu shadow map
  let lightProjCoords = in.lightSpacePos.xyz / in.lightSpacePos.w;
  let uv = vec2<f32>(lightProjCoords.x * 0.5 + 0.5, -lightProjCoords.y * 0.5 + 0.5);
  let depth = lightProjCoords.z;

  // Gọi textureSampleCompare vô điều kiện bên ngoài khối rẽ nhánh (uniform control flow)
  let shadowMapValue = textureSampleCompare(shadowMap, shadowSampler, uv, depth - 0.003);

  var shadow: f32 = 1.0;
  if (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0 && depth <= 1.0) {
    shadow = shadowMapValue;
  }

  return vec4<f32>(ambient + shadow * (diffuse + specular), 1.0);
}
shadow-setup.js
// 1. Khởi tạo kết cấu Shadow Map Depth Texture
const shadowSize = 1024;
const shadowTexture = device.createTexture({
  size: [shadowSize, shadowSize, 1],
  format: 'depth24plus',
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
});

// 2. Tạo bộ mẫu so sánh (Comparison Sampler)
const shadowSampler = device.createSampler({
  compare: 'less-equal',
  minFilter: 'nearest',
  magFilter: 'nearest',
});

// 3. Khởi tạo BindGroupLayout có shadow map & comparison sampler
const bgl = device.createBindGroupLayout({
  entries: [
    { binding: 0, visibility: GPUShaderStage.VERTEX, buffer: { type: 'uniform' } },
    { binding: 1, visibility: GPUShaderStage.FRAGMENT, buffer: { type: 'uniform' } },
    { binding: 2, visibility: GPUShaderStage.FRAGMENT, texture: { sampleType: 'depth' } },
    { binding: 3, visibility: GPUShaderStage.FRAGMENT, sampler: { type: 'comparison' } },
  ],
});

Trắc nghiệm ôn tập

Câu 1: Tại sao ta cần sử dụng Ma trận Pháp tuyến (Normal Matrix = Transpose of Inverse of Model) thay vì dùng trực tiếp Model Matrix để biến đổi vector pháp tuyến?

Trắc nghiệm ôn tập

Câu 2: Trong kỹ thuật Shadow Mapping hai lượt (Two-Pass), lượt kết xuất thứ nhất (Shadow Pass) ghi thông tin gì vào kết cấu texture?

🔬 Tài liệu tham khảo học thuật (Academic References)
Tìm hiểu thêm toán học ánh sáng và các kỹ thuật bóng đổ nâng cao:

Related Articles

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

Lesson 4: Pipeline State & Depth Testing — 3D Rubik's Cube Bài 4: Pipeline State & Depth Testing — Khối Rubik 3D Lesson 6: Compute Shaders & GPU Parallel Threading Bài 6: Compute Shader & Lập Trình Song Song GPU Back to WebGPU Course Overview Quay lại Lộ trình Series WebGPU