(八)unity自帶的着色器源碼剖析之——————Unity3D的全局光照和陰影:下篇(unity3D中的球諧光照和SH球諧函數、unity實時陰影抗鋸齒解決方案)

一、探針基於球諧函數的全局光照

球諧光照是基於預計算輻射度傳輸理論實現的一種實時渲染技術。預計算輻射度傳輸技術能夠重現在區域面光源照射下的全局照明效果。這種技術通過在運行前對場景中光線的相互作用進行預計算,計算每個場景中每個物體表麪點的光照信息,然後用球諧函數對這些預計算的光照信息數據進行編碼,在運行時讀取數據進行解碼,重現光照效果

球諧光照使用新的光照方程來代替傳統的光照方程,並將這些新方程中的相關信息使用球諧基函數投影到頻域,存儲成一系列的係數。在運行渲染過程中,利用這些預先存儲的係數信息對原始的光照方程進行還原,並對待渲染的場景進行着色計算。這個計算過程是對無限積分進行有限近似的過程。

簡單說,球諧光照的基本步驟就是把真實環境中連續的光照方程離散化,得到離散的光照方程,然後對這些離散的光照方程進行分解,分解後進行球諧變換,得到球諧係數,在運行時根據球諧係數重新還原光照方程。

關於球諧函數的數學公式推導,後面有空會補上這塊內容。

總結下球諧光照操作過程:

爲了求得原始光照計算函數light(s)的積分結果,選用若干組基函數Bi(s)和這些基函數對應的加權係數ci,將個組基函數和加權係數相乘,然後累加乘積值,這個累計值就近似等於原始函數light(s)z的積分結果。Bi(s)就是球諧函數,而ci則未知待求得。採樣若干個自變量si,得到每一個自變量si對應的原始計算函數和基函數乘積,再累加可得到ci。最後便可以求得原始光照計算函數積分近似計算結果。

二、Unity3D中的球諧光照

Unity3D內置shader庫中定義了一些工具函數,可以計算球諧光照。使用了3階的伴隨勒讓得多多項式作爲基涵,即l的最大取值爲2。直角座標系下3階球諧函數的係數如下所示,共9個,其中r=根號下x方+y方+z方。

最終的光照函數就是上表中每一項Y和基函數係數c的疊加,又因爲球諧光照所討論的球面空間是在一個單位球空間中的,所以上表的r項的值爲1,故而在考察的單位球球面上的位置點(x,y,z)組成的向量也是一個單位化的向量。

在重建光照過程中,因爲使用的光照顏色是RGB顏色,每一個分量都需要和這9個係數進行運算操作,並且因爲每個光顏色分量使用的波長不同,所以對應的c也不同。因此需要27個數字去做這些操作,這27個數字存儲在7個float4變量中,這7個float4變量再文件中定義,由引擎在程序運行時傳遞進來。

2.1球諧光照要用到的係數

 // SH lighting environment
    half4 unity_SHAr;
    half4 unity_SHAg;
    half4 unity_SHAb;
    half4 unity_SHBr;
    half4 unity_SHBg;
    half4 unity_SHBb;
    half4 unity_SHC;

上述代碼中,unity_SHAr的3個分量對應於表中l=1的各項Y的紅色光分量對應的c的乘積,最後一個分量對應於l=0時Y常數值與對應的c的乘積,unity_SHAg對應於綠光分量;untiy_SHAb對應於藍光分量。

2.2 SHEvalLinearL0L1函數

// normal should be normalized, w=1.0
//傳遞進來的normal參數是一個單位化的half4類型的向量,其w分量爲=1.0
//即單位球面的一點,又可視爲從單位球圓心到這一點的連線的向量
half3 SHEvalLinearL0L1 (half4 normal)
{
    half3 x;

    // Linear (L1) + constant (L0) polynomial terms
	//因爲normal.w的值爲1,所以unity_SHAr最後一項與normal.w的乘積和傳進來的球面上的點的位置沒有關係,這一項是重建光照效果時,l=0時的多項式Y(m,l)的值
    x.r = dot(unity_SHAr,normal);
    x.g = dot(unity_SHAg,normal);
    x.b = dot(unity_SHAb,normal);

    return x;
}

