iT邦幫忙

2025 iThome 鐵人賽

DAY 6
0
Software Development

渲染與GPU編程系列 第 6

Day 5|PBR(Physically Based Rendering)原理與實作

  • 分享至 

  • xImage
  •  

先建立最大的直覺:PBR 的目標不是「看起來炫」,而是「像真實世界那樣反光」
它用幾個簡單好懂的旋鈕(Base Color、Metallic、Roughness…)去描述材料,背後再用一套物理合理的「光怎麼被表面反射/吸收」的規則來計算顏色。


1. 為什麼要 PBR?(真實世界的兩種光)

拿手電筒照桌面觀察:你會看到兩種光的混合:

  • 鏡面反射(Specular):像高光,一個很亮的小斑點,會隨觀察角度移動。
  • 漫反射(Diffuse):像粉筆牆的整片顏色,光進去被吸收、散得很均勻。

reflection

PBR 的核心就是把這兩種光「合理地加起來」,並且遵守幾條物理常識(能量守恆、角度越斜反射越亮的菲涅耳效應、表面粗糙度決定高光形狀…)。

示意圖:兩個分量相加 = 你看到的材質

入射光 → [鏡面分量] + [漫反射分量] → 最終顏色

2. PBR 材質有哪幾個旋鈕?(先記住,等下每個解釋)

最常見的「金屬度流程(Metallic-Roughness Workflow)」會用到:

  • Base Color / Albedo(基色):物體本身的顏色,不含打光產生的高光。
  • Metallic(金屬度,0~1):非金屬 ≈ 0、金屬 ≈ 1,中間值多用於過渡或特殊材質。
  • Roughness(粗糙度,0~1):越粗糙越霧、越平滑越亮點細小而銳利。
  • Normal Map(法線貼圖):讓表面看起來有小顆粒/刻紋。
  • AO(Ambient Occlusion,環境遮蔽):凹縫處比較暗的程度。
  • Emissive(自發光):會自己發亮的部分(霓虹燈、LED)。

色彩空間小抄(非常重要!)

貼圖 讀取空間
Base Color/Albedo、Emissive sRGB(要自動轉線性)
Normal、Metallic、Roughness、AO 線性(不要做 sRGB 校正)

3. 先不講公式:表面為什麼有「高光形狀」?

想像表面由**超多微小鏡子(microfacets)**組成:

  • 光滑(Roughness 小):微鏡子大多朝同一方向 → 高光集中、亮而銳利。
  • 粗糙(Roughness 大):微鏡子亂七八糟 → 高光散開、整片霧霧的。

還有一個必然現象叫菲涅耳(Fresnel)
越斜角看表面,越容易反光,金屬邊緣特別亮、非金屬邊緣也會微亮。
ref2

直覺圖

Roughness 小 → ● 小而亮的高光
Roughness 大 → ◯ 大而霧的高光
視角越斜 → 邊緣越亮(Fresnel)

4. 金屬 vs 非金屬(為什麼要有 Metallic?)

  • 非金屬(Dielectric):像塑膠、木頭、皮膚。

    • 漫反射占比高(顏色多來自 Base Color)。
    • 鏡面反射的基底亮度(F0)固定在約 0.04(灰白色小高光)。
  • 金屬(Metal):像金、鋼、銅、鋁。

    • 幾乎沒有漫反射(光大多在表面反射)。
    • 鏡面反射的顏色來自 Base Color(金是金色高光、銅是銅色高光)。

一張圖理解

Metallic=0(木):顏色主要來自漫反射;高光小、灰白
Metallic=1(金):顏色主要來自鏡面;高光帶材質色

5. PBR 的物理規則(用白話說)

  • 能量守恆:反射 + 吸收 ≤ 進來的光。不能憑空變更亮。
  • Fresnel(菲涅耳):角度越斜越反光。
  • Microfacet(微表面):高光形狀由粗糙度決定。
  • 幾何遮蔽(Geometry):凹凸讓部分微鏡子被遮住,看起來會暗一點。
  • 漫反射(Diffuse):非金屬把能量傳到內部,再散出來;常用 Lambert 作為基礎。

如果你想看數學,一句話就是:

BRDF = Diffuse + Specular
≈ (kD * Albedo/π) + (D * G * F) / (4 (N·L)(N·V))

你不用背,記住它只是把「高光形狀 D」、「幾何遮蔽 G」、「角度變亮 F」乘起來,再跟漫反射相加。


