幾個月前类垦,偶然接觸了PBR(Physically Based Rendering)褐缠,找了很多博客看是怎么回事脱货,并照著公式寫了個shader岛都,感覺還可以。現(xiàn)在回頭來整理下振峻,本來我是想寫些關(guān)于PBR的理論的臼疫,不過逛知乎發(fā)現(xiàn)大神毛星云已經(jīng)對PBR的相關(guān)理論寫了好幾篇博客,非常具有體系性扣孟,所以我就在GitHub上fork了他的文章基于物理的渲染(PBR)白皮書 | PBR White Paper烫堤,我自己就不獻丑了,本文就來談?wù)勅绾伟牙碚撟兂煽蓤?zhí)行的代碼吧凤价。
首先把PBR的渲染公式貼一下塔逃,以下所有內(nèi)容都將圍繞此公式展開
我知道這個公式不在說人話,所以把它翻譯一下是這樣的
是不是好理解多了料仗?
其中DFG三項我是用了Epic他們于2013年發(fā)表的論文Real Shading in Unreal Engine 4湾盗,里面的公式和最初Disney在2012年發(fā)表的論文Physically-Based Shading at Disney有點不一樣,比如說Epic他們覺得Disney用的F項有點消耗過大立轧,所以擬合了一個公式來代替Disney用的F項格粪。當然最近幾年P(guān)BR大火,有更多更好的公式被發(fā)現(xiàn)氛改,大家如果在實踐中發(fā)現(xiàn)公式不一樣帐萎,也無需糾結(jié)。
D項為
這一項代表法線的分布函數(shù)胜卤,什么意思呢疆导?我們在傳統(tǒng)的光照模型中需要一個表面的法線方向來進行一系列的計算,從宏觀上來說這就是一條法線葛躏。然而澈段,我們都知道PBR是基于微表面理論的,所以宏觀上的一條法線在微表面上其實代表的是有許許多多的微表面法線都朝著某個方向舰攒,組成了宏觀上我們計算的那條法線败富。這個D項即是在說明,在微表面上摩窃,有多少微表面的法線可是正確的朝向(正確意味著光線l可以被反射到視線方向v)兽叮,這些能被觀察到的法線會對最終的計算結(jié)果產(chǎn)生影響。所以,這個函數(shù)的輸出是一個統(tǒng)計分布鹦聪,表明根據(jù)現(xiàn)在的表面粗糙度等輸入账阻,計算得到有多少法線會對最終結(jié)果產(chǎn)生實際影響。
G項為
這一項代表著自陰影這一屬性泽本。其實上面的D項雖然可以算出所有有用的微表面的法線宰僧,然而這些法線并不一定都能被觀察方向所看見。由于表面的幾何結(jié)構(gòu)观挎,也許存在一些表面被其他表面擋住琴儿,所以這一項實際上是在對D項的再過濾,把真正有用的法線提取出來參與到最后的計算嘁捷。
F項為
這項為菲涅爾項造成。根據(jù)物理研究,萬物皆有菲涅爾雄嚣,菲涅爾項在表達所見光的反射率與視角相關(guān)的現(xiàn)象晒屎。具體來說,從掠射角(與法線呈接近90度)下觀察缓升,光的反射率會增加鼓鲁。舉個例子,我們在海灘邊港谊,看著腳下的水會覺得很清澈骇吭,地下的沙看的很清楚,而遠處卻是浮光掠金歧寺,看不清底下到底有什么燥狰,這就是菲涅爾所在表達的現(xiàn)象。
這里要注意的是斜筐,宏觀上我們看見的菲涅爾其實是在微觀上所有菲涅爾的平均值龙致,也就是說微平面上每道光的入射角和法線都在影響著最終宏觀上菲涅爾的結(jié)果。
不同材質(zhì)的菲涅爾是不同的(好像是句廢話顷链。目代。。)嗤练。一般金屬的菲涅爾會很弱榛了,因為金屬的反射本身就很強了。拿鋁做個例子潭苞,其反射率在所有角度幾乎都保持在86%以上忽冻,隨角度變化很小。而絕緣體則相反(這里想科普一下此疹,水不導(dǎo)電,水能導(dǎo)電是因為水中的其他物質(zhì)在導(dǎo)電,純凈的水是不導(dǎo)電的)蝗碎,比如玻璃湖笨,在法線方向的反射率僅為4%,到掠射角度的時候可以接近100%蹦骑。
如果大家看見代碼里涉及菲涅爾時有變量名叫F0慈省,F(xiàn)90的時候,不要奇怪眠菇,F(xiàn)0代表從法線方向觀察材質(zhì)的反射率边败,而F90就是與法線垂直方向觀察材質(zhì)的反射率。但代碼里不會直接使用反射率捎废,而是在此反射率下材質(zhì)應(yīng)該是什么顏色笑窜,毛星云已經(jīng)總結(jié)了一張F0處的表,方便大家快速查找登疗。
從表上來看排截,金屬的F0值在0.5-1.0之間,電介質(zhì)(Dielectric)或者叫絕緣體大都在0.02-0.05之間辐益,半導(dǎo)體在0.3-0.5之間断傲。
公式有了,那就寫成代碼吧
//Specular D, normal distribution function, α = roughtness^2
float GGX(float NdotH, float r_2){
float alpha_2 = pow2(r_2);
float res = (alpha_2 * _GGX) / (UNITY_PI * pow2(pow2(NdotH) * (alpha_2 - 1) + 1) + MINNUM);//加個非常小的數(shù)以防是0
return res;
}
//Specular G智政,Geometry Term
float SmithJoint(float NdotL, float NdotV,float r){
float k = pow2(r+1) / 8;
float g1 = NdotV / (NdotV * (1 - k) + k);
float g2 = NdotL / (NdotL * (1 - k) + k);
return g1 * g2;
}
//Specular F, Fresnel Term
float4 FresnelSchlick(float4 F0, float VdotH){
return F0 + (1 - F0) * exp2((-5.55473 * VdotH - 6.98316) * VdotH);
}
float4 CookTorranceBRDF(float NdotH,float NdotL,float NdotV,float VdotH,float roughness,float4 specularColor){
float D = GGX(NdotH,pow2(roughness));
float G = SmithJoint(NdotL,NdotV,roughness);
float4 F = FresnelSchlick(specularColor,VdotH);
float4 res = (D * G * F) / (4 * NdotL * NdotV + MINNUM);
return res;
}
寫代碼的時候注意除的時候分母要加個比較小的數(shù)认罩,防止除0發(fā)生。
那么有了以上代碼续捂,我們要怎么調(diào)用呢猜年?
我們先要得到NdotH
,NdotL
,NdotV
,VdotH
,由于一般材質(zhì)會提供法線貼圖疾忍,所以得到的法線是處于tangent space的乔外,雖然可以把光照方向,觀察方向轉(zhuǎn)到tangent space一罩,但我決定這次把法線轉(zhuǎn)到world space處理杨幼,所以要得到這些變量,我們應(yīng)該現(xiàn)在vertex shader里構(gòu)建好一個變換矩陣聂渊。
float3 worldPos = mul(unity_ObjectToWorld,v.vertex).xyz;
float3 worldNoraml = UnityObjectToWorldNormal(v.normal);
float3 worldTangent = UnityObjectToWorldDir(v.tan.xyz);
float3 worldBinormal = cross(worldNoraml,worldTangent) * v.tan.w;
o.TtoW0 = float4(worldTangent.x,worldBinormal.x,worldNoraml.x,worldPos.x);
o.TtoW1 = float4(worldTangent.y,worldBinormal.y,worldNoraml.y,worldPos.y);
o.TtoW2 = float4(worldTangent.z,worldBinormal.z,worldNoraml.z,worldPos.z);
然后在fragment shader里把法線轉(zhuǎn)換好差购。
float3 worldPos = float3(i.TtoW0.w,i.TtoW1.w,i.TtoW2.w);
float3 viewDir = normalize(UnityWorldSpaceViewDir(worldPos));
float3 lightDir = normalize(UnityWorldSpaceLightDir(worldPos));
float3 halfDir = normalize(viewDir + lightDir);
//normal in tangent space
float3 normal = UnpackNormal(tex2D(_NormalTex,i.uv));
normal.xy *= _NormalScale;
normal.z = sqrt(1 - saturate(dot(normal.xy,normal.xy)));
//normal in world space
float3 normalWorld = normalize(float3(dot(i.TtoW0.xyz,normal),dot(i.TtoW1.xyz,normal),dot(i.TtoW2.xyz,normal)));
有了world space的法線后,上述的變量就好解決了汉嗽。
float NdotL = saturate(dot(normalWorld,lightDir));
float NdotV = saturate(dot(normalWorld,viewDir));
float VdotH = saturate(dot(viewDir,halfDir));
float NdotH = saturate(dot(normalWorld,halfDir));
float LdotH = saturate(dot(lightDir,halfDir));
現(xiàn)在可以開始調(diào)用公式的代碼了欲逃。
//direct light part
float4 ambient = UNITY_LIGHTMODEL_AMBIENT * _MainCol * col * _LightFactor;
float4 diffuse = OneMinusReflectivityFromMetallic(Metalness.r) * _MainCol * col / UNITY_PI;
float3 F0 = lerp(unity_ColorSpaceDielectricSpec.rgb,col.rgb,Metalness.r);//區(qū)分金屬非金屬
float4 specular = CookTorranceBRDF(NdotH,NdotL,NdotV,VdotH,roughness,float4(F0,1) * _SpecularColor);
重點來看下diffuse和specular,diffuse我沒有用Disney的公式饼暑,完全按照Epic論文中的公式
然后乘以了一個漫反射系數(shù)OneMinusReflectivityFromMetallic(Metalness.r)
稳析,這個OneMinusReflectivityFromMetallic
方法定義在UnityStandardUtils.cginc中洗做,源碼
inline half OneMinusReflectivityFromMetallic(half metallic)
{
// We'll need oneMinusReflectivity, so
// 1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)
// store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec.a, then
// 1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =
// = alpha - metallic * alpha
half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec.a;
return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}
注釋推導(dǎo)得很明白了,但我想說下從理論上來說彰居,漫反射系數(shù) = 1 - 金屬度诚纸,金屬度決定了鏡面反射系數(shù),所以漫反射是1 - 金屬度陈惰,但非金屬或多或少也有鏡面反射畦徘,所以,簡單的減法并不能滿足抬闯,所以有了以上的源碼來計算漫反射系數(shù)井辆。在這里金屬度是金屬貼圖的r通道提供的,這張貼圖的a通道提供了光滑度溶握,bg通道空置杯缺。Unity中有兩種PBR的工作流,Metallic和Specular奈虾,現(xiàn)在我用的這種是Metallic夺谁,欲了解詳細請前往探究PBR的兩種流程以及Unity中的PBS。
spcular項中肉微,unity中有個變量unity_ColorSpaceDielectricSpec
匾鸥,定義在UnityCG.cginc中,它的rgb存了介于金屬與非金屬之間的F0的顏色碉纳,這個顏色與金屬貼圖上的顏色通過金屬度進行插值勿负,并承以自定義的顏色,來決定最終傳入F項公式的F0的顏色劳曹。
這里有人會問了奴愉,上面的代碼里沒有項啊铁孵?你寫代碼時寫漏了锭硼?其實
項已經(jīng)被F項表達出來了,他們倆是重復(fù)的蜕劝,之前的公式有那么一點瑕疵檀头,所以在實現(xiàn)時這個
就沒有了。
由于這個公式中的積分項無法實時計算出來
所以我們通過一些手段讓公式簡化為
其中即
岖沛,
是光源的顏色
那么代碼里就是
return (diffuse + specular) * _LightColor0 * UNITY_PI * NdotL;
最后暑始,為了得到更真實的光照,我們還需要計算IBL部分婴削。說起IBL又得另外寫一篇廊镜,所以這里你可以認為是對天空盒進行采樣,并且在采樣天空盒時唉俗,我們需要一個級數(shù)嗤朴。這個級數(shù)呢是代表天空盒那張貼圖(稱作環(huán)境貼圖)的級數(shù)配椭,級數(shù)越高對應(yīng)的紋理越小,圖像越模糊播赁。我們把這樣一個技術(shù)叫做多級漸遠紋理(mipmaps)颂郎。
粗糙度越大吼渡,反射應(yīng)該越模糊容为,那么采樣的環(huán)境貼圖的級數(shù)也應(yīng)該越高。然而寺酪,粗糙度和級數(shù)的關(guān)系并不是一個線性關(guān)系坎背,Unity內(nèi)使用的轉(zhuǎn)換公式為mip = r(1.7 - 0.7r),可在UnityImageBasedLighting.cginc內(nèi)找到寄雀。有時我們還會再乘以一個常數(shù)得滤,表明整個粗糙度范圍內(nèi)多級漸遠紋理的總級數(shù)。
然后盒犹,我們會在F0和F90之間進行插值懂更,來為IBL添加高質(zhì)量的菲涅爾反射效果,再考慮一個由粗糙度計算得到的surfaceReduction進一步對IBL進行修正急膀。
//indirect light part
float3 reflectDir = normalize(reflect(-viewDir,normalWorld));
float percetualRoughness = roughness * (1.7 - 0.7 * roughness);
float mip = percetualRoughness * 6;
float4 envMap = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0,reflectDir,mip);
float grazing = saturate((1 - roughness) + 1 - OneMinusReflectivityFromMetallic(Metalness.r));
float surfaceReduction = 1 / (pow2(roughness) + 1);
float4 indirectSpecualr = surfaceReduction * envMap * FresnelLerp(float4(F0,1) * _SpecularColor,grazing,NdotV);
放上效果圖沮协,有兩種球,鐵銹的球和竹子材質(zhì)的球慷暂。其中有兩個球是官方自帶的standard shader行瑞,另外兩個球是我自己寫的shader帮非,看起來還不錯副砍。貼圖出自https://freepbr.com/
PS. PBR需要把color space調(diào)到linear space,原來的gamma space并不適合做PBR邦尊,為什么呢?因為gamma space本身是為了渲染出來的物體看起來更加真實而對我們眼睛所看到的顏色進行了修正又沾,而PBR本身就是基于物理的渲染驳癌,所有涉及到的貼圖都是在真實光照環(huán)境下設(shè)計出來的表窘,不需要再進行修正了。至于有朋友對lienar space和gamma space感興趣的話,可以看看【圖形學(xué)】我理解的伽馬校正(Gamma Correction),聊聊Unity的Gamma校正以及線性工作流以及Unite 2018 | 淺談伽瑪和線性顏色空間。
2020.09.17更新
之前對于IBL部分講得太簡單了填物,有些地方寫的也不太滿意,所以在此做一些補充。
首先IBL部分的代碼我改成了這個樣子:
float3 fresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
return F0 + (max(float3(1.0 - roughness, 1.0 - roughness, 1.0 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}
//indirect light part
//indirct diffuse
float3 sh = ShadeSH9(float4(normalWorld,1));
float3 iblDiffuse = max(float3(0,0,0),sh + (0.03 * ambient));
float3 Flast = fresnelSchlickRoughness(max(NdotV, 0.0), F0, roughness);
float kd = (1 - Flast) * OneMinusReflectivityFromMetallic(Metalness.r);
iblDiffuse = iblDiffuse * kd / UNITY_PI;
//indirect specular
float3 reflectDir = normalize(reflect(-viewDir,normalWorld));
float percetualRoughness = roughness * (1.7 - 0.7 * roughness);
float mip = percetualRoughness * 6;
float4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(unity_SpecCube0,reflectDir,mip);
float4 iblSpecular = float4(DecodeHDR(rgbm,unity_SpecCube0_HDR),1);
//LUT part, use surfaceReduction instead
float grazing = saturate(smoothness + OneMinusReflectivityFromMetallic(Metalness.r));
float surfaceReduction = 1 / (pow2(roughness) + 1);
float4 indirectSpecualr = surfaceReduction * iblSpecular * FresnelLerp(float4(F0,1) * _SpecularColor,grazing,NdotV);
同樣間接光也是由間接漫反射和間接鏡面反射組成的。在這里間接漫反射約等于球諧函數(shù)編碼后的全局光照信息乘上漫反射比例,球諧部分unity中是有API可直接調(diào)用的盲镶,就是ShadeSH9
送漠,然后加上很小的環(huán)境光影響(所以環(huán)境光乘了0.03),而漫反射比例根據(jù)Adopting a physically based shading model這篇博文來看,我們需要用粗糙度來計算這個比例涎永,得出后兩部分乘起來就是間接漫反射項妈倔。而間接鏡面反射被epic公司簡化成了下面的形式:
左邊括號內(nèi)的東西是上文寫的關(guān)于mipmap的采樣天空盒的內(nèi)容盯蝴,然后天空盒可能是HDR格式存儲的,所以要用DecodeHDR
將HDR信息轉(zhuǎn)換成正常信息听怕。
右側(cè)括號內(nèi)的東西一般來說是個定值捧挺,最常見的做法是把值放到一張LUT中,根據(jù)nv和粗糙度采樣即可尿瞭。
然而采樣必然會給帶寬帶來壓力闽烙,帶寬有了壓力就發(fā)熱,所以Unity內(nèi)部并不用LUT的方式來實現(xiàn)右側(cè)括號內(nèi)的東西筷厘,而是用一個擬合函數(shù)來模擬鸣峭,這個擬合函數(shù)就是上面代碼中的surfaceReduction
乘上一個菲涅爾系數(shù)(這個系數(shù)是在高亮顏色和grazing項之間插值所得)宏所。
2021.06.18
我升級到了unity2021,然后我自己寫的shader基本沒變摊溶,效果卻和以前天差地別爬骤,還是玩shadergraph保平安吧= =
參考
第 18 章 基于物理的渲染
如何在Unity中造一個PBR Shader輪子
【學(xué)習(xí)筆記】Unity PBR的實現(xiàn)
淺墨的游戲編程
基于物理著色:BRDF
PBR Step by Step(一)立體角
猴子都能看懂的PBR(才怪)