實質上就是把每個待計算的球面上的某個點傳遞給分別執行表中l=1時的各項多項式進行計算操作,得到各項的乘積之後,再疊加起來返回。這也是在此使用''各分量先各自相乘最後相加“的點積函數的原因。

2.3 SHEvalLinearL2函數

函數功能是計算l=2時的各個對應值。

// normal should be normalized, w=1.0
half3 SHEvalLinearL2 (half4 normal)
{
    half3 x1, x2;
    // 4 of the quadratic (L2) polynomials
    half4 vB = normal.xyzz * normal.yzzx;
    x1.r = dot(unity_SHBr,vB);
    x1.g = dot(unity_SHBg,vB);
    x1.b = dot(unity_SHBb,vB);

    // Final (5th) quadratic (L2) polynomial
    half vC = normal.x*normal.x - normal.y*normal.y;
    x2 = unity_SHC.rgb * vC;

    return x1 + x2;
}

上面代碼中half4 vB=normal.xyzz*normal.yzzx,就是構造表中l=2時左數前4項的多項式中的xy、yz、z方、xz。變量half vC=normal.x*normal.x-normal.y*normal.y就對應表中l=2右數第一項中的x方-y方。有了計算l=0,1,2時的光照結果的函數,便可以把他們組合起來,提供3階的球諧光照計算,函數ShadeSH9就用於實現該功能。

2.4 ShaderSH9 函數

// normal should be normalized, w=1.0
// output in active color space
half3 ShadeSH9 (half4 normal)
{
    // Linear + constant polynomial terms
	//計算l=0,1時的光照結果
    half3 res = SHEvalLinearL0L1 (normal);

    // Quadratic polynomials
	//計算l=2時的光照結果並累加上一個計算結果
    res += SHEvalLinearL2 (normal);
	//光照結果是在線性空間中定義的,如果啓用了伽馬空間則要換算
#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif

    return res;
}

函數ShaderSH9就是把SHEvalLinearL0L1和SHEvalLinearL2兩個函數的計算結果疊加起來,然後根據伽馬空間的宏是否啓用決定是否要把計算結果從線性顏色空間中,變換到伽馬空間中。

ShadeSH3Order函數則是隻保留l=2時的重建光照計算部分的ShaderSH9函數,在本版本中該函數不在使用,如下。

2.5 ShadeSH3Order函數

// OBSOLETE: for backwards compatibility with 5.0
half3 ShadeSH3Order(half4 normal)
{
    // Quadratic polynomials
    half3 res = SHEvalLinearL2 (normal);

#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif

    return res;
}

2.6 ShaderSH12Order函數

// normal should be normalized, w=1.0
half3 ShadeSH12Order (half4 normal)
{
    // Linear + constant polynomial terms
    half3 res = SHEvalLinearL0L1 (normal);

#   ifdef UNITY_COLORSPACE_GAMMA
        res = LinearToGammaSpace (res);
#   endif

    return res;
}

2.7 unity_ProbeVolumeSH變量

#if UNITY_LIGHT_PROBE_PROXY_VOLUME
    UNITY_DECLARE_TEX3D_FLOAT(unity_ProbeVolumeSH);

2.8 宏UNITY_DECLARE_TEX3D_FLOAT

#if defined(UNITY_COMPILER_HLSLCC) && !defined(SHADER_API_GLCORE) // GL Core doesn't have the _half mangling, the rest of them do.
    #define UNITY_DECLARE_TEX3D_FLOAT(tex) Texture3D_float tex; SamplerState sampler##tex
    #define UNITY_DECLARE_TEX3D_HALF(tex) Texture3D_half tex; SamplerState sampler##tex
#else
    #define UNITY_DECLARE_TEX3D_FLOAT(tex) Texture3D tex; SamplerState sampler##tex
    #define UNITY_DECLARE_TEX3D_HALF(tex) Texture3D tex; SamplerState sampler##tex
#endif