6. 光從哪裡來?(直接光 + 環境光)

  • 直接光(Direct Light):場景中的點光、方向光、聚光燈。
  • 環境光(Image-Based Lighting, IBL):來自四面八方的環境反射(天空、房間牆面)。
    IBL
    • 以兩個貼圖近似:Irradiance Map(給漫反射)+ Prefiltered Env Map(給不同粗糙度的鏡面)。
    • 再配一張 BRDF LUT,就能快速估計鏡面環境反射。

初學時你可以先只做「一盞燈」,等理解後再加上 IBL(畫面會瞬間真實很多!)。


7. 從 0 到 1:做一個最小 PBR 片段 Shader(單光源版)

重點是流程與組合;數學細節先不用鑽牛角尖。以下是 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),質感會大躍進。


8. Normal Map 到底怎麼用?(避免常見踩雷)

  • Normal Map 是**在貼圖空間(切線空間, Tangent Space)**描述法線的貼圖。
  • 要把它轉回世界空間視角空間才能正確跟光線計算,需建立 TBN 矩陣(Tangent、Bitangent、Normal)。
  • 記得:Normal Map 讀出來多是 [0,1] 範圍,要先變回 [-1,1],再乘以 TBN、最後重新正規化

小片段(概念)

vec3 n_tangent = texture(uNormalMap, vUV).xyz * 2.0 - 1.0;
vec3 N = normalize(TBN * n_tangent);

9. IBL 的三件寶(等你準備好就上)

  1. Irradiance Map:把整個環境的低頻光線存成一張球狀貼圖,用於漫反射
  2. Prefiltered Env Map(PMREM):把同一張環境圖按不同粗糙度做模糊,形成 MIP 鏈,用於鏡面
  3. BRDF LUT:一張 2D 貼圖,輸入(N·V、Roughness)回傳鏡面能量修正係數,搭配第 2 項做「Split Sum」近似。

效果:材質會反映出「天空是藍的、牆是白的、地是木頭色」,環境色真的貼到物體上


10. 常見問題與 Debug 方法

  • 顏色灰、暗:BaseColor 沒做 sRGB→線性;或最後忘記做 Gamma(線性→sRGB)。
  • 高光奇怪、邊緣亮爆:Roughness 太低、F0 設錯;或法線沒正規化。
  • 金屬看起來像塑膠:Metallic 貼圖錯誤(或採樣在 sRGB 下);記得金屬幾乎不應有漫反射。
  • 表面顆粒閃爍:缺 Mipmap/anisotropy;或粗糙度貼圖過於尖銳,試模糊。
  • 陰影硬、黑:先把陰影關掉對照;確認 N·L/N·V 都有 clamp 到 ≥0。

超好用小技巧

  • Furnace Test(烤箱測試):把場景放在純均勻的環境光,材質顏色應該不會溢出或變亮得不合理,用來檢查能量守恆。
  • 灰球/金球:用灰球看漫反射,金球看鏡面,最容易觀察參數是否合理。
  • 分色檢查:分別顯示 kD*diffusespecular,看誰怪。

11. 美術合作小抄(講人話)

  • Base Color:不要把打光的高光烘進去(那是動態光該做的)。
  • Metallic/Roughness:盡量乾淨;金屬區域 Metallic 靠近 1、非金屬靠近 0。
  • Normal:解析度足夠、方向一致;與模型切線對齊(切線不穩就會出怪光)。
  • AO:適度使用,不要一股腦全黑;只加在漫反射分量上比較合理。
  • Emissive:別太亮,之後配合曝光與色調映射調整。

12. 你可以在 10 分鐘內做的三個小實驗

  1. 只改 Roughness:0.05 / 0.5 / 0.9,體會高光形狀變化。
  2. 切換 Metallic:0 ↔ 1,用同一張 Base Color,感受非金屬與金屬的反差。
  3. 加上簡易環境色ambient = 0.1 * albedo ↔ IBL(若你有現成環境貼圖),對比真實感差距。

13. 一句話總結

PBR = 「漫反射 + 鏡面」在物理規則下的合理加總
用 BaseColor / Metallic / Roughness / Normal / AO 這幾個旋鈕,就能描述大多數常見材質。
先把單光源 PBR 跑起來,再加 IBL 與正確的色調映射,你的畫面就會從「像是 3D 模型」進化成「像是現實世界」。

下一次我會帶大家在Unity裡面實作出來!


上一篇
Day 4|GPU 記憶體架構與資料流:如何有效管理資源
下一篇
Day 6|Shader 入門:Unity Shader 簡介
系列文
渲染與GPU編程7
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言