掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

遊資網發表於2020-11-23
在17日晚的Unity線上技術大會中,掌趣科技技術專家林若峰針對Unity 2019新特性進行了分享。他所提到的技術點離手遊行業更近,一系列實操的方法也充分考慮了專案落地的問題,因此對一些正在考慮品質提升的手遊專案來說,或許有更多的參考價值。

本次視訊也已經上傳至Unity B站官方,視訊中還有現場的精彩問答環節,大家也可以前往觀看:

https://space.bilibili.com/386224375

以下內容為林若峰的分享:

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

大家好,我叫林若峰,目前任職於掌趣科技,擔任技術專家的職位,主要在天馬時空負責次世代手遊《黑暗之潮》的客戶端技術,以及公司的客戶端框架的開發和維護。今天以《黑暗之潮》的開發經驗為基礎,聊一聊Unity2019的新特性,在實際商業專案中的應用以及落地的相關實操經驗。

從Unity2018開始,Unity就引入了不少的新技術,不過一直以來相關技術分享仍以教程介紹為主,所以今天我們來看一下DOTS和URP這兩項技術的實操,以及我們的一些心得和體驗。

首先簡單介紹一下《黑暗之潮》,這是一款頂視角的次世代手遊,目前處於內測階段,雖然它鎖定了視角,但實際對畫質和戰鬥細節的要求很高。大家可以從截圖上看出,遊戲採用了PBR的渲染,場景當中有不少的動態光影效果,場景的細節也相當豐富。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

今天主要分享幾塊內容:首先是製作《黑暗之潮》這款專案的挑戰,其次是我們在渲染管線的選擇和定製方面的經驗,包括當中可能遇到的問題,以及應用的地方;接下來我會就DOTS技術棧在我們專案當中的運用,聊一聊有關DOTS的常見誤解;最後,我會分享一些專案工作流的簡化和改善經驗。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

正題開始之前,先給ILRuntime打個小廣告,ILRuntime是我製作的C#熱更解決方案,目前已經在大量的商業專案當中得到了驗證,比如掌趣旗下四款上線很長時間的遊戲,都是採用ILRuntime進行的熱更,大家如果對C#語言的熱更方案感興趣,可以在Github上進一步瞭解。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

現在開始正題,先看當時《黑暗之潮》中遇到的挑戰。

首先顯而易見的,我們的遊戲採用了PBR的次世代渲染技術,在畫面表現上面有不少挑戰。第二,我們希望適配儘可能廣的機型,接觸更多的玩家。第三,如下圖,這款遊戲的戰鬥強度非常高,會有大量的怪以及技能特效。

除此之外,這款遊戲的戰鬥機制也有非常多特殊的定製,單一個職業來說,可能有上百個技能供玩家選擇和搭配,在這些技能的實現過程中,對效能的要求會非常高。最後,由於採用了PBR的模型製作流程,在工作流方面也有繁多、複雜、出錯的環節,需要針對這些環節進行簡化。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

下面第一個主題,是關於渲染管線的選擇和定製。

《黑暗之潮》選用了URP技術,它是一個比較適合移動平臺開發的PBR渲染管線,雖然說它是PBR渲染管線,但實際上非PBR的東西也可以用它來渲染。

我們當初非常看重的一點是,URP擁有非侵入式修改的能力,我們在不修改URP原始碼的情況下,可以對它進行比較多的定製。此外,URP有全部的C#原始碼,整個渲染過程基本全部能掌控在我們自己手裡,當出現問題或者遇到bug的時候,比較容易定位。原始碼的結構清晰,組織也非常合理,所以我們擴充套件和自定義起來也會相對方便。

還有最關鍵的是,URP的效能比Builtin內建管線更好。可能有人會問,為什麼我們要對渲染管線進行自定義,是不是因為URP有坑,或者不能實現什麼效果,所以必須去自定義?實際上不是的,因為每個專案都有各自獨特的需求,在更好地滿足這些需求的情況下,就需要對渲染管線去進行定製。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

舉一個例子,這個角色釋放了一個火焰效果的技能,但是這個火焰效果的特效被渲染在了地面的裂紋之上,這個其實是一個錯誤的表現。正確的表現是火焰的特效能夠蓋在這些地表裂痕的上方。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

