翻譯:王成林(麥克斯韋的麥斯威爾)
審校:黃秀美(厚德載物)
只烘焙間接光
混合烘焙陰影和實時陰影
處理代碼的變化和問題
支持消減光照(subtractivelighting)
這是渲染系列教程的第17部分。在上一篇中,我們使用光照貼圖添加支持了靜態光照。在這一篇教程中我們將烘焙光照和實施光照的特徵融合起來。
系列回顧:
Unity 渲染教程(一):矩陣
Unity 渲染教程(二):著色器基礎
Unity 渲染教程(三):使用多張紋理貼圖
Unity 渲染教程(四):第一個光源
Unity 渲染教程(五):多個光源
Unity 渲染教程(六):凹凸度
Unity 渲染教程(七):陰影
Unity 渲染教程(八):反射
Unity 渲染教程(九):複雜材質
Unity 渲染教程(十):更多複雜的應用場景
Unity 渲染教程(十一):透明度
Unity 渲染教程(十二):半透明材質的陰影
Unity 渲染教程(十三):延遲渲染
Unity 渲染教程(十四):霧
Unity 渲染教程(十五):延遲光源
Unity 渲染教程(十六):靜態光照
混合烘焙光照和實時光照
光照貼圖可以使我們提前計算光照。這樣以紋理內存為代價減少了GPU在實時中的工作量。此外,它還加入了間接光。但是如我們上次所見,它有一些限制。首先,高光不能被烘焙。其次,烘焙光只通過光照探頭影響動態物體。最後,烘焙光不產生實時陰影。
你可以在下面的截圖中看到完全實時光照和完全烘焙光照之間的區別。這是前一篇教程中的一個場景,唯一的不同是我將所有的球體都設置為動態並重新改變了一些球體的位置。其它一切都是靜態的。這是使用前向渲染的方法。
完全實時和完全烘焙光照
我還沒有調整光照探頭,由於現在還沒有那麼多靜態幾何體,所以探頭的位置不是很重要。現在的探頭光照不是那麼強烈,使我們在使用它時可以更容易地注意到它。
1.1 混合模式
烘焙光照有間接光而實時光照沒有,因為間接光需要光照貼圖。由於間接光可以為場景加入很大的真實感,如果我們可以將它和實時光照融合在一起就再好不過了。這是可以的,儘管這意味著著色的開銷會增加。我們需要將混合光(Mixed Lighting)的光照模式(Lighting Mode)設置為烘焙間接(Baked Indirect)。
混合光照,烘焙間接
我們已經在前一篇教程中切換到這個模式了,但是之前我們只使用了完全烘焙光照。結果,混合光照模式沒有任何區別。為了使用混合光照,光照的模式必須要設置為混合。
混合模式的主光源
在將主定向光改為混合光後,兩件事會發生。首先,Unity會再次烘焙光照貼圖。這一次光照貼圖只會存儲間接光,所以它會比之前的暗很多。
完全烘焙的光照貼圖vs只有間接光的光照貼圖
另外,所有物體都會像主光源被設置為實時那樣被照亮,只有一點不同。光照貼圖被用來為靜態物體添加間接光,而不是球諧光或探頭。動態物體的間接光仍要使用光照探頭。
混合光照,實時直接光照 烘焙間接光
我們不需要改變我們的著色器來支持這點,因為前向基礎通道(forward base pass)已經融合了光照貼圖數據和主定向光源。和往常一樣,額外的光照會得到附加通道(additive pass)。當使用延遲渲染通道時,主光源也會得到一個通道。
混合光可以在運行時調整嗎?
是的,因為它們被用於實時光照。但是,它們的烘焙數據時靜態的。所以在運行時你只能稍微調整光照,比如稍微調整它的強度。更大的變化會使人明顯看出烘焙光照和實時光照之間的不同步。
1.2 更新我們的著色器
剛開始一切似乎正常運行。但是,定向光的陰影衰減發生了錯誤。我們通過極大降低陰影距離觀察到陰影被剪掉了。
陰影衰減,標準著色器vs我們的著色器
雖然Unity很長一段時間都有混合光照模式,但實際上它在Unity5中就不起作用了。Unity5.6中新加入了一個混合光照模式,即我們現在使用的這個。當該新模式被加入時,UNITY_LIGHT_ATTENUATION宏下面的代碼發生了變化。我們在使用完全烘焙光照或者實時光照時沒有注意到這一點,但是我們必須更新我們的代碼以適應混合光照的新方法。由於這是最近的一個巨大的變化,我們必須要注意它所帶來的問題。
我們要改變的第一點是不再使用SHADOW_COORDS宏來定義陰影坐標的插值子(interpolater)。我們必須使用新的UNITY_SHADOW_COORDS宏來代替它。
同樣,TRANSFER_SHADOW應該替換為UNITY_TRANSFER_SHADOW。
然而,這會產生一個編譯錯誤,因為該宏需要一個額外的參數。從Unity 5.6開始,只有定向陰影的屏幕空間坐標中被放入一個插值子。點光源和聚光源的陰影坐標現在在片段程序(fragment program)中進行計算。有個新變化:在一些情況中光照貼圖的坐標被用在陰影蒙版(shadow mask)中,我們會在後面講解這一點。為了該宏能正常工作,我們必須為它提供第二個UV通道中的數據,其中包含光照貼圖的坐標。
這樣會再次產生一個編譯錯誤。這是因為在一些情況下UNITY_SHADOW_COORDS錯誤地創建了一個插值子,儘管實際上並不需要。在這種情況下,TRANSFER_SHADOW不會初始化它,因而導致錯誤。這個問題出現在5.6.0中,一直到5.6.2和2017.1.0beta版本中都有。
人們通常不會注意到這個問題,因為Unity的標準著色器使用UNITY_INITIALIZE_OUTPUT宏來完全地初始化它的插值子結構體。因為我們不使用這個宏,所以出現了問題。為了解決它,我們使用UNITY_INITIALIZE_OUTPUT宏來初始化我們的插值子。那樣的話,我們的代碼就可以編譯成功了。
UNITY_INITIALIZE_OUTPUT有什麼作用?
它只是為變量分配數值0,將其轉換為正確的類型。至少是當程序支持該宏時會這樣,否則它不會做任何事。
我傾向於不使用這個宏,而是只使用顯式賦值,因為這樣可以像上述範例那樣隱藏問題。
1.3 手動衰減陰影
現在我們正確地使用了新的宏定義,但是主光源的陰影仍然沒有按照它們應該的那樣衰減。結果我們發現當同時使用定向陰影和光照貼圖時,UNITY_LIGHT_ATTENUATION不會對光源進行衰減。使用混合模式的主定向光源就會產生這個問題。所以我們必須手動設置。
為什麼在這個例子中陰影沒有衰減?
UNITY_LIGHT_ATTENUATION宏之前是獨立使用的,但是自從Unity5. 6它開始和Unity的標準全局光照函數一同使用。我們沒有採用同樣的方法,因此它不能正常工作。
至於為什麼要做這個改動,唯一的線索就是AutoLight中的一段注釋:「為了性能的原因以GI函數的深度處理陰影」。由於著色器編譯器會隨意地移動代碼,這句話沒有任何價值。如果這個特殊情況有任何原因的話,那麼也很難發現,因為Unity的著色器代碼非常混亂。我反正不知道。
對於我們的延遲光照著色器,我們已經有了進行陰影衰減的代碼。將相關代碼片段從MyDeferredShading中複製到My Lighting中的一個新函數中。唯一實際的區別在於我們必須使用視圖向量和視圖矩陣構建viewZ。我們只需要Z分量,所以無需進行一次完整的矩陣乘法。
該手動衰減必須在使用UNITY_LIGHT_ATTENUATION之後完成。
但是只有當UNITY_LIGHT_ATTENUATION決定跳過衰減時。這是當HANDLE_SHADOW_BLENDING_IN_GI在UnityShadowLibrary包含文件中有定義時才會發生。因此FadeShadows只有在HANDLE_SHADOWS_BLENDING_IN_GI被定義時才會工作。
最後,我們的陰影如它們應該的那樣正常衰減了。
烘焙間接光的混合模式成本很高。它們需要實時光照外加間接光的光照貼圖那麼大的工作量。它和完全烘焙光照相比最重要的是加入了實時陰影。幸運的是,有一個方法仍可以將陰影烘焙到光照貼圖中,將其和實時陰影綜合起來。為了開啟這個功能,我們將混合光照模式改為Shadowmask。
Shadowmask模式
在這個模式中,混合光照的間接光和陰影衰減都存儲在了光照貼圖中。陰影被存儲在一張額外的貼圖(即陰影蒙版)上。當只有主定向光源時,所有被照亮的物體都會作為紅色出現在陰影蒙版中。紅色是因為陰影信息被存儲在紋理的R通道中。事實上,貼圖中至多可以儲存四個光照的陰影,因為它只有四個通道。
烘焙的強度以及陰影蒙版
在Unity創建了陰影蒙版後,靜態物體的陰影投射會消失。只有光照探頭仍會處理它們。動態物體的陰影不受影響。
沒有烘焙陰影
2.1 對陰影蒙版取樣
為了重新得到烘焙陰影,我們必須對陰影蒙版取樣。Unity的宏已經對點光源和聚光源進行了取樣,不過我們必須也要將它包含在我們的FadeShadows函數中。為此我們可以使用UnityShadowLibrary中的UnitySampleBakedOcclusions函數。它需要光照貼圖的UV坐標和世界位置作為輸入參數。
UnitySampleBakedOcclusion是什麼樣子的?
它使用光照貼圖坐標對陰影蒙版取樣,然後選擇適當的通道。unity_OcclusionMaskSelector變量是一個含有一個分量的向量,該分量被設置為1以匹配當前正在被著色的光源。
該函數還處理了光照探頭代理體積的衰減,但是我們還沒有支持這點所以我去掉了那部分的代碼。這就是為什麼該函數有一個世界位置的參數。
當使用陰影蒙版時,UnitySampleBakedOcclusions提供給我們烘焙陰影衰減,在其他情況下它的值都為1。現在我們必須將它和我們已經有的衰減綜合起來然後對陰影進行衰減。UnityMixRealtimeAndBakedShadows函數為我們實現了這些。
UnityMixRealtimeAndBakedShadows是如何工作的?
它也是UnityShadowLibrary中的一個函數。它還處理光照探頭代理體積以及一些其他極端情況。那些情況和我們無關,所以我刪除了一些內容。
如果沒有動態陰影,那麼結果我們得到烘焙的衰減。這意味著動態物體沒有陰影,以及被映射到光照貼圖上的物體沒有烘焙陰影。
當沒有使用陰影蒙版時,它會進行原來的衰減。否則,它會根據我們是否做了陰影混合進行表現,我們後面再講。現在,它只是在實時衰減和烘焙衰減之間進行一個插值。
實時陰影和陰影蒙版陰影
現在靜態物體有了實時陰影和烘焙陰影,且它們正確地混合。實時陰影的衰減仍然超過了陰影距離,但是烘焙陰影沒有。
只有實時陰影衰減了
2.2 添加一個陰影蒙版G-緩存
現在陰影蒙版用於前向渲染,但是我們需要做一番工作使它也可用於延遲渲染。具體來說,當需要時,我們添加陰影蒙版信息作為一個額外的G-緩存。所以當SHADOWS_SHADOWMASK被定義時在我們的FragmentOutput結構體中添加另一個緩存。
這是我們的第五個G-緩存,稍微有些複雜。並不是所有的平臺都支持它。Unity只在有足夠多的渲染目標可用時才支持陰影蒙版,因此我們也應該這樣做。
我們只需在G-緩存中存儲取樣得到的陰影蒙版數據,因為現在我們沒有一個確切的光照。為此我們可以使用UnityGetRawBakedOcclusions函數。它和UnitySampleBakedOcclusion相似,唯一不同在於它沒有選擇其中一個通道。
為了可以在沒有光照貼圖的時候也能成功編譯,當光照貼圖坐標不可用時我們使用0代替它。
2.3 使用陰影蒙版G-緩存
為了使我們的著色器和默認的延遲光照著色器共同工作,這樣做已經足夠了。但是為了我們的自定義著色器正常工作,我們要調整MyDeferredShading。第一步先為額外的G-緩存添加一個變量。
接下來,創建一個函數來得到適當的陰影衰減。如果我們有一個陰影蒙版,這可以通過對紋理取樣然後和unity_OcclusionMaskSelector進行一次顏色飽和點乘來實現。這個變量是在UnityShaderVariables中定義的,包含了一個用於選擇當前正在被渲染的光照通道的向量。
在CreateLight中,即使當前光照沒有實時陰影,我們在有陰影蒙版時也要衰減陰影。
為了正確地包含烘焙陰影,再次使用UnityMixRealtimeAndBakedShadows代替我們之前的衰減計算。
現在我們也可以使用我們自定義的延遲光照著色器得到正確的烘焙陰影了。有一個例外,即當我們的優化分支被使用時會跳過陰影混合。該捷徑在陰影蒙版被使用時不可用。
2.4 距離陰影蒙版模式
雖然使用陰影蒙版模式我們可以得到不錯的靜態物體的烘焙陰影,動態物體卻不能從中獲利。動態物體只能接收到實時陰影以及光照探頭數據。如果我們希望得到動態物體的陰影,那麼靜態物體必須也要投射實時陰影。這裡的混合光照模式我們要用到距離陰影蒙版(Distance Shadowmask)了。
距離陰影蒙版模式
距離陰影蒙版
在Unity2017中,你使用哪個陰影蒙版模式是通過質量設置進行控制的。
當使用DistanceShadowmask模式時,所有物體都使用實時陰影。第一眼看去,好像和Baked Indirect模式完全一樣。
所有物體都有實時陰影
不過這裡仍有一個陰影蒙版。在這個模式中,烘焙陰影和光照探頭的使用超出了陰影距離。因此該模式是成本最高的模式,在陰影距離範圍內等價於烘焙間接模式,超出該範圍則等價於陰影蒙版模式。
近處為實時陰影,遠處為陰影蒙版和探頭
我們已經支持這個模式了,因為我們正在使用UnityMixRealtimeAndBakedShadows。為了正確地混合完全實時陰影和烘焙陰影,它像往常那樣衰減實時陰影,然後取其和烘焙陰影的最小值。
2.5 多重光照
因為陰影蒙版有四個通道,它可以最多同時支持4個光照體積重疊在一起。例如,在以下場景中有三個額外的聚光源。我調低了主光源的強度以使你更容易看到聚光源。
四個光源,都是混合光
主定向光源的陰影仍存儲在R通道中。你還能夠看到存儲在G通道和B通道中的聚光源的陰影。最後一個聚光源的陰影存儲在A通道中,我們看不到它。
當光照體積不重疊時,它們使用相同的通道來存儲它們的陰影數據。所以你可以有任意多個混合光照。但是你必須確保至多四個光照體積彼此重疊。如果有太多個混合光影響同一篇區域,那麼一些就會改回到完全烘焙模式。為了說明這一點,下面這張截圖顯示的是在多加入一個聚光源以後的光照貼圖。你可以在強度貼圖中清楚地看到其中一個已經變成了烘焙光。
5個重疊的光照,其中一個為完全烘焙光
2.6 支持多個有蒙版的定向光
不幸的是,陰影蒙版只有當包含至多一個混合模式的定向光源存在時才能正常工作。對於額外的定向光,陰影衰減會發生錯誤,至少是在使用前向渲染通道時。延遲渲染倒沒有問題。
兩個定向光源產生錯誤的衰減
Unity的標準著色器在5.6.2 2017.1. 0f1版本以前也存在這個問題。不過,這不是擁有光照貼圖的引擎的一個內在限制。這是使用UNITY_LIGHT_ATTENUATION的新方法中的一個漏洞。Unity使用通過UNITY_SHADOW_COORDS定義的陰影插值子來存儲定向陰影的屏幕空間坐標,或者其它擁有陰影蒙版的光源的光照貼圖坐標。
使用陰影蒙版的定向光還需要光照貼圖坐標。在前向基礎通道中,這些坐標會被包含,因為LIGHTMAP_ON會在需要的時候被定義。然而,LIGHTMAP_ON在附加通道中永遠不會被定義。這意味著附加定向光沒有可用的光照貼圖坐標。結果UNITY_LIGHT_ATTENUATION在這種情況下只會使用0,導致錯誤的光照貼圖取樣。
所以我們不能依靠UNITY_LIGHT_ATTENUATION額外獲得使用陰影蒙版的定向光源。讓我們能夠輕鬆地分辨出這個情況。假設我們實際使用屏幕空間的定向陰影,在一些平臺上不是這樣。
接下來,對那些額外有蒙版的定向陰影,我們也要包含光照貼圖坐標。
當光照貼圖坐標可用時,我們可以再次使用FadeShadows函數進行我們自己控制的衰減。
但是,這仍然不正確,因為我們為其輸入了錯誤的衰減數據。我們必須繞開UNITY_LIGHT_ATTENUATION,只得到烘焙後的衰減,在這個情況中我們可以使用SHADOW_ATTENUATION宏。
兩個定向光源正確的衰減
是否可以完全依賴UNITY_LIGHT_ATTENUATION?
很長一段時間內宏代碼都很穩定。對於自定義著色器,最好使用Unity的光照配置。這種情況在5.6.0中發生了變化,其中一個新的方法強行使用一個舊的宏結構。理想狀態下,他們會儘快修復這個宏,這樣我們就可以繼續使用了。否則,我會修改這篇教程不再使用它。
混合光照很好,但是它不像完全烘焙光照那樣成本低廉。如果你以低性能硬體為目標,那麼混合光照不太可行。烘焙光照會管用,但是事實上你也許需要動態物體對靜態物體投射陰影。那樣的話,你可以使用消減混合光照模式。
消減模式
在切換到消減模式後,場景會亮很多。這是由於靜態物體現在同時使用完全烘焙的光照貼圖和定向光源。和前面一樣,動態物體仍然使用光照探頭和定向光源。
靜態物體受到兩次光照
消減模式只可用於前向渲染。當使用延遲渲染路徑時,相關的物體會回到前向渲染路徑,就像透明物體那樣。
3.1 消減光照
在消減模式中,靜態物體通過光照貼圖被照亮,同時還將動態陰影考慮在內。這是通過降低光照貼圖在陰影區域的強度來實現的。為此,著色器需要使用光照貼圖和實時陰影。它還需要使用實時光照來計算出要將光照貼圖調暗多少。這就是為什麼我們在切換到這個模式後得到了雙重光照。
消減光照是一個近似,只在一個單一定向光下起作用,因此它只支持主定向光的陰影。另外,我們必須以某種方式了解在動態著色區域內間接光的環境是什麼。由於我們使用的是一個完全烘焙的光照貼圖,我們沒有這個信息。Unity沒有包含一個額外的只有間接光的光照貼圖,而是使用了一個統一的顏色對環境光取近似值。即實時陰影顏色(Realtime Shadow Color),你可以在混合光照選項中調整它。
在著色器中,我們知道當LIGHTMAP_ON,SHADOWS_SCREEN,和LIGHTMAP_SHADOW_MIXING關鍵詞被定義而SHADOWS_SHADOWMASK沒有被定義時我們應該使用消減光照。如果這樣的話我們定義SUBTRACTIVE_LIGHTING,以便更容易使用它。
在做其他事情之前,我們必須去除掉雙重陰影。為此我們可以關閉動態光照,就像我們對延遲通道所做的那樣。
靜態物體只有烘焙光
3.2 為烘焙光打陰影
為了應用消減陰影,我們創建一個函數以在需要的時候調整間接光。通常它不會做任何事。
我們在獲取光照貼圖數據後要調用該函數。
如果有消減光照,那麼我們必須獲取陰影衰減。我們可以簡單地從CreateLight中將代碼複製過來。
下一步,我們要計算出如果使用實時光照的話我們可以接收到多少光。我們假設該信息和烘焙在光照貼圖中的信息相吻合。由於光照貼圖只包含漫射光,我們只需計算定向光的蘭伯特項。
為了達到陰影光照的強度,我們必須將蘭伯特項乘以衰減。但是我們已經有了完全不含陰影的烘焙光照。因此我們估算一下有多少光被陰影擋住了。
通過從烘焙光中減去該估值,我們最終得到了調整好的光照。
減去後得到的光照
無論在什麼環境光場景中,這總會產生純黑色陰影。為了更好地符合場景的需要,我們可以使用我們的消減陰影顏色,可以通過unity_ShadowColor實現。陰影區域不應比這個顏色更暗,不過它們可以更亮些。所以我們取計算出的光照和陰影顏色的最大值。
我們還要考慮到陰影強度被設置為小於1這個情況。為了應用陰影強度,在有陰影和無陰影光照之間基於_LightShadowData的X分量做插值。
有顏色的陰影
因為我們的場景的環境強度(ambient intensity)被設置為0,所以默認的陰影顏色和場景不太搭配。但是人們可以很輕鬆地發現消減陰影,因此我沒有調整它。還有一點非常明顯,即陰影顏色現在覆蓋了所有的烘焙陰影,而實際不應該這樣。它應該只影響那些接收動態陰影的區域,不應該使烘焙陰影變亮。為此,使用消減光照和烘焙光照的最小值。
正確的消減陰影
現在只要我們使用適當的陰影顏色,我們就會得到正確的消減陰影。但是記住這只是一個近似,而且它不太適用於多重光照。例如,其它的烘焙光會產生錯誤的陰影。
其它光照錯誤的消減
下一篇教程將探討實時全局光照。
【版權聲明】
原文作者未做權利聲明,視為共享智慧財產權進入公共領域,自動獲得授權。
加小編微信,享雙重福利
1.加入GAD程序猿交流群,獲取行業乾貨;
2.領取60G騰訊內部分享等獨家程序資料。