如果着色器編譯器使用HLSLcc編譯器,並且不是把Cg/HLSL代碼轉換爲GLSL代碼, UNITY_DECLARE_TEX3D_FLOAT(unity_ProbeVolumeSH)的含義是聲明一個Texture3D_float類型的紋理變量unity_ProbeVolumeSH,同時聲明一個SamplerState類型的採樣器狀態變量,其名字爲samplerunity_ProbeVolumeSH;如果使用其他着色器編譯器,無論是FLOAT還是HALF就統一不加類型大小後綴,unity_ProbeVolumeSH統一聲明成Texture3D類型。

Cg/HLSL代碼中聲明誒Texture3D類型的紋理是一種異於一般二維的稱爲立體紋理的紋理。立體紋理是傳統二維紋理在邏輯上的擴展。二維紋理是一張簡單的位圖片,用來給三維模型提供表面顏色值;而一個三維紋理可以認爲是由多張二維紋理堆疊而成的,用於描述三維空間數據的圖片。立體紋理通過三維紋理座標進行訪問。立體紋理可以有一系列細節級別,每一級都較上一級縮小1/2。

Unity還提供了對立體紋理進行採樣操作的宏,根據使用不同着色器編譯和目標平臺這個採樣操作宏對應着不同的實現。

2.9 UNITY_SAMPLE_TEX3D_SAMPLER宏版本 1

//這個宏使用了Direct3D 11版本的語法
 #define UNITY_SAMPLE_TEX3D_SAMPLER(tex,samplertex,coord) tex.Sample (sampler##samplertex,coord)

2.10 UNITY_SAMPLE_TEX3D_SAMPLER宏版本 2

  //這個宏使用了Direct 3D 9版本的語法
#define UNITY_SAMPLE_TEX3D_SAMPLER(tex,samplertex,coord) tex3D (tex,coord)

2.11 和光照探針代理體相關的着色器變量

CBUFFER_START(UnityProbeVolume)
        // x = Disabled(0)/Enabled(1)
        // y = Computation are done in global space(0) or local space(1)
        // z = Texel size on U texture coordinate
        float4 unity_ProbeVolumeParams;

        float4x4 unity_ProbeVolumeWorldToObject;
        float3 unity_ProbeVolumeSizeInv;
        float3 unity_ProbeVolumeMin;
    CBUFFER_END
#endif

unity_ProbeVolumeParams變量的4個分量函數:x分量爲1時表示啓用本光照體代理體,爲0時表示不啓用;y分量爲0時表示在世界空間中進行計算,爲1時表示在代理體的模型空間中計算;z分量則表示體積紋理的寬度方向上紋素的大小。假如該方向上紋素的個數爲64,則該值爲1/64。

unity_ProbeVolumeWorldToObject定義了從世界空間轉換到光探針代理體局部空間的變換矩陣。unity_ProbeVolumeSizeInv是該光探針代理體的長寬高的倒數。unity_ProbeVolumeMin是該光探針代理體左下角的x、y、z座標。

2.12 SHEvalLinearL0L1_SampleProbeVolume函數

#if UNITY_LIGHT_PROBE_PROXY_VOLUME

