iT邦幫忙

2025 iThome 鐵人賽

DAY 8
0
Software Development

渲染與GPU編程系列 第 8

Day 7|光照模型比較:Phong vs Blinn-Phong vs Cook-Torrance

  • 分享至 

  • xImage
  •  

這篇要幫你把三種常見光照模型的「長相、調法、優缺點」一次看懂,並附上 Unity Built-in Render Pipeline(舊版標準管線)的範例,你可以直接貼進專案跑起來。
讀完你會知道:什麼時候用傳統模型(Phong/Blinn-Phong)來快速做出好看的高光,什麼時候改用較寫實的 PBR(Cook-Torrance)


0)先把符號認識一下(不需要會數學)

  • N:表面法線(Normal)
  • L:從表面往「光」的方向(Light direction)
  • V:從表面往「鏡頭」的方向(View direction)
  • R:把 LN 做鏡面反射得到的方向(Reflect direction)
  • HLV 的「半角向量」(Half vector),介於兩者中間
  • N·L:N 跟 L 的夾角餘弦,代表「光打上來的有效程度」
  • N·V:你看表面的角度正不正
  • specular:高光(像亮點)
  • diffuse:漫反射(整片顏色)

圖像心智

  • Phong:比對 R 與 V 的角度 → 高光出現
  • Blinn-Phong:比對 N 與 H 的角度 → 高光出現
  • Cook-Torrance:把「微小鏡面、角度變亮、彼此遮擋」都考慮到(較寫實)

1)Phong 模型(經典、直覺、好理解)

觀念

  • 漫反射:Lambert,強度 ≈ max(N·L, 0)
  • 鏡面:把光向量 LN 反射成 R,再看 RV 多接近
    spec = pow( max( dot(R, V), 0 ), shininess )
  • Shininess 越大,高光越小越尖。

特性

  • 優點:簡單、參數好調(顏色+高光大小)。
  • 缺點:在某些角度(尤其斜視)高光不穩定,跟現實差距較大;能量不是嚴格守恆。

2)Blinn-Phong(比 Phong 更穩一些的經典)

觀念

  • LV 加起來、正規化得到 H
  • 高光用 pow( max( dot(N, H), 0 ), shininess )
  • 與 Phong 相比,對角度變化更穩,也常被認為稍微高效。

特性

  • 優點:比 Phong 好調、比較不容易「亂跳」,經典實用。
  • 缺點:仍不是物理嚴謹;不同材質(金屬/非金屬)的表現差異有限。

3)Cook-Torrance(PBR 代表,較真實)

觀念(白話版)

  • 把鏡面高光拆成三個因素:

    • D(NDF,微表面分佈):表面「微小鏡子」朝向半角 H 的機率(決定高光形狀)
    • F(Fresnel):角度越斜越亮(邊緣更反光)
    • G(幾何遮蔽):凹凸讓部分微鏡子互相擋住
  • Specular ≈ (DGF) / (4 (N·L)(N·V))

  • Diffuse 走 Lambert,但要遵守能量概念:金屬幾乎沒有漫反射;非金屬漫反射 ≈ albedo/π

  • 當不計算體積光時,我們假設是無限小的入射光立體角,可看作入射方向向量 (縮成一個點)。

特性

  • 優點:視角自然、金屬/非金屬差異明顯、與 IBL(環境反射)相容性好。
  • 缺點:比傳統模型貴一些(但現代硬體很足夠);要注意色彩空間/貼圖通道分工。

多光源Attenuation

attn

// 4) 像素著色器:輸出顏色
      float4 frag (v2f i) : SV_Target
      {
        float3 albedo=tex2D(_MainTex ,i.uv).rgb ;   

        float3 worldNormal=normalize(i.worldNormal);
        float3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos)); 
        float3 viewDir=normalize(_WorldSpaceCameraPos-i.worldPos);
        float3 Lo= float3(0,0,0);
        
        for(int index = 0; index < 4; index++){
            float4 lightPosition = float4(unity_4LightPosX0[index], 
                                            unity_4LightPosY0[index], 
                                            unity_4LightPosZ0[index], 1.0);
            // Cook-Torrance BRDF
            float3 L = normalize(lightPosition.xyz - i.worldPos);
            float3 H = normalize(L + viewDir);
            float distance = length(lightPosition.xyz - i.worldPos);
            float attenuation = 1.0 /  (distance * distance); // (inverse-square law)
            float3 radiance =  attenuation; 
            
            Lo += float3(attenuation,  attenuation,attenuation)*  unity_LightColor[index].rgb ; // 測試用              
        }
        return float4(color,1);
      }      

FresnelSchlick

Fresnel-Schlick shading 是一種用於計算光在表面反射強度的近似方法,常見於現代 PBR(Physically Based Rendering)渲染模型中,尤其是在 Cook-Torrance BRDF 模型裡扮演重要角色。它主要用來模擬「視角越斜,反光越強」的現象,也就是所謂的 Fresnel 效應。

F = F0 + (1 - F0) * pow(1 - dot(V, H), 5)

fresnel

