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:
// 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;
}
-
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
-
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_lightTrong đó:
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; } -
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_lightTrong đó:
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:
Thay vì tính dot product giữa R và V, mô hình Blinn-Phong tính:
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.
// 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:
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
Comments
Bình luận