為了解決這個問題,以前我們在Builtin管線當中只能通過修改不穩定的Renderqueue,或者通過程式碼去修改這些物體的Renderqueue來規避問題。這麼做有一個比較大的弊端,這時可能需要對這些物體新建一個Shader,或者要寫比較複雜的邏輯來規避。

一旦引入了新的Shader,就有可能要重新去做一遍剛才做的這些效果,非常麻煩,而且容易反覆出問題。在Builtin當中,一些效果其實只能通過Shader Pass去實現。比如要給這個物體增加一個描邊,就需要在角色的Shader裡額外增加一個Pass去實現。弊端是,在渲染過程中,勢必會被多Pass給打斷合批。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

大家可以看到下面這幅圖,我們如果在渲染object1的時候,如果它的Shader有多個Pass,我們需要首先渲染Pass1,然後通過一個Set Pass call渲染Pass2,然後再Set Pass call渲染Pass3,這個時候渲染完object1再渲染第二個物體時,又會把剛才的操作重新重複一遍,Pass1、Pass2、Pass3……渲染這兩個物體的時候,就會有非常多的DrawCall,而且每次DrawCall切換開銷都比較大。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

實際上有更好的方式,我們可以用這種流水線一樣的方式渲染這兩個Pass。我們在渲染Pass1的時候,我們會以一口氣把所有的object1、2、3,一次性全都渲染了,渲染完畢之後通過一次Set Pass Call去渲染這個Pass2,實際上我們這三個物體總共需要兩個Pass就可以渲染完畢,自然而然它的效率會高不少。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

還有一個問題是,Unity是一個通用引擎,會考慮各種專案的情況,為了相容性,它可能會在渲染的過程當中,在一些情況下加入Blit操作,相當於是把全屏的結果進行一次複製。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

這個複製開銷對於移動平臺來說非常大,因為移動平臺的頻寬很有限。實際上,我們在自己的專案當中,因為對整個渲染流程比較清楚,知道哪些情況下可以使用Blit,或者不需要Blit,就可以看情況把它去掉。如果它能夠去掉,對整個遊戲效能會有比較大的改進,也能降低很多頻寬開銷。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

另外,每個專案都會有一些特有的效果,比如下面截圖裡,對於URP本身,在預設情況下,像扭曲,空氣擾動效果只對不透明物體生效,火焰效果在這裡就會顯得比較突兀,因為它不受擾動效果的影響。對美術而言,這樣的效果就不是特別理想,所以我們可以對它進行定製,最後實現空氣擾動同樣能對火焰產生影響的效果。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

接下來介紹一下URP在預設情況下的渲染管線的流程。

在開啟了動態光影的情況下,URP它會首先去渲染主光源的Shadowmap,然後再去渲染附加光源的為Shadowmap,主光源在URP裡面其實主要是指的充當太陽光的那麼一棧方向光,附加光源除了那棧方向光以外的,比如點光源,射燈之類的動態光源,在渲染完這兩張shadowmap之後,URP會進行一個叫做Depth Prepass的操作。

稍微岔開一些話題,Depth Prepass這個名字可能會有一些誤導。通常來說,Depth Prepass的最主要的作用就是預先把整個場景所有物體的深度渲染一次,後面再進行不透明物體渲染的時候直接使用深度的結果進行深度測試,從而儘可能去利用Early Z把一些不必要的片源去掉。在AlphaTest的時候,畫素的深度實際上要在比較後期才能夠決定的,如果沒有Depth Prepass的話,有可能Early Z會在這些地方失效。

但在URP當中,Depth Prepass並沒有上述這個作用,實際上只是把場景裡面所有的物體深度渲染到一張單獨的RT當中,給後面的效果進行使用。

回到管線流程,做完Depth Prepass之後,會進行所有不透明物體的繪製,繪製完畢之後會進行天空盒的繪製,繪製完天空盒之後,會進行Copy Color的操作,如果使用者在渲染管線的設定當中,開啟了Color Pictures這個功能,它就會進行這個操作,把當前的渲染結果複製到一張獨立的RT上面,供後期的效果使用。

接下來會進行所有透明物體的繪製,繪製完透明物體,會對全屏進行後效處理。如果大家還有UI,會在這個時候去繪製,繪製完UI之後,會把當前所有的渲染結果進行最後的一次Blit操作,把它給複製到螢幕緩衝區當中。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

那麼我們怎麼去對URP內建進行定製?

