先建立最大的直覺:PBR 的目標不是「看起來炫」,而是「像真實世界那樣反光」。
它用幾個簡單好懂的旋鈕(Base Color、Metallic、Roughness…)去描述材料,背後再用一套物理合理的「光怎麼被表面反射/吸收」的規則來計算顏色。
拿手電筒照桌面觀察:你會看到兩種光的混合:
PBR 的核心就是把這兩種光「合理地加起來」,並且遵守幾條物理常識(能量守恆、角度越斜反射越亮的菲涅耳效應、表面粗糙度決定高光形狀…)。
示意圖:兩個分量相加 = 你看到的材質
入射光 → [鏡面分量] + [漫反射分量] → 最終顏色
最常見的「金屬度流程(Metallic-Roughness Workflow)」會用到:
色彩空間小抄(非常重要!)
貼圖 | 讀取空間 |
---|---|
Base Color/Albedo、Emissive | sRGB(要自動轉線性) |
Normal、Metallic、Roughness、AO | 線性(不要做 sRGB 校正) |
想像表面由**超多微小鏡子(microfacets)**組成:
還有一個必然現象叫菲涅耳(Fresnel):
越斜角看表面,越容易反光,金屬邊緣特別亮、非金屬邊緣也會微亮。
直覺圖
Roughness 小 → ● 小而亮的高光
Roughness 大 → ◯ 大而霧的高光
視角越斜 → 邊緣越亮(Fresnel)
非金屬(Dielectric):像塑膠、木頭、皮膚。
金屬(Metal):像金、鋼、銅、鋁。
一張圖理解
Metallic=0(木):顏色主要來自漫反射;高光小、灰白
Metallic=1(金):顏色主要來自鏡面;高光帶材質色
如果你想看數學,一句話就是:
BRDF = Diffuse + Specular
≈ (kD * Albedo/π) + (D * G * F) / (4 (N·L)(N·V))
但你不用背,記住它只是把「高光形狀 D」、「幾何遮蔽 G」、「角度變亮 F」乘起來,再跟漫反射相加。
初學時你可以先只做「一盞燈」,等理解後再加上 IBL(畫面會瞬間真實很多!)。
重點是流程與組合;數學細節先不用鑽牛角尖。以下是 GLSL 風偽碼。
// ===== 輸入(從 VS 傳進來 & 資源) =====
in vec3 vPos_ws; // 世界位置
in vec3 vNormal_ws; // 世界法線(或由TBN+NormalMap重建)
in vec2 vUV; // 貼圖座標
uniform sampler2D uBaseColor; // sRGB 讀取(需轉線性)
uniform sampler2D uMetallicRough; // R通道:Metallic, G通道:Roughness (常見壓縮方式)
uniform sampler2D uNormalMap; // 線性讀取;如用則需TBN
uniform vec3 uCamPos;
uniform vec3 uLightPos;
uniform vec3 uLightColor; // 光的顏色(線性)
uniform float uLightIntensity;
// ===== 常用小函式(Schlick Fresnel / GGX / 幾何項) =====
vec3 fresnelSchlick(float cosTheta, vec3 F0){
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
float DistributionGGX(vec3 N, vec3 H, float rough){
float a = rough*rough;
float a2 = a*a;
float NdotH = max(dot(N,H), 0.0);
float NdotH2 = NdotH*NdotH;
float denom = (NdotH2*(a2-1.0)+1.0);
return a2 / (3.14159 * denom * denom);
}
float GeometrySchlickGGX(float NdotV, float rough){
float r = rough + 1.0;
float k = (r*r) / 8.0;
return NdotV / (NdotV * (1.0 - k) + k);
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float rough){
float NdotV = max(dot(N,V), 0.0);
float NdotL = max(dot(N,L), 0.0);
float ggx1 = GeometrySchlickGGX(NdotV, rough);
float ggx2 = GeometrySchlickGGX(NdotL, rough);
return ggx1 * ggx2;
}
// ===== 主流程 =====
out vec4 outColor;
void main(){
// 1) 讀材質(記得 BaseColor 要從 sRGB 轉線性)
vec3 albedo = pow(texture(uBaseColor, vUV).rgb, vec3(2.2)); // sRGB→Linear
vec2 mr = texture(uMetallicRough, vUV).rg;
float metallic = mr.r;
float roughness = clamp(mr.g, 0.04, 1.0); // 避免0帶來極端高光
// 2) 法線(若用NormalMap需TBN轉到世界空間,這裡先用已給的世界法線)
vec3 N = normalize(vNormal_ws);
vec3 V = normalize(uCamPos - vPos_ws);
vec3 L = normalize(uLightPos - vPos_ws);
vec3 H = normalize(V + L);
// 3) 光能與角度
float NdotL = max(dot(N, L), 0.0);
if (NdotL <= 0.0) { outColor = vec4(0,0,0,1); return; }
// 4) 設定F0(金屬:用albedo;非金屬:~0.04)
vec3 F0 = mix(vec3(0.04), albedo, metallic);
// 5) 三兄弟:D/G/F + 漫反射權重kD
float D = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = fresnelSchlick(max(dot(H, V), 0.0), F0);
vec3 kS = F; // 鏡面比例
vec3 kD = (1.0 - kS) * (1.0 - metallic); // 金屬幾乎沒有漫反射
// 6) 鏡面與漫反射
float denom = 4.0 * max(dot(N,V),0.0) * NdotL + 1e-4;
vec3 specular = (D * G * F) / denom;
vec3 diffuse = (albedo / 3.14159);
// 7) 單一光源的貢獻(可加距離衰減)
float distance2 = max(length(uLightPos - vPos_ws), 0.001);
float attenuation = uLightIntensity / (distance2 * distance2); // 反平方衰減(需依場景調整)
vec3 radiance = uLightColor * attenuation;
vec3 Lo = (kD * diffuse + specular) * radiance * NdotL;
// 8) 簡易環境(之後可換 IBL)
vec3 ambient = 0.03 * albedo; // 可乘 AO
vec3 colorLinear = ambient + Lo;
// 9) 色調映射 + Gamma(這裡用最簡單版本)
vec3 mapped = colorLinear / (colorLinear + vec3(1.0)); // Reinhard
vec3 colorSRGB = pow(mapped, vec3(1.0/2.2));
outColor = vec4(colorSRGB, 1.0);
}
先能跑、能改、能看差異。等你熟悉後,再把第 8 步的「簡易環境」換成 IBL(Irradiance + Prefilter + BRDF LUT),質感會大躍進。
[0,1]
範圍,要先變回 [-1,1]
,再乘以 TBN、最後重新正規化。小片段(概念)
vec3 n_tangent = texture(uNormalMap, vUV).xyz * 2.0 - 1.0;
vec3 N = normalize(TBN * n_tangent);
效果:材質會反映出「天空是藍的、牆是白的、地是木頭色」,環境色真的貼到物體上。
超好用小技巧
kD*diffuse
與 specular
,看誰怪。ambient = 0.1 * albedo
↔ IBL(若你有現成環境貼圖),對比真實感差距。PBR = 「漫反射 + 鏡面」在物理規則下的合理加總,
用 BaseColor / Metallic / Roughness / Normal / AO 這幾個旋鈕,就能描述大多數常見材質。
先把單光源 PBR 跑起來,再加 IBL 與正確的色調映射,你的畫面就會從「像是 3D 模型」進化成「像是現實世界」。
下一次我會帶大家在Unity裡面實作出來!