日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Unity SRP自定义渲染管线 -- 4.Spotlight Shadows

發布時間:2023/12/13 编程问答 38 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Unity SRP自定义渲染管线 -- 4.Spotlight Shadows 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

英文原文:https://catlikecoding.com/unity/tutorials/scriptable-render-pipeline/spotlight-shadows/

  • 渲染并且讀取紋理
  • 從光空間(光源角度)渲染
  • 為陰影投射(shadow casters)添加一個著色器pass
  • 采樣陰影貼圖
  • 支持軟陰影和硬陰影混合
  • 在單個圖集中組合存儲最多16個陰影貼圖。

這是Unity可編程渲染管線教程的第四篇。在這篇里我們讓聚光燈能投射陰影,并最多同時支持16個光源的陰影。該教程基于Unity2018.3.0.f2。

?1. A Spotlight With Shadows

陰影非常重要,它不僅可以提升真實感,還可以讓物體之間的空間層次關系更加明顯。沒有陰影,我們就很難分辨一個物體究竟是漂浮在表面上還是和表面相接觸。

在這一章,我們只完成聚光燈的陰影,畢竟它是最簡單的。我們先從支持單光源陰影開始。我們需要創建一個場景,其中包含一個聚光燈以及一些游戲物體。一個平面用于接受陰影。所有的物體都使用我們之前自己創建的Lit Opaque材質。

1.1 Shadow map

陰影的渲染有很多種方法,比如體陰影等,這里我們使用傳統的陰影貼圖方法(shadow map)。這意味著我們的需要從光源方向渲染場景,但我們只需渲染深度信息。深度會告訴我們光線在碰撞到物體前走了多久,在這距離之后的物體則處于陰影之中。

首先我們需要創建一個陰影貼圖,相機將會將內容渲染只該貼圖,為了以后能對陰影貼圖采樣,我們需要用一個獨立的渲染紋理來存儲渲染結果而不是幀緩沖中,在MyPipeline中添加一個RenderTextrue的字段來存儲與陰影貼圖的引用。

新建一個獨立的函數來渲染陰影,用context作為參數。首先要獲取一個渲染紋理。我們通過調用?RenderTexture.GetTemporary來實現。如果有還未被清理的閑置紋理,該方法則會拿來它重復利用,不然就創建一個新紋理。因為我們的陰貼圖幾乎在每一幀都會用到,所以我們可以一直重復使用同一紋理。RenderTexture.GetTemporary方法要求提供貼圖的寬高,深度通道的存儲位數以及紋理格式等參數。我們使用固定的512x512大小,并使用16位的深度通道提高精度。我們創建的是深度紋理,格式是RenderTextureFormat.Shadowmap。

?過濾模式設為雙線性,紋理環繞模式設為Clamp

陰影貼圖的渲染應當先于常規場景的渲染。因此在Render方法中,我們在配置常規相機操作前,剔除操作之后調用RenderShadows方法。

在我們傳遞上下文完成渲染后,要釋放渲染紋理。將shadow map傳給?RenderTexture.ReleaseTemporary就可以釋放紋理,同時清空引用。

1.2?Shadow Command Buffer

我們使用單獨的command buffer完成陰影相關的工作,這樣在frame debugger中我們就可以看到陰影渲染和常規渲染被分成了兩個部分。

影的渲染應當放在BeginSample和EndSample之間

1.3?Setting the Render Target

在渲染陰影前,應當需要先讓GPU渲染信息到陰影貼圖中。我們可以調用CoreUtils.SetRenderTarget來實現這一點,傳入我們的command buffer以及shadow map作為參數即可。這個方法一開始會清理貼圖,所以在BeginSample之前調用,來避免frame debugger里出現額外一層Render Shadows嵌套。

我們只關注深度通道,為SetRenderTarget添加第三個參數?ClearFlag.Depth來指明這一點。

雖然不是必須的,但我們可以對紋理的加載和儲存設置更加精確的需求。因為我們會清理這個紋理,所以我們并不關注它來自哪,可以用RenderBufferLoadAction.DontCare來指明這一點,這將使得tile-based的GPU會有更高的執行效率。因為我們隨后需要采樣該紋理,所以需要將其存儲在內存中,通過RenderBufferStoreAction.Store指明這一點。

我們陰影貼圖的清除操作現在能在frame debugger里看到了,位于常規渲染之前。

