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

Trong không gian 3D, nếu không có ánh sáng, mọi vật thể sẽ trông phẳng và thiếu chiều sâu vì chúng chỉ có một màu đơn nhất. Để giả lập lại thế giới thực trong đồ họa máy tính, chúng ta áp dụng các mô hình toán học để mô phỏng sự tương tác giữa tia sáng và bề mặt vật liệu. Bài viết này sẽ đi sâu vào mô hình chiếu sáng cục bộ Phong/Blinn-Phong và cách lập trình chúng trên GPU.

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

Được đề xuất bởi nhà khoa học Bùi Tường Phong vào năm 1975, mô hình Phong chia ánh sáng tương tác trên bề mặt vật thể thành 3 thành phần chính:

I_total = I_ambient + I_diffuse + I_specular
ambient.frag
// Thành phần Ambient: ánh sáng nền tối thiểu
// Bảo đảm vùng khuất vẫn nhìn thấy được, không bị đen tuyền
uniform float u_ambientStrength;  // hệ số K_ambient (vd 0.1)
uniform vec3  u_lightColor;       // màu nguồn sáng

vec3 calcAmbient() {
    // Không phụ thuộc pháp tuyến hay góc nhìn -> hằng số
    return u_ambientStrength * u_lightColor;
}
  1. Ambient Light (Ánh sáng môi trường): Giả lập lại việc ánh sáng nảy xung quanh căn phòng. Đây là một hằng số nhỏ để ngăn các phần không được chiếu sáng trực tiếp của vật thể biến thành màu đen hoàn toàn.
    I_ambient = K_ambient * Color_light
  2. Diffuse Light (Ánh sáng khuếch tán): Đại diện cho ánh sáng phản xạ đều ra mọi hướng từ một bề mặt nhám. Nó phụ thuộc trực tiếp vào góc tới của tia sáng đối với bề mặt vật thể, được tính bằng Định luật Cosine Lambert:
    I_diffuse = K_diffuse * max(dot(N, L), 0.0) * Color_light
    Trong đó:
    • N: Vector pháp tuyến (Normal) vuông góc với bề mặt.
    • L: Vector hướng từ điểm tính toán tới nguồn sáng (Light).
    diffuse.frag
    // Thành phần Diffuse: theo định luật Cosine Lambert
    varying vec3 v_normal;   // pháp tuyến nội suy
    uniform vec3 u_lightPos; // vị trí nguồn sáng
    uniform vec3 u_lightColor;
    
    vec3 calcDiffuse(vec3 fragPos) {
        vec3 N = normalize(v_normal);
        vec3 L = normalize(u_lightPos - fragPos);
        // max(...,0.0) loại bỏ ánh sáng âm ở mặt quay lưng lại nguồn sáng
        float diff = max(dot(N, L), 0.0);
        return diff * u_lightColor;
    }
  3. Specular Light (Ánh sáng phản xạ gương): Tạo ra điểm sáng chói (highlight) trên các bề mặt bóng loáng (như kim loại, nhựa bóng). Điểm chói này phụ thuộc vào góc nhìn của camera:
    I_specular = K_specular * pow(max(dot(R, V), 0.0), Shininess) * Color_light
    Trong đó:
    • R: Vector phản xạ của tia sáng đối với pháp tuyến bề mặt (reflect(-L, N)).
    • V: Vector hướng nhìn từ điểm tính toán tới camera (View).
    • Shininess: Hệ số bóng (mũ lũy thừa). Càng lớn thì điểm bóng càng nhỏ và nét.

2. Cải tiến Blinn-Phong

Mô hình Phong yêu cầu tính toán vector phản xạ `R = reflect(-L, N)` cho từng pixel. Lệnh tính toán phản chiếu này khá đắt đỏ trên GPU cũ. Jim Blinn đã đề xuất cải tiến vào năm 1977 bằng cách giới thiệu Halfway Vector (Vector nửa chừng - H) nằm giữa nguồn sáng L và hướng nhìn V:

H = normalize(L + V)

Thay vì tính dot product giữa RV, mô hình Blinn-Phong tính:

I_specular = K_specular * pow(max(dot(N, H), 0.0), Shininess) * Color_light

Mô hình Blinn-Phong cho kết quả trực quan mượt mà hơn ở các góc nghiêng lớn và tiết kiệm hiệu năng tính toán hơn Phong truyền thống.