第一,URP預設的情況下提供了一個比較簡單的東西,叫做RenderObject,它是URP已經實現好的RenderFeature和RenderPass,關於RenderFeature我後面詳細再說。通過它,我們可以在不寫一行程式碼的情況下,對渲染管線進行定製。我們可以明確設定一個layer,以及這個layer需要在哪一個具體的時間點進行渲染。此外,我們還可以在選擇透明物體渲染之前,去做RenderFeature,並且做一些額外的設定。比如繪製塗層的時候,選擇需要使用哪個彩色球,也可以選擇不進行過載等。

我們還可以對一些渲染狀態進行過載 ,對深度進行過載,來決定這個東西是否寫深度或者做深度測試,以及對模板快取的方式進行具體設定。對於攝像機的引數,我們可以去設定,包括單獨對某一層的物體使用不一樣的FOV,這在FPS類遊戲應用比較。甚至我們可以對camera變換矩陣進行過載,拍一個跟主相機完全不一樣的區域,這也是可以實現的。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

在《黑暗之潮》中,我們利用RenderObject主要是進行了這些的操作。

第一,解決我們最開始提到的例子,地面的裂紋這些透明物體的渲染,解決它的渲染不確定性。我們單獨使用了一個RenderObject,選中了剛才地表的那一層layer,讓它在透明物體之前去渲染這一整層,就能保證會在所有技能特效之前去渲染地面的裂紋,這樣就不會出現剛才例子裡面提到的錯誤情況。

第二,RenderObject也可以輔助其他的自定義RenderPass,我們在後面講RenderFeature和RenderPass的時候會具體說這樣的用法。

第三,剛才提到我們想要對透明物體也能夠實現扭曲的效果,需要把複製那張ColorTexture的時機往後挪,挪到透明物體之後,用單獨的Pass額外的去渲染這些需要扭曲的效果的特效,才能完成正確的渲染。這就是通過RenderObject去實現的。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

接下來介紹一下RenderFeature和RenderPass的自定義。

這是URP提供的比RenderObject更高一層級的自定義,通過RenderFeature基本上可以做到在任意一個時間點插入自己想要的渲染操作,我們就會擁有更強的控制能力。因為在RenderFeature裡面可以通過手動呼叫CommandBuffer底層渲染介面,這能實現非常多的效果。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

此外,在使用RenderPass的時候,可以在切換RT的時候,通過RenderBuffer的LoadStore操作來進行效能優化。我需要提一下,在切換RT時的RenderBuffer的LoadStore操作具體是什麼含義?

現在移動GPU基本上都採用了tile base的架構,渲染的時候GPU會有一個叫片上記憶體的東西,它所有的渲染結果實際上是直接對片上記憶體進行操作,而不是直接對視訊記憶體進行操作,就能夠減少頻繁讀視訊記憶體所帶來的頻寬開銷。

我們在渲染的時候需要提前告訴GPU,現在切換了一個RT,告訴GPU我們是否需要把RT本來儲存的顏色系統首先載入到片上記憶體,然後再進行接下來的渲染操作。

實際上在很多時候,我們能知道這個操作是不必要的,每次渲染新的一幀時,肯定要對螢幕上所有的畫素進行重繪,或者類似做後效的時候,肯定需要對所有的畫素進行重新繪製,RT之前本來儲存什麼樣的資訊,完全就沒有任何意義。

這時候我們可以告訴GPU,你不需要幫我們把RT上面的記憶體載入到片上記憶體,自然而然這個載入就不需要有任何的頻寬開銷,我們就可以省掉一大部分的頻寬開銷。

同樣,對於寫操作也是一樣,如果說一個深度圖,這個深度只是拿來做深度測試,深度的結果不需要寫回RT裡面,那我們就可以在切換RT的時候告訴GPU,渲染結果不需要寫回RT內容。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

接下來介紹一下我們《黑暗之潮》專案當中利用RenderFeature做了什麼效果?

第一,平面陰影,這是一種作假的陰影渲染方式,它只適用於遊戲大部分都是平地的情況,正好《黑暗之潮》就是這樣一款遊戲。

平面陰影有一個優點,大家可以看到下面的截圖,陰影是非常銳利、非常清晰的,它的整個的渲染質量很高,不會出現任何的鋸齒。還有一個比較大的好處是,因為它不需要去額外渲染shadowmap,在渲染地表的時候也不會需要對shadowmap進行取樣,這樣的話,這個渲染的整體開銷要比使用shadowmap省非常非常多的。