float3 F0 = float3(0.04, 0.04, 0.04);
F0 = lerp(F0,albedo,_Metallic); // 通過金屬度屬性在跟之間進行線性插值,才能得到一個不同的非金屬
float3 F = fresnelSchlick(max(dot(H,viewDir),0.0),F0);
Lo+=F;

完整的BEDF

brdf
更多資訊請參考這個網頁

// 4) 像素著色器:輸出顏色
      float4 frag (v2f i) : SV_Target
      {
        const float pi = 3.14159265;
        float3 albedo=tex2D(_MainTex ,i.uv).rgb ;   

        float3 worldNormal=normalize(i.worldNormal);
        float3 worldLightDir=normalize(UnityWorldSpaceLightDir(i.worldPos)); 
        float3 viewDir=normalize(_WorldSpaceCameraPos-i.worldPos);
        float3 Lo= float3(0,0,0);
        
        for(int index = 0; index < 4; index++){
            float4 lightPosition = float4(unity_4LightPosX0[index], 
                                            unity_4LightPosY0[index], 
                                            unity_4LightPosZ0[index], 1.0);
            // Cook-Torrance BRDF
            float3 L = normalize(lightPosition.xyz - i.worldPos);
            float3 H = normalize(L + viewDir);
            float distance = length(lightPosition.xyz - i.worldPos);
            float attenuation = 1.0 /  (distance * distance); // 光源的衰減會更符合物理上的反平方律(inverse-square law)(不是物理學正確的)            
            float3 radiance = unity_LightColor[index].rgb * attenuation;  // 變數表格: https://docs.unity3d.com/6000.2/Documentation/Manual/SL-UnityShaderVariables.html             
            
            // D (NDF(Normal Distribution Function))
            float NDF = DistributionGGX(worldNormal, H, _Roughness);
            // F
            float3 F0 = float3(0.04, 0.04, 0.04);
            F0 = lerp(F0,albedo,_Metallic); // 通過金屬度屬性在跟之間進行線性插值,才能得到一個不同的非金屬
            float3 F = fresnelSchlick(max(dot(H,viewDir),0.0),F0);

            // G
            float G = GeometrySmith(worldNormal, viewDir, L, _Roughness);
            
            // Cook-Torrance BRDF
            float3 numerator = NDF * G * F;   // G 效益不大         
            float denominator = 4.0 * max(dot(worldNormal, viewDir), 0.0) * max(dot(worldNormal, L), 0.0) + 0.001; // 防止除以零
            float3 specular = numerator / denominator;
            

            float3 kS = F;  //表示光能有多少被反射了
            float3 kD = float3(1.0,1.0,1.0) - kS; //反射後剩餘的光亮

            kD *= 1.0 - _Metallic; // 由于金属表面不折射光,没有漫反射颜色,通过归零kD来实现这个规则
            float NdotL = max(dot(worldNormal, L), 0.0);
            Lo += (kD * albedo / pi + specular) * radiance * NdotL;  // 半球積分結果            
            
        }
        //AO
        float3 ambient = float3(0.03, 0.03, 0.03) * albedo * _AO;
        float3 color = ambient + Lo;
        
        return float4(color,1);
      }

LDR+Gamma correction

  • 重要! 先計算完再gamma 校正
    • 以上我們假設所有計算都在線性空間,為了使用這個結果我們還需要在著色器的最後進行伽馬校正(Gamma Correct),在線性空間計算光照對於PBR是非常非常重要的,所有輸入參數同樣要求是線性的,不考慮這一點將會得到錯誤的光照結果。
 float gamma = 1.0/2.2;
 color = color / (color + float3(1.0,1.0,1.0)); // (HDR to LDR)
 color = pow(color, float3(gamma,gamma,gamma)); 	 // gamma校正

gamma


何時用哪一個?

  • Phong / Blinn-Phong

    • 你要:快速上手、老專案、行動裝置極限、或是「非寫實」風格(卡通渲染再加工)。
    • Blinn-Phong 通常是 Phong 的更佳預設。
  • Cook-Torrance(PBR)

    • 你要:比較真實的材質、需要跟金屬/粗糙貼圖配合、美術資產跟 UE/Unity 標準流程互通。
    • 未來要加 IBL/反射探頭、物理一致的整體曝光/色調映射,首選。

常見坑位(新手最容易踩)

  1. 色彩空間亂:Albedo/Emissive 是 sRGB,金屬/粗糙/法線是 Linear
  2. 法線沒正規化:高光髒、方向會飄。
  3. Phong/Blinn 太亮:Specular 沒夾範圍或 Shininess 太小。
  4. PBR 亮到噴Roughness=0F0 用錯、或 diffuse 沒有除以 π
  5. 透明材質:請改用對應 Queue 與 Blend,並注意排序(本文範例以不透明為主)。

一句話總結

  • Phong:最直覺,拿 R·V 做高光。
  • Blinn-Phong:更穩、更好調,拿 N·H 做高光。
  • Cook-Torrance:較真實,D×G×F 描述鏡面,配合金屬/粗糙與能量觀念。
    選模型沒有對錯,只有目標與成本:要快、要簡單就用 Blinn-Phong;要真實、要資產互通就上 Cook-Torrance。祝玩得順手!

上一篇
Day 6|Shader 入門:Unity Shader 簡介
系列文
渲染與GPU編程8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言