1. 程式人生 > 實用技巧 >XDRender_ShaderMode_StandardLit(1) 物理渲染-預設PBR-實現(1)

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)。
    從表面折射入介質的光,會發生吸收(absorption)和散射(scattering),而介質的整體外觀由其散射和吸收特性的組合決定,其中:
  • 散射(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.......等等

備註