這個效果用RenderFeature就可以非常容易的實現,我們直接新增一個Shadow RenderFeature,把需要有陰影的角色用一個特殊的shadow繪製一遍就可以了。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

第二,我們用RenderFeature實現了沙盤地圖地塊描邊的效果,大家可以看到這個截圖,描邊需要嚴絲合縫地對應這個區塊範圍。同時,區塊下半部分,牆、山體不能有描邊。所以我們在做這個效果的時候,沒有辦法運用到傳統的描邊方式,即利用法線往外擴的方式去渲染這個描邊。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

我們採用的流程是這樣的。首先我們用一個純色去渲染這個地塊,渲染出來了之後我們對這個渲染結果進行降取樣,縮解析度,在比較低解析度的情況下,再利用BoxFilter進行模糊操作,這樣做的好處是,可以利用盡可能小的頻寬開銷來對這個結果進行模糊操作。

然後再將模糊完畢的結果進行升取樣提高解析度,最後再用透明的顏色繪製一次地塊,就把中間這個區域扣除了,只剩下外面的描邊,這樣就可以實現剛才描邊的效果,並且還能實現從描邊從靠近物體的部分往外慢慢漸變漸影的柔和的過渡效果。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

接下來還有更深一層次的自定義,有一些效果或者需求我們必須需要更深層次的自定義才能夠實現的。在URP當中,提供了一個叫做Renderre的機制,它是一個抽象層,URP裡面內建了兩個渲染器,一個是Forward,也就是我們常說前向渲染器,另外一個就是2DRenderer,主要是用來渲染2D物體的,一些2D遊戲可能會選擇這個渲染器。

在最新版的URP當中,還會整合了一個叫做defer Renderer延遲渲染器,在《黑暗之潮》當中我們可以對Renderer進行寄存,通過它去實現一些通過RenderFeature做不到的事情。

URP有一個好處,雖然說我們想要自定義Renderer,但並不意味著我們所有東西必須要從頭開始做,因為URP裡面已經實現了各種各樣的Pass,我們是可以直接使用的,所以我們只需要對這些Pass進行重新編排就能完成我們對這個Renderer的自定義。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

《黑暗之潮》當中對ForwardRenderer基礎上面進行了一些修改做到了自定義。能做到,比如之前提到的全屏Blit操作,是否把它給避免掉。

我們觀察到,比如做後效時,這個後效不可避免對全屏所有的畫素進行操作,正常情況下,如果說我們後面還需要渲染UI,會在這個後效計算完畢之後渲染UI,最後通過Frame Blit去複製到FramBuffer裡面。

所以我們就在想,這兩個過程能否合併,答案肯定是可以的,在做後效的時候,在計算完畢後直接將結果寫入FrameBuffer裡,實際上我們就能夠省掉Final Blit。最後在渲染UI的時候,我們就把這個UI直接在FrameBuffer上面去進行繪製。這個樣子就可以省掉最後這個Blit的操作。

這麼做還有一個好處,我們可以把3D場景的渲染解析度和UI的渲染解析度分開。以前如果我們因為受制高低配 ,對整個渲染結果的解析度進行降解析度操作,那麼UI也會跟著一起被降解析度,但是UI對解析度很敏感,只要一降解析度就能夠肉眼可見,而且對整個遊戲的品質影響很大。

所以如果我們能夠把3D場景的渲染解析度和UI解析度分開,就能在降低渲染開銷的情況下又不對整個遊戲的品質產生比較大的影響。經過剛才的介紹方式,就能夠實現這兩個解析度的分開,因為我們3D場景在RT上面渲染,渲染完畢之後,通過後效複製到FrameBuffer上面,UI是直接在FrameBuffer上面繪製的,所以說UI的解析度是不受降解析度的影響的。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

總結一下,《黑暗之潮》最終渲染管線由流程圖展示如下,前半部分跟預設的URP沒有太大的區別,主要是在渲染不透明物體之後,我們加入ECS模型渲染。我們這個地方還有一個Copy Depth ,把不透明物體的深度給複製到一張單獨的RT上面。

這個Pass不是每次渲染都會有,而是隻有開啟沙盤地圖的時候才會用,因為沙盤地圖在渲染水體的時候會需要那張深度圖。接下來我們就會去渲染地表的這些不透明物體,渲染所有的平面陰影以及ECS物體的平面陰影,繪製沙盤地圖的描邊。