// normal should be normalized, w=1.0
half3 SHEvalLinearL0L1_SampleProbeVolume (half4 normal, float3 worldPos)
{
	//根據當前空間的取值,判斷是在局部空間還是在全局空間中處理光照探針
    const float transformToLocal = unity_ProbeVolumeParams.y;
	//取得紋理U座標方向上的紋素的大小,假如U方向的紋素個數爲64,則紋素大小爲0.015625
    const float texelSizeX = unity_ProbeVolumeParams.z;
	//吧球諧函數的3階係數以及探針遮蓋信息打包到一個紋素中
    //The SH coefficients textures and probe occlusion are packed into 1 atlas.
    //-------------------------
    //| ShR | ShG | ShB | Occ |
    //-------------------------

	//如果在局部空間中處理,就要把待處理點乘一個從世界座標到局部空間上的矩陣轉換回去
    float3 position = (transformToLocal == 1.0f) ? mul(unity_ProbeVolumeWorldToObject, float4(worldPos, 1.0)).xyz : worldPos;
	//根據當前傳遞進來的位置點的座標,求得當前位置點相對於光探針代理體的最左下角位置點的偏移,然後各種除以光探針代理體的長寬高,得到歸一化的紋理映射座標
    float3 texCoord = (position - unity_ProbeVolumeMin.xyz) * unity_ProbeVolumeSizeInv.xyz;
    texCoord.x = texCoord.x * 0.25f;

    // We need to compute proper X coordinate to sample.
    // Clamp the coordinate otherwize we'll have leaking between RGB coefficients
	
    float texCoordX = clamp(texCoord.x, 0.5f * texelSizeX, 0.25f - 0.5f * texelSizeX);

    // sampler state comes from SHr (all SH textures share the same sampler)
	//依次取得對紅、綠、藍顏色分量的球諧函數係數
    texCoord.x = texCoordX;
    half4 SHAr = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord);

    texCoord.x = texCoordX + 0.25f;
    half4 SHAg = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord);

    texCoord.x = texCoordX + 0.5f;
    half4 SHAb = UNITY_SAMPLE_TEX3D_SAMPLER(unity_ProbeVolumeSH, unity_ProbeVolumeSH, texCoord);

    // Linear + constant polynomial terms
    half3 x1;
    x1.r = dot(SHAr, normal);
    x1.g = dot(SHAg, normal);
    x1.b = dot(SHAb, normal);

    return x1;
}
#endif

在預計算階段,光探針把空間中某一點的球諧函數係數編碼進一個立體紋理中,在運行時再從立體紋理中採樣得到係數,然後根據該點的法線值重建出光照效果。SHEvalLinearL0L1_SampleProbeVolume函數就是實現該功能。該函數從紋理中採樣得到的顏色信息,其數值就是當l=1時和l=0時的各項Y與對應的C的乘積。

SHEvalLinearL0L1_SampleProbeVolume函數把對紋理進行採樣時使用的紋理映射座標按R、G、B分段壓縮到長度爲0.25的一個段中,在對顏色取樣時就需要重新計算出各R、G、B分量對應的紋理映射座標,上面代碼已經註釋瞭如何計算,流程如下圖:

三、引擎中的渲染陰影的功能

 Unity3D支持一個遊戲對象所表徵的物體投射陰影到其他在它附近的物體表面上,或者它自身的表面上。在畫面上顯示陰影效果,可以有效提升場景的縱深感和真實感。

3.1 在光源空間中確定產生陰影的區域

確定產生陰影的區域方法是把光源想象成一個攝像機(暫稱光源相機),光源相機的位置就是光源位置,光源相機的朝向就是光線發出的方向。在要繪製的場景中的物體前,用光源相機對場景執行一次取景操作。這個取景操作不做任何繪製處理,僅把在光源相機所在角度所有可視的片元深度信息存儲在一個幀緩衝區(frame buffer)中,稱爲陰影貼圖(shadowmap)。在真正渲染時,把每一個待輸出的片元再次放到光源相機的角度下去計算深度值。如果這次計算的深度值比深度圖中國的深度值要離光源相機遠,就表示它落在某個陰影區域了。Unity採用的算法原理其實就是這一種。

3.2 在屏幕空間中確定產生陰影區域

除了在光源空間中就能確定陰影覆蓋範圍外,還可以基於屏幕空間確定產生陰影的區域,有下面步驟:

1.從當前攝像機位置處進行取景操作,這個取景操作不做任何繪製處理,僅將當前攝像機所在角度下的所有可視片元的深度信息存儲在一個深度紋理(第一紋理)中,如果是延遲渲染,深度紋理原本就已經存在了,就在G-Buffer中。如果是前向渲染,就需要做一遍取景操作生成此深度紋理。

2.利用光源相機對場景執行一次取景操作。這次取景操作也是不做任何繪製處理,僅將在光源相機所在的角度下的所有可視片元的深度信息存儲在深度紋理中。