specular.frag
// Thành phần Specular: điểm chói trên bề mặt bóng
vec3 calcSpecular(vec3 N, vec3 L, vec3 V, float shininess) {
    float spec;

    // --- Phong gốc: phản chiếu tia sáng R rồi so với hướng nhìn V ---
    vec3 R = reflect(-L, N);
    spec = pow(max(dot(R, V), 0.0), shininess);

    // --- Blinn-Phong: dùng vector nửa chừng H = normalize(L + V) ---
    vec3 H = normalize(L + V);
    spec = pow(max(dot(N, H), 0.0), shininess);

    // So sánh: Phong cần reflect() đắt đỏ và highlight tắt sớm khi
    // dot(R,V) < 0; Blinn-Phong rẻ hơn, highlight mượt và rộng hơn
    // ở góc nghiêng lớn (thường nhân shininess ~ x4 để khớp Phong).
    return vec3(spec);
}

3. Sự khác biệt giữa Gouraud Shading và Phong Shading

Việc tính toán phương trình ánh sáng có thể được thực hiện ở 2 giai đoạn khác nhau trong Graphics Pipeline:

  • Gouraud Shading (Vertex Lighting): Tính toán toàn bộ công thức ánh sáng tại Vertex Shader cho các đỉnh đơn lẻ, sau đó nội suy cường độ sáng (màu sắc) qua bề mặt tam giác tới Fragment Shader. Thuật toán này chạy cực nhanh nhưng gây ra hiện tượng điểm chói (specular highlight) bị méo mó hoặc biến mất nếu lưới đa giác quá thưa.
  • Phong Shading (Fragment/Pixel Lighting): Chỉ nội suy vector pháp tuyến `varying vec3 v_normal` qua đa giác, sau đó chạy toàn bộ phương trình ánh sáng trên từng điểm ảnh (pixel) ở Fragment Shader. Phong Shading cho chất lượng hình ảnh mịn màng tuyệt đối nhưng tốn tài nguyên tính toán hơn.

4. Ma trận Pháp tuyến (Normal Matrix)

Khi chúng ta xoay, dịch chuyển hay phóng to/thu nhỏ mô hình bằng ma trận Model Matrix, các vector pháp tuyến đỉnh `a_normal` cũng cần được biến đổi theo. Tuy nhiên, nếu Model Matrix có chứa phép co giãn không đều (Non-uniform Scaling, ví dụ: phóng to trục X gấp 3 lần nhưng trục Y giữ nguyên), việc nhân trực tiếp với Model Matrix sẽ làm lệch vector pháp tuyến, khiến nó không còn vuông góc với bề mặt.

Để giải quyết việc này, chúng ta phải biến đổi pháp tuyến đỉnh bằng ma trận Normal Matrix, được tính bằng nghịch đảo chuyển vị (inverse transpose) của ma trận 3x3 góc trái trên của Model Matrix:

NormalMatrix = transpose(inverse(mat3(ModelMatrix)))
🎮 Demo tương tác: 3D Sphere Phong Lighting Visualizer

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

Trắc nghiệm 1: Thuật toán Blinn-Phong

Tại sao mô hình Blinn-Phong lại tối ưu hơn mô hình Phong Reflection truyền thống về mặt hiệu năng tính toán?

Trắc nghiệm 2: Ma trận Pháp tuyến Normal Matrix

Trong trường hợp nào ta bắt buộc phải sử dụng Normal Matrix để biến đổi vector pháp tuyến thay vì dùng trực tiếp Model Matrix?

Trắc nghiệm 3: Chuẩn hóa lại pháp tuyến trong Fragment Shader

Tại sao ta phải gọi lại normalize() cho vector pháp tuyến `v_normal` ngay trong Fragment Shader, dù nó đã được chuẩn hóa ở Vertex Shader?

Mã nguồn đầy đủ của bài thực hành chiếu sáng Phong:

Tải về mã nguồn Phong lighting mẫu

Related Articles

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

Lesson 6: Textures & UV Mapping Techniques Bài 6: Textures & Kỹ Thuật Bản Đồ UV Lesson 4: Buffers Management & Vertex Array Objects (VAO) Bài 4: Quản Lý Buffers & Vertex Array Objects (VAO) Back to WebGL Course Overview Quay lại Lộ trình WebGL Series