最後再去渲染我們的透明物體,也就是特效這些東西,渲染完特效我們會在進行這個copy color,把整個渲染結果複製到一張單獨的RT上面,而且這個RT是進行了降解析度操作,實際上抓取的並不是全屏,大概只有1/4螢幕的解析度的顏色資訊。

這個顏色資訊給類似於扭曲這些效果去使用,因為這些效果對解析度的要求並不是特別高,因為本身已經扭曲了,之前採用1/4解析度的貼圖是沒有任何問題的。渲染完扭曲之後,我們會對整個螢幕進行後效處理,後效處理完畢之後,結果可以直接寫在FrameBuffer螢幕緩衝區裡面,最後再去對UI直接進行繪製,完成了整個渲染流程。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

說完URP的功能,下面說一下關於URP效能方面的優勢。

首先第一點,URP的特點它是一個單Pass的前向渲染管線,單Pass也就是說所有的動態光照是在一個Pass裡面完成計算的。單Pass最好的好處是,我們在新增動態光源的時候,不需要把場景裡面所有的物體再去渲染一遍。以前在內建管線的時候,這個問題會比較嚴重的,如果我們新增一盞動態的點光,場景的DrawCall直接翻倍了,這個渲染開銷根本沒有辦法忍受,所以之前在移動遊戲基本上不會使用動態的點光源。

大家可以看到下面的截圖,我們這個場景實際上已經有好幾盞動態光源了,通過單Pass的方式渲染的話,只要我們同場景裡面同時能看見的這些光源的數量能夠有一個比較好的控制,實際上是能夠實現很好的一個渲染效果的,而且這個渲染開銷相對來說也比之前多Pass光照渲染就會是有非常大的優勢的。經過我們測試,在目前主流的終端機以上,中高階機都是沒有任何問題的。

第二點,URP它採用了單Pass的Color Texture去替代GrabPass,之前我們在Builtin管線裡面做類似於空氣擾動之類的效果,必須要使用GrabPass無這個功能的。這個功能雖然非常方便也比較簡單,但是它有一個非常嚴重的問題,我們使用GrabPass之後,我們完全沒有辦法預知當前渲染螢幕會被全屏抓屏幾次,而且這個抓取是不會降解析度的,真的就是全屏抓取,全屏抓取操作是非常非常廢資源的,尤其是在移動平臺上面,基本不大能忍受。

通過這個單Pass的ColorTexture就可以通過一次抓取來完成所有需要扭曲操作的渲染,這個無疑效能就會高的非常多。也就是剛才介紹到的通過RT可以去自定義LoadStore這些操作,也能進一步減少頻寬。剛才也說了,我們可以根據實際情況去掉一些不必要的Blit操作。

最後還有一點非常重要,SRP Batcher,單這一條就已經不能拒絕URP的使用。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

我們看一下SRP Batcher對專案有什麼具體的影響?

首先在內建管線當中有三種方式去進行合批,第一是Dynamic Batching,它實際上對合批有比較嚴格的要求,對於三角面數要求比較高的。它還有一個問題,它是通過CPU降低DrawCall我們降低DrawCall的目的也是為了降低CPU開銷,相互意義已消,只有在一些特定的情況下,Dynamic Batching才能夠有效能提升,絕大多數情況下是沒有太大作用的。

第二,靜態合批Static Batching,這個東西確實是非常有效,對降低DrawCall和提升效能都很有效,但是它最大的問題是它只對靜態物體生效。對於動態物體完全沒有任何效果的,而且進行靜態合批之後,整個場景的記憶體佔用會提高非常多。還有一點,隨著現在場景複雜度的提升,現在次世代的遊戲都已經場景都已經非常複雜了,LOD就是一個非常不可或缺的功能了。Static Batching對LOD是非常不友好的。

最後還有一種方式就是GPU Instancing,這種方式只對網格Mesh以及Materia均一致的情況下才能生效,這個應用的範圍比較窄了,一大片的草,一片大的石頭,對於普通的物件,比如房子,場景物件,沒有辦法對它進行合批。

綜上所述,上面三種合批方式如果用於次世代遊戲是有些捉襟見肘,很多都沒有辦法合批,就會造成對於做這樣的遊戲,效能方面的優化就會非常的困難。

而SRP Batcher就能很好的解決這個問題,因為我們觀察之後能得出一個結論,實際上DrawCall裡面,開銷最大的就是SetPassCall,SRP Batcher它的原理就是通過降低SetPassCall的數量來去打造效能提升,它降低的並不是DrawCall的數量。