1.4?Configuring the View and Projection Matrices

我們從光源的視角渲染場景,就猶如我們將聚光燈看做是一個攝像機一樣。因此,我們需要提供適當的視角投影矩陣。我們可以通過剔除結果中的?ComputeSpotShadowMatricesAndCullingPrimitives方法得到該矩陣。該方法的第一個參數是光源序列,因為我們只有一個光源,所以就是0。視野矩陣和投影矩陣則是在后兩個輸出參數中。最后一個參數ShadowSplitData我們用不到,但作為輸出參數,我們必須提供。

當我們獲得了該矩陣,調用陰影命令緩沖區的SetViewProjectionMatrices?方法,然后執行command buffer并清理。?

1.5?Rendering Shadow Casters

有了正確的矩陣信息,我們現在可以渲染所有投射陰影的物體了。我們通過調用DrawShadows方法來實現。這個方法需要一個?DrawShadowsSettings?類型的引用參數。我們用剔除結果和光源索引作為參數來創建一個該實例。

只有我們把聚光燈的Shadow Type類型設為hard或者soft才有用。如果我們設為none,Unity會說這不是一個有效的投射陰影的光源。?

2.?Shadow Caster Pass

此時所有受光源影響的物體都應該渲染進陰影貼圖中,但是frame debugger告訴我們這并沒有發生。因為?DrawShadows?函數會使用著色器的ShadowCaster?pass,但是目前我們的著色器并沒有這個pass。

2.1?Shadow Include File

為了創建一個shadow-caster pass,我們復制Lit.hlsl文件并重命名為ShadowCaster.hlsl。我們只需要深度信息,所以移除所有和片元位置無關的東西。片元程序簡單的輸出0。重命名相應的方法以及導入guard define。