3.在屏幕空間中,當前攝像機做一次陰影收集計算,這個蒐集過程的流程是:當前最終要繪製的每一個片元,根據它在深度紋理中對應的深度值D1,反求得該片元在世界空間中的座標。接着把這個座標從世界空間轉換到光源空間中得到陰影座標,用該陰影座標在深度紋理中進行採樣,得到深度值D2,如果D1>D2,表示本片元光源無法照射到,所以是在陰影內。把這些“某片元是否在陰影內,如果在的話該處的陰影有多濃”等信息存儲進一個紋理中,便形成最終的屏幕空間陰影貼圖。

最後再渲染場景物體計算其陰影時,只需要在片元着色器中按照當前處理的片元在屏幕空間中的位置,對第三步生成的屏幕空間陰影進行貼圖採樣即可。

3.3 如何啓用陰影

在unity中產生陰影需要遊戲對象的多個組件共同配合指定。首先在每一個可渲染gameobject上的MeshRenderer組件上有沒有Cast Shadow和Receive Shadow兩個屬性。除此之外,對每一個光源上的Light組件,也需要指定它的相關陰影屬性去控制光源投射陰影,如下表:

 3.4 透視走樣和層疊式陰影貼圖

當使用陰影貼圖時,通常會有透視走樣(perspective aliasing)的問題,透視走樣是指陰影越靠近攝像機,其邊緣的鋸齒化(走樣)現象就越嚴重,這是因爲陰影貼圖的分辨率是固定的,而透視投影的效果是近大遠小,同樣大小的一個陰影所對應的陰影貼圖中紋素的大小也是固定的。而在渲染時,當陰影越靠近攝像機,就越容易出現多個片元從陰影貼圖中的同一個紋素進行採樣的情況,這幾個片元得到的同一個陰影值,因而產生鋸齒邊。

使用更高分辨率的陰影貼圖可以降低邊緣鋸齒,但是這樣會在渲染時佔用更多的內存和帶寬。層疊式陰影貼圖(cascaded shadow map,CSM)可以較好兼顧精度和性能的問題。這種技術與使用單個陰影貼圖的區別主要在於:將攝像機的視截體(view frustum)按一定比例分成若干層級(cascade),每個層級對應一個子視截體,每一層級單獨計算相關的陰影貼圖,這樣在渲染大場景的陰影時,就可以避免使用單張陰影貼圖的各種缺點。Unity3D引擎使用層疊式陰影貼圖技術實現了大部分的陰影。層疊式陰影貼圖原理如下圖所示:

要使用層疊式陰影貼圖技術,首先要爲每一個層次對應的子視截體構建一個投影矩陣。在構建投影矩陣時,必須要使得生成的陰影貼圖中,不在當前視野範圍內的無關區域儘可能少(或者說生成陰影貼圖的有效分辨率要儘可能高)。也就是說,要計算出一個和當前層級所對應的子視截體儘可能重合的投影矩陣。這個投影矩陣一般使用正交投影矩陣,該投影矩陣由一個能包住子視截體的,且與光源空間座標系軸對齊的軸對齊包圍盒所對應生成

 因爲在渲染時,攝像機的位置朝向等屬性會即時改變,所以它對應的視截體,以及每個層級對應的子視截體也會不斷變換,子視截體的軸對齊包圍盒也要跟着對應變化。這樣可能導致出現先後兩幀中軸對齊包圍盒發生突變,進而導致生成的陰影貼圖的有效分辨率可能在這連線的兩幀中也發生突變,產生陰影抖動(flickering)問題。解決這個問題的方法之一是把使用軸對齊包圍盒改爲使用包圍球,因爲包圍球隨着子視截體的變化而發生大小變化的程度相對軸對齊包圍盒而言要小得多,如下圖所示:

更好的解決方案是一種稱爲“漸進式變換視截體”的方法,該方法在改善陰影抖動問題的同時,還能在一定程度上改善因爲光柵化帶來的抖動的問題。

Unity引擎在Quality Settings對話框中提供了和層疊式陰影設置相關的選項,如下圖所示,可以指定把視截體分爲多少個層級,每個層級各佔的比例是多少。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章