Unity SRP自定义渲染管线 -- 3.Lights
Lights
Single-Pass Forward Rendering
- 實(shí)現(xiàn) diffuse shading.
- 支持 directional(方向光), point(點(diǎn)光源), and spotlights(聚光燈).
- 每幀可允許最多16個(gè)可見光參與渲染
- 每個(gè)物體可以最多由4個(gè)像素光和4個(gè)頂點(diǎn)光參與計(jì)算光照。
這是本系列教程的第三篇,在這一篇中,我們將實(shí)現(xiàn)每個(gè)物體由8個(gè)光源進(jìn)行shading且僅消耗一個(gè)draw call。
1. Shading With a Light
為了渲染光照,我們得為我們的渲染管線加入一個(gè)最基礎(chǔ)的lit shader。光照渲染可以非常簡(jiǎn)單,比如只包括光的漫反射,也可以非常非常復(fù)雜,比如基于物理的渲染(PBS)。我們現(xiàn)在先從最基礎(chǔ)的開始,只計(jì)算方向光的漫反射,不考慮陰影。
1.1 Lit Shader
復(fù)制Unlit.hlsl并重命名為L(zhǎng)it.hlsl. 在文件中,用lit代替unlit,尤其是定義vertex和fragment 函數(shù)的名字。
?同樣復(fù)制Unlit.shader并重命名為L(zhǎng)it.shader. 在文件中,用lit代替unlit。
現(xiàn)在我們可以通過新建的lit shader創(chuàng)建material了,雖然目前渲染的效果和unlit一樣(沒寫呢還當(dāng)然一樣)
1.2 Normal Vectors
為了計(jì)算方向光,我們需要知道表面的法線。我們?yōu)関ertext函數(shù)的輸入和輸出結(jié)構(gòu)體添加法線信息。
我們假設(shè)物體使用統(tǒng)一的scale,因此用3X3 模型矩陣簡(jiǎn)化法線的坐標(biāo)變換,如果不是統(tǒng)一的scale,我們需要用world to object的轉(zhuǎn)置矩陣進(jìn)行計(jì)算(具體原理可以搜索其他資料,法線的坐標(biāo)變換)。坐標(biāo)變換后在fragment函數(shù)中進(jìn)行歸一化。
為了證明我們獲得了正確的法線信息,我們?cè)趂ragment函數(shù)中輸出法線看看效果
1.3 Diffuse Light
漫反射由光照與表面法線的角度夾角決定,目前我們先硬編碼,將光照的方向設(shè)置為 (0 ,1,0)。
2. Visible Lights
為了使用場(chǎng)景中的光源,我們的渲染管線需要將光源數(shù)據(jù)傳輸?shù)紾PU中,場(chǎng)景中可能存在多光源,所以我們也需要支持多光源的渲染。Unity中默認(rèn)的渲染管線會(huì)為每個(gè)物體每個(gè)光源分配一個(gè)pass進(jìn)行渲染(M?X N 即M個(gè)物體N個(gè)光源需要M X N個(gè)Pass)。LWRP渲染管線則對(duì)每個(gè)物體只使用一個(gè)Pass渲染所有光源。HDRP則使用Deferred rendering,先渲染所有物體的表面信息,再對(duì)每個(gè)光源使用一個(gè)pass進(jìn)行渲染。
在本文里,我們使用和LWRP相同的策略,對(duì)每個(gè)物體用一個(gè)pass渲染所有光源,所以要求我們將當(dāng)前可見的所有光源信息傳輸?shù)紾PU,那些雖然在場(chǎng)景中,但是對(duì)物體沒有產(chǎn)生任何影響的光源將被忽略不參與計(jì)算。
2.1 Light Buffer
在一個(gè)Pass中渲染所有光源意味著所有光源的信息必須在同時(shí)都準(zhǔn)備好,我們目前暫且將所有光源類型限制為方向光,這意味著我們需要知道每個(gè)光源的顏色和方向信息。為了支持多光源,我們采用數(shù)組來存儲(chǔ)。我們用一個(gè)單獨(dú)的buffer存儲(chǔ)光源的信息,給這個(gè)buffer命名為_LightBuffer.
然而我們并不能夠在定義數(shù)組時(shí)不指定數(shù)組大小,我們聲明一個(gè)宏來定義最大可見光源數(shù)量,用它來指定數(shù)組大小
加入一個(gè)DIffuseLight函數(shù),它用傳入進(jìn)來的光源信息計(jì)算Diffuse光照
在LitPassFragment函數(shù)中加入for循環(huán)來支持多光源的渲染?
2.2 Filling the Buffer
現(xiàn)在我們渲染出來的東西還是一片漆黑,這是因?yàn)槲覀冞€沒把光源數(shù)據(jù)傳進(jìn)GPU來,我們需要在我們的渲染管線MyPipeline中聲明同樣大小的數(shù)組,再使用Shader.PropertyToID方法獲取shader中相關(guān)屬性的引用,
?通過函數(shù)SetGlobalVectorArray操作command buffer,可以將數(shù)組數(shù)據(jù)傳入到GPU中。
?2.3?Configuring the Lights
我們現(xiàn)在是可以將光源數(shù)據(jù)每幀傳輸?shù)紾PU中了,但是現(xiàn)在確依然顯示漆黑,這是因?yàn)槲覀冞€得先設(shè)置數(shù)據(jù),我們聲明個(gè)ConfigureLights函數(shù)來完成這項(xiàng)工作。
在culling剪裁中,Unity同時(shí)指出了哪些光源是可見的。這一信息可以從cull結(jié)果中獲得,這一信息以visibleLights名字的list變量存儲(chǔ)在cull結(jié)果中。
finalColor字段存儲(chǔ)了光源的顏色,該顏色數(shù)據(jù)是由光源的color屬性和intensity屬性相乘后的結(jié)果,并經(jīng)過了顏色空間的校正,所以我們可以直接使用該信息將其賦值給visibleLightColors數(shù)組。
然后,unity默認(rèn)的渲染管線中,intensity定義在gamma空間,我們工作中線性空間,所以通過GraphicsSettings.lightsUseLinearIntensity 屬性我們將其設(shè)置為線性空間。
?方向光的光源方向信息可以通過光源的旋轉(zhuǎn)信息獲得,光源的方向是它的z軸方向。我們可以通過VisibleLight.localtoWorld矩陣獲取在世界坐標(biāo)系中的該信息。這個(gè)矩陣的第三列定義了光源的本地Z軸方向。
在shader中我們使用從物體朝向光源的向量方向進(jìn)行計(jì)算,所以將獲得的光源方向進(jìn)行取反操作。
我們的shader目前將會(huì)計(jì)算四個(gè)光源,即使場(chǎng)景中沒有四個(gè)光源,也將會(huì)計(jì)算四次。在場(chǎng)景中加入四個(gè)光源后,渲染的效果如下。
在frame debugger中可以查看到傳入GPU的light data。
2.4?Varying the Number of Lights?
當(dāng)可見光的數(shù)量大于我們?cè)O(shè)定的maxVisibleLights時(shí),會(huì)產(chǎn)生越界的錯(cuò)誤,所以我們要對(duì)邊界條件進(jìn)行處理,當(dāng)可見光數(shù)量大于maxVisibleLights時(shí),忽略掉多出的那些光源(Unity光源排序的規(guī)則可以參考其他資料,簡(jiǎn)單來講是通過光源的重要程度排序)
我們還要處理的一種情形是當(dāng)光源數(shù)目由多變少,這時(shí)候需要清理重置光源的信息,確保下一幀的正確渲染。
3.?Point Lights
這一節(jié)我們將實(shí)現(xiàn)渲染管線中的點(diǎn)光源。
3.1?Light Position
和方向光不同,點(diǎn)光源不關(guān)心光的方向而關(guān)心光源的位置。我們不另外開辟新數(shù)組存儲(chǔ)位置信息,而是使用之前聲明用于存儲(chǔ)方向光方向信息的數(shù)組來存儲(chǔ)點(diǎn)光源的位置數(shù)據(jù)。在Mypipeline中重新命名該數(shù)組
使用VisibleLight.lightType來判斷當(dāng)前光源的類型,當(dāng)是方向光時(shí)存入方向信息,當(dāng)是點(diǎn)光源時(shí)存入位置信息。
在shader函數(shù)中,使用該數(shù)據(jù)信息獲取光源位置信息,并傳入worldPos,兩者相減即可獲得光線的方向。
當(dāng)是方向光時(shí),w是0,當(dāng)是點(diǎn)光源時(shí)w是1,我們利用該性質(zhì)將worldPos 與 w分量相乘,這樣就可以用同一個(gè)公式計(jì)算點(diǎn)光源和方向光的信息。
為了獲取片段的位置信息,我們需要在shader中進(jìn)行處理,由vertex函數(shù)輸出到fragement函數(shù)。
至此,我們就可以看到點(diǎn)光源的效果了。
3.2?Distance Attenuation
和方向光不同,點(diǎn)光源要考慮光源強(qiáng)度隨著距離而衰減。這里的衰減關(guān)系是距離平方的倒數(shù)。為了避免除數(shù)是0出現(xiàn)錯(cuò)誤,因此加入一個(gè)極小的值0.00001
3.3 Light Range
?點(diǎn)光源還有個(gè)屬性是光照范圍。 在范圍外的物體將不會(huì)受該光源的影響,雖然在事實(shí)上它們可能會(huì)被物體照亮,但是用范圍這個(gè)屬性,我們可以更好的規(guī)定哪些物體受到該光源到影響,沒有這個(gè)范圍屬性限制,所有的光源都會(huì)被認(rèn)為是可見的。
范圍屬性不是突變的而是平滑漸變的,其公式為:
范圍屬性是場(chǎng)景中的數(shù)據(jù),所以我們也需要將其傳入GPU,這回我們將使用一個(gè)新的數(shù)組來存儲(chǔ)它。
像之前做的一樣,把數(shù)據(jù)用command buffer輸入到GPU中
填充數(shù)據(jù)時(shí),我們計(jì)算好,將結(jié)果存入數(shù)組后傳入GPU,這樣可以減少GPU的工作。
在shader中計(jì)算范圍的影響,進(jìn)行著色
?
Light fades out based on range?
4.?Spotlights
接下來我們添加聚光燈光源.聚光燈和點(diǎn)光源很像,但是有方向的限制
4.1?pot Direction
像方向光源,聚光燈也是沿著它的z方向發(fā)射光,但是是一個(gè)圓錐形范圍,它也有個(gè)位置屬性,所以我們得新添加個(gè)數(shù)組來支持聚光燈。
判斷光源類型,如果是聚光燈,將方向信息填入新的數(shù)組中。
?在shader中添加方向數(shù)據(jù)。
4.2?Angle Falloff
聚光燈類型光源也是漸變的衰減,這個(gè)范圍可以被定義為一個(gè)內(nèi)層的角度和一個(gè)外層的角度,從內(nèi)層的角度開始衰減,直到外層衰減到0.
Unity LWRP中,spot light類型光源只允許我們控制其外層角度,其衰減的方法被假定為與外層的角度有一個(gè)固定算法。
為了得到fallof,先把spot 光源的角度的一半由角度轉(zhuǎn)換成弧度,并計(jì)算其cosine值。
根據(jù)外層的角度計(jì)算內(nèi)層的角度的公式以及衰減函數(shù)的公式和計(jì)算如下所示:?
?其中衰減函數(shù)可以進(jìn)行簡(jiǎn)化:
?最后在shader中用計(jì)算出來的光照進(jìn)行著色
?為了保證不同類型的光照計(jì)算的一致性(用同樣的shader代碼),將w分量設(shè)置為1
5.??Lights Per Object
目前我們支持了對(duì)一個(gè)物體用四個(gè)光源進(jìn)行光照,實(shí)際上,無論有幾個(gè)光源,目前每個(gè)物體都將計(jì)算4次,但其實(shí)很多時(shí)候是不必要的。不如如下的例子。9乘9的方格,共有81個(gè)球體,場(chǎng)景中有4個(gè)光源在四個(gè)角,當(dāng)光源的范圍并不是很大時(shí),大多數(shù)球體只受到一個(gè)光源的影響,甚至有的球體不受到任何影響。
目前81個(gè)球體在開啟GPU Instaing的時(shí)候?qū)⒅粫?huì)消耗一個(gè)draw call,但是球體的每個(gè)fragment將在fragment shader中計(jì)算4次光照,我們應(yīng)該改進(jìn)成只計(jì)算影響該fragment的光源。
5.1 Light Indices
在Culling期間,Unity也會(huì)計(jì)算出哪些光是可見的,每個(gè)物體受哪些光源的影響的信息可以以光照索引list的形式傳輸?shù)紾PU
Unity目前支持兩種形式的光源索引,第一種是對(duì)每個(gè)物體,將其受影響的光源存入兩個(gè)float4類型變量中。第二種是將所有物體受光源影響的信息以list形式一起存入單獨(dú)的buffer中。然而目前Unity 2018.3版本只支持第一種,因此我們采用第一種。
設(shè)置rendererConfiguration字段為RendererConfiguration.PerObjectLightIndices8來開啟光源的索引功能。
Unity現(xiàn)在需要為每個(gè)物體設(shè)置額外的數(shù)據(jù)以提供給GPU,這將會(huì)影響到GPU instancing。相較于根據(jù)受影響的光源分組,Unity更傾向于根據(jù)距離分組,另外光源的重要性也會(huì)影響到索引的排序,這些都會(huì)影響到合批。在我們的這個(gè)例子中,會(huì)由30個(gè)draw call,遠(yuǎn)大于1,當(dāng)然也遠(yuǎn)小于81.
索引通過unity_4LightIndices0?and?unity_4LightIndices1引通變量可以獲得,它們應(yīng)該存在UnityPerDraw Buffer中。另外
unity_LightIndicesOffsetAndCount變量中的Y分量存有當(dāng)前物體受多少光源影響的數(shù)量。
現(xiàn)在我們可以限制調(diào)用DiffuseLight著色的次數(shù)為實(shí)際需要的了,但是我們還需要取出正確的索引來使用。我們目前限制燈光數(shù)量最多為4個(gè),所以只需從unity_4LightIndices0變量中獲取。
限制GPU的開銷變小了,我們只需要計(jì)算真正影響到物體的光源,通過frame debugger我們可以查看傳入的光源的數(shù)量以及索引。
現(xiàn)在不在需要使用固定的數(shù)值來循環(huán)計(jì)算了,也不需要再去每次清除data。
5.2?More Visible Lights
現(xiàn)在可以支持更多可見的光源,讓我們把場(chǎng)景中最大的可見光源數(shù)量提升到16,但是大部分物體只會(huì)受少量光源的影響。修改變量值為16:
對(duì)于unity_4LightIndices0變量,最多只能存儲(chǔ)4個(gè)值,所以我們要注意不要越界:
但是我們可以不必限制單個(gè)物體最多受4個(gè)光源的影響,因?yàn)槲覀冞€可以用unity_4LightIndices1變量。但是我們不能超過8個(gè),這已經(jīng)是對(duì)當(dāng)個(gè)物體來講,目前能夠支持最多數(shù)量的光源了:
光源的索引是按照重要程度排序的,對(duì)于大多數(shù)物體,后四個(gè)光源的影響其實(shí)很小,關(guān)掉前四個(gè)光源的效果,可以查看后四個(gè)光源的效果:?
?
5.3?Vertex Lights
由于后四個(gè)光源其實(shí)并沒有那么重要,我們可以將其計(jì)算從fragment函數(shù)中移到vertex函數(shù)中,也就是從逐像素光照改為逐頂點(diǎn)光照,這樣雖然著色的精度會(huì)損失一些,但是可以減少GPU的消耗。現(xiàn)在,意味著我們支持4個(gè)逐像素光照,4個(gè)逐頂點(diǎn)光照,注逐頂點(diǎn)光照的結(jié)果要傳入到fragment函數(shù)中,作為初始值參與光照的計(jì)算,和逐像素光照相加后輸出:
5.4?Too Many Visible Lights
盡管目前我們已經(jīng)支持到場(chǎng)景中最多16個(gè)光源,但是依然無法避免有可能會(huì)存在更多光源的情況。當(dāng)超出時(shí),我們需要告訴Unity需要將一些光源舍棄以避免數(shù)組的越界。
我們可以通過GetLightIndexMap函數(shù)獲得光源索引的list,修改該list后再通過SetLightIndexMap函數(shù)存回去。Unity將對(duì)索引數(shù)組中為-1的值進(jìn)行忽略,所以我們可以將超出的光源的索引改為-1:
進(jìn)一步優(yōu)化,我們可以只需要當(dāng)數(shù)量確實(shí)超出時(shí)進(jìn)行該操作:?
?
5.5 Zero Visible Lights
另一個(gè)可能性是場(chǎng)景中沒有一個(gè)光源,這時(shí)為了避免錯(cuò)誤崩潰,我們需要先判斷場(chǎng)景中的光源數(shù)量大于0再設(shè)置drawSettings.rendererConfiguration變量,同時(shí)只有在場(chǎng)景中光源數(shù)量大于0的情況下才設(shè)置光源數(shù)據(jù):
?不設(shè)置光源數(shù)據(jù)的一個(gè)副作用是這些數(shù)據(jù)將一直保持最后一個(gè)物體的數(shù)據(jù),為了避免這個(gè)問題,我們需要手動(dòng)將unity_LightIndicesOffsetAndCount設(shè)置為0:
?
?
總結(jié)
以上是生活随笔為你收集整理的Unity SRP自定义渲染管线 -- 3.Lights的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: git重构提交树:分支、变基和挑拣提交的
- 下一篇: Faceware 面部捕捉在Unity中