This programming guide is only available in Vietnamese. Switch the language toggle to Vietnamese to read the full article.
Mô hình Phong/Blinn-Phong ở Bài 5 cho kết quả đẹp nhưng mang tính kinh nghiệm (empirical) —
các hệ số shininess hay specular được chỉnh bằng cảm tính chứ không dựa trên
vật lý thực. Physically-Based Rendering (PBR) thay đổi triết lý đó: nó mô phỏng cách
ánh sáng tương tác với bề mặt dựa trên các định luật bảo toàn năng lượng và lý thuyết vi mặt
(microfacet theory). Nhờ vậy, cùng một vật liệu sẽ trông nhất quán dưới mọi điều kiện ánh
sáng — đây là lý do PBR trở thành chuẩn công nghiệp trong Unreal Engine, Unity, Blender, glTF và mọi
DCC hiện đại.
1. Vì sao cần PBR? Bảo toàn năng lượng & lý thuyết vi mặt
Một mô hình chiếu sáng "đúng vật lý" phải tuân thủ hai nguyên tắc nền tảng mà Phong truyền thống vi phạm:
-
Bảo toàn năng lượng (Energy Conservation): Tổng năng lượng ánh sáng phản xạ khỏi bề
mặt không bao giờ được lớn hơn năng lượng tới. Trong Phong, khi tăng cả Diffuse lẫn Specular, vật
thể có thể "phát sáng" hơn cả nguồn sáng — điều bất khả thi trong tự nhiên. PBR ràng buộc:
kD + kS ≤ 1. - Lý thuyết vi mặt (Microfacet Theory): Mọi bề mặt ở cấp độ hiển vi đều là tập hợp vô số gương phẳng tí hon gọi là microfacets. Độ nhám (roughness) quyết định các gương này hướng loạn xạ tới mức nào. Bề mặt nhẵn (roughness thấp) có microfacets gần như cùng hướng → phản xạ gương sắc nét; bề mặt nhám (roughness cao) có microfacets hỗn loạn → highlight bị tán rộng và mờ.
Yếu tố quyết định một microfacet có phản chiếu ánh sáng từ nguồn L tới mắt
V hay không chính là vector trung tuyến (halfway vector)
H = normalize(L + V). Chỉ những microfacet có pháp tuyến trùng với H mới góp
phần vào highlight. Đây là hạt nhân toán học của toàn bộ PBR.
2. Phương trình phản xạ & BRDF Cook-Torrance
PBR giải gần đúng phương trình kết xuất (Rendering Equation) của Kajiya (1986). Với
chiếu sáng trực tiếp từ các nguồn sáng điểm, ánh sáng phản xạ ra hướng mắt Lo được tính
bằng tổng đóng góp của từng nguồn sáng:
trong đó fr = kD × (albedo / π) + kS × fCook-Torrance
Phần khuếch tán (diffuse) dùng mô hình Lambert albedo/π. Phần phản xạ gương (specular)
dùng BRDF Cook-Torrance — gồm ba hàm nhân với nhau, thường gọi tắt là
DFG:
-
D — Normal Distribution Function (Trowbridge-Reitz GGX): Ước lượng tỉ lệ
microfacets có pháp tuyến trùng
H. Quyết định hình dạng & kích thước của highlight theo roughness. - F — Fresnel (Schlick approximation): Tỉ lệ ánh sáng được phản xạ so với khúc xạ, tăng mạnh khi nhìn lướt (grazing angle). Quyết định màu phản chiếu & hiệu ứng viền sáng.
- G — Geometry Function (Smith + Schlick-GGX): Mô phỏng hiện tượng microfacets tự che bóng & che khuất lẫn nhau (self-shadowing / masking), rõ rệt ở bề mặt nhám.
Dưới đây là cài đặt đầy đủ ba hàm này trong GLSL — đây chính là trái tim của mọi engine PBR:
#define PI 3.14159265359
// D: Phân bố pháp tuyến microfacet (Trowbridge-Reitz GGX)
float DistributionGGX(vec3 N, vec3 H, float roughness) {
float a = roughness * roughness; // Disney remap: dùng roughness^2
float a2 = a * a;
float NdotH = max(dot(N, H), 0.0);
float NdotH2 = NdotH * NdotH;
float denom = (NdotH2 * (a2 - 1.0) + 1.0);
denom = PI * denom * denom;
return a2 / max(denom, 1e-4);
}
// G: Hàm hình học (Smith) — kết hợp masking & shadowing
float GeometrySchlickGGX(float NdotX, float roughness) {
float r = roughness + 1.0;
float k = (r * r) / 8.0; // hằng số k cho chiếu sáng trực tiếp
return NdotX / (NdotX * (1.0 - k) + k);
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) {
float NdotV = max(dot(N, V), 0.0);
float NdotL = max(dot(N, L), 0.0);
return GeometrySchlickGGX(NdotV, roughness) * GeometrySchlickGGX(NdotL, roughness);
}
// F: Xấp xỉ Fresnel-Schlick
vec3 fresnelSchlick(float cosTheta, vec3 F0) {
return F0 + (1.0 - F0) * pow(clamp(1.0 - cosTheta, 0.0, 1.0), 5.0);
}
3. Quy trình Metallic-Roughness & tham số F0
glTF 2.0 và hầu hết engine hiện đại dùng quy trình Metallic-Roughness vì nó trực quan với nghệ sĩ và chỉ cần ít texture. Một vật liệu được mô tả bằng 3 thông số chính:
- Albedo (Base Color): Màu cơ bản của bề mặt (đã loại bỏ thông tin ánh sáng/bóng).
-
Metallic (0 → 1): Vật liệu là kim loại (1) hay phi kim/điện môi (0). Quyết định
cách tính hệ số phản xạ cơ sở
F0. - Roughness (0 → 1): Độ nhám bề mặt — nhẵn bóng (0) đến mờ đục (1).
Điểm mấu chốt nằm ở F0 (phản xạ khi nhìn vuông góc). Điện môi (gỗ, nhựa,
da, đá) có F0 gần như không màu, xấp xỉ 0.04 (4%).
Kim loại thì ngược lại: chúng không có khuếch tán (kD = 0) và F0 chính
là màu albedo của chúng (vàng phản chiếu ánh vàng, đồng phản chiếu ánh đỏ-cam). Ta nội suy giữa hai
trạng thái bằng tham số metallic:
void main() {
vec3 N = normalize(v_normal);
vec3 V = normalize(u_viewPos - v_fragPos);
// Điện môi: F0 = 0.04. Kim loại: F0 = albedo. Nội suy theo metallic.
vec3 F0 = mix(vec3(0.04), u_albedo, u_metallic);
vec3 Lo = vec3(0.0);
// --- Vòng lặp qua từng nguồn sáng (ở đây là 1 nguồn điểm) ---
vec3 L = normalize(u_lightPos - v_fragPos);
vec3 H = normalize(V + L);
float dist = length(u_lightPos - v_fragPos);
float attenuation = 1.0 / (dist * dist); // suy giảm theo bình phương khoảng cách
vec3 radiance = u_lightColor * attenuation;
// BRDF Cook-Torrance
float NDF = DistributionGGX(N, H, u_roughness);
float G = GeometrySmith(N, V, L, u_roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 numerator = NDF * G * F;
float denominator = 4.0 * max(dot(N, V), 0.0) * max(dot(N, L), 0.0) + 1e-4;
vec3 specular = numerator / denominator;
// kS = Fresnel; kD là phần năng lượng còn lại (bảo toàn năng lượng)
vec3 kS = F;
vec3 kD = (vec3(1.0) - kS) * (1.0 - u_metallic); // kim loại không có diffuse
float NdotL = max(dot(N, L), 0.0);
Lo += (kD * u_albedo / PI + specular) * radiance * NdotL;
vec3 ambient = vec3(0.03) * u_albedo; // ánh sáng môi trường tối giản
vec3 color = ambient + Lo;
// HDR -> LDR: tone mapping Reinhard + hiệu chỉnh gamma sRGB
color = color / (color + vec3(1.0));
color = pow(color, vec3(1.0 / 2.2));
gl_FragColor = vec4(color, 1.0);
}
4. Demo tương tác: Quả cầu PBR Cook-Torrance thời gian thực
Hãy tự tay khám phá không gian vật liệu PBR. Kéo các thanh trượt Metallic và Roughness để quan sát cùng một quả cầu biến đổi từ nhựa mờ → kim loại bóng loáng. Chú ý cách highlight thu nhỏ và sắc nét lại khi giảm roughness, và cách màu phản chiếu chuyển sang ánh kim khi tăng metallic — tất cả được tính theo BRDF Cook-Torrance ngay trên GPU của bạn:
5. Tone Mapping & Gamma Correction — bước không thể thiếu
Tính toán PBR diễn ra trong không gian tuyến tính (linear HDR), nơi giá trị màu có
thể vượt xa 1.0 (ví dụ một highlight kim loại có thể đạt giá trị 15.0). Màn hình lại chỉ hiển thị được
dải [0, 1] trong không gian sRGB phi tuyến. Nếu bỏ qua bước chuyển đổi,
ảnh sẽ bị cháy sáng (clipping) và tối thui sai lệch. Vì vậy mọi pipeline PBR luôn kết thúc bằng hai
bước:
-
Tone Mapping (HDR → LDR): Nén dải động cao về
[0, 1]. Reinhardcolor / (color + 1)là cách đơn giản nhất; các engine AAA thường dùng đường cong ACES Filmic cho màu sắc điện ảnh hơn. -
Gamma Correction (Linear → sRGB): Nâng màu lên lũy thừa
1/2.2để bù lại đường cong gamma của màn hình, giúp độ sáng cảm nhận đúng như mắt người.
Bỏ qua gamma correction là lỗi kinh điển khiến vật thể PBR trông "nhựa" và tương phản sai. Bạn có thể
thử xóa dòng pow(color, vec3(1.0/2.2)) trong demo trên để thấy sự khác biệt rõ rệt.
6. Câu hỏi trắc nghiệm ôn tập
Trắc nghiệm 1: Tham số F0
Trong quy trình Metallic-Roughness, vì sao hệ số phản xạ cơ sở F0 của vật liệu kim loại
lại được gán bằng chính màu albedo của nó?
Trắc nghiệm 2: Hàm phân bố D (GGX)
Khi tăng giá trị roughness trong hàm phân bố GGX (D), điều gì xảy ra với vùng highlight
phản xạ gương trên bề mặt?
Trắc nghiệm 3: Bảo toàn năng lượng
Trong cài đặt PBR, dòng vec3 kD = (vec3(1.0) - kS) * (1.0 - u_metallic); phục vụ mục
đích chính nào?
Tải mã nguồn thực hành
File chứa shader Cook-Torrance PBR hoàn chỉnh và demo quả cầu vật liệu để bạn tự thử nghiệm:
Tải về mã nguồn PBR mẫu
Comments
Bình luận