通過把所有的渲染當中所需要用到的引數變數拆分成幾個若干個Constant為Buffer分別儲存,比如儲存的是全域性的靜態引數,有一些可能儲存的是當前這一幀資料,剩下的一個Buffer儲存的是當前這個材質球特有的引數,這樣做好處比較明顯了。

如果說我們同一個Shader物體,它實際上變化的就只有它的模型以及材質球上的引數。至於像它的Shader的program,以及它的渲染狀態,這些都是不需要改變的。所以說我們一次DrawCall基本只需要傳一些引數,ConstantBuffer的內容,再去繫結一個Mesh的指標就可以完成了,這樣整個DrawCall的開銷就會非常低。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

大家可以看這兩張圖的對比,左邊是開起來SRP Batcher,右邊沒有開啟,這個圖是通過RenderDoc抓取的一次DrawCall的渲染流程,左邊大家可以看到繫結了一個貼圖,傳了一些頂點的指標,最後通過一個BannerBuffer把ConstantBuffer資料更新一下,最後可以直接去繪製了。

但是在不開啟SRP Batcher的情況下,大家可以看到整個渲染流程非常的長,會進行非常多的設定,還會去更改Shader program,還要更改非常多的渲染狀態,這個截圖還不全,這個列表下面還有很長一段,大家通過對比直接列表的長度就能說明這兩個DrawCall之間的效能開銷差別有多大。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

對此我們也進行了一個測試,我們拿了一個測試場景,這個場景有三棧動態光源,這個場景在頂配的情況下,大概有40W三角面,以及500dc;中配進行簡化過有32W三角面和400dc,低配是25W三角面和280dc。三檔機型上面實際測試都有比較大幅度的提升,這個地方我想拿低端機舉個例子,低配大家可以看到25W三角面和280DrawCall,實際上之前在Builtin的專案裡面已經是一個高配才能比較流暢執行的標準了。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

大家可以看一下這個Profiler的結果,我們是在一個驍龍450SoC上進行的測試,這是一個非常低端的處理器。大家可以看到我們的主執行緒Render Camera是4.3毫秒,在下面渲染執行緒Camera的開銷是14毫秒。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

我們再把SRP Batcher關了之後再看一下,相同的場景一模一樣的東西同樣的視角,主執行緒的Render Camera的開銷已經直接漲到7.8毫秒,渲染執行緒實際提交的過程中我們整個渲染開銷就已經達到了22毫秒。22毫秒已經相當於說,我們場景裡面,只有場景,沒有任何的技能特效,沒有其他的角色,也沒有任何的業務邏輯,就已經不大能跑30幀了,這個渲染就不大能夠接受了。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

講完了剛才關於URP的東西,功能性以及效能上面的優勢,接下來我想跟大家分享一些關於DOTS技術棧在商業上的運用。

我跟其他開發者進行交流的時候,會發現有一個問題,大多數的開發者都會有一些常見的誤解,對於DOTS技術棧,第一個非常典型的,會聽到非常多的人說,我們在專案裡面沒有用到多執行緒,所以也不需要用DOTS。

還有一個,可能大家會覺得用DOTS必須要用於大規模的叢集模擬才能帶動比較大的提升,因為之前大多數的分享,都是去演示的大規模叢集模擬的效能的提升,可能會給大家帶來這樣的一個錯覺。

大家會覺得使用ECS的代價會非常高,因為首先ECS是一個全新的東西需要重新學習,把現在的專案轉換成ECS,代價也非常的高。可能也就用不上DOTS,這三個都是多多少少是一些誤解,我在後面會給大家介紹,DOTS應該怎麼樣使用。

首先我們需要了解一下DOTS具體是什麼?它實際上叫Data-Oriented Tech Stack,它的意思實際上就是面向資料的開發棧。它主要是由三個組建組成的,ECS、JobSystem、Burst。這三個組建是可以相互獨立使用,並不是說使用一個這三個必須同時用,你可以任意選擇其中一個來進行使用,用於不同的應用場景。

如果說我們需要使用JobSystem,其實它跟ECS沒有太大的關係,你可以在ECS裡面用,也可以不在ECS裡面用,只要是需要平行計算的地方都可以使用。

