現(xiàn))
這個(gè)問題我在無數(shù)場(chǎng)合見過了技術(shù)群、面試現(xiàn)場(chǎng)、項(xiàng)目評(píng)審甚至美術(shù)朋友拿著材質(zhì)參數(shù)來問我“為什么金屬度調(diào)0.6高光就臟”。對(duì)著“Unity PBR的渲染方程是啥”這個(gè)標(biāo)題如果只丟出一句“Cook-Torrance BRDF”那真的不夠用。因?yàn)榇鸢高B著D項(xiàng)、G項(xiàng)、F項(xiàng)連著Metallic、Smoothness在Shader里到底怎么變成光的參數(shù)連著環(huán)境光怎么不靠光追也能算鏡面反射。只有把渲染方程拆到和Unity參數(shù)一一對(duì)應(yīng)才算真正吃透。這篇東西我打算按六步來講先把渲染方程的框架立起來再把Cook-Torrance的D、G、F三個(gè)項(xiàng)逐一拆開然后到Unity源碼里找落點(diǎn)接著手寫一個(gè)簡(jiǎn)化PBR Shader做驗(yàn)證再列一批常見問題和排查手段最后從面試角度看看這個(gè)題目怎么答才不翻車。1. 從渲染方程到Unity先有一張全局圖1.1 渲染方程和反射率方程不是一回事渲染方程本身是一個(gè)積分方程描述某點(diǎn)往出射方向的總輻射亮度等于自發(fā)光加上入射半球積分Lo(p, ωo) Le(p, ωo) ∫Ω f_r(p, ωi, ωo) Li(p, ωi) (n·ωi) dωi其中Lo是出射輻射Le是自發(fā)光Li是入射輻射f_r就是BRDFn是法線ωi是入射方向積分范圍是整個(gè)法線半球Ω。這是個(gè)全局方程場(chǎng)景里任何光源、反射、折射到最后都會(huì)通過Li和f_r耦合在一起想完整解它只能走離線渲染或者路徑追蹤一輪一輪往里彈光線。實(shí)時(shí)渲染干不了這個(gè)事。Unity這類引擎做的事是把那個(gè)巨大的積分“砍”成兩部分一部分是對(duì)所有離散光源逐盞累加另一部分是用IBL采樣把環(huán)境光砸進(jìn)來。這時(shí)真正使用的其實(shí)是反射率方程它只計(jì)算直接光照下的一次彈射不再追“光源照到A、A反射到B、B反射到眼睛”這種鏈路。很多人面試時(shí)說“Unity用的是渲染方程”嚴(yán)格講不太對(duì)Unity用的是反射率方程的實(shí)時(shí)近似只不過日常語境里大家把這兩者混著叫約定俗成也就接受了。理解了這個(gè)區(qū)別再看PBR就順了PBR的核心是選擇并實(shí)現(xiàn)一個(gè)有物理依據(jù)的BRDF讓能量守恒、讓高光形態(tài)接近真實(shí)微表面反射而不是美術(shù)自己拍腦袋去調(diào)“假高光”。1.2 Unity的PBR為什么是Cook-Torrance體系實(shí)時(shí)PBR的BRDF主流方案是微表面模型的Cook-TorranceUnity、UE、Godot大體都是這個(gè)方向。它的標(biāo)準(zhǔn)形態(tài)是把反射光分開成漫反射和鏡面反射兩部分f_r k_d * f_diffuse k_s * f_specular其中鏡面反射項(xiàng)長(zhǎng)這樣f_specular D * G * F / (4 * (n·l) * (n·v))這里D是法線分布函數(shù)G是幾何遮蔽函數(shù)F是菲涅爾項(xiàng)。分子上三個(gè)函數(shù)負(fù)責(zé)“高光的形狀、遮擋和邊界”分母的4倍NoL乘NoV來自微表面投影面積的推導(dǎo)是一個(gè)幾何換算因子。Unity并不發(fā)明BRDF它只是把Cook-Torrance針對(duì)實(shí)時(shí)渲染做了一大堆性能適配再把參數(shù)改成美術(shù)熟悉的Metallic和Smoothness。因此不同引擎的PBR看起來差異不大根源就在于大家共用同一套物理模型差異只在近似程度和實(shí)現(xiàn)細(xì)節(jié)。搞清這一點(diǎn)后面所有代碼都容易理解得多。2. 拆開Cook-TorranceD、G、F三兄弟逐個(gè)過關(guān)2.1 D項(xiàng)GGX法線分布函數(shù)高光的“寬度”微表面模型假設(shè)粗糙表面是大量朝向各異的小鏡面組成的宏觀上只有一個(gè)法線n微觀上每個(gè)微表面有各自的法線m。光線從l方向進(jìn)只有那些m恰好等于半角向量h的微表面才會(huì)把光按鏡面反射規(guī)則彈向v方向。D項(xiàng)回答的問題就是在這個(gè)粗糙度下有多少比例的微表面法線正好等于h。法線分布越集中D峰值越大高光越窄越亮分布越散高光越寬越暗淡。早期常用BlinnPhong模型做這個(gè)分布但BlinnPhong高光衰減太快形狀和真實(shí)金屬對(duì)不上。業(yè)界現(xiàn)在默認(rèn)使用GGX也就是Trowbridge-Reitz分布它的公式是D_GGX(α, cosθh) α2 / (π * ((cosθh2 * (α2 ? 1) 1)2))其中α是幾何粗糙度通常由美術(shù)感知粗糙度的平方得到。這個(gè)分布最大的特點(diǎn)是有一條“長(zhǎng)尾”主高光衰減之后還能保持較長(zhǎng)的漸暈非常接近真實(shí)測(cè)量數(shù)據(jù)。在Shader里落地時(shí)大致是這個(gè)樣子half D_GGX(half NoH, half alpha) { half a2 alpha * alpha; half d (NoH * a2 - NoH) * NoH half(1.0); return a2 / (PI * d * d); }實(shí)際使用里有幾個(gè)坑。一是alpha不能為0否則分母會(huì)爆常用做法是給一個(gè)最小下限比如把粗糙度clamp到0.04以上。二是NoH必須做saturate保證角度在合理范圍。三是移動(dòng)端half精度下d很小的區(qū)域非常容易出噪點(diǎn)遇到閃爍可以改用float算或者提高最小值。2.2 G項(xiàng)幾何遮蔽與可見性合并高光的“衰減”光找到了法線對(duì)路的微表面但這片微表面在入射方向和出射方向可能被旁邊的微表面擋住。入射時(shí)被擋叫Shadowing出射時(shí)被擋叫Masking合在一起就是幾何遮蔽項(xiàng)G。為什么必須有它因?yàn)闆]有G粗糙表面的高光在掠射角會(huì)亮到失真能量明顯不守恒。真實(shí)材質(zhì)在接近180度角時(shí)微表面遮擋極其嚴(yán)重高光應(yīng)該快速衰減而不是越掠射越刺眼。Smith模型是業(yè)界標(biāo)準(zhǔn)做法它把G拆成兩個(gè)獨(dú)立遮擋函數(shù)的乘積G G1(n·l) * G1(n·v)G1的近似形式五花八門最常見的是Schlick-GGX或者Joint Approx。更進(jìn)一步的優(yōu)化是把G和Cook-Torrance分母里的4NoLNoV合并成一個(gè)可見性項(xiàng)V這樣既能少算幾步除法也能避免0除風(fēng)險(xiǎn)。Unity的URP里有個(gè)Vis_SmithJointApprox就是這種合并后的形式half Vis_SmithJointApprox(half alpha, half NoV, half NoL) { half a max(alpha, half(0.05)); half a2 a * a; half lambdaV NoL * (NoV * (1.0 - a) a); half lambdaL NoV * (NoL * (1.0 - a) a); return half(0.5) / (lambdaV lambdaL half(1e-5)); }很多人把Unity里看到的代碼和書上公式對(duì)著看會(huì)發(fā)現(xiàn)“分母的4哪去了”正是因?yàn)橐呀?jīng)被V吞進(jìn)去所以你在Unity代碼里看到的是0.5/(lambdaVlambdaL)而不是G/(4NoLNoV)。如果自己從零寫PBR這個(gè)細(xì)節(jié)很容易踩到建議直接把V和D、F一起封裝好不要在Shader里臨時(shí)拼湊。2.3 F項(xiàng)Schlick菲涅爾近似高光的“顏色與強(qiáng)度”菲涅爾說的是一個(gè)日常現(xiàn)象視線垂直看水面時(shí)能看到水底視線貼近水面時(shí)看到的幾乎全是天空倒影。入射角越傾斜表面反射越強(qiáng)。BRDF里的F0是垂直入射時(shí)的反射率對(duì)非金屬大約在0.02到0.06之間業(yè)界習(xí)慣統(tǒng)一用0.04做默認(rèn)值。金屬則完全不同它的F0直接由albedo顏色決定這就是金屬高光為什么帶強(qiáng)烈本色而非金屬高光基本是白色的根本原因。Unity里F0和Metallic之間有一條典型公式half3 F0 lerp(half3(0.04, 0.04, 0.04), albedo, metallic)Schlick近似把完整菲涅爾公式壓成一個(gè)五次冪表達(dá)F_Schlick F0 (1 ? F0) * (1 ? cosθ)^5half3 F_Schlick(half3 f0, half LoH) { half f90 half(1.0); half LoH5 pow(saturate(half(1.0) - LoH), half(5.0)); return f0 (f90 - f0) * LoH5; }Schlick不是精確的菲涅爾但誤差很小從垂直角度看已經(jīng)完全夠用實(shí)時(shí)渲染里幾乎沒有人用完整公式。對(duì)金屬來說高光顏色等于反射率顏色對(duì)非金屬來說高光接近白。下面這幾個(gè)參考值可以幫我們建立直覺材質(zhì)類型線性空間F0參考值視覺效果水、玻璃、多數(shù)塑料0.02 ~ 0.04高光偏白強(qiáng)度不高鉆石0.15 ~ 0.18高光強(qiáng)且銳利鐵、鉻0.55 ~ 0.65高光帶明顯的金屬本色金約(1.0, 0.71, 0.29)金黃高光掠射角高光轉(zhuǎn)白還有一個(gè)特別重要但容易忽略的點(diǎn)F0必須在線性空間下計(jì)算。如果工程渲染在Gamma空間F0從sRGB色空間端進(jìn)來高光顏色會(huì)偏整體對(duì)比會(huì)發(fā)灰。這也是為什么很多初次接觸PBR的人發(fā)現(xiàn)“我這F0沒問題、G沒問題為什么高光顏色就是不對(duì)”的原因之一。2.4 漫反射項(xiàng)Unity并不是無腦用albedo/π嚴(yán)格Lambert漫反射是albedo除以π但Unity的實(shí)際實(shí)現(xiàn)并沒有簡(jiǎn)單到就這一步。關(guān)鍵差異在于金屬表面幾乎沒有漫反射因?yàn)楣膺M(jìn)入金屬后立刻被自由電子吸掉沒有辦法進(jìn)入體內(nèi)散射再出來。所以材質(zhì)上“光進(jìn)來后有多少能量可以變成漫反射顏色”是要按金屬度從albedo里扣出來的。對(duì)應(yīng)的代碼邏輯是oneMinusReflectivity 0.96 * (1.0 - metallic) diffuseColor albedo * oneMinusReflectivity非金屬情況下反射率只有0.04所以留給漫反射的能量是96%和0.96這個(gè)系數(shù)完全對(duì)得上。金屬情況下oneMinusReflectivity接近0漫反射就被扣掉albedo直接拿去當(dāng)F0了。傳統(tǒng)Blinn-Phong時(shí)代沒有這層“能量預(yù)算”概念給金屬貼一個(gè)卡通顏色當(dāng)漫反射高光再單獨(dú)疊一層。PBR把a(bǔ)lbedo同時(shí)承擔(dān)了漫反射色和金屬反射色的雙重職責(zé)靠Metallic來分配預(yù)算。這就是為什么很多美術(shù)調(diào)金屬材質(zhì)時(shí)總覺得“臟”如果鋁塊上albedo給了一個(gè)灰暗的深色那不僅漫反射變暗連F0這個(gè)高光基色也一起變暗高光整片死氣沉沉。3. 在實(shí)際代碼里找到這些公式的落點(diǎn)3.1 Standard Shader與URP/Lit的代碼入口內(nèi)置渲染管線的Standard Shader底層一直維護(hù)在UnityStandardBRDF.cginc里老項(xiàng)目里能直接搜到DirectBRDF、SpecularTerm這類函數(shù)名。到了URP時(shí)代Cginc換成了Core RP Library的BRDF.hlsl函數(shù)入口是UniversalFragmentPBR。代碼結(jié)構(gòu)變了命名換了但核心那套公式幾乎沒有動(dòng)過。建議的追碼路徑是進(jìn)入U(xiǎn)niversalFragmentPBR后先找到BRDFData結(jié)構(gòu)再看DirectBDRF怎么算直接光最后看GlobalIllumination怎么算間接光。在這條路徑上一定能看到D_GGX、Vis_SmithJointApprox、F_Schlick和某個(gè)漫反射函數(shù)和前面拆的公式一一對(duì)應(yīng)。要注意的是不同Unity版本對(duì)roughness的命名和映射并不完全一致。有的版本BRDFData里已經(jīng)把roughness平方過D_GGX內(nèi)部又做了一次平方等效alpha變成感知粗糙度的四次方看著不物理但工程上大家也都接受了。有的版本在調(diào)用前才做映射。所以別被函數(shù)名和變量名忽悠跟著數(shù)值走看它實(shí)際推進(jìn)公式的是哪個(gè)量。3.2 Smoothness、roughness、alpha美術(shù)參數(shù)到底怎么進(jìn)公式美術(shù)面板上那個(gè)Smoothness理解成roughness的補(bǔ)數(shù)就好關(guān)系是perceptualRoughness 1.0 - smoothness它叫感知粗糙度是給美術(shù)用的線性概念。進(jìn)入GGX之前還要再平方一次得到幾何粗糙度alphaalpha perceptualRoughness * perceptualRoughnessalpha才是真正進(jìn)D和G函數(shù)的參數(shù)。也就是說如果Smoothness是0.9perceptualRoughness0.1alpha0.01高光非常集中如果Smoothness是0.5perceptualRoughness0.5alpha0.25高光明顯散開。這個(gè)平方關(guān)系導(dǎo)致一個(gè)常見現(xiàn)象粗糙度從0.3調(diào)到0.4視覺變化不大從0.9調(diào)到1.0視覺變化也不大但在0.2附近微調(diào)一點(diǎn)點(diǎn)高光就會(huì)有明顯跳動(dòng)。調(diào)參時(shí)心里要有這根弦。把這個(gè)關(guān)系整理成一個(gè)速查表方便日常使用SmoothnessperceptualRoughnessGGX alpha高光形態(tài)1.00.00.0理想鏡面實(shí)際需下限保護(hù)0.90.10.01小而銳利的高光0.70.30.09中等集中高光0.30.70.49明顯擴(kuò)散接近亞光0.01.01.0非常發(fā)散幾乎全漫反射另外有個(gè)很典型的翻車現(xiàn)場(chǎng)美術(shù)在Substance Painter里導(dǎo)出一張Roughness貼圖回到Unity里順手接到Smoothness槽。Roughness和Smoothness本來就是反的這樣會(huì)得到完全相反的粗糙度表現(xiàn)材質(zhì)像被整體調(diào)換過。正確做法是確認(rèn)你的貼圖表示的是Smoothness還是Roughness再?zèng)Q定要不要反相或者直接用線性插值節(jié)點(diǎn)處理。3.3 IBL沒有光追環(huán)境光怎么也能有高光實(shí)時(shí)渲染里環(huán)境光不能逐方向積分但真實(shí)材質(zhì)在環(huán)境下的高光又非常重要。于是業(yè)界普遍用Split-sum近似來拆成兩步第一步把環(huán)境貼圖按不同粗糙度預(yù)濾波存成一組miproughness越大采樣的mip越模糊第二步把BRDF對(duì)NoV和roughness的積分結(jié)果預(yù)計(jì)算成一張LUT運(yùn)行時(shí)按NoV和粗糙度查表或者用解析近似模擬。最后合成時(shí)用roughness去采樣預(yù)濾波貼圖再乘LUT就得到了近似的IBL鏡面反射。Unity的Standard和URP對(duì)這個(gè)過程都有完整封裝常見的是采樣Cubemap的LOD后再乘一個(gè)EnvBRDF近似函數(shù)。因此反射探針質(zhì)量對(duì)金屬物體的影響極大一個(gè)金屬壺如果周圍反射探針分辨率不夠它就像“最亮的鏡子照了一張糊掉的圖”高光糊成一片。還有一點(diǎn)值得特別注意IBL和直接光照是兩套獨(dú)立的近似如果他們內(nèi)部對(duì)roughness的映射不一致就會(huì)出現(xiàn)“關(guān)掉環(huán)境光時(shí)很自然打開IBL后高光突然變亮或變暗”的斷層感。排查時(shí)把直接光和間接光分開驗(yàn)證比在最終畫面上瞎調(diào)有效得多。4. 實(shí)戰(zhàn)手搓一個(gè)簡(jiǎn)化PBR Shader來驗(yàn)證公式4.1 初始化BRDFData金屬度如何拆出specular和diffuse與其背公式不如直接寫一段可用的HLSL骨架。下面這段代碼來自URP風(fēng)格的實(shí)現(xiàn)思路但已經(jīng)簡(jiǎn)化過特意把變量命名改得更清晰便于理解struct BRDFData { half3 albedo; half3 diffuse; half3 specular; half perceptualRoughness; half roughness; // 幾何粗糙度即GGX中的alpha half oneMinusReflectivity; }; BRDFData InitializeBRDFData(half3 albedo, half metallic, half smoothness) { half oneMinusReflectivity half(0.96) * (half(1.0) - metallic); half reflectivity half(1.0) - oneMinusReflectivity; half3 specular lerp(half3(0.04, 0.04, 0.04), albedo, metallic); BRDFData data; data.albedo albedo; data.diffuse albedo * oneMinusReflectivity; data.specular specular; data.perceptualRoughness half(1.0) - smoothness; data.roughness data.perceptualRoughness * data.perceptualRoughness; data.oneMinusReflectivity oneMinusReflectivity; return data; }注意這一步已經(jīng)把漫反射色像前面說的那樣扣掉了金屬所占的能量。如果你拿到一個(gè)metallic1的材質(zhì)它的diffuseColor幾乎為0整張紋理就只剩下specular那條通道在發(fā)光。這也是為什么金屬材質(zhì)在燈光暗的環(huán)境里看起來特別“吸光”。4.2 D、V、F三個(gè)函數(shù)的HLSL落地接下來是三個(gè)核心函數(shù)與前面章節(jié)的公式一一對(duì)應(yīng)。這里alpha就是初始化函數(shù)里的roughness字段D_GGX內(nèi)部再平方一次是為了得到alpha2half D_GGX(half NoH, half alpha) { half a2 alpha * alpha; half d (NoH * a2 - NoH) * NoH half(1.0); return a2 / (PI * d * d); } half Vis_SmithJointApprox(half alpha, half NoV, half NoL) { half a max(alpha, half(0.05)); half a2 a * a; half lambdaV NoL * (NoV * (half(1.0) - a) a); half lambdaL NoV * (NoL * (half(1.0) - a) a); return half(0.5) / (lambdaV lambdaL half(1e-5)); } half3 F_Schlick(half3 f0, half LoH) { half f90 half(1.0); half LoH5 pow(saturate(half(1.0) - LoH), half(5.0)); return f0 (f90 - f0) * LoH5; }最后在DirectBDRF里把它們組合到一起再乘光源顏色、NoL和衰減half3 DirectBDRF(BRDFData brdfData, half3 N, half3 V, half3 L, half3 lightColor, half lightAttenuation) { half3 H normalize(L V); half NoL saturate(dot(N, L)); half NoV saturate(dot(N, V)); half NoH saturate(dot(N, H)); half LoH saturate(dot(L, H)); half D D_GGX(NoH, brdfData.roughness); half Vis Vis_SmithJointApprox(brdfData.roughness, NoV, NoL); half3 F F_Schlick(brdfData.specular, LoH); half3 specular D * Vis * F; half3 diffuse brdfData.diffuse; return (specular diffuse) * lightColor * NoL * lightAttenuation; }如果你自己想驗(yàn)證把specular和diffuse分別打到一個(gè)Debug顏色通道上就能很直觀地看到金屬度對(duì)兩者的影響。例如強(qiáng)行讓diffuse0只輸出specular金屬高光還是帶顏色反過來只輸出diffuse金屬表面會(huì)變成純黑這正好對(duì)應(yīng)PBR的能量分配邏輯。4.3 用Frame Debugger和RenderDoc驗(yàn)證每個(gè)中間量Shader寫完別急著看最終畫面先把中間量可視化。Unity的Frame Debugger可以在Game窗口定位到Lit PassRenderDoc更是可以直接抓取每張中間RT和數(shù)值。我的習(xí)慣是給Shader加一個(gè)Debug開關(guān)把D、Vis、F、NoL、lightAttenuation分別輸出成顏色先看單通道對(duì)不對(duì)再合成最終結(jié)果。例如把Vis輸出成灰度圖應(yīng)該能看到邊緣區(qū)域比中心暗這是幾何遮蔽在起作用把F輸出成RGB圖應(yīng)該能看到金屬物體在高光區(qū)域有強(qiáng)烈本色非金屬則接近白。如果這些中間量看起來都不對(duì)那問題一定在更上游的輸入比如法線、UV或者貼圖通道映射此時(shí)直接調(diào)最終顏色只會(huì)越調(diào)越亂。這套驗(yàn)證方法比什么魔法調(diào)參都管用。大部分PBR材質(zhì)“發(fā)灰”“臟”“過曝”的問題其實(shí)在中間量的灰度圖里就已經(jīng)露出馬腳了。5. 常見問題與排查實(shí)錄5.1 高光過曝或發(fā)灰高光過曝最常見的原因是G項(xiàng)沒用好。很多人按公式寫G/(4NoLNoV)但NoL或NoV沒有saturate負(fù)數(shù)進(jìn)入分母后就會(huì)在背光面莫名其妙發(fā)亮。更隱蔽的問題是直接在Gamma空間算F結(jié)果高光色溫偏暖或偏灰。還有人在URP里把specular和diffuse簡(jiǎn)單相加后不乘NoL導(dǎo)致垂直于法線的區(qū)域照樣有光照畫面一下子平了。排查時(shí)優(yōu)先把NoL單獨(dú)可視化燈從正面照NoL應(yīng)當(dāng)是一張平滑的明暗圖邊緣銳度取決于燈光角度而不是材質(zhì)。NoL正常后再看Vis如果Vis在掠射角不衰減問題就在G項(xiàng)。最后看F檢查F0是否是lerp(0.04, albedo, metallic)得來的而不是拿原始albedo直接往高光里塞。下面這個(gè)速查表可以輔助定位現(xiàn)象常見原因處理方式高光整片發(fā)白過曝F項(xiàng)缺少能量守恒或F0計(jì)算錯(cuò)誤檢查F0lerp(0.04, albedo, metallic)光照方向正常但高光平specular未乘NoL或attenuation確認(rèn)最終光照乘了NoL和衰減背光面莫名高亮NoL或NoV未clamp對(duì)NoL、NoV做saturate高光顏色偏色Gamma空間計(jì)算F切換線性空間并在線性空間做F計(jì)算5.2 金屬材質(zhì)怎么設(shè)置都是臟兮兮金屬材質(zhì)最大的坑是把a(bǔ)lbedo當(dāng)“漫反射顏色”來調(diào)。金屬?zèng)]有漫反射albedo直接成為F0基色所以想表現(xiàn)一塊干凈的鐵albedo必須給到接近鐵的反射率色值而不是一張黑灰色插畫。常見錯(cuò)誤是把金屬度設(shè)為1后又把a(bǔ)lbedo調(diào)得極暗結(jié)果高光和漫反射一起消失材質(zhì)看起來像一團(tuán)焦炭。另一個(gè)問題是金屬度貼圖本身沒配好0和1之間根本沒有平滑過渡邊緣鋸齒感會(huì)直接把“金屬感”破壞掉。如果你想要一個(gè)相對(duì)折中的方案非金屬表面Metallic給0Smoothness給0.5~0.7金屬表面Metallic給1Smoothness按打磨程度給。不要在一張材質(zhì)上到處拉0.4、0.5這種“半吊子金屬度”除非你在做介于金屬和非金屬之間的混合材質(zhì)而且你清楚自己為什么這么做。5.3 粗糙度調(diào)來調(diào)去高光形態(tài)不對(duì)這種情況首先檢查Smoothness和Roughness有沒有反了。從Substance導(dǎo)出的Roughness貼圖如果不經(jīng)過反相就接到Unity的Smoothness槽高光形態(tài)會(huì)完全反著走。還有一種情況是貼圖導(dǎo)入時(shí)Unity把線性數(shù)據(jù)當(dāng)sRGB解導(dǎo)致粗糙度數(shù)值被壓暗或提亮整個(gè)材質(zhì)“軟塌塌”。另一個(gè)容易被忽略的問題是GGX的alpha映射差異。你在代碼里看到D_GGX內(nèi)部又乘了一次roughness可能就是引擎在走“perceptualRoughness的平方在初始化時(shí)做一次、D_GGX內(nèi)部再做一次”的雙平方路徑。如果你的工程恰好碰到這種情況視覺高光比普通PBR更“緊”調(diào)參時(shí)要參考這個(gè)歷史版本的行為而不是死磕物理公式。順手提一下Shader編譯錯(cuò)誤的表現(xiàn)材質(zhì)變成紫紅色或者粉色。這通常代表Shader有編譯錯(cuò)誤或分支寫錯(cuò)不是渲染方程本身的問題。如果看到粉紅材質(zhì)先看Console里到底報(bào)了什么錯(cuò)別去調(diào)參數(shù)折騰半天。5.4 直接光和間接光打架這個(gè)問題比較難排查因?yàn)樵谥挥兄苯庸獾陌咨臻g里測(cè)PBR渲染怎么都是對(duì)的一開環(huán)境光就出問題。常見的割裂來源有三個(gè)反射探針沒有更新導(dǎo)致動(dòng)態(tài)物體周圍的環(huán)境信息是舊的IBL的roughness映射和直接光不一致導(dǎo)致同一材質(zhì)在直接光高光集中、在IBL里卻像涂了層油環(huán)境貼圖不是HDR格式高光范圍被壓縮物體顯得死灰。解決思路是把“直接光”和“間接光”獨(dú)立測(cè)試。先關(guān)掉所有環(huán)境光把太陽光拉成明顯方向光確認(rèn)直接光表現(xiàn)再只開反射探針關(guān)掉直接光看IBL單獨(dú)的表現(xiàn)。兩者都正常再打開一起看。如果一合起來就發(fā)灰大概率是環(huán)境光照強(qiáng)度或者探針分辨率與直接光不匹配而不是光照模型算錯(cuò)了。6. 面試視角與延伸思考6.1 答這道題的正確順序和追問應(yīng)對(duì)面試答這道題背公式是最低級(jí)的答法。一個(gè)明確的回答順序大概是先講實(shí)時(shí)渲染不能用完整渲染方程所以退化成反射率方程再說BRDF選型Unity用的是Cook-Torrance微表面模型分漫反射和鏡面反射然后拆鏡面項(xiàng)里的D、G、F各自說什么接著提到Unity的參數(shù)化路徑Metallic里藏著F0Smoothness先變perceptualRoughness再平方成GGX的alpha最后補(bǔ)一句IBL用Split-sum近似環(huán)境光和BRDF積分。面試官大概率會(huì)追三個(gè)問題。第一個(gè)是“為什么分母是4NoLNoV”。這個(gè)要答出來自微表面投影面積的幾何推導(dǎo)把宏表面的微表面面積投影到入射和出射兩套坐標(biāo)系后產(chǎn)生的系數(shù)實(shí)時(shí)實(shí)現(xiàn)里經(jīng)常把它和G合并進(jìn)可見性項(xiàng)。第二個(gè)是“為什么roughness要平方”。要答出美術(shù)給的perceptualRoughness是感知值跟微表面法線分布的標(biāo)準(zhǔn)差有關(guān)系幾何roughness近似等于它的平方同時(shí)GGX公式本身也需要一個(gè)二次方項(xiàng)來控制高光衰減形狀。第三個(gè)是“金屬度為什么能在0到1連續(xù)變化”。要答出這本質(zhì)上是金屬和非金屬兩種反射模式的線性插值F0從0.04插到albedo顏色同時(shí)漫反射能量從0.96逐級(jí)扣到0并不是說0.5金屬度就是一個(gè)真實(shí)的中間態(tài)金屬而是一種材質(zhì)混合控制參數(shù)。這三個(gè)追問能流利答下來基本就證明不是死記硬背了。6.2 理解渲染方程之后還能往哪兒走這套東西不光用于寫實(shí)渲染。搞明白PBR之后去看NPR卡通渲染完全就是一個(gè)“反著改”的樂趣把GGX的D項(xiàng)換成自定義的閃點(diǎn)分布把Fresnel從連續(xù)曲線換成硬閾值把漫反射從Lambert換成Ramp貼圖很多卡通效果的原理就是在打斷PBR的能量守恒強(qiáng)行塑造非物理的觀察信號(hào)。知道PBR每一步在做什么就能精確決定在哪里“破壞規(guī)則”這就是從“被shader推著走”變成“推著shader走”的轉(zhuǎn)折點(diǎn)。對(duì)性能敏感的項(xiàng)目PBR也不是什么洪水猛獸。核心BRDF就是D加Vis加F再乘一次NoL移動(dòng)端用half精度完全跑得動(dòng)真正貴的是多光源迭代和反射探針瀑布。日常優(yōu)化時(shí)優(yōu)先砍光源數(shù)量、降反射探頭更新頻率、把不參與高級(jí)光照的粒子改成Unlit比在Shader里扣float比half的細(xì)節(jié)有效得多。一點(diǎn)自己的實(shí)操體會(huì)做圖形渲染這些年我?guī)缀趺磕甓紩?huì)被問到一次“Unity PBR的渲染方程是啥”問法五花八門但考察點(diǎn)始終穩(wěn)定你知不知道能量守恒、知不知道微表面模型、知不知道Unity每個(gè)參數(shù)最終落在公式里的哪個(gè)位置。建議想在這個(gè)方向上走遠(yuǎn)的同學(xué)拿到任何PBR材質(zhì)后都按這個(gè)順序調(diào)試先只開直接光把環(huán)境光關(guān)掉用金屬度和粗糙度把F0與alpha調(diào)對(duì)再打開IBL看整體融合。我在項(xiàng)目里只要嚴(yán)格按這套流程走基本沒有調(diào)不出來的材質(zhì)也沒有解釋不清的高光問題。