XDRender_ShaderMode_StandardLit(1) 物理渲染-預設PBR-實現(1)
XDRender_ShaderMode_StandardLit(1) 物理渲染-預設PBR-實現(1)
@白袍小道
目錄前言
理論部分可以參考PRT部分、RealTimeRender,網上大部分資料.
其中相互聯絡圖-參考
https://www.cnblogs.com/BaiPao-XD/p/13751667.html
這裡特別推薦
毛星雲:
https://zhuanlan.zhihu.com/p/56967462
以及博主關於法線分佈函式和幾何分佈的總結.
文刀秋二:
https://zhuanlan.zhihu.com/p/20091064
正文
這裡作為實現部分,主要是考慮三件事:
1、HLSL的結構,BRDF的相關資料結構,以及傳遞和組織這些資料
2、關於菲涅爾反射、法線分佈函式、幾何分佈函式、積分處理部分
3、公式的演化、特性、以及實現和組織部分
先放出一個數據流圖
一、理論引進
簡化公式
第一個大公式類
引數說明:
第二個大公式類
拆開後
對於每一個細節部分實現中會單獨說明和處理, 抱著敬畏和大膽的步伐,開始實現.
光和介質的互動
當一束光線入射到物體表面時,由於物體表面與空氣兩種介質之間折射率的快速變化,光線會發生反射和折射:
- 反射(Reflection)。光線在兩種介質交界處的直接反射即鏡面反射(Specular)。金屬的鏡面反射顏色為三通道的彩色,而非金屬的鏡面反射顏色為單通道的單色。
- 折射(Refraction)。
- 散射(Scattering)。折射率的快速變化引起散射,光的方向會改變(分裂成多個方向),但是光的總量或光譜分佈不會改變。散射最終被視作的型別與觀察尺度有關:
- 次表面散射(Subsurface Scattering)。觀察畫素小於散射距離,散射被視作次表面散射。
- 漫反射(Diffuse)。觀察畫素大於散射距離,散射被視作漫反射。
- 透射(Transmission)。入射光經過折射穿過物體後的出射現象。透射為次表面散射的特例。
- 吸收(Absorption)。具有復折射率的物質區域會引起吸收,具體原理是光波頻率與該材質原子中的電子振動的頻率相匹配。復折射率(complex number)的虛部(imaginary part)確定了光在傳播時是否被吸收(轉換成其他形式的能量)。發生吸收的介質的光量會隨傳播的距離而減小(如果吸收優先發生於某些波長,則可能也會改變光的顏色),而光的方向不會因為吸收而改變。任何顏色色調通常都是由吸收的波長相關性引起的。
二、實現過程
假定最最最理想的情況
這裡實現我們先假設一大堆條件,避免分散注意.比如多直接光,環境光照, 光衰減, 透射,暫時不守恆,只包含Forward方式等等大量的條件.
我們的最簡化引數: 粗超度, 基礎顏色, 金屬度
流程
入口:這裡是Shader部分, 通過填寫LightMode啟用,(LightMode被OpoPass識別)
其實我們也可以將直接光的資料在前面的階段(比如ConfigLightPass, Deff的話是先搞出光照需要所有紋理資料,再等到後續直接進行對紋理處理)傳遞.當然這是
第一步: XDRender/ShaderMode/DefaultLit.Shader
第二步: 進入到構建必要資料
a、Surface:這裡是將Shader資料處理為統一, 比如一些用BaseColor紋理,一些不用.這樣我們到這裡統一搞出Color.
#ifdef _DIFFCUSE_MAP
half4 albedoAlpha = _BaseColor * SAMPLE_TEXTURE2D(_DiffcuseMap,sampler_DiffcuseMap, input.baseUV);
#else
half4 albedoAlpha = _BaseColor;
#endif
outSurfaceData.alpha = albedoAlpha.r;
outSurfaceData.albedo = albedoAlpha.rgb;
float3 rmao_sample = SAMPLE_TEXTURE2D(_RouAOMap,sampler_RouAOMap, input.baseUV).b * _OcclusionStrength;
outSurfaceData.metallic = rmao_sample.r;
outSurfaceData.specular = _SpecColor;
outSurfaceData.smoothness = 1.0-rmao_sample.g;
b、Input:頂點屬性相關
nputData.viewDirectionWS = normalize(_WorldSpaceCameraPos.xyz - input.positionWS.xyz);
inputData.positionWS = input.positionWS;
inputData.normalWS = normalWS;
inputData.bakedGI = SAMPLE_GI(lightmapUV, input.vertexSH, inputData.normalWS);
第三步:組織BRDF的資料結構
第四步:
BRDF必要資料
1、這裡我們就不考慮兩種PBR資料流區別,先就簡單把資料傳過來
2、我們先簡單傳遞,然後在下一步過程中去處理金屬和電解質的特殊性.
注意一下,HLSL可以類似多型的函式寫法,還可以建立多個InitializeBRDFData輔助與巨集來處理多種資料結構. 減少不斷增加.
這裡還可以YY一下, 就一種Float4 * X結構去組織
inline void InitializeBRDFData(half3 albedo, half metallic, half3 specular, half smoothness, half alpha,
out BRDFData outBRDFData)
{
//http://richbabe.top/2018/06/25/%E6%8E%A2%E7%A9%B6PBR%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%B5%81%E7%A8%8B%E4%BB%A5%E5%8F%8AUnity%E4%B8%AD%E7%9A%84PBS/
outBRDFData.diffuse = albedo;
outBRDFData.specular = specular;
outBRDFData.grazing = saturate(smoothness + 1 - metallic);
outBRDFData.perceptualRoughness = PerceptualSmoothnessToPerceptualRoughness(smoothness);
outBRDFData.roughness = max(PerceptualRoughnessToRoughness(outBRDFData.perceptualRoughness), HALF_MIN);
outBRDFData.roughness2 = outBRDFData.perceptualRoughness * outBRDFData.perceptualRoughness;
outBRDFData.lerpRoughness = lerp(0.01, 1.0, outBRDFData.perceptualRoughness);
outBRDFData.metallic = metallic;
outBRDFData.clearcoat = 0;
outBRDFData.clearcoatgloss = 0;
outBRDFData.secondcolor=0;
BRDF計算
這裡我們只關注BSDF中的BRDF的直接光照部分,其中光照按照介質的不同對光照會做不同的分化影響
0 基礎引數計算
主要是計算 :法向量(巨集觀非亞畫素),入射點光照方向, 射點視野, 半形方向(這裡注意一般來說主要使用的NH來作為後續幾個函式的因變數, 推導過程網上已經大量這裡就不詳細說明)幾個向量的叉積.
注意:這裡我們就會注意到是在取樣點進行的計算, 所以這幾個引數不是物理意義上的點,偏差出現.但整體相對可接受.
float3 halfVector = normalize(lightDir + viewDir); //半形向量
float nl = max(saturate(dot(normal, lightDir)), 0.000001); //防止除0
float vh = max(saturate(dot(viewDir, halfVector)), 0.000001);
float lh = max(saturate(dot(lightDir, halfVector)), 0.000001);
float nh = max(saturate(dot(normal, halfVector)), 0.000001);
float nv = max(saturate(dot(normal, viewDir)), 0.000001);
為何是半形?
URL:
1 確定漫反射和鏡面反射的比例
這裡我們假定光線在介質交界處(預設為空氣到其他),只發生了漫反射和鏡面反射,兩者合起來輻射度為入射處光線輻射度,從而滿足一定意義上的能量守恆原則.
所以我們需要對這個比例進行計算,反過來說於分配輻射度.
L=Kd^Diff + Ks^Spec
其中這兩個引數,主要會受到介質的影響.
a\金屬度(或高光):
金屬度 | 表示了反射時發生鏡面反射和漫反射的光線的佔比。 |
---|---|
Metallic度越大 | 發生鏡面反射的佔比越大,漫反射diffuse佔比越小,一般金屬物體的金屬度比較大:70% ~ 100%之間; |
Metallic越小 | 發生鏡面反射的佔比越小,漫反射diffuse佔比越大,一般非金屬物質金屬度比較小;2% ~ 5%之間,寶石的大概8%; |
b\菲涅爾反射.
(這裡注意是金屬菲涅而F0是帶有顏色,具體我們在後詳細)
可參考程式碼如下:
loat3 F0 = float3(0.04,0.04,0.04);
//使用mix(lerp)函式利用 metallic對diffuse,最低值做插值,
//metallic越接近0,那麼就越接近絕緣體,
//metalness越接近1,那麼就越接近金屬的Diff。
F0 = lerp(F0, pBRDF.diffuse, pBRDF.metallic);
float3 F = BaseFresnelSchlick(vh,F0);
float3 kS = F;
float3 kD = float3(1.0,1.0,1.0) - kS;
kD *= 1.0 - pBRDF.metallic;
這裡我們也可以提前將Diff進行減弱.
先(1.0-0.04 ) - (pBRDF.metallic)(1.0-0.04 ),然後Diff = Diff *這個值來修復Diff
#define kDieletricSpec half4(0.04, 0.04, 0.04, 1.0-0.04)
half OneMinusReflectivityMetallic(half metallic)
{
half oneMinusDielectricSpec = kDieletricSpec.a;
return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}
有了這個比例我們可以先假定已經計算出漫反射和鏡面高光反射
float3 DirectLightResult = (kD * pBRDF.diffuse + specular) * lightColor * nl;
return DirectLightResult;
///------------直接光照部分--------------------
vLight = GetMainLight ();
lightDir= normalize(vLight.direction).xyz;
lightColor= vLight.color * (1-vLight.distanceAttenuation);
DirectLightResult += BRDF01_DirectLight(lightColor,normal,lightDir,viewDir,pBRDFData);
2 菲涅爾反射
定義:根據物理學,麥克斯韋方程組可以在折射率(IOR)變化時計算光的行為。對於空氣中通常的物體表面而言,物體的表面便是空氣折射率和物體折射率的交介面,對此特殊的折射率交介面而言,麥克斯韋方程組的解被我們稱為菲涅爾方程
a\萬物皆有菲涅爾效應?
在巨集觀層面看到的實際上是微觀層面微平面菲涅爾效應
(表示觀察看到的反射光線的量與視角相關的現象,且掠射角度(90度)
下反射率最大)的平均值,即影響菲涅爾效應的關鍵引數在於每個微平面
的法向量和入射光線的角度,而不是巨集觀平面的法向量和入射光線的角度
b\其中F0的含義是
F0即0度角入射的菲涅爾反射率。任意角度的菲涅爾反射率
b1 簡化的公式
可由F0和入射角度計算得出,而上文我們提到過不同的材質(物理意義上)
對演算法有一定的區別,比如非金屬的F0是一個float,金屬的F0是一個float3. 這
樣就暗示一個比較重要的問題如何去統一的處理.?總不能if/#if幾個吧, 況且
對美術而言很難去規定這個含義並記錄.
這裡電介質具有相當低的F0值 - 通常為0.06或更低, 故而我們用這個邊界值0.04來模擬一下
b2 通過IOR來處理
以上只是一種最簡化的求F0, 而光在傳播到兩種不同介質交界處時,原始光波和新的光波的相速度(Phase Velocity)的比率定義了介質的光學性質,就是折射率(Index of Refraction,IOR)故而我們還有通過IOR來實現.
而這個折射率,我們可以通過查詢表( 或者說將表編碼:https://link.zhihu.com/?target=https%3A//github.com/QianMo/PBR-White-Paper/raw/master/bonus/%255BPBR-White-Paper%255D%2520PBR-Material-F0-Quick-Reference-Chart.pdf 轉為紋理查詢等等方式)
(這裡補充F0為0度角入射時的菲涅爾反射率。而折射(refracted)到表面中的光量則為為1-F0。)
但我們大部分是空氣到其他,故而可以換成
//原始F0求法
inline float3 FresnelF0_IOR(float IOR1, float IOR2)
{
float IOROffset = IOR2 - IOR1;
float IORAdd = IOR2 + IOR1;
IORAdd = IORAdd >0?IORAdd:0.001;
return Pow2(IOROffset / IORAdd);
}
inline float3 FresnelF0_IOR(float IOR2)
{
float IOROffset = IOR2 - 1;
float IORAdd = IOR2 + 1;
return Pow2(IOROffset / IORAdd);
}
有了F0,我們接下來就可以選則不同的菲尼爾演算法來執行計算, 我們選取的法制包括了效能, 和接下來的法線分佈、幾何分佈以及整體的統一性做衡量.
(這裡額外說明 : 菲涅爾反射效應,和我們用來做一些效果的菲涅爾效果不要搞混淆,雖然在含義有類似部分, 其中菲涅爾效果等額外效果會在後面節點統一),
以下是一些實現: 這裡注意下傳入的是VH, 原因可以Google
inline float3 BaseFresnelSchlick(float HdotV, float3 F0)
{
return F0 + (1 - F0) * Pow5(1 - HdotV);
}
inline half3 FresnelLerp (half3 F0, half3 F90, half cosA)
{
half t = Pow5 (1 - cosA); // ala Schlick interpoliation
return lerp (F0, F90, t);
}
inline half3 FresnelLerpFast (half3 F0, half3 F90, half cosA)
{
half t = Pow4 (1 - cosA);
return lerp (F0, F90, t);
}
以及在間接光部分用的
inline float3 FresnelSchlickRoughness(float cosTheta, float3 F0, float roughness)
{
return F0 + (max(float3(1, 1, 1) * (1 - roughness), F0) - F0) * pow(1.0 - cosTheta, 5.0);
}
以及迪士尼
//傳入某個角度的f,得到漫反射的1-f
inline float DSL_SchlickFresnel(float f)
{
float fd = clamp(1-f, 0, 1);
return Pow5(fd);
}
//漫反射的菲尼爾係數。
//沒有次表面散射時,我們的F90:掠射角0.5, 垂直角度1
//並且基於粗糙度插值
inline float Fd_burley (float NdotL, float NdotV, float LdotH, float roughness){
float FL= DSL_SchlickFresnel(NdotL);
//F0時,NdotV=1. FV=0 Lerp(1, Fd90, FV) =1
//F90時,NdotV=0. FV=1 Lerp(1, Fd90, FV) =0
float FV= DSL_SchlickFresnel(NdotV);
float Fd90 = 0.5 + 2.0 * LdotH*LdotH * roughness;
return lerp(1, Fd90, FL) * lerp(1, Fd90, FV);
}
3 漫反射
這裡選取了DiffR= Diff/PI,當然也有其他公式, 限於篇幅, 這些放在後續單獨的章節實現總結.
這裡我們假定用最Easy的方式 : diffcuseResult = pBRDF.diffuse. 然後進行KD衰減.
float3 diffcuseResult = Kd * pBRDF.diffuse / PI;
//DSL
float FD90 = 0.5 + 2 * VoH * VoH * Roughness;
float FdV = 1 + (FD90 - 1) * Pow5( 1 - NoV );
float FdL = 1 + (FD90 - 1) * Pow5( 1 - NoL );
return DiffuseColor * ( (1 / PI) * FdV * FdL );
4 鏡面反射
NDF:法線分佈函式,(這裡我們先實現各向同性部分)
a\這個函式讓材質有了亞畫素級更精細的把控和更科學的定量, 是一種近似擬合. 通過BRDF進行建模,由粗糙度貼圖(Roughness Map)配合法線分佈函式,提供每亞畫素(subpixel)法線資訊
b\我們使用半向量h來表示微觀表面法線m,因為僅m = h的表面點的朝向才會將光線l反射到視線v的方向,其他朝向的表面點對BRDF沒有貢獻(正負相互抵消)
同時出現許多變化: 基本分為形狀可變,不變.
這是一個合法的法線分佈函式應該具備的特性
G:幾何函式:遮蔽+陰影
在基於物理的渲染中,幾何函式(Geometry Function)是一個0到1之間的標量,描述了微平面自陰影的屬性,表示了具有半向量法線的微平面(microfacet)中,同時被入射方向和反射方向可見(沒有被遮擋的)的比例,即
未被遮擋的m= h微表面的百分比
The Visibility Term
其中,在部分遊戲引擎和文獻中,幾何函式G(l,v,h)和分母中的校正因子4(n·l)(n·v)會合併為可見性項(The Visibility Term),Vis項,簡稱V項
Unity(預設Render)和Unreal
Unity:
UnityStandardBRDF(感興趣可以去看下這片Paper)
// Ref: http://jcgt.org/published/0003/02/03/paper.pdf
inline float SmithJointGGXVisibilityTerm (float NdotL, float NdotV, float roughness)
{
#if 0
// Original formulation:
// lambda_v = (-1 + sqrt(a2 * (1 - NdotL2) / NdotL2 + 1)) * 0.5f;
// lambda_l = (-1 + sqrt(a2 * (1 - NdotV2) / NdotV2 + 1)) * 0.5f;
// G = 1 / (1 + lambda_v + lambda_l);
// Reorder code to be more optimal
half a = roughness;
half a2 = a * a;
half lambdaV = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2);
half lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2);
// Simplify visibility term: (2.0f * NdotL * NdotV) / ((4.0f * NdotL * NdotV) * (lambda_v + lambda_l + 1e-5f));
return 0.5f / (lambdaV + lambdaL + 1e-5f); // This function is not intended to be running on Mobile,
// therefore epsilon is smaller than can be represented by half
#else
// Approximation of the above formulation (simplify the sqrt, not mathematically correct but close enough)
float a = roughness;
float lambdaV = NdotL * (NdotV * (1 - a) + a);
float lambdaL = NdotV * (NdotL * (1 - a) + a);
#if defined(SHADER_API_SWITCH)
return 0.5f / (lambdaV + lambdaL + 1e-4f); // work-around against hlslcc rounding error
#else
return 0.5f / (lambdaV + lambdaL + 1e-5f);
#endif
#endif
}
Unreal:
這裡我們注意一下,(引用:https://zhuanlan.zhihu.com/p/81708753)
最後總結: 可以根據擬合程度選取.或近似實現. 這裡要求選擇合適的微表面輪廓(microsurface profile),從而對G項進行具象化建模。但G對BRDF的整體影響不大,但能保證能量守恆,在我們不斷的分化過程會出現一定的比重.
具有相同法線分佈但具有不同輪廓(profiles)的微表面導致不同的BRDF
-
分離的遮蔽陰影型(Separable Masking and Shadowing)
-
高度相關的遮蔽陰影型(Height-Correlated Masking and Shadowing)
-
方向相關的遮蔽陰影型(Direction-Correlated Masking and Shadowing)
-
高度-方向相關遮蔽陰影型(Height-Direction-Correlated Masking and Shadowing)
基本要求
- 標量性
- 對稱性
- 同向可見性
- 拉伸不變性(Stretch Invariance)
最後給出一個測試的方法:
https://github.com/knarkowicz/FurnaceTest, 這部分會在接下來的HLSLLightFunction去實現,
F: 相對的菲涅爾反射方程結果
最後我們簡單的實現一下
//法線分佈: Input nh
float NDF = DistributionGGX(nh, pBRDF.roughness);
//幾何: Input Nv, NL
float G = GeometrySmith(nv, nl, pBRDF.roughness);
float3 nominator = NDF * G * F;
float denominator = 4.0 * nv * nl + 0.001;
float3 specular = nominator / denominator;
5 積分部分
微表面模型的半球積分, 這裡其實會涉及到蒙特卡洛積分(離散方式計算積分,概率搞一搞)
float3 DirectLightResult = (kD * diffcuseResult + specular) * lightColor * nl;
6 線性空間
還原現實世界方式的光與物質的互動的方式, 其中詳細的顏色空間部分在後續說明.
1、我們輸入的BaseColor(Diff), 金屬度, 粗糙度需要轉化到線性空間下, 保證後續的計算全部線上性空間.
2、完成後,需要再次將結果返回到伽馬空間, 這裡不一定是立馬(關鍵看我們的Pipeline)
7 色調對映
Toonmap: 通過演算法,將大於1的部分分散開來, 這樣可以保證過亮部分得以在視覺上保留.(具體同理在後續)
8 對部分特性的處理
(具體同理在後續)
三、說明
這裡梳理一下相關檔案結構
XDArt_RenderLightFunction.hlsl: 函式庫
XDArt_RenderLight_BRDF.hlsl : BRDF多個實現,BRDF輸入結構定義
XDArt_RenderLight_BSSRDF.hlsl: BSSRDF多個實現,BSSRDF輸入結構定義
XDArt_ShaderMode_DefaultLit.hlsl: 內建的多個LitMode實現
接下來就是具體使用, 和擴充套件
XDArt_ShaderMode_DefaultLit.shader
XDArt_ShaderMode_DefaultLit_RoughAO.shader
XDArt_ShaderMode_DefaultLit_Rough.shader
XDArt_ShaderMode_ClearCoat/Fab/Skin/Mon.shader.......等等