Burst也一樣,它也不需要配合ECS使用,不需要跟平行計算捆綁使用,它的作用僅僅是對於一些複雜的計算密集的東西去進行編譯器優化,來達到效能提升。

只要是計算密集型的東西,都可以使用Burst,同步方法也是可以的。

最後關於ECS,一個比較大的誤解,可能大家會覺得用ECS之後,所有東西都可以用ECS來寫,就會想UI的業務邏輯怎麼用ECS實現。大可不必,並不是說用ECS,所有東西全部都要用ECS來做,而是大家可以根據專案需求選擇其中適合那部分來用ECS去寫,剩下的部分還是使用傳統的物件導向的方式去寫,沒有任何問題,只要用程式碼稍微結合一下就可以了。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

第一個我想給大家看一下,我們在《黑暗之潮》當中利用ECS的例子,我們通過ECS渲染了大量的怪物。我們遊戲裡面怪物通常有一個特點,一組怪由幾名精英配合一兩種大量的存在的爪牙組成的,大家可以看到右面的圖只有三種怪,如果說用預設的SkinMeshRenderer的話,就有一個非常嚴重的問題,沒有辦法合批了,畫面上面有多少個怪,有多少個DrawCall而且Animator開銷也不小,還有一個問題,GameObject為.Instantiate開銷也是比較大的,如果說我要同時刷出來三四十隻怪的話,肯定會卡頓,用ECS就能比較好的解決這三個問題。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

使用ECS先把整個動畫資訊去烘焙到這麼一張動畫貼圖上面,在GPU當中進行蒙皮操作,我們再通過JobSystem和Burst實現視錐剔除和動畫系統的更新,最後我們再在物件導向那塊業務邏輯那塊控制ECS Enity就可以了。也就是說ECS的部分,我們只是提供渲染的和動作的結構,其他部分業務邏輯還是完全用物件導向去實現的,相當於各取所長。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

用ECS最大的好處就是效能。

首先第一個,因為我們採用了GPU蒙皮,整個DrawCall的數量下降到有幾種怪就是幾個DrawCall,這個就非常好了。例項化也是非常快,ECS基本上就是無感的,在極端機上消耗,即便同時刷一千隻怪也不足1毫秒,藉助Burst力量類似於視錐剔除這些計算量比較大的操作,在低端機上也是可以忽略不計的。

大家可以看到下面的截圖,演示我們整個動畫更新階段,也是同樣在驍龍450 SoC上測的,100只怪左右的情況,動畫整個更新過程只用了0.008毫秒,這就是忽略不計,根本不需要考慮的一個量級。通過ECS,我們畫面上怪物的渲染完全取決於GPU本身的渲染效能,CPU的開銷完全不需要去考慮了,所以也不會出現卡頓。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

第二個,我們通過Jobsystem去實現了怪物擊飛的效果,大家可以看到這個怪物被打下懸崖,它如果說碰到牆壁必須要被牆壁擋下來,需要進行一些物理運算,如果直接使用Unity的Ragdoll也就是布娃娃系統,它的物理計算非常複雜,對於低端機會造成比較大的效能負擔。我們把這個過程稍微簡化了一下,所有的這些怪物在被擊飛的時候,使用的是預先製作好的動畫,我們只需要計算它的執行軌跡就行了。

我們首先用Job去平行計算這些怪物的分析軌跡,再通過Unity提供的多執行緒Raycast方法去進行射線檢測來判斷它是否撞到牆或者碰到地面了。最後如果說我們還有一些非ECS了物件,我們可以在計算完畢之後再通過一個單獨的Job把這個所有GameObject的位置給同步一下就可以了。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

第三個,我們通過Burst實現的就是射線技能,這個東西看上去很簡單,實際上需要對整個場景以及所有的怪物和其他物件產生互動。射線打到牆上能夠實時產生反映,我們這個東西需要每幀對整個場景進行射線檢測,整個計算過程實際上是開銷比較大的。通過Burst我們相當於把這個東西做成了一個Job,通過Job.Run的方法去直接進行呼叫,就是在當前這個執行緒進行的操作。

使用起來跟一個靜態方法沒有太大的差別,還有像大家看到的這個技能,會有大量的子彈,對這些子彈我們同樣需要進行執行軌跡的計算。通過Burst非常有效的把這兩個計算開銷降的非常低,Burst開啟之後,它的效能提升基本能上百倍,通過剛才也提到Job.Run的方式實現同步呼叫,我們在整個計算流程當中不需要開額外的執行緒,直接在當前執行緒,單個靜態方法直接呼叫就可以了,也是非常方便的。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