現在足夠渲染陰影了,但是有可能會出現陰影投射物和近平面相交的情況,這時候就會導致陰影中有漏洞(想象一下,本應該在前面產生遮擋的部分,因為沒在近平面范圍內而被舍棄)為了避免這種情況,我們在頂點函數中,限制頂點不超出近平面。我們可以通過取裁減空間位置z,w分量中較大者的來完成這一操作。(為什么比較z和w就可以?這牽扯到比較深入的投影矩陣知識,請參考該文列出的幾個文章https://blog.csdn.net/yinfourever/article/details/96481332,簡單科普下基礎知識,投影空間z值從齊次坐標轉換為正常坐標后的范圍為-1 到 1,也就是clipPos除以w之后的結果,這也是為什么比較z和w就可以確保其不超出近平面,因為當點在近平面時,z值為-1,當點在遠平面時,z值為1)

然而,裁減空間的一些細節讓情況變得復雜起來。我們往往很直觀的將深度值為-1的地方想象為近平面,隨著距離增加,值不斷上升。但實際上,除了OpenGL之外的?API,情況與我們想象的相反,在近平面上值為1。而對于OpenGL,近平面則是-1。我們通過?UNITY_REVERSED_Z?和?UNITY_NEAR_CLIP_VALUE這兩個宏覆蓋所有情況。我們導入Common.hlsl來獲取這兩個宏。

2.2?A Second Pass

在我們的Lit著色器中添加ShadowCaster?pass,我們復制一個pass語句塊,并將第二個pass中Tags中的?LightMode設為ShadowCaster。接著引入?ShadowCaster.hlsl而非Lit.hlsl?。并使用對應的頂點片元函數。

?現在我們的物體能夠渲染進陰影貼圖里了。因為物體目前只受單個光源影響,所以GPU instancing的效率非常好。

選擇Shadows.Draw項,你就能夠看到最終的陰影貼圖了。因為是僅深度貼圖,frame debugger會為我們顯示深度信息,白色為近處,黑色為遠處。

因為陰影貼圖是在聚光燈假設成相機的方式下渲染的,所以它的朝向和光源是相匹配的。如果發現陰影貼圖是顛倒的,可能是你對光源進行了旋轉,導致本地空間的向上方向在世界空間反而是向下的。

3.?Sampling the Shadow Map

我們現在有了包含所需要數據的陰影貼圖,但暫時還沒有使用它。所以下一步就是采樣陰影貼圖

3.1?From World Space to Shadow Space

儲存在深度貼圖的中的深度信息,是依據在渲染該貼圖時所使用的聚光燈當作攝像機的裁減空間計算的,我們把它叫做陰影空間。這與我們正常渲染場景所用到的坐標空間不匹配。想知道一個片元如果存儲在深度貼圖中深度值該是多少,我們要將片元的位置轉從世界空間換到陰影空間。

首先我們得讓我們的著色器可以訪問陰影貼圖。為此我們添加一個著色器材質變量?_ShadowMap。并在MyPipeline中持有指向它的標識符。

在RenderShadows通函數中通過SetGlobalTexture方法,來將陰影貼圖和全局變量相綁定。?

接著我們添加一個矩陣變量用于從世界空間轉換至陰影空間,命名為_WorldToShadowMatrix。同樣持有它的標識符。

?通過陰影空間的視角矩陣和投影矩陣相乘可以得到改矩陣。用SetGlobalMatrix函數將它傳給GPU。

我們又會遇到裁減空間z軸是否反向這一問題,好在我們可以用SystemInfo.usesReversedZBuffer來檢查,如果反向,那就在相乘之前修改投影矩陣的z列分量(列序列號為2)。直接修改原矩陣的m20至m23字段即可。

我們現在有了世界空間至陰影空間的轉換矩陣。裁減空間范圍是-1到1,但我們的紋理坐標和深度范圍在0到1。要映射至該范圍就得就得再額外乘一個能在所有維度縮放和偏移?0.5個單位的轉換矩陣。我們可以用Matrix4x4.TRS方法來得到想要的縮放、旋轉或偏移。

但是其實這是一個simple matrix,我們簡單的在單位矩陣的基礎上修改合適的分量即可。

3.2?Sampling Depth

在Lit.hlsl,中,新增一個緩存區并在其中定義float4x4?_WorldToShadowMatrix?。

紋理資源不屬于buffer的一部分,我們得分開另外定義。我們可以用宏?TEXTURE2D_SHADOW來定義?_ShadowMap。

?接下來,我們需定義采樣器狀態用于采樣紋理。通常我們是用的是宏SAMPLER?,但是這里我們需要使用另外一個特殊的比較采樣器,所以使用SAMPLER_CMP。為了得到正確的采樣器狀態,應使用sampler前綴再加上貼圖名字作為參數

什么是紋理采樣器?

在舊的GLSL代碼中,我們使用sampler2D來同時定義紋理和采樣器狀態。但其實這是兩個分開的東西,都會占用資源。采樣器狀態可以從紋理中分離開來,就為混合使用兩者提供了可能。典型的例子就是多張紋理重復利用同一個采樣器狀態。在我們的例子里,我們通過MyPipeline設置采樣器狀態的過濾模式為雙線性以及紋理映射模式為clamping?我們使用的comparison sampler還會在雙線性插值之前就為我們進行深度比較。這會比在插值之后才進行比較效果更好。

創建一個以世界位置作為參數的ShadowAttenuation方法。它會返回我們光源陰影的衰減因子。在方法里首先要做的就是將世界位置轉為陰影空間位置。

就像之前轉換到裁減空間一樣,得到的位置是定義在齊次坐標系中的。我們需要的是常規坐標,所以我們讓xyz分量除以w分量。

現在我們可以通過SAMPLE_TEXTURE2D_SHADOW這個宏采樣陰影貼圖。它需要一張貼圖,一個采樣器狀態,以及對應的陰影空間位置作為參數。如果該點位置的z值比在陰影貼圖中對應點的值要小就會返回1,這說明他比任何投射陰影的物體離光源都要近。反之,在陰影投射物后面就會返回0。因為采樣器會在雙線性插值之前先進行比較,所以陰影邊緣會混合陰影貼圖的多個紋素(texels)。

3.3 Fading when Shadowed

讓陰影產生影響,只需在DiffuseLight函數中為陰影衰減添加一個參數。將它與其他的漸變因子一起作用于漫反射強度。

頂點光源現在不會有陰影,所以在LitPassVertex.中將陰影衰減值設為1。?

在?LitPassFragment中,調用ShadowAttenuation方法并傳入世界位置,將返回值傳給DiffuseLight函數產生陰影。

現在陰影出現了,但是有非常嚴重的瑕疵。

4. Shadow Settings

影響陰影質量表現的因素有很多。我們暫時支持一部分:陰影分辨率、深度偏移、強度、軟陰影。我們可以在每個光源的檢視面板對這些以及其他選項進行配置。

4.1?Shadow Map Size

雖然光源的inspector中有設置陰影分辨率的選項但這只會間接的影響深度貼圖的大小,真正是取決于項目設置中的quality settings,至少對于Unity默認的渲染管線是這樣的。我們用的是自己的渲染管線,因此我們選擇將陰影貼圖大小的設置選項放到MyPipelineAsset中。

陰影貼圖是正方形貼圖,我們允許設為256x256到4096x4096之間的任意二次方大小。為此我們在MyPipelineAsset中定義一個名為ShadowMapSize的枚舉類型,其中包含了256、512、1024、2048這幾個枚舉。因為枚舉不能為數字,所以我們加一個下劃線前綴,Unity編輯器在顯示時會抹去下劃線。我們用這個枚舉類型添加一個配置字段用于設置陰影貼圖尺寸。

枚舉代表的整數默認從0開始。但如果枚舉選項正好與相同大小的整數相通會很方便,因此我們對枚舉項進行顯示賦值。

默認值0將無法表示任何枚舉項,所以我們需設置一個有效的默認值。?

?將該參數傳入渲染管線的構造函數

在MyPipeline中添加一個變量,并在構造函數中初始化。

?在RenderShadows中分配渲染紋理時,我們就可以使用該變量設置陰影貼圖尺寸了。

4.2?Shadow Bias

陰影瑕疵的問題更詳細的解釋請看Rendering 7, Shadows,我們用最簡單的方式掩蓋這些瑕疵。那就是在渲染深度貼圖時在深度上添加一點偏移。這個深度偏移在每個光源中單獨配置,所以必須把它傳給GPU。我們添加一個_ShadowBias著色器屬性,并記錄下它的標識符。

在RenderShadows中設置視角投影矩陣后,設置深度偏移。?VisibleLight中不能直接得到該信息,但其中的light字段有深度偏移。

ShadowCaster.hlsl?文件中陰影Buffer中添加相應的變量。對裁減空間位置的z分量應用z分量。如果z軸是翻轉的那就用減法,否則用加法。

shadow bias應當盡可能小,避免陰影偏移的太遠引起peter-panning效果(看起來影子漂浮在地面上)

?

關于bias以及shadow map這篇博文寫的非常好,強烈建議讀一下https://blog.csdn.net/ronintao/article/details/51649664

根本原因就是 shadow depth map 的分辨率不夠,因此多個 pixel 會對應 map 上的同一個點。

? ? ? ? ?圖中黃色箭頭是照射的光線,黑色長方形是實際物體表面,黃色的波浪線是 shadow map中的對應值的情況。

????????可以看到,由于map是對場景的離散取樣,所以黃色的線段呈階梯狀的波浪變化,相對于實際場景中的情況,就有一部分比實際場景中的深度要大(對應著黑色線段部分),著部分不會產生陰影(注意圖畫反了);一部分比實際場景中的深度要小(對應著黃色線段部分),這部分會產生陰影,所以就出現了條紋狀的陰影。

????????由于這種情況,是物體的實際深度,與自己的采樣深度,相比較不相等(實際深度大于采樣深度)導致的,所以可謂是自己(采樣的副本)遮擋了自己(實際的物體),所以被稱為 self shadowing。
? ? ? ?解決的方法很簡單,其實只有實際深度大于采樣深度的時候才有問題,那么我們在計算實際深度的時候,往燈光方向拉一點,讓他減小一點就可以了

4.3?Shadow Strength

我們只有單個光源并且沒有任何環境光,所以我們的陰影是純黑的。但是我們可以調和一下陰影衰減的輕度,讓他只淡化部分光源貢獻而不是完全排除。這會讓我們的陰影看起來是半透明的。我們?_ShadowStrength屬性表示陰影強度,并記錄下它的標識符。

?采樣陰影貼圖時會用到陰影強度,所以將它與世界-陰影空間矩陣和陰影貼圖在一塊設置。和深度偏移一樣,我們在?Light字段中獲取該值。

在陰影緩存區添加陰影強度。在ShadowAttenuation函數中用它在1和采樣得到的衰減值之間插值。

4.4?Soft Shadows?

最后一個設置就是支持軟硬陰影的切換。我們現在使用的是硬陰影,陰影邊緣的平滑過渡全靠在采樣陰影貼圖時使用的雙線性插值。當開啟平滑的軟陰影時,陰影和非陰影的過渡是模糊的,陰影中有很大的半影區域。但不像在現實世界中,半影的產生取決于光源、投射物,接受陰影物體之間的空間關系,在這里半影范圍是固定統一的。

軟陰影需要采樣陰影貼圖多次。次數越靠后,采樣點越偏離原采樣位置,貢獻度也越低。我們使用5x5 tent filter,需要九次紋理采樣。為此我們要用到在Shadow/ShadowSamplingTent.hlsl文件中的一個函數方法,將它導入Lit.hlsl。

tent filter需要知道陰影貼圖的尺寸。該方法要求一個特定的向量,四個分量分別為寬的倒數、高的倒數、寬度、高度。我們將其添加到shadow buffer。

?在MyPipeline中保存相應的標識符。

在?RenderShadows函數中設置該變量。?

當_SHADOWS_SOFT?關鍵字被定義時,在ShadowAttenuation方法中我們用tent filter替換常規的陰影貼圖采樣。

不再是單次采樣,我們創建一個5x5的tent filter用來疊加九次采樣結果。SampleShadow_ComputeSamples_Tent_5x5方法會給分配好每次采樣的權重和UV坐標,我們需要傳入陰影貼圖尺寸和陰影空間位置。權重和uv通過輸出參數獲取,一個是flaot數組,另一個是float2數組,兩者都有9個元素。

然而,方法中的輸出參數定義的類型為real而不是float。它不是一個實際的數字類型,而是一個宏,根據需要自動選擇flaot或者half。我們通常可以忽略這個情況,但是為了避免在某些平臺出現編譯錯誤,最好還是為輸出參數使用real類型。

現在我們可以在循環中,使用數組里的權重和uv坐標,對陰影貼圖采樣九次。這是一個固定次數的循環,所以shader編譯器會會將循環展開。我們還需要陰影空間位置的z坐標,每次采樣都用這三者構造一個flaot3變量作為參數。

為了設置使用軟陰影,創建一個定義了_SHADOWS_SOFT關鍵字的著色器變種。在我們的Lit著色器的默認pass中添加一個多重編譯指令。我們需要兩個變種,一個有該關鍵字,一個沒有,所以我們用下劃線表示沒有關鍵字,后面跟著_SHADOWS_SOFT關鍵字。

最后,我們在?RenderShadow函數中基于光照的shadows屬性設置關鍵字。如果設置為LightShadows.Soft就會在我們的陰影緩沖區調用?EnableShaderKeyword方法,否則調用DisableShaderKeyword方法,Unity根據關鍵字狀態決定在渲染時使用哪一個變種。

用一個bool值切換關鍵字很普遍,我們可以用方法CoreUtils.SetKeyword代替。

?

5.?More Lights With Shadows

目前我們只支持單光源投射陰影,但是我們的管線支持最多16個光源,接下來我們將實現最多支持16個聚光燈的陰影。

5.1?Shadow Data Per Light

我們目前的管線只能用單個pass完成所有的光源工作,所以如果我們想支持多光源陰影,我們就得確保每個光源的數據(如強度等)可同時訪問。我們在ConfigureLights函數中收集這些數據,就像設置其他的光源數據一樣。所以我們將該方法移到RenderShadows前,并且只在有可見光源時調用?RenderShadows。

我們用一個四維向量數組存儲陰影數據,每個向量代表一個光源。在ConfigureLights函數遍歷光源的循環中先將每個向量初始化為0,就像之前設置衰減數據一樣。

在光源是聚光燈類型的情況下獲取Light腳本。如果shadows屬性沒有被設置為LightShadows.None,就將陰影強度存儲在向量的x分量中。

?我們用向量的y分量存儲使用硬陰影或軟陰影。1表示軟陰影,0表示硬陰影。

5.2?Excluding Lights

一個光源可見并且開啟了陰影并不能保證一定需要陰影貼圖。如果在光源的視角中并沒有任何陰影的投射物或接受物,自然不需要陰影貼圖。我們可以調用剔除結果的?GetShadowCasterBounds函數,傳入一個光源索引來檢查該光源是否需要陰影貼圖。他會檢查該光源的陰影體積是否在一個有效的范圍內。如果沒有,我們就跳過設置陰影數據。盡管我們沒有用到輸出結果,但是我們還是得提供一個陰影范圍作為參數。

5.3?Rendering All Shadow Maps

我們把首次執行陰影緩沖區和設置陰影貼圖紋理之間的代碼用一個循環包括。用這個循環再次遍歷所有可見光源,并在光源數量超過可支持最大光源數是打斷循環。將其中所有原本固定的索引0,修改為迭代值變量。

跳過不需要陰影貼圖的光源,我們用陰影數據中的陰影強度來判斷。小于等于0(有可能原本的強度就這樣,也有可能是我們之前設0來跳過)就直接用continue跳到下個迭代。

omputeSpotShadowMatricesAndCullingPrimitives方法返回是否可以生成有效的矩陣的布爾值。理論上應該和?GetShadowCasterBounds方法的結果一致,但以防萬一還是考慮在失敗時將強度設為0并跳過此次迭代。

當我們開啟多個光源的陰影(只要它們的位置能夠讓他們產生可見的陰影),frame debugger會顯示我們確實渲染了多次陰影貼圖。

然而,陰影顯示地一團糟,我們還需要進一步做一些工作。

5.4?Using the Correct Shadow Data

不再是使用單一的ShadowStrength屬性,我們需要傳入shadow data數組。

同樣的,我們也需要設置投影矩陣數組,將其傳入GPU

在shader里,修改陰影緩存區使之匹配。

ShadowAttenuation?方法新增一個參數接受光源索引以便取得正確的數組元素。我們檢查陰影強度是否為正數。如果不是,直接將1作為衰減值返回。代替依賴_SHADOWS_SOFT關鍵字判斷,我們基于陰影數據的y分量來進行條件分支。

最后在?LitPassFragment里調用ShadowAttenuation?時傳入光源索引。

5.5?Shadow Map Atlas

雖然我們現在有了正確的用于渲染陰影所需要的陰影數據和矩陣信息,但是在超過一個光源有陰影時,最終產生的仍然是錯誤的陰影。這是因為所有的陰影貼圖都渲染進了同一張紋理之中,多個信息混合在一起,導致得到的陰影貼圖沒有意義。Unity輕量級渲染管線通過陰影貼圖圖集解決這一問題。將渲染紋理分割為多個方形區域,每個光源個占據其一。我們也使用這種方法。

為什么不使用紋理數組?

這是可行的,但可惜使用陰影投射渲染紋理數組并不是一個普遍的做法。比如,在Metal上這是可行的,但是OpenGL core要求4.6的著色器等級,即使生效了,Unity也會打印一連串的斷言錯誤。所以還是老老實實的用單個渲染紋理吧。

我們最多支持16個光源,所以就應該把單張陰影貼圖分成4x4網格的平鋪塊(tiles)。每個平鋪塊的大小應該和陰影貼圖除以4的大小一樣。我們要將渲染時的視口約束在這個大小,所以在RenderShadows一開始創建一個Rect結構體,并填充合適的值。

在我們設置視口和投影矩陣前,用SetViewport函數告訴GPU使用合適的視口大小。

現在所有的陰影貼圖都渲染在渲染紋理一角的單個平鋪塊中。下一步就是偏移每個光源的視口。我們可以依據每個平鋪塊的xy序號得到視口位置。Y軸偏移序號通過光源序列除以四(整數除法)得到。x軸偏移序號通過整數取余得到。最終視口的xy位置等于序號乘以平鋪塊大小。

這樣的圖集有一個缺點,在一個平鋪塊邊緣采樣時,可能會在兩個平鋪塊之間插值,從而導致錯誤的結果。當使用軟陰影時效果會更加的糟糕,因為tent filter可能會在離原始采樣點偏移最多4個紋素的地方采樣。相比混合附近的平鋪塊,能夠淡出陰影肯定更好。所以我們在每個平鋪塊周圍添加一圈空值邊緣,讓GPU寫入數據時使用比平鋪塊略小一點的視口。這稱之為裁減矩形。我們可以使用?shadowBuffer.EnableScissorRect?方法,傳遞一個比視口略小的矩形來實現。我們需要邊緣寬度為四個紋素,所以這個矩形位置應該是視口位置加4,大小為視口大小減8。

我們在渲染陰影后調用DisableScissorRect關閉裁剪矩形,不然會影響到后面的常規渲染。

最后要做的就是調整world-to-shadow矩陣,讓它能采樣到正確的平鋪塊。我們可以乘以一個有適當xy偏移的轉換矩陣。shader不需要關心我們是否使用了圖集。

要記住我們現在每個物體最多支持4個像素光,所以你讓第5個聚光源照射到平面時,其中一個光源會退化為頂點光源,進而無法接受該光源的陰影。(16個陰影指的是整體場景總的陰影來源,而不是單個物體的)

6.?Dynamic Tiling

使用陰影貼圖圖集的優點是無論有多少陰影貼圖,我們用的都是同一張渲染紋理,所以紋理占用的內存是固定的。缺點則是每個光源只占紋理的一部分,所以最終的陰影貼圖分辨率會比我們想象的要低。并且最終可能有很大一部分的紋理面積沒有利用。

我們可以更好的利用紋理,而不是固定的將紋理分成16塊。我們可以用一個變量表示平鋪塊的大小,可以根據有多少平鋪塊決定值設為多大。這種方式可以確保我們至少能用到一半的紋理。

6.1?Counting Shadow Tiles

首先,我們需要明確我們需要多少個平鋪塊。我們可以在ConfigureLights中記錄我們有多少帶陰影的光源。并用一個字段記錄總數以便在之后使用。

6.2?Splitting the Shadow Map

接下來在RenderShadows里一開始就算好如何分割陰影貼圖。我用一個整數變量來表示。如果我們只要一個平鋪塊,就不需要分割,所以split值設為1,否則如果是4個以下值為2,8個以下值為3,只有超過8個,值才為4。

平鋪塊的大小可以通過陰影貼圖大小除以split值得到(整數除法)。這意味著在除以3的時候我們會舍棄部分紋素。平鋪塊的縮放值應該改為1/split(浮點數除法)。我們使用split值計算平鋪塊的縮放和偏移用于調整世界-陰影矩陣。

為了在可用的空間打包陰影貼圖,我們需要在確實設置好一個平鋪塊后,再遞增序列。因此我們使用獨立的一個變量而不是直接使用光源索引。在沒我們沒有跳過的迭代的末尾自遞增變量。

6.3?One Tile is No Tile

最后,如果我們最終只需要一個平鋪塊,那就沒必要設置視口的裁減模式了。我們只需要在有多個平鋪塊時需要這么做。

6.4?Shader Keywords

目前,我們每個片元最多可以采樣來自四個光源的陰影,它們可能是軟硬光源的組合。最麻煩的情況就是4個軟陰影,一共需采樣36次。好在我們shader中的多個分支可以為我們很好的按要求采樣陰影,因為來自同一物體的片元最終使用的是同一條分支。但是我們可以通過分離不同的陰影組合來切換復雜度更低的備選shader。

共有四種可能的組合,第一種是完全沒有陰影,第二種只有硬陰影,第三種只有軟陰影,最復雜的一種就是軟硬陰影的組合。我們可以使用shader變種處理所有可能的情況,通過使用關鍵字_SHADOWS_HARD?和?_SHADOWS_SOFT。

在RenderShadows中,使用兩個布爾變量記錄是否使用了軟硬陰影,我們依靠陰影信息的Y分量來判斷。在循環之后使用這些布爾值切換關鍵字。

在shader添加另一個多重編譯指令,這次是_SHADOWS_HARD

在ShadowAttenuation?方法中,如果兩個關鍵字都沒定義就在一開始直接返回1。這樣就可以省方法的剩余部分,完全的消除陰影。

為了讓代碼更整潔優雅,我們將采樣軟陰影和硬陰影的代碼各自分離成獨立的函數。

現在我們用關鍵字為其他三種情況填寫代碼。最開始的分支在兩個關鍵字都定義時才有。

最后,如果我們不需要陰影平鋪塊?,在MyPipeline.Render里直接跳過RenderShadows方法。我們甚至都不需要清理陰影貼圖了。如果跳過了,要確保兩個陰影的關鍵字都關掉了。沒有可見光時我們也要把兩個關鍵字關掉。

?

?

?

?

?

?

?

?

?

?

?

創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

總結

以上是生活随笔為你收集整理的Unity SRP自定义渲染管线 -- 4.Spotlight Shadows的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。