指南)
這問題隔三差五就會在群里看到一次Unity PBR的渲染方程是啥而且每次都會有人順手把PBR打成“策略路由”我一開始也會懵畢竟網(wǎng)絡那邊確實也有個PBR。先把概念拉直——Unity社區(qū)里說的PBR幾乎都是基于物理渲染Physically Based Rendering跟“PBR策略路由”完全兩碼事。你既然搜的是渲染方程說明你是想從底層搞明白Unity里的材質為什么長這樣為什么金屬幾乎沒有漫反射為什么粗糙度調低后高光銳利得像刀為什么同一套材質在完全不同光照環(huán)境下的一致性遠好過BlinnPhong。這篇文章我打算從渲染方程本身拆到Unity Shader里的實際實現(xiàn)再把我在真實項目里調PBR材質踩過的坑一并整理出來。適合人群很明確已經(jīng)會用Unity基礎材質和Shader但看到Standard或Lit Shader里的GGXTerm、SmithJointGGXTerm、FresnelTerm這些名詞一頭霧水的同學或者做卡通渲染做膩了想回頭理解“物理正確”到底是怎么回事的同學。對美術同學也友好我不會把公式堆到讓你睡著每個符號都會翻譯成人話。1. 先理清一件事Unity里的PBR到底指什么1.1 PBR是策略路由還是物理渲染我?guī)缀醺铱隙ㄋ选癠nity PBR的渲染方程”的人里至少有一半最開始是被“PBR”這個詞帶偏的。PBR在計算機網(wǎng)絡領域確實是Policy-Based Routing的縮寫叫策略路由主要用來處理包轉發(fā)路徑選擇問題。你如果在搜索引擎里看到“PBR策略路由”“穩(wěn)定”“分流”這類詞基本是網(wǎng)絡方向的內容和Unity沒有任何關系。但在Unity、UE、Blender、Substance這類工具的用戶群體里PBR就一個意思基于物理的渲染。它是一套材質描述規(guī)范加一套光照模型核心讓材質在不同環(huán)境下表現(xiàn)一致。你在Asset Store里看到“PBR Master”“PBR Textures”在Shader里看到SurfaceOutputStandard全都是這個意思。所以第一步就是別再把兩個PBR混在一起搜否則你會發(fā)現(xiàn)自己在Unity社區(qū)找了一晚上“路由策略怎么配”。1.2 PBR的真實訴求能量守恒與微表面理論PBR不是一個具體Shader而是一套理論框架。傳統(tǒng)光照模型里高光強度經(jīng)常是美術或TA拍腦袋調出來的換個燈光角度材質表現(xiàn)就可能崩掉同一種材質在暗環(huán)境下像塑料在亮環(huán)境下像金屬缺乏一致性。PBR用兩個底層法則約束所有材質讓結果盡量接近物理世界。第一個法則是能量守恒出射光的總能量不能超過入射光的總能量。這句話聽起來像廢話但實際很多自定義Shader就是栽在這里——漫反射和鏡面反射各算各的加起來超過1材質看起來就“溢出”“過曝”。PBR通過BRDF的數(shù)學結構天然保證這一點。第二個法則是微表面模型宏觀上平整的物體表面放大到微米級其實是無數(shù)朝向各異的小平面。光打到表面每個微平面都在做鏡面反射但它們朝向不同有的微平面會被相鄰微平面遮擋。最終我們看到的高光模糊程度就是微平面朝向分布和遮擋關系共同作用的結果。這就是為什么PBR要用D法線分布、G幾何遮蔽、F菲涅爾這三類函數(shù)來描述鏡面反射。2. 渲染方程本體所有PBR的源頭2.1 那個讓人頭疼的積分式到底在算什么渲染方程可以追溯到Kajiya在1986年提出的形式圖形學里叫Rendering Equation。PBR只是它的一個實時渲染近似實現(xiàn)。完整形式是Lo(p, ωo) Le(p, ωo) ∫Ω f_r(p, ωi, ωo) Li(p, ωi) (n · ωi) dωi逐項翻譯Lo(p, ωo)從表面點p朝觀察方向ωo出射的光線輻射率也就是最終進眼睛的值。Le(p, ωo)p點自己發(fā)出的光比如自發(fā)光材質。實時渲染里常見PBR材質一般把這個置0。Li(p, ωi)從入射方向ωi打到p點的光。f_r(p, ωi, ωo)BRDF雙向反射分布函數(shù)描述“一束光從ωi進來有多少比例從ωo出去”。n · ωi法線和入射方向的余弦用于把光投影到表面面積上?!姚?... dωi對法線半球上所有可能入射方向做積分。我第一次看這個式子也暈后來找到一個人話解釋它就是把四面八方可能照到這個點的光全累加起來乘上材質反射比例得到你看到的結果。積分符號只是“把所有方向都算一遍并加總”的數(shù)學寫法?,F(xiàn)在實時渲染不可能真的對半球連續(xù)積分于是拆成兩部分——直接光和間接光分別處理。2.2 BRDF一體兩面漫反射加鏡面反射渲染方程里真正決定材質長相的是BRDF也就是f_r函數(shù)。實時PBR里幾乎所有的方案都把BRDF拆成兩項f_r f_d漫反射項 f_s鏡面反射項漫反射項對應光線進入材質內部經(jīng)過多次散射后重新從表面射出來的那部分。經(jīng)典Lambert模型認為這一項與觀察方向無關只和光的入射角度有關公式是f_d diffuseColor / π這里除以π不是湊數(shù)而是為了保證球面積分時能量守恒——漫反射在半球上積分后的總能量正好是入射能量的比例除π后歸一。如果你在Unity代碼里看到UNITY_INV_PI那其實就是1 / π在漫反射項里出現(xiàn)。鏡面反射項才是PBR真正精彩的部分負責高光和反射由Cook-Torrance模型承擔。實時渲染中99%的PBR鏡面BRDF都長這樣f_s D * G * F / (4 * n·l * n·v)D是法線分布函數(shù)G是幾何遮蔽函數(shù)F是菲涅爾函數(shù)。每一項代表微表面模型的不同側面下一章逐項拆。3. Cook-Torrance鏡面反射Unity PBR的真正核心3.1 D項GGX法線分布函數(shù)為什么是默認選擇D項描述微表面中法線恰好朝向半矢量h的比例。這里的h是指向入射光和視線中間方向的單位向量。D值越大說明越多微表面的朝向正好反射光線到眼睛高光就越亮越銳利。歷史上用過很多NDF最早常用Beckmann分布數(shù)學上接近高斯高光衰減比較“圓滑”。后來業(yè)界發(fā)現(xiàn)GGX分布——也就是Trowbridge-Reitz分布——對真實材質的模擬更好尤其是粗糙金屬表面因此成為主流D_GGX(n, h, α) α2 / (π * ((n·h)2 * (α2 ? 1) 1)2)α是粗糙度參數(shù)。這里有個Unity新手必踩的坑材質面板上你調的其實是Smoothness光滑度它和粗糙度是相反數(shù)關系roughness 1 - smoothness然后進入GGX公式時很多實現(xiàn)還會再平方alpha roughness * roughness所以你在Unity里把光滑度從0.9調到0.99視覺上高光變化會非常劇烈因為GGX里的α按平方關系縮小。老Unity Standard Shader里有GGXTerm函數(shù)URP里也保留類似實現(xiàn)inline half GGXTerm(half NdotH, half roughness) { half a2 roughness * roughness; half d (NdotH * a2 - NdotH) * NdotH 1.0h; return UNITY_INV_PI * a2 / (d * d 1e-7h); }GGX和Beckmann相比最大的特點是長尾高光中心衰減后還保留一層淡淡的拖尾余暉。粗糙金屬離高光中心較遠的位置仍然有反射能量視覺上更接近現(xiàn)實。后來很多延伸模型——比如GGX各向異性、帶Clear Coat的多層GGX——主干還是它。3.2 G項Smith幾何遮蔽管的其實是“遮擋”G項解決的是微表面互相遮擋的問題。一個微平面上的反射光可能在到達眼睛前被另一個微平面擋住同理入射光也可能被相鄰微平面先擋住一部分。前者叫Masking后者叫Shadowing。這在粗糙表面和掠射角觀察時會特別明顯。如果漏掉G項粗糙金屬在邊緣會亮得反??雌饋硐癜l(fā)光。Smith近似把G拆成兩個獨立函數(shù)的乘積G(l, v) G1(l) * G1(v)每個方向單獨計算被遮擋的概率。配合GGX分布時常用Smith-Schlick-GGX形式k α / 2 G1(x) (n·x) / ((n·x) * (1 - k) k)Unity里你會看到兩個相關函數(shù)SmithBeckmannTerm和SmithJointGGXTerm。前者是老實現(xiàn)后者把陰影和遮蔽聯(lián)合計算性能和匹配度更好。URP/HDRP里主推SmithJointGGXTerm。實際調Shader時如果發(fā)現(xiàn)粗糙表面高光異常刺眼可以先懷疑G項沒接對或者接成了只乘一個方向的簡化版。3.3 F項Schlick近似讓菲涅爾變得便宜菲涅爾效應在生活中隨處可見正面看水面是透明的斜著看水面變成一面鏡子。反射率會隨視線與法線的夾角變化。數(shù)學上精確的菲涅爾方程涉及折射率、吸收系數(shù)運算量不小。實時PBR普遍用Schlick近似F(v,h) F0 (1 - F0) * (1 - v·h)^5F0是垂直入射視線和表面垂直時的反射率。對非金屬電介質F0大約0.04就是這個原因導致所有塑料材質暗部都有一層微弱的灰色高光對金屬F0是彩色值直接取Albedo顏色作為反射主色所以金屬會反射出自身的顏色。Unity里的FresnelTerm就是按Schlick實現(xiàn)的inline half3 FresnelTerm(half3 F0, half cosA) { return F0 (1 - F0) * pow(1 - cosA, 5); }Schlick近似的好處是只用一個5次方就能得到視覺上很準的結果。實際問題中它的表現(xiàn)也基本夠用——除非你是寫離線渲染器或者對掠射角要求極高否則不需要上完整菲涅爾公式。調材質時建議經(jīng)常調整觀察角度因為正面的菲涅爾變化不明顯掠射角才是判斷F項是否設置正確的關鍵。3.4 分母里的4(n·l)(n·v)能量守恒里最容易被漏掉的除數(shù)Cook-Torrance鏡面BRDF里那個分母經(jīng)常被人忽略但它非常重要f_s D * G * F / (4 * n·l * n·v)這個分母的作用是把微表面局部立體角轉換到宏觀表面立體角本質是幾何與積分的對應關系。沒有這個分母BRDF不符合能量守恒高光會明顯過亮。但由于n·l和n·v可能趨近0分母會帶來數(shù)值爆炸風險所以引擎代碼里幾乎都要加一個極小保護值float specularTerm D * G * F / max(4.0h * NdotL * NdotV, 0.001);我見過不少同學手寫PBR時忘記這個除數(shù)高光亮度翻倍還找不到原因。另一個容易漏的是順序BRDF里已經(jīng)有一個n·l而直接光照的radiance里通常也會乘一個n·l代碼里經(jīng)常把這兩個乘到一起再約掉你能看到實現(xiàn)里變成了只除一個4NdotV。這是合法的代數(shù)化簡不是bug。4. 從公式到ShaderUnity里PBR參數(shù)是怎么一步步進方程4.1 URP Lit Shader里的PBS代碼長什么樣理論說了一堆也該看看Unity里實際代碼了。Built-in管線的Standard Shader在UnityStandardBRDF.cginc里實現(xiàn)BRDFURP管線則把相關代碼集中到了Packages/com.unity.render-pipelines.universal/Shaders/BRDF.hlsl。核心函數(shù)是InitializeBRDFData和DirectBRDF。InitializeBRDFData干的事是把材質面板上的Albedo、Metallic、Smoothness這些美術參數(shù)轉成BRDF真正使用的diffuse、specular、roughnessinline void InitializeBRDFData( half3 albedo, half metallic, half specular, half smoothness, half alpha, out BRDFData outBRDFData) { half oneMinusReflectivity; half3 specularTint; // 高光工作流specular是美術給的彩色高光 // 金屬工作流specular由albedo和metallic混合得到 #if defined(_SPECULAR_SETUP) specularTint specular; oneMinusReflectivity 1.0h - Max3(specularTint.r, specularTint.g, specularTint.b); #else specularTint lerp(kDielectricSpec.rgb, albedo, metallic); oneMinusReflectivity 1.0h - metallic; #endif outBRDFData.diffuse albedo * oneMinusReflectivity; outBRDFData.specular specularTint; half perceptualRoughness 1.0h - smoothness; outBRDFData.roughness perceptualRoughness * perceptualRoughness; outBRDFData.roughness2 outBRDFData.roughness * outBRDFData.roughness; ... }這里的kDielectricSpec.rgb就是那個0.04的非金屬基準反射率。金屬工作流里金屬度等于1時漫反射系數(shù)歸零specular完全取Albedo顏色金屬度等于0時diffuse取Albedospecular固定0.04灰。這樣就從代碼層面保證了金屬材質沒有漫反射這個物理事實。DirectBRDF里就是實際光照計算大致邏輯half3 DirectBDRF(BRDFData brdfData, half3 normalWS, half3 lightDirectionWS, half3 viewDirectionWS, half NdotL, half NdotV) { half NdotH saturate(dot(normalWS, normalize(lightDirectionWS viewDirectionWS))); half VdotH saturate(dot(viewDirectionWS, normalize(lightDirectionWS viewDirectionWS))); half3 radiance lightColor * lightAttenuation * NdotL; half3 brdf brdfData.diffuse; brdf brdfData.specular * DirectBRDFSpecular(brdfData, normalWS, lightDirectionWS, viewDirectionWS, NdotL, NdotV, NdotH, VdotH); return brdf * radiance; }DirectBRDFSpecular內部就是DGF除以保護后的分母D用GGXG用Smith聯(lián)合項F用Schlick。你把這套代碼和第三章方程逐項對照完全能一一對應上。4.2 金屬度、光滑度、AO分別流向哪里美術同學最關心的通常是貼圖實際影響哪部分。我用一張表概括三個核心輸入貼圖/參數(shù)進入方程的哪部分視覺影響Albedo非金屬進漫反射diffuse金屬進菲涅爾F0決定固有色和金屬反射主色Metallic控制diffuse與specular的混合權重0是電介質1是金屬不能出現(xiàn)中間值Smoothness轉換后進入GGX的α和Smith的k值控制高光銳利程度與反射模糊程度AO主要乘間接光diffuse可擴展到specular讓接觸陰影更扎實理論不參與直接光這里我特別想提醒一個Unity上游經(jīng)驗金屬度不要設置成0.5這種中間值。物理上材質要么導電金屬要么不導電電介質不存在半金屬半塑料。程序員測試時隨手把Metallic拖到0.5會得到一片既不漫反射也不鏡面反射的“死灰”區(qū)域這種現(xiàn)象本質是能量不守恒不是PBR做錯了。磨損舊金屬的正確表達應該是用多張貼圖或上下兩層材質混合而不是單個材質上畫0.5過渡。另外金屬度和AO貼圖在導入時要注意Color Space。金屬度和Smoothness這類數(shù)據(jù)是線性變化如果被Unity當成sRGB貼圖處理渲染結果會明顯偏灰或偏暗。老項目里經(jīng)常出現(xiàn)“我把材質參數(shù)都調對了但畫面就是不對勁”的情況最后發(fā)現(xiàn)是貼圖導入設置里沒關sRGB。4.3 間接光與IBL光靠公式是渲染不出PBR的渲染方程的半球積分在實時渲染里沒法逐方向算。主流方案是IBL也就是基于圖像的光照。Unity里漫反射間接光和鏡面間接光是分開算的漫反射間接光把環(huán)境立方體貼圖卷積成輻照度圖IrradianceMap運行時采樣一次再乘漫反射項。Unity里可以理解為Light Probe和環(huán)境光采樣的來源。鏡面間接光把環(huán)境立方體貼圖按不同粗糙度預過濾成不同Mip級別的PrefilteredEnvMap運行時根據(jù)材質粗糙度選擇對應的Mip層采樣再用菲涅爾項加權。于是又能解釋一個常見問題為什么場景沒有反射探針時金屬材質會發(fā)黑或者像涂了鍋底灰因為鏡面IBL沒有數(shù)據(jù)來源直接光只能提供一個太陽點光源級別的高光剩下的半球環(huán)境反射全是零。金屬材質反射率本來就高沒了環(huán)境輸入就變成黑塊。解決辦法是在場景里布置反射探針或者至少讓Skybox作為默認環(huán)境源存在。后處理管線里現(xiàn)在的SSR屏幕空間反射能在沒有探針的情況下補銳利反射HDRP做得尤其完整但本質上還是對渲染方程中間接光部分的一種近似采樣。移動端上SSR很難跑反射探針依然是最可靠的方案。5. PBR調參與性能優(yōu)化實操中的坑和思路5.1 材質偏暗、過曝、發(fā)灰的排查順序在Unity里做PBR最容易遇到的怪異現(xiàn)象就是這三個偏暗、過曝、發(fā)灰。我按自己排查經(jīng)驗給一個固定順序第一驗證光照環(huán)境是否完整。PBR對環(huán)境的依賴遠超舊光照模型。先給場景加一個反射探針把環(huán)境光強度拉到一個合理范圍再調整PBR材質本身。很多時候材質沒問題是環(huán)境沒喂夠。第二檢查貼圖導入設置。金屬度、粗糙度、AO這些線性數(shù)據(jù)千萬別按sRGB導入。Unity默認UI看著挺正常實際渲染里可能已經(jīng)偏了一個量級。你可以在材質面板上右鍵Shader著色查中間值顏色也可以直接用Frame Debugger看輸入貼圖。第三檢查高光是否過曝。BRDF的F項在掠射角會快速逼近1配合高動態(tài)燈光很容易爆。Unity里燈光強度如果繼續(xù)沿用舊式幾千Lux的做法配合PBR高光會飛得很厲害。用ACES或Filmic色調映射能救回來但最好從源頭把燈光物理單位調合理。第四檢查能量守恒是否被破壞。比如金屬度用了中間過渡或自定義Shader漏掉除以π和分母4。把這些除數(shù)補上過曝基本能緩解60%。發(fā)灰往往發(fā)生在粗糙度平均值過高、鏡面反射缺乏環(huán)境細節(jié)的情況。一把低粗糙度材質靠探針恢復反射后灰感會立刻消失。5.2 Built-in、URP、HDRP同一方程的不同落地方式很多人關心自己在Built-in里做的PBR材質遷到URP會不會變樣。先給結論底層方程完全相同仍然是GGX加Smith加Schlick的Cook-Torrance框架。差異在配套功能Built-in Standard包含完整的PBS代碼在UnityStandardBRDF.cginc間接光采樣用了球諧和反射探針功能豐富但源碼歷史包袱重。URP LitShader更結構化BRDF集中在BRDF.hlsl去掉了部分非物理擴展項參數(shù)語義更清晰。如果你原來依賴高光調得夸張的舊Shader切到URP會發(fā)現(xiàn)高光表現(xiàn)明顯收斂——這不是引擎壞了是URP默認把非物理的“魔法值”去掉了。HDRP Lit在同一個BRDF主干上擴展了各向異性、清漆、次表面散射、SSS等高級功能。底層仍然是加過料的GGX體系。我自己的經(jīng)驗是中小項目直接用URP跟著Unity 6走材質遷移時不要指望自動完美重點看金屬度、光滑度、AO這三張貼圖導入設置以及反射探針是否同步遷移。Surface Type和Render Face這些選項在URP里還會影響陰影和深度寫入需要逐材質過一遍。還要提一個熱詞里不少人問到的“Unity 2018入門與實戰(zhàn)”相關點如果你是從老版本項目升上來Unity會自動把Standard Shader材質轉成URP Lit但轉換過程中Custom Shader可能變成洋紅色材質需要手工重新指定。這個不用慌把Shader換成Lit重新調一遍參數(shù)就行。5.3 移動端上PBR的性能賬移動端跑PBR性能瓶頸往往不是BRDF那幾次乘除而是間接光采樣和后處理。BRDF本身的GGX和Smith計算在現(xiàn)代手機GPU上不算重真正貴的是反射探針立方體貼圖采樣尤其多個探針插值時。后處理鏈Bloom、SSR、TAA這些與PBR配套的效果開銷遠大于材質本身。高分辨率陰影貼圖與PBR高光的匹配粗糙度低時高光對陰影邊緣敏感陰影質量差甚至會直接破壞材質質感。優(yōu)化方向我給三條。第一移動端盡量用單反射探針或干脆用環(huán)境貼圖模擬鏡面IBL不要場景里鋪一堆探針做實時插值。第二把PBR相關的高頻噪聲貼圖壓縮能走單通道的貼圖盡量不要存RGBA四通道帶寬省下來效果更明顯。第三如果手寫PBR可以考慮把預積分的DFG項存成LUT運行時采樣省掉每像素多次pow運算。URP內部已經(jīng)有類似優(yōu)化自定義Shader時要記得接上。另外還有一個冷門但常見的問題粒子特效內存泄露和PBR無關但只要粒子系統(tǒng)上用了帶鏡面反射的材質在高反射環(huán)境下DrawCall和Overdraw會暴漲。做移動端時粒子材質盡量選Unlit必要的高光投射交給美術貼圖模擬比實時PBR劃算得多。我個人在實際項目里最后留下一個習慣無論腳本還是Shader都先把手寫的PBR核心單獨抽成一個工具材質場景里放一個標準光源和反射探針用對照法調參。等美術把一套材質調完再整批鋪到場景。這比一邊調材質一邊被場景燈光干擾要高效得多。另外如果你是從二次元Shader或NPR卡通渲染轉過來的請記住PBR默認假設的是物理光照環(huán)境——它不會像卡通渲染那樣“逆光也清晰”。你需要給它配套合理的環(huán)境光、反射源和色調映射才能得到那種真實但不發(fā)灰的畫面。這也是Unity PBR渲染方程之外真正需要時間積累的東西。