大家可以看一下開啟和不開啟Burst效果的差別,左邊是開啟,右邊是不開啟,我們在一個計算體系化模型工具中測試,左邊只用241毫秒,右邊用了20毫,真是一百倍的差別。而不是說它用了多執行緒所以更快,大家可以看到每個執行緒都快了100倍,如果算總耗時,這邊用了143秒,這邊只用了1秒鐘,如果把所有執行緒的時間加起來,就是100倍的差別,效果非常明顯。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

最後跟大家介紹一下我們在工作流方面的簡化和改善。

這部分由於時間的關係可能就只能講的比較粗略了,大家稍微理解一下。首先隨著我們採用PBR流程,Prefab的製作就會比較麻煩,而且以往這個Prefab的製作都是交給美術同學,美術需要把模型匯入Unity,再規範建立材質和Prefab。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

在採用PBR流程之後,這個建立過程就會麻煩了非常多,首先貼圖多了很多張,跟各式各樣的PBR的設定,是非常繁雜的。尤其是ECS的單位,我們還需要對這個動畫進行烘焙。這是一個非常複雜而且操作量非常大的操作,非常的耗時,而且容易出錯。

為了解決這個問題,我們引入了AssetGraph這個工具,這個工具是Unity開發的一個節點式的自動化資源匯入流程的工具,非常好用。通過自定義節點,我們可以完全根據專案的需求對資源的匯入進行自定義,通過這個工具,這個節點自定義完成之後,我們就可以實現一鍵就能夠建立所有的角色的Prefab,所以說美術也就能從工作當中解放出來了,美術也只需要做完了之後把FBX和貼圖檔案按照我們定好的規定就放到指定的目錄下就可以,它連Unity我不需要開,美術非常喜歡這個功能。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

我們通過這個工具對這些模型進行批量的一次性Prefab生成,能夠直接穩定的生成一個符合規範的Prefab檔案了。這個過程當中大家肯定也知道我們如果用Animator都需要建立動作狀態機這個東西,這個東西手動建立非常麻煩,所以我們也可以用剛才那個工具就能夠實現在美術把這些動作檔案、模型檔案上傳之後可以一鍵把整套東西自動生成了。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

我們在匯出場景的時候有些時候需要對渲染物件進行渲染設定,來達到最佳的渲染效能,具體的設定方式實際上是技術團隊根據Profiling的結果進行不斷的迭代和調整才能形成一個調整的方案。每一次調整,都需要去修改美術資源,如果說這個都需要美術去進行操作,整個工作量會非常的大。這是美術那邊沒辦法接受的,所以說我們需要把這個過程稍微自動化一下。

為了提升切換場景的載入速度,我們需要對場景進行切塊和分簇,大家可以從下面的截圖看到,這些藍綠色的這些盒子就是我們分簇切塊之後的結果,它所展示的分塊Bounding Volume。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

結合剛才所講的我們整個場景的匯出流程就會按照這個流程圖的方式進行一步一步做。

第一步,我們會檢查美術設定的LOD的選項是否正確,會把美術那些臨時物件給剔除,有一些碰撞Fix Mesh Collider ReadWrite這些設定是否正確,還會把LOD的點面工具的臨時指令碼給刪掉,最後還會對ShadowMask去進行一些設定,因為URP裡面沒有shadowMask,這是我們自己實現的,所以會需要一些額外的設定。然後會根據我們Prefab的結果去進行一些詳細的設定,比如Instancing的設定該怎麼設?哪些物體適合Instancing,那些適合,我們都會去進行設定。我們會對整個場景進行分簇會看哪些物體適合進行Static Batch,Static Batch不是所有物體都會適合,我們會進行一些選擇。

剩下一些物體適合轉換成ECS hybrid方式渲染,我們會轉換成hybrid,最後我們再把每一個簇進行Bounding Volume的計算就完成整個場景流程的匯出。我們在場景匯出完畢之後,整個場景就是這樣一個空場景的狀態, 裡面只剩下錯的節點,就會對進入這個範圍之後再進行動態的載入,這就是我們生成的每一簇的Prefab以及靜態合併的Mesh。

掌趣科技林若峰:次世代手遊《黑暗之潮》渲染應用經驗及技術分享

以上就是本次分享的全部內容,謝謝大家!

相關文章