譯者:崔嘉藝(milan21)
審校:王磊(未來的未來)
• 使用一個自定義的光源著色器。
• 解碼低動態光照渲染的顏色。
• 在單獨的渲染通道中添加光照。
• 支持方向光源、聚光光源和點光源。
• 手動採樣陰影貼圖。
這是關於渲染基礎的系列教程的第十五部分。在前面的部分裡我們在場景中加上了霧。這一次我們將創建我們自己的延遲光源。
系列回顧:
Unity 渲染教程(一):矩陣
Unity 渲染教程(二):著色器基礎
Unity 渲染教程(三):使用多張紋理貼圖
Unity 渲染教程(四):第一個光源
Unity 渲染教程(五):多個光源
Unity 渲染教程(六):凹凸度
Unity 渲染教程(七):陰影
Unity 渲染教程(八):反射
Unity 渲染教程(九):複雜材質
Unity 渲染教程(十):更多複雜的應用場景
Unity 渲染教程(十一):透明度
Unity 渲染教程(十二):半透明材質的陰影
Unity 渲染教程(十三):延遲渲染
Unity 渲染教程(十四):霧
從現在開始,關於渲染基礎的系列教程將由Unity 5.6.0製作。 這個Unity版本在編輯器和著色器中改變了一些東西,但是你仍然可以在這裡面找到你自己的方式。
使用我們自己的延遲光源。
我們在《渲染13:延遲渲染》中添加了對延遲渲染路徑的支持。我們所要做的只是填補G緩衝區。光源在後面的過程中會在後面渲染。那個教程簡要地介紹了Unity如何添加這些光源。 這一次,我們將自己渲染這些光源。
為了測試光源,我將使用一個簡單的場景,其環境強度設置為零。它用延遲模式下的高動態光照渲染攝像機進行渲染。
測試場景,有方向光源和沒有方向光源的效果對比。
場景中的所有對象都使用我們自己的著色器渲染到G緩衝區。但是,這些光源是用Unity的默認延遲著色器渲染的,它被命名為Hidden / Internal-DefferedShader。你可以通過「編輯/項目設置/圖形」轉到圖形設置並將「延遲」著色器模式切換為「自定義著色器」來進行驗證。
默認的延遲光源著色器。
使用一個自定義的著色器
每個延遲光源使用單獨的渲染通道進行渲染,修改圖像的顏色。實際上,它們是圖像效果,就像我們之前教程中的延遲渲染下霧的著色器一樣。 我們從一個簡單的著色器開始,用黑色覆蓋一切。
指示Unity在渲染延遲光源的時候使用此著色器。
使用我們的自定義著色器。
第二個渲染通道
切換到我們的著色器後,Unity抱怨說它沒有足夠的渲染通道。顯然,需要第二個渲染通道。我們只是複製我們已經擁有的渲染通道,看看會發生什麼。
Unity現在接受我們的著色器並使用它來渲染方向光源。 結果,一切都變黑了。 唯一的例外是天空。模板緩衝區用作掩碼以避免在那裡渲染,因為方向光源不影響背景。
自定義著色器,照亮和不照亮的效果對比。
但是第二個渲染通道呢? 請記住,當高動態光照渲染被禁用的時候,光源數據被用對數編碼。最後一個渲染通道需要、反轉這個編碼。這是第二個渲染通道的用處。 因此,如果你禁用了攝像機的高動態光照渲染,我們的著色器的第二個渲染通道還是會被使用,一次。
避開對天空的影響
當以低動態光照渲染模式渲染時,你可能會看到天空變黑。 這可能發生在場景視圖或遊戲視圖中。如果天空變黑,那麼執行轉換的渲染通道沒有正確地使用模板緩衝區作為掩碼。要解決此問題,請明確設置第二個渲染通道的模板。當我們處理的是非背景部分的片段的時候,我們只應該進行渲染。 通過_StencilNonBackground提供了適當的模板值。
我們可以調試模板緩衝區嗎?
不幸的是,幀調試器不會顯示有關模板緩衝區的任何信息,也不顯示其內容以及通道如何使用。 也許這些信息將在以後的版本中添加。
轉換顏色
為了使第二個渲染通道工作,我們必須轉換光源緩衝區中的數據。像我們的霧著色器一樣,使用UV坐標來繪製全屏的四邊形,我們可以使用它來採樣緩衝區。
光源緩衝區本身通過_LightBuffer變量提供給著色器。
當沒有被照亮的時候,原始的低動態光照渲染數據。
使用公式2-C對低動態光照渲染的顏色進行對數編碼。 為了解碼,我們必須使用公式-log2 C。
當沒有被照亮的時候的低動態光照渲染的圖像。
現在我們知道它可以正常工作,再次啟用高動態光照渲染。
第一個渲染通道會負責渲染光源,所以這將是相當複雜的。讓我們為它創建一個名為MyDeferredShading.cginc的導入文件。 將渲染通道中的所有代碼複製到此文件。
然後在第一個渲染通道導入MyDeferredShading。
因為我們應該添加光照到圖像,我們必須確保我們不會移除已經渲染的內容。 我們可以通過更改混合模式來組合完整的源顏色和目標顏色。
我們需要所有可能的光照配置的著色器變體。multi_compile_lightpasscompiler指令創建我們需要的所有關鍵字組合。唯一的例外是高動態光照渲染模式。 我們必須為此添加一個單獨的多編譯指令。
雖然這種著色器用於所有這三種類型的光源,但我們首先限制我們使用方向光源。
G緩衝區的UV坐標
我們需要UV坐標從G緩衝區中進行採樣。不幸的是,Unity不提供帶有紋理坐標的光照渲染通道。相反,我們必須從裁剪空間的位置來派生它們。為此,我們可以使用UnityCG中定義的ComputeScreenPos函數。這個函數會產生齊次坐標,就像裁剪空間的坐標一樣,所以我們必須使用一個float4類型的變量來存儲它們。
在片段程序中,我們可以計算最終的2D坐標。如在《渲染7:陰影》中所解釋的那樣,這必須在插值之後發生。
世界位置
當我們創建我們的延遲渲染下的霧的圖像效果的時候,我們必須弄清楚片段與相機的距離。我們通過從相機發射光線通過每個片段到遠平面,然後對片段的深度值進行縮放,從而實現了這一點。 我們可以使用相同的方法來重建片段的世界位置。
在方向光源的情況下,四邊形的四個頂點的光線會作為法向矢量提供。所以我們可以把它們傳遞給頂點程序進行插值。
我們可以通過對_CameraDepthTexturetexture進行採樣並將其線性化,就像我們對霧的效果所做的一樣,在片段程序中找到深度值。
不過,我們提供的光線到達遠平面的處理與我們的霧著色器中的光線到達遠平面的處理差別很大。在這種情況下,我們被提供的是到達近平面的光線。 我們必須對它們進行放縮,所以我們得到的是到達遠平面的光線。 這可以通過縮放光線來完成,使其Z坐標變為1,並將其與遠平面的距離相乘。
將此光線縮放深度值來給我們一個位置。所提供的光線在視圖空間中定義,這是相機的本地空間。 所以我們最終得到了片段在視圖空間中的位置。
從這個空間到世界空間的轉換是用ShaderVariables中定義的unity_CameraToWorld矩陣完成的。
讀取G緩衝區數據
接下來,我們需要訪問G緩衝區來檢索表面屬性。 緩衝區可通過三個_CameraGBufferTexture變量變得可用。
我們在《渲染13:延遲著色器》教程中填充了相同的緩衝區。現在我們從這個緩衝區那裡讀取數據出來。 我們需要反照率、鏡面高光的色澤、光滑度和法線值。
計算雙向反射分布函數
雙向反射分布函數函數在UnityPBSLighting中定義,因此我們必須導入該文件。
現在如果我們要在我們的片段程序中調用雙向反射分布函數需要三個額外的數據位。首先是視圖方向,這是用通常的方法得到的。
第二個數據是表面反射率。我們可以從鏡面色調中推導出來。它只是最強的顏色分量。 我們可以使用SpecularStrength函數來提取它。
第三,我們需要光源數據。 讓我們從虛擬光源開始吧。
最後,我們可以使用雙向反射分布函數來計算該片段中光源的貢獻。
配置光源
間接光源在這裡不適用,所以它仍然是黑色的。 但是直接光源必須進行配置,使其與當前正在渲染的光源相匹配。對於方向光源,我們需要一個顏色和方向。這些可以通過_LightColor和_LightDirvariables來獲取。
讓我們創建一個單獨的函數來設置光源。簡單地將變量複製到光源結構中並返回。
在片段程序中使用這個函數。
來自錯誤的方向的光源。
我們終於得到了光照,但光線似乎來自錯誤的方向。這是因為_LightDir設置為光線行進的方向。對於我們的計算,我們需要的是從表面到光源的方向,所以應該相反。
方向光源,沒有陰影的效果。
陰影
在My Lighting中,我們依靠AutoLight的宏來確定由陰影引起的光衰減。 不幸的是,該文件沒有寫入延遲光源。 所以我們自己來進行陰影採樣。陰影貼圖可以通過_ShadowMapTexture變量來訪問。
但是,我們不能隨便聲明這個變量。 它已經為UnityShadowLibrary中被定義點光源和聚光光源的陰影,我們間接導入了它們。 所以我們不應該自己定義它,除非使用方向光源的陰影。
要應用方向光源的陰影,我們只需要對陰影紋理進行採樣,並使用它來衰減光的眼色。 在CreateLight函數中執行此操作意味著必須將UV坐標作為參數添加到其中。
在片段程序中將UV坐標傳遞給它。
有陰影的方向光源的效果。
當然,這隻有在方向光源啟用了陰影的時候才有效。 如果方向光源沒有啟用陰影的話,陰影衰減始終為1。
陰影的衰減
陰影貼圖是有限的。 它不能覆蓋整個世界。它覆蓋的面積越大,陰影的解析度越低。 Unity有一個繪製陰影的最大距離。 除此之外,還沒有實時陰影。這個距離可以通過「編輯/項目設置/質量」進行調整。
陰影距離質量設置。
當陰影接近這個距離的時候,它們會消失。 至少這就是Unity的著色器所做的事情。 因為我們是手動採樣陰影貼圖,所以當到達地圖邊緣的時候,陰影會被截斷。得到的結果就是陰影被急劇地切斷或者超出了衰減的距離。
陰影距離設置的比較大和比較小的效果對比。
為了讓陰影衰減,我們首先要知道陰影應該完全消失的距離。這個距離取決於方向光源的陰影的投影方式。在「穩定擬合」模式中,這個衰減是按照球面進行衰減的,以貼圖的中間為中心。在」緊密擬合」模式下,它基於視圖深度來進行衰減的。
UnityComputeShadowFadeDistance函數可以為我們找出正確的指標。 它具有世界位置和視圖深度作為參數。它將返回距離影子中心的距離或是未修改的視圖深度。
陰影應該逐漸消失,因為它們逐漸接近褪色距離,一旦到達這個距離陰影就會完全消失。UnityComputeShadowFade函數會計算適當的漸變因子。
這些函數是什麼樣子的?
它們在UnityShadowLibrary中定義。 unity_ShadowFadeCenterAndType變量包含陰影的中心和陰影的類型。 _LightShadowData變量的Z和W組件包含了用於衰減的縮放和偏移量。
陰影衰減因子是一個從0到1的值,表示陰影應該衰減多少。實際的衰減可以通過簡單地將該值加到陰影衰減中,並將計算出來的值限制到0到1這個範圍來完成。
float shadowFade = UnityComputeShadowFade(shadowFadeDistance);
shadowAttenuation = saturate(shadowAttenuation + shadowFade);
為了做到這一點,在我們的片段程序中提供世界的位置和深度到CreateLight函數。 視圖深度是片段在視圖空間中的位置的Z分量。
發生了衰減的陰影效果。
光源的Cookie
我們必須支持的另一件事情是光源的cookie。通過_LightTexture0可以獲得cookie紋理。此外,我們還必須從世界空間轉換為光源所在空間,因此我們可以對紋理進行採樣。該轉換通過unity_WorldToLightmatrix變量提供。
在CreateLight函數中,使用矩陣將世界位置轉換為光源空間的坐標。然後使用它們來對cookie紋理進行採樣。我們使用單獨的衰減變量來跟蹤cookie的衰減。
帶有cookie的方向光源。
結果看起來很好,除非你密切注意幾何邊緣才會注意到問題。
沿著幾何邊緣的瑕疵。
當相鄰片段的cookie坐標之間存在很大差異的時候,會出現這些瑕疵。在這些情況下,圖形處理器會選擇對於最接近的表面來說太低的mipmap級別。 Aras Pranckevičius給Unity指出了這個問題。Unity使用的解決方案是在採樣mip地圖的時候應用一個偏差,因此我們也將這樣做。
帶有偏差的cookie採樣。
支持低動態光照渲染
現在我們可以正確地渲染方向光源,但只能在高動態光照渲染模式下。低動態光照渲染模式下會出錯。
低動態光照渲染模式下顏色不正確。
首先,編碼的低動態光照渲染的顏色必須乘以光源緩衝區的顏色,而不是加上光源緩衝區的顏色。我們可以通過將我們的著色器的混合模式更改為Blend DstColor Zero來實現。但是,如果我們這樣做的話,那麼高動態光照渲染模式下的渲染會出錯。相反,我們必須使混合模式變量。Unity為此使用_SrcBlend和_DstBlend。
效果不同,但仍然不正確。
在未定義UNITY_HDR_ON的時候,我們會在片段程序結束的時候還是必須應用2-C這個轉換。
因為方向光源會影響一切,所以它們被畫成全屏四邊形。相比之下,聚光光源僅影響位於其錐體內的場景部分。通常不需要為整個圖像計算聚光光源的光照。相反,金字塔被渲染成與聚光光源影響區域相匹配的方式。
繪製一個金字塔區域
禁用方向光源,並使用聚光光源來代替。因為我們的著色器只能在方向光源上正常工作,所以結果將是錯誤的。但是它可以讓你看到金字塔區域的哪些部分被渲染。
金字塔區域的一部分。
事實證明,金字塔區域被渲染為一個常規的3D對象。它的背面被剔除,所以我們看到金字塔區域的前方。只有在前面沒有任何東西的時候才會被繪製出來。此外,添加了一個渲染通道,設置模板緩衝區以將繪製限制在位於金字塔區域內的片段上。你可以通過幀調試器來驗證這些設置。
它是如何繪製的。
這意味著我們的著色器的裁剪和z檢驗設置被推翻了。所以讓我們從著色器中刪除它們。
當聚光光源照亮的體積離攝像機足夠遠的時候,此方法有效。但是,當光線太靠近攝像機的時候,會失敗。當這種情況發生的時候,攝像機可能會在聚光光源照亮的體積的內部。 甚至可能近平面的一部分都在聚光光源照亮的體積的內部,而其餘部分位於聚光光源照亮的體積的外部。在這些情況下,模板緩衝區不能用於限制渲染。
這個技巧用於仍然在金字塔的內表面渲染光,而不是金字塔的外表面渲染光。 這是通過渲染金字塔的背面而不是金字塔的的正面來完成的。此外,這些曲面只有在它們之前的物體已經渲染出來之後才會被渲染。這種方法還涵蓋了聚光光源體積內的所有片段。但是它最終會渲染太多的片段,因為金字塔通常隱藏的部分現在也被渲染了。所以只有在必要的時候才這麼做。
在靠近攝像機的時候繪製背面
如果你將攝像機或是聚光光源靠近彼此來移動,你會看到Unity會根據需要在這兩種渲染方法之間進行切換。一旦我們的著色器在聚光光源下工作正常,兩種方法之間就沒有視覺上的區別。
支持多種光源類型
目前,CreateLight只適用於方向光源。讓我們確保僅在適的時候時才使用特定於方向光源的代碼。
雖然陰影衰減可以在基於方向光源陰影貼圖的時候正常工作,那麼其他光源類型的陰影也是需要衰減的。這樣可以確保所有的陰影都以相同的方式消失,而不是只有一些陰影會消失。 因此,陰影衰落代碼適用於所有光源類型,只要這個光源類型會產生陰影。所以我們把這塊的代碼移到特定光源塊之外。
我們可以使用布爾值來控制是否使用陰影衰落代碼。 由於布爾值作為常量值,如果該值保持為false,則這段代碼不會被執行。
非方向光源具有一個位置。這個位置通過_LightPos提供。
現在我們可以確定聚光光源的光矢量和光線方向。
再次計算世界位置
光線方向似乎不正確,結果為黑色。發生這種情況是因為聚光光源的世界位置計算不正確。當我們在場景中的某個地方渲染一個金字塔的時候,我們沒有一個方便的全屏四邊形,會在法線通道中存儲光線。相反,MyVertexProgram必須從頂點位置導出光線。 這是通過將點轉換為視圖空間的位置來完成的,我們可以使用UnityObjectToViewPos函數。
然而,這會產生具有錯誤方向的光線。我們必須對它們的X和Y坐標取負。
正確的世界空間下的位置。
UnityObjectToViewPos是如何工作的?
這個函數是在UnityCG中定義的。 它首先將點轉換為世界空間的位置,然後使用視圖矩陣將世界空間的位置轉換為相機空間的位置。
當在場景中渲染光的幾何體的時候,這種替代方法起作用。當使用全屏四邊形的時候,我們應該使用頂點法線。 Unity通過_LightAsQuad變量告訴我們現在是正在處理哪種情況。
如果這個變量設置為1,那麼我們正在處理一個四邊形,可以使用法線。 否則,我們必須使用UnityObjectToViewPos。
Cookie的衰減
聚光光源的圓錐衰減是通過Cookie紋理創建的,無論是默認圓圈還是自定義Cookie。 我們可以從複製方向光源的cookie代碼開始。
然而,聚光光源的cookie在越遠離光源位置的情況下會變得越大。這是通過透視變換完成的。所以矩陣乘法會產生四維的齊次坐標。為了得到常規的二維坐標,我們必須將X分量和Y分量除以W分量。
Cookie的衰減。
這實際上會導致兩個光錐,一個光錐向前,一個光錐向後。向後的錐體通常在渲染區域的外部結束,但這不能保證。我們只想要向前的錐體,它與負的W坐標相對應。
距離衰減
來自聚光光源的光線也會根據距離進行衰減。該衰減存儲在查找紋理中,可通過_LightTextureB0獲得。
紋理被設計為必須採用平方後光源的距離進行採樣,按照光源的範圍進行縮放。光源的範圍存儲在_LightPos的第四個變量中。應該使用哪個紋理通道,因平臺而異,由UNITY_ATTEN_CHANNEL宏進行定義。
Cookie和距離衰減。
陰影
當聚光光源有陰影的時候,會定義SHADOWS_DEPTH關鍵字。
聚光光源和方向光源使用相同的變量來對其陰影貼圖進行採樣。在聚光光源的情況下,我們可以使用UnitySampleShadowmap來處理採樣硬陰影或軟陰影的細節。我們必須在陰影空間中提供片段位置。 unity_WorldToShadow數組中的第一個矩陣可用於從世界空間轉換為陰影空間。
帶有陰影的聚光光源。
點光源使用與聚光光源相同的光矢量、方向和距離衰減。所以他們可以共享該代碼。聚光光原始碼的其餘部分只能在定義SPOT關鍵字的時候使用。
這已經足夠讓點光源工作了。它們的渲染與聚光光源相同,不同之處在於使用的是一個圈而不是金字塔。
高強度的點光源。
陰影
點光源的陰影存儲在立方體貼圖之中。UnitySampleShadowmap負責我們的採樣。在這種情況下,我們必須提供一個從光源到表面的向量,以便對立方體貼圖進行採樣。這與光矢量的方向相反。
帶有陰影的點光源。
Cookie
點光源的cookie也可以通過_LightTexture0獲得。然而,在這種情況下,我們需要一個立方體貼圖而不是一個普通的紋理。
要對cookie進行採樣,請將片段的世界位置轉換為光照空間的位置,並使用該位置對立方體貼圖進行採樣。
帶有cookie的點光源。
點光源的cookie紋理不起作用?
如果你最初使用的是較舊的Unity版本來導入Cookie的立方體貼圖紋理,那麼導入設置可能會出錯。這隻發生在立方體貼圖上。確保其紋理類型為Cookie,映射設置為「自動」,光源類型為」點光源「。
點光源cookie紋理的導入設置。
跳過陰影
我們現在可以使用我們自己的著色器渲染所有動態光源了。在現在這個時間點上,我們不太注意優化,但這裡有一個潛在的大型優化值得考慮。
最終超出陰影褪色距離的片段不會被遮擋。但是,我們仍然在對其陰影進行採樣,這個操作可能是昂貴的。我們可以通過基於陰影褪色因子做分支來避免這種情況。 如果這個值接近1,那麼我們可以完全跳過陰影衰減。
然而,分支本身可能是昂貴的。 這只是一個改進,因為這是一個連貫的分支。 除了陰影區域的邊緣附近,所有碎片都落入其內部或外部。 但是,如果圖形處理器能夠利用這一點,這會是重要的。 在這種情況下,HLSLSupport定義了UNITY_FAST_COHERENT_DYNAMIC_BRANCHING宏。
即使這樣,只有當陰影需要多個紋理採樣的時候,它才是真正值得的。對於聚光光源和點光源的軟陰影來說,這是用SHADOWS_SOFT關鍵詞來表示的。 方向光源的陰影總是需要一個單一的紋理採樣,而這便宜。
下一個教程將涉及更多的渲染技術。
【版權聲明】
原文作者未做權利聲明,視為共享智慧財產權進入公共領域,自動獲得授權。
加小編微信,享雙重福利
1.加入GAD程序猿交流群,獲取行業乾貨;
2.領取60G騰訊內部分享等獨家程序資料。