Unity SRP自定义渲染管线 -- 5.Directional Shadows
原文:https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/directional-shadows/
- 支持多個方向光陰影
- 控制陰影距離
- 定義獨立的主光源
- 渲染和采樣級聯(lián)陰影(cascaded shadow map)
- 使用球形剔除
1.?Shadows for Directional Lights
直射光和聚光源在概念上沒有什么本質(zhì)不同。除了直射光來自無限遠(yuǎn)的地方,兩者基本相同。所以只需要一些調(diào)整,就可以讓聚光源陰影的方法適用于直射光。我們將進(jìn)一步改進(jìn)我們的渲染管線,讓它能混合直射光和聚光源的陰影。
1.1?Configuring Shadows
目前ConfigureLights中只處理了聚光源的陰影數(shù)據(jù)。而處理直射光源陰影的代碼和聚光源的是相同的。為了重復(fù)利用這些代碼,我們對代碼進(jìn)行一些重構(gòu),把它們寫成單獨的方法。
與聚光燈相比,渲染方向光陰影貼圖時有些不同,因此需要在處理時指明它是直射光。我們用陰影數(shù)據(jù)的z分量來作為判斷的標(biāo)識
1.2?Rendering Shadows
對于方向光,我們需要用ComputeDirectionalShadowMatricesAndCullingPrimitives函數(shù)來獲得裁剪信息,然而聚光源是用ComputeSpotShadowMatricesAndCullingPrimitives函數(shù)。重構(gòu)部分代碼,聲明一個bool變量,用該變量保存是否有有效的方向光或者聚光源。
如果陰影數(shù)據(jù)表明我們處理的是方向光,就調(diào)用ComputeDirectionalShadowMatricesAndCullingPrimitives?。這個方法有更多的參數(shù),因為它支持陰影級聯(lián)(這里我們先不用)。第一個參數(shù)要求提供一個光源序列,接著是級聯(lián)的序列以及級聯(lián)的數(shù)量。我們現(xiàn)在用不到級聯(lián),所以序列設(shè)為0,數(shù)量設(shè)為1。之后是定義了級聯(lián)分級的三維向量,我們使用(1,0,0)。然后是整型的圖塊尺寸和陰影近平面值。最后是投影矩陣等輸出參數(shù)。?
splitData數(shù)據(jù)中包含了一個有效的剔除球體。該球體包裹了所有需要被渲染進(jìn)直射光陰影貼圖的物體。這對于直射光來說非常重要,因為直射光不像聚光源,他會影響所有物體,我們需要有一個剔除球體來限制渲染進(jìn)陰影貼圖的圖形數(shù)量。對于聚廣源該設(shè)置沒有效果,沒有什么影響。
1.3?Shadow Distance
此時我們應(yīng)該可以得到方向光的陰影貼圖了。但是看起來我們得到的貼圖是空的,即使有也可能只是幾個小點。造成這個現(xiàn)象的原因是這個貼圖需要涵蓋的范圍太廣了,覆蓋了攝像機能夠看到的所有東西,默認(rèn)是1000單位,由相機的遠(yuǎn)平面控制。想看到貼圖的內(nèi)容就要大幅降低相機遠(yuǎn)平面距離。
但陰影的渲染范圍并不應(yīng)該取決于相機遠(yuǎn)平面,這只是默認(rèn)的情況。陰影距離也能控制陰影的渲染范圍。陰影距離通常遠(yuǎn)小于相機的遠(yuǎn)平面。兩者綜合考慮既可以限制陰影渲染的范圍,又可以處理比陰影距離更小范圍的情況。
在MyPipeline中為陰影距離添加一個字段,并在構(gòu)造方法中設(shè)置。我們從相機中提取陰影距離并賦值給剔除參數(shù)。因為渲染相機可見范圍外的陰影沒有意義,所以使用陰影距離和相機遠(yuǎn)平面中的最小值更合理。
在MyPipelineAsset中為陰影距離添加一個配置選項,并設(shè)置一個合理的默認(rèn)值,比如100。
1.4?Investigating Shadows
陰影距離降低到合理的值,直射光的陰影終于出現(xiàn)了。我們將光源的陰影偏移設(shè)為0并使用一個更大的平面作為地面。使用單個直射光照射。
因為偏移設(shè)為了0,我們可以大致看到陰影貼圖覆蓋的區(qū)域。和聚光源的陰影不同,方向光的陰影貼圖隨相機移動而改變。另外,陰影貼圖的邊緣也會影響超出陰影范圍的場景物體,這是因為我們采樣坐標(biāo)會超出貼圖邊緣(結(jié)果會采樣邊緣的值,因為我們紋理設(shè)置的是clamp),這導(dǎo)致貼圖的邊緣被拉伸至無窮遠(yuǎn)。當(dāng)我們的陰影光源多于一個時,拉伸消失了,因為我們?yōu)槊總€圖塊使用scissoring清理了邊緣。
然而當(dāng)采樣多個超出范圍的tiles時我們會得到錯誤的結(jié)果,tiles越多,結(jié)果越糟糕。?
1.5?Clamping to Shadow Tile
方向光陰影貼圖有些麻煩是因為當(dāng)它們被采樣時,無論物體是否在陰影貼圖的覆蓋范圍內(nèi)都會進(jìn)行采樣。解決方法就是限制陰影采樣坐標(biāo)在tile內(nèi)。我們在縮放到正確的圖塊前將陰影空間位置限制在0-1之間。之前沒有做限制我們可以在MyPipeline里完成整個轉(zhuǎn)換矩陣的計算,現(xiàn)在我們不得不把這一步移到shader里了。
tile scale信息對于變換時是必須的,我們將其傳入shader。聲明一個全局的vector用于保存陰影信息,將其命名為_GlobalShadowData?并持有它的標(biāo)識符。
offset信息也是必需的,我們將其存儲在ZW分量中。
之后移除對圖塊矩陣的乘法計算并在shader中在陰影緩存區(qū)添加一個全局陰影向量
在ShadowAttenuation函數(shù)中,在透視除法后對陰影位置的xy坐標(biāo)做限制,將其限制在0-1范圍內(nèi),之后再應(yīng)用圖塊的坐標(biāo)變換。?
直射光陰影需要透視除法嗎?
不需要,因為直射光陰影貼圖用的是正交投影。陰影位置的w向量恒為1,但是我們要混合直射光和聚光源,所以我們統(tǒng)一執(zhí)行透視除法。
1.6?Always Use Scissors
我們解決了在多個圖塊時的陰影雜亂( shadow soup)問題,但是當(dāng)場景中僅有一個方向光時陰影貼圖的邊緣仍會被拉伸。我們在RenderShadows中設(shè)置成無論幾個光源都使用裁剪來解決這個問題。
1.7?Clipping Shadows Based on Distance
盡管陰影的距離是根據(jù)相機視角的距離,但是陰影并不是在當(dāng)物體離開這個范圍后立刻消失。這是因為陰影貼圖覆蓋了一個立方體的空間區(qū)域,只要這個區(qū)域的一部分在范圍內(nèi),就會整個都渲染。方向光隨著相機的移動陰影貼圖也會重新渲染,所以對于這一問題方向光很合適,但是聚光源就不一樣了,它的陰影空間區(qū)域和光源鎖定,即使陰影距離只占空間的很小一部分,最終渲染的還是整個區(qū)域。結(jié)果就是聚光燈陰影貼圖包含的所有陰影同時出現(xiàn)和消失。
我們可以使用配置的陰影距離來剪切陰影使陰影消失的邊界線更統(tǒng)一。要想這么做,就得把陰影距離傳給shader。我們將它放在全局陰影數(shù)據(jù)向量的第二個分量中。在我們實際裁剪時,我們用的是它的平方來進(jìn)行比較,所以我們就直接存儲陰影距離的平方。
在shader中,我們還需知道相機的位置。Unity在配置相機的時候就會自動提供這個信息。所以我們要做的只是在UnityPerCamera?緩存區(qū)中添加一個_WorldSpaceCameraPos?變量。
創(chuàng)建一個DistanceToCameraSqr函數(shù),該函數(shù)輸入世界位置,輸出與相機的平方距離。
在ShadowAttenuation中調(diào)用這個方法,檢查是否超出了陰影距離,如果是就跳過陰影采樣。
現(xiàn)在所有的陰影都在相同的距離消失,而不會突然的出現(xiàn)和消失了。
我們可以平滑的過渡陰影嗎?
你可以添加一個漸變距離并使用一些過渡函數(shù)來實現(xiàn),如線性插值,smoothstep等。
2.?Cascaded Shadow Map
陰影貼圖的缺點就是作為紋理,其分辨率必然是有限的。雖然你可以提高紋理分辨率來獲得更好的效果,但仍沒有擺脫這個限制。聚光源只覆蓋了一小塊區(qū)域,所以它的效果可以接受。但對于方向光,它的照射范圍是無限大的。在視野遠(yuǎn)處的陰影效果也許還可以接受,但是近處的陰影卻會顯得非常塊狀。我們稱之為透視鋸齒(perspective aliasing)
我們需要給近處的陰影提供更高的分辨率,遠(yuǎn)處的可以分辨率低一些。我們可以根據(jù)距離使用不同的分辨率,解決方案就是為同一個光源渲染多張陰影貼圖。我們在近處使用高分辨率陰影貼圖,在遠(yuǎn)處使用低分辨率。這些陰影貼圖稱之為級聯(lián)陰影(shadow cascade)
2.1?Cascade Amount
Unity通常為級聯(lián)陰影數(shù)量提供三個選項:0、2、4。我們也一樣,在MyPipelineAsset添加一個ShadowCascades枚舉用于配置數(shù)量,默認(rèn)為4。
2.2?Cascade Split
Untiy還允許指定級聯(lián)在陰影距離中的分布情況。通過將整個陰影距離劃分成二或四個部分來實現(xiàn)。如果是2個級聯(lián),就用一個值來決定在哪里劃分兩者。如果是四個級聯(lián),就用存儲在向量中的三個值,將陰影距離劃分成四個部分。我們使用與輕量級渲染管線相同的默認(rèn)值。
但是Unity不會直接將這些值暴露在檢視面板,而是顯示一個特殊的GUI控件來允許你調(diào)整級聯(lián)的區(qū)域。我們也來實現(xiàn)這種效果,先把這些屬性隱藏起來。
我們需要創(chuàng)建一個自定義編輯器來顯示聯(lián)級劃分的GUI,我們先創(chuàng)建一個最基礎(chǔ)的。將它的腳本資源放在Editor文件夾中。獲取三個相關(guān)屬性,并繪制默認(rèn)的檢視器。我們還需要使用UnityEditor.Experimental.Rendering命名空間
在繪制默認(rèn)檢視器之后使用switch語句來決定我們繪制哪種級聯(lián)的GUI。使用CoreEditorUtils.DrawCascadeSplitGUI?函數(shù)去繪制,之后調(diào)用序列化對象的ApplyModifiedProperties?方法來確保用戶的修改可以應(yīng)用到我們的資源中。
?MyPipeline只需要知道要使用多少級聯(lián)以及他們的分布值是多少。我們可以使用單個三維向量同時處理二段和四段級聯(lián)的分布數(shù)據(jù)。按要求添加字段和構(gòu)造參數(shù)。
當(dāng)MyPipelineAsset調(diào)用渲染管線的構(gòu)造方法是,總是要求傳入一個分布情況向量,即使實際是二段級聯(lián)。這這種情況下,我們將唯一的分布值作為向量的第一個分量,另外兩個設(shè)為0。
2.3?Cascades for Main Directional Light Only
我們不為所有的方向光都提供級聯(lián)陰影功能,因為渲染多個陰影貼圖性能消耗很大。我們將最明亮最重要的一個方向光源作為主光源,為其提供級聯(lián)陰影,其他方向光只提供單陰影貼圖。
主光源總是可見光列表中的第一個元素。我們可以在ConfigureLights中判斷第一個光源是否符合標(biāo)準(zhǔn),如果是方向光、陰影強度為正數(shù),并且開啟了陰影級聯(lián),那就說明是有效的主光源。我們用一個bool字段來記錄這個情況。
我們會為主光源提供單獨的渲染貼圖,所以當(dāng)我們擁有主光源時,讓圖塊計數(shù)減1,并且在RenderShadows函數(shù)中將其從常規(guī)陰影貼圖渲染中排除。
將級聯(lián)陰影以tiles的形式渲染到一張單獨的陰影貼圖中,將其命名為_CascadedShadowMap。添加相關(guān)的標(biāo)識符和字段。并在最后和其他陰影貼圖一樣釋放紋理資源。
2.4?Reusing Code
渲染級聯(lián)陰影和之前我們做的陰影渲染很相似,但是其中的差異還是有必要用一個單獨的方法才能完成。然而這兩個方法里許多代碼都是重復(fù)的,我們把這部分代碼重構(gòu)成單獨的方法。
首先是關(guān)于陰影渲染目標(biāo)的設(shè)置,兩者在這部分的代碼是相同的。我們只需要用兩個字段記錄兩者渲染目標(biāo)對應(yīng)的渲染紋理即可。
然后是設(shè)置陰影tiles。計算tiles偏移,設(shè)置視口以及剪裁,偏移值可以用二維向量返回值得到。
再然后,計算world-to-shadow矩陣這部分也可以放在一個單獨的方法里,我們將視角和投影矩陣作為引用類型的參數(shù)傳入,這樣可以避免不必要的拷貝變量。同樣,將world-to-shadow矩陣作為輸出參數(shù)。
最后,調(diào)整RenderShadows函數(shù)使其使用重構(gòu)后的函數(shù)。
2.5?Rendering Cascades
級聯(lián)陰影的world-to-shadow矩陣需要單獨存儲在數(shù)組中,添加對應(yīng)的字段,因為我們級聯(lián)數(shù)量最多為4,所以數(shù)組大小設(shè)置為4。
創(chuàng)建一個RenderCascadedShadows方法,首先復(fù)制RenderShadows的代碼。接下來就簡單了,我們不需要考慮聚光源并且只會用到第一個光源。我們不需要處理每個光源的陰影數(shù)據(jù),而且陰影設(shè)置肯定是開啟級聯(lián)的。級聯(lián)不是四段就是兩段,也就是說陰影貼圖總是分為四個tile。
在調(diào)用ComputeDirectionalShadowMatricesAndCullingPrimitives時,我們光源序列為0,并使用for循環(huán)的迭代值作為級聯(lián)序列。在這里我們就需要提供實際的級聯(lián)數(shù)量和分布向量了。最后,我們再把圖塊的坐標(biāo)轉(zhuǎn)換附加到world-to-shadow中,在tile界限范圍內(nèi)渲染陰影是之后shader能正確采樣到級聯(lián)陰影的重要前提。
在ConfigureLights后,如果有主光源調(diào)用該函數(shù)
現(xiàn)在我們最終可能有0張,一張或者兩張渲染紋理。如果只有主光源,只需要渲染級聯(lián)陰影貼圖。如果有另外帶陰影的光源還需要渲染常規(guī)的陰影貼圖。或者我們有陰影但沒有主光源,那我們就只需要常規(guī)陰影貼圖。如果你在frame debugger中檢查級聯(lián)陰影貼圖,你會看到它由四個圖塊組成。它們內(nèi)容是否可見取決于陰影距離和級聯(lián)分布。
2.6 Sampling the Cascaded Shadow Map
在shader里使用級聯(lián)陰影貼圖,需要做一些事。首先,我們得知道使用軟陰影還是硬陰影的方式采樣貼圖,這一點我們可以通過shader關(guān)鍵字來控制。我們使用兩個關(guān)鍵字來區(qū)分級聯(lián)的軟硬陰影,省去了在shader中創(chuàng)建分支的需要。
接下來我們需要知道陰影貼圖的尺寸和陰影強度,雖然我們可以直接用_ShadowMapSize但是為了讓shader能分開處理兩者的大小,我們使用單獨的_CascadedShadowMapSize?來表示。
在RenderCascadedShadows函數(shù)末尾設(shè)置這些值和關(guān)鍵字。
同樣也需要在RenderCascadedShadow?沒有被調(diào)用的情況下關(guān)閉級聯(lián)陰影的關(guān)鍵字
在Lit?shader中為級聯(lián)陰影關(guān)鍵字添加多重編譯指令。共有三個選項:無/級聯(lián)軟陰影/級聯(lián)硬陰影。
之后向shadow buffer中添加所需的變量,定義級聯(lián)陰影貼圖紋理和采樣器
添加一個表明是否采樣級聯(lián)陰影的bool參數(shù)來使HardShadowAttenuation既可以用于常規(guī)的陰影采樣也可以用于級聯(lián)陰影,參數(shù)默認(rèn)為false。用這個bool值來決定具體使用哪張紋理和采樣器。我們使用的bool參數(shù)是硬編碼的,也就是說在實際編譯時并不會產(chǎn)生條件分支。
SoftShadowAttenuation?也一樣,不過在這里只需要選擇正確的紋理就好了,其余的由HardShadowAttenuation?函數(shù)完成,沒必要再寫一遍。
創(chuàng)建一個CascadedShadowAttenuation?方法,他就像ShadowAttenuation的簡化版。如果沒有級聯(lián)陰影,衰減直接設(shè)為1,反之才會計算陰影位置獲取軟硬陰影的衰減值并應(yīng)用應(yīng)用強度。
選擇正確的級聯(lián)陰影貼圖是下一節(jié)介紹的內(nèi)容,這一節(jié)我們先硬編碼統(tǒng)一使用第三張級聯(lián)陰影貼圖,也就是使用_WorldToShadowCascadeMatrices?中序列為2的轉(zhuǎn)換矩陣。如果你用的是四段級聯(lián),使用第三張貼圖可以讓我們看到大部分區(qū)域的陰影。如果用第四張的話區(qū)域是大了,但是近處的陰影分辨率太低,影響觀察。
接下來,創(chuàng)建一個MainLight函數(shù)來計算主光源,它和DiffuseLight方法做的事一樣,但是限制了只計算索引為0的方向光,并且使用CascadedShadowAttenuation?來獲取陰影
如果有級聯(lián)陰影,就把主光源也加入LitPassFragment?計算漫反射總和中。?
主光源的級聯(lián)陰影現(xiàn)在終于能夠看到了,但是主光源在光照循環(huán)中被計算了兩次,這是錯誤的。我們不能簡單地跳過循環(huán)中的第一個光源,因為對每個物體而言,無法確保主光源就是最重要的那個光源。對此我們要么在shader的循環(huán)中添加分支,要么渲染前就干脆將主光源移出可見光列表。我們選擇后者,修改中ConfigureLight?的光源數(shù)量。這樣的副作用就是當(dāng)我們有主光源時,像素光數(shù)量上限變成了5個。
從可見光列表中移除主光源的問題是如果我們使用了級聯(lián)陰影,每一幀都會修改可見光列表,從而導(dǎo)致臨時內(nèi)存的分配。現(xiàn)在也沒什么好辦法,除非以后會出一個不會分配新數(shù)組的GetLightIndexMap?方法。
2.7?Selecting the Correct Cascade
現(xiàn)在主光源的級聯(lián)陰影貼圖終于能用了,但用的都是同一級別的級聯(lián)貼圖。第三張級聯(lián)貼圖對于遠(yuǎn)處的陰影效果挺好,但是對于近處效果就很差。而第二張級聯(lián)貼圖恰恰相反,近處表現(xiàn)的很好,但是范圍實在太小了,遠(yuǎn)處根本沒陰影。
Unity使用級聯(lián)分段值劃分每個級聯(lián)貼圖負(fù)責(zé)的陰影空間區(qū)域。它使用一個剔除球體來定義每個級聯(lián)貼圖的范圍。剔除球的半徑依次增加,球的位置也同樣。想知道我們使用哪一等級的級聯(lián)貼圖,我們就得找出片元處于哪個剔除球內(nèi)部。
我們要把剔除球的信息傳給shader。使用數(shù)組是最方便的方法。在MyPipeline中添加對應(yīng)的標(biāo)識符和字段。用四維向量表示每個球。xyz分量描述球的位置。w分量定義球的半徑。
在RenderCascadedShadows函數(shù)里我們可以獲取每個級聯(lián)的剔除球。我們只需要簡單的把它拷貝到我們的數(shù)組然后再傳給shader就ok了。因為在判斷片元位于哪個剔除球時只會用到半徑的平方,所以我們傳入半徑的平方來減少shader的運算。
shader中,將剔除球數(shù)組變量添加到陰影緩沖區(qū)
?創(chuàng)建一個很便捷的方法,用于判斷一個點是否在剔除球體內(nèi)。
在CascadedShadowAttenuation里為四個剔除球各調(diào)用一次這個方法。返回1表示該點位于剔除球內(nèi),返回0就在球外面。返回值就是表示這些球是否有效的標(biāo)志。在確定級聯(lián)等級前將這四個值放在一個flaot4類型變量中。
一點位于一個球的同時,還躺在更大的球里面。我們最終可能得到五種情況: (1,1,1,1), (0,1,1,1), (0,0,1,1), (0,0,0,1),(0,0,0,0)。我們將這四個值加起來除以四得到的值返回來觀察級聯(lián)層次。也就是點乘?。
我們使用第一張符合要求的貼圖(所渲染的點所在范圍最小的級聯(lián)貼圖),也就是說我們需要把其對應(yīng)標(biāo)志位后邊的標(biāo)志值清零。
?
一個點至少在一個剔除球里面時,結(jié)果是沒問題的,但點如果在所有剔除球外面,結(jié)果為0,會錯誤的采樣了第一張級聯(lián)陰影貼圖。Unity在這里用了一個小技巧,它在world-to-shadow中添加一個零矩陣作為第五個數(shù)組元素來表示不存在的那個聯(lián)級貼圖。零矩陣會將陰影位置轉(zhuǎn)換到近平面,自然就不可能產(chǎn)生陰影了。我們也這樣做,為MyPipeline的worldToShadowCascadeMatrices?數(shù)組添加第五個元素。
?然而,如果z緩沖區(qū)反轉(zhuǎn),那我們就得將陰影空間的z坐標(biāo)設(shè)為1才能表示近平面,我們在構(gòu)造函數(shù)把這個矩陣的m33字段改為1即可。
增加shader中對應(yīng)數(shù)組的長度,并完成(0,0,0,0) → 4的轉(zhuǎn)換,我們該為將值和(4,3,2,1)點乘,讓4減去它來得到級聯(lián)等級
我們可以混合級聯(lián)貼圖嗎?
和Unity的渲染管線一樣,我們直接選擇一個級聯(lián)貼圖采樣。結(jié)果可能在每個級聯(lián)之間會有不連續(xù)的圖像。也就是陰影的像素突然發(fā)生變化。你也可以定義一個過渡區(qū)域,并在其中對兩個相鄰的級聯(lián)貼圖插值。這要求我們尋找兩個級聯(lián)貼圖的序列,一個混合因子,以及雙倍的陰影采樣。
因為剔除球不會與相機和陰影距離對齊,所以級聯(lián)陰影不會和其他陰影一樣在同一距離消失。我們也一樣可以在CascadedShadowAttenuation?中檢查陰影距離來實現(xiàn)統(tǒng)一的效果。
Unity采樣級聯(lián)陰影貼圖時,不是應(yīng)該用一個屏幕空間的pass嗎?
沒錯,Unity使用一個單獨的屏幕空間pass,將級聯(lián)陰影渲染到另一張紋理中去。這其實和我們做的一樣,只不過它會有一個整體的顯示。以便在forward pass的每個片元中采樣其中的陰影數(shù)據(jù)。屏幕空間pass會比較迂回地完成這些工作,而逐片元的計算方式則更為直白簡單,這也是我為什么會選擇這種方法作為教程教學(xué)。
使用單獨的全屏pass的一個原因是可以更快的采樣陰影。在有大量重復(fù)繪制的情況下,效果會更好,因為此時可能會有多個片元對同一個位置采樣。通過增加一個僅深度的pass存入深度緩存,來消除不透明物體的重復(fù)繪制以減少計算量。屏幕空間陰影的方法總是需要一個depth-only?的pass來提取片元的深度值。
另一個原因則是因為Unity的舊版渲染管線可以用它展現(xiàn)高質(zhì)量的軟陰影濾波結(jié)果。但是在輕量級渲染管線里就用不到了,他對于所有的陰影采樣都使用相同的代碼。
哪個方法最好呢?你可以自己測試這三種情況:逐片元,depth-only?的逐片元,以及depth-only?的屏幕空間
?
?
?
?
總結(jié)
以上是生活随笔為你收集整理的Unity SRP自定义渲染管线 -- 5.Directional Shadows的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 光大中青旅信用卡好吗?值得申请吗?
- 下一篇: 光大中青旅信用卡额度多少?额度太低怎么提