1. 程式人生 > 實用技巧 >UE4 渲染模組概述 摘自知乎Jerry的系列文章

UE4 渲染模組概述 摘自知乎Jerry的系列文章

UE4渲染模組概述(一)---方法論、遮擋剔除、Geometry Rendering

渲染模組是自己比較薄弱的環節,但這是深入做遊戲必備的核心知識,是區別一般軟體開發的精髓所在。我也是本著學習的態度來更新此文,所以有理解不對的地方,還望各位同仁指正。做為渲染模組的開篇,我打算總結下我近一週學習的UE官方視訊課程(這裡用的是截圖,因為如果打出網址,知乎會試圖解析,進而轉變成使用者login的URL來):

本中絕大多數圖片取自於視訊截圖,這裡大多都是些概念的介紹,比較粗,所以我稱之為“概述”,後面還打算出自己更加細緻的研究,敬請期待。

渲染方法論

遊戲中討論的都是實時渲染(RealTime Rendering,RTR),也就是隨著遊戲內相機的移動,要能夠儘可能無延遲地展示視口內容,效果不可能做到CG那麼好。

RTR流程的本質是管理效能的損耗,以這張圖為例:

X軸表示視覺質量,Y軸表示效能,那麼要想獲得高質量的渲染,就會帶來效能的下降,這個趨勢是不會變的。但做的好的優化,是可以降低這種下降的坡度,圖中黃色曲線就要好於藍色曲線。一般地,我們會根據使用者體驗能接受的角度,來設定一個目標幀率(一般是30幀,幀數表示一秒可以執行多少次主迴圈),只要能保證在這個幀率之上,就可以儘可能地把渲染視覺效果做好。

圖形工程師一直在追求渲染質量、效能、功能這三者的平衡。一個空場景效能必然是最佳的,但這沒有意義,另一方面若場景都用最高精度的圖片或是堆砌各種功能點,遊戲卡得根本玩不了,也是不行的。

在圖形優化的路上有很多具體的實現,但都可以歸納成以下六點方法論:

1)流水線:併發多執行緒,使硬體的利用率達到最高;

2)預計算:能事先計算的就儘量事先做,降低Runtime的計算成本,比如靜態光的lightmap;

3)區域性化:只在可見的區域性區域裡做事情,比如螢幕空間反射的計算(SSR);

4)先粗後精:先用消耗低的方法縮小計算範圍,再用消耗大的方法執行精確計算,比如八叉樹遮擋查詢;

5)時空互換:用時間換空間或是空間換時間,比如lightmap就是空間換時間的例子;

6)負擔轉移:使用stat unit檢視如果是CPU的瓶頸,又不可進一步優化了,可以考慮將CPU的計算移至GPU計算,如骨骼動畫計算可以移至GPU做。

渲染之前:遮擋查詢

渲染有一個看似很簡單,但卻非常重要的概念:只渲染鏡頭可見的場景。看不見的場景並不需要浪費效能,直接剔除掉即可,這個過程就是遮擋查詢,這部分發生在GPU渲染之前,是在CPU端做的逐Object的遮擋查詢。

UE4使用了四種遮擋處理方法,按效能消耗由低到高的排列如下:

1)距離剔除:離鏡頭太遠的物件直接隱去,不去繪製;

2)視錐剔除:鏡頭能看到的區域是一個錐體,錐體以外的部分可以不去繪製(具體是用八叉樹先粗後精篩選進入視錐體的物件);

3)預計算可見性剔除:主要適用於一些室內的場景,比如透過室內的門可以看到哪些區域,這些可以預計算好,不用實時計算了;

4)遮擋剔除:最費效能,需要逐物件查詢;

UE會優先使用消耗低的方法去排除掉大部分不用渲染的物件,再用遮擋剔除這種精細的方法進一步計算。

幾何渲染

幾何渲染可以細分成三部分:

1)GPU端畫素級別的prez遮擋查詢

2)解析CPU傳來的draw primitive繪製指令(draw call)

3)頂點計算(vertex shader)

第一部分是GPU端prepass/prez,防止畫素overdraw。舉個例子,我們有一張背景圖:

在背景圖之前放了一盞茶壺,如下:

正常情況下我們先繪製背景圖,再繪製茶壺,看似沒有問題,但茶壺所在位置的畫素其實被重複渲染兩次了。那麼如果先茶壺再是背景圖如何呢?還是會遇到如下交錯的情況。

為了真正解決這個問題,在渲染primitive頂點之前,需要拿到深度資訊,這樣我們就知道哪個頂點應該被渲,哪個不需要渲了。比如一開始的茶壺場景,渲染背景其實只要這樣即可:

第二部分是drawcall。GPU是逐個drawcall渲染的,所謂drawcall,是指CPU向GPU發出的一次渲染指令,讓GPU繪製指定的幾何體。比如下圖:

CPU會提交五次drawcall,一次天空,一次地面,三個柱子各一次(因為是三個獨立的模型)

而下面這個圖:

因為最右側柱子上下兩部分採用了不同的材質,所以需要提交兩次渲染資訊,這樣就多一次drawcall,共計6次drawcall。

這張圖展示了渲染的順序,藍色的是地面,因為事先做了prez,所以為地圖上原來柱子的區域留空了沒去繪製。

從左到右依次是:渲染地面,渲染右側柱子的下半截,渲染中間柱子,渲染左側柱子,渲染右側柱子的上半截,渲染天空。

渲染順序也就側面反映了提交drawcall的次序,其實對於圖形工程師而言,不必特別注意drawcall提交次序,只要最終結果是對的就行。不過這裡也可以發現一個問題,那就是為啥沒有完整地渲染完右側的柱子,再去渲染中間的呢?那是因為渲染器會把相同材質的物件放到相鄰drawcall中處理,這樣可以節省讓硬體切換渲染狀態的耗時。可以注意到這裡右側柱子下半截的材質與其他柱子材質是一樣的,所以會放到連續的drawcall中處理,而右側柱子上半截材質不同,被放到了所有相同材質柱子渲染完成後才進行繪製。

UE4看drawcall數量的命令是stat RHI,在命令列敲入後可見:

draw primitivec calls的數量就是drawcall的數量,一般來說移動端在1000個drawcall左右比較好,PC在2000~3000左右為宜,而超過5000就有可能出效能問題了。這時需要check下出問題的區域。Triangles drawn就是這個視口中三角形面數的總量。

drawcall涉及到CPU向GPU資料的傳遞,這個傳遞的代價一般遠大於三角形本身的繪製成本,可以舉個對應例子,你覺得是拷貝一個1GB的檔案快,還是拷貝一百萬個1KB的檔案快?

降低drawcall的通用處理方案是進行模型的合併,將空間鄰近、相同材質、相同渲染狀態的小模型合併成一個大模型,這樣就只需要提交一次drawcall就可以完成繪製了。特別注意這與元件化的設計概念是不同的,不要認為將物件作為component掛在同一個Actor上就可以自動合併成同一個drawcall了,這些component仍會分開渲染。

模型合併後,雖然drawcall數量降低,但也要考慮它的副作用:

1)遮擋剔除力度減弱,哪怕是鏡頭錐體內只出現合併模型的一個面片,也會去渲染整個合併模型;

2)lightmap佔用更多空間,這個是跟模型走的;

3)碰撞計算變得困難;

4)模型佔用記憶體更大,傳統擺一個建築的方式是將一些區域性小模型拼湊,相同的小模型在記憶體中只要一份就行了,但如果合併,多個相同模型頂點資訊都要被完整儲存下來。

所以還是推薦美術同學先使用小模型按樂高積木的方式拼接,等接近終版時再按實際效能需要進行合併(可以用3d美術工具或者UE4自帶的MeregeActorTools)。

雖然面數相對於drawcall來說,影響不是那麼大,但我們仍然要控制面數,可以減少後續光照計算與pixel shader的複雜度。減面的方法常用的就是LOD(Level Of Detail),它可以按鏡頭與物體的距離來替換不同精度的模型,比如離得很近的話用1000面的模型,離得很遠的話就用100面的模型,因為離得遠在螢幕上繪製的畫素個數也比較少,就看不出來模型的細節了。

HLOD相對於LOD來說可以進一步優化面數和drawcall,它的原理是近處正常顯示,比如4個模型就繪製成四個單獨的模型,但在遠距離上就用合併成一個低精度模型來替換這一組物件。

第三部分是頂點的處理,CPU傳過來的頂點是三維的,但螢幕上能顯示的是二維資料,因此需要降維投影,這個演算法過程被稱為vertex shader。shader可以理解成一種程式,它接受輸入資料,再經過某種演算法,給出輸出。vertex shader就是將三維的頂點資料輸出成螢幕空間的二維頂點,這其中要經歷模型空間到世界空間,世界空間到視口空間,視口空間到螢幕空間的矩陣變換。

vertex shader的一個形象例子就是在shader裡面給輸入頂點一定偏移量,如下圖所示:

這種看似無聊的效果,其實可以用於水流的波動,草隨風擺的效果等。不過要注意的是此時只是GPU端改變了頂點顯示的位置,它的實際碰撞資訊(CPU端控制的)是沒有變化的,也就是你不能試圖蹲著從中間那個柱子經過。

UE4渲染模組概述(二)---光柵化與紋理

下面來介紹畫素處理階段。

光柵化

拿到二維的頂點並將之組合成三角形之後,就需要考慮如何繪製到螢幕上了。顯示屏實際上是由一格格畫素構成的,所以接下來的一步就是要計算出哪些畫素被三角形覆蓋了,然後再對這些被覆蓋到的頂點進行著色。

對頂點/三角形畫素化的過程稱之為光柵化,如下圖所示(還是要強調下,本文中所有圖片均來自於官方課程的PPT,在前一講給出了URL):

理論上對下圖三角形,只需要對其覆蓋的三個畫素點進行著色即可,如綠色格子所示

但實際由於硬體特性,最小的繪製單位是2*2的正方形,所以實際處理的畫素區域如下圖橙色區域所示:

好,這裡貌似並沒有什麼問題,只是有些畫素遍歷到了但沒有繪製罷了。但如果換成下面示例(添加了獨立的藍色的三角形,並假定它對應不同的drawcall):

那麼中間的兩塊2*2區域實際上在兩個不同的drawcall中被繪製了兩次,我們標記為紅色區域:

這種現象稱之為overshading/quad overdraw(注意與前面說的同一畫素因為深度問題重複繪製的overdraw不同)。

在UE4引擎裡,可以對這種現象視覺化觀察,方法如下:

選擇後可見視口變化如下:

冷色調的區域表示overshading比較少,暖色甚至是白色表示overshading非常多,需要檢視到底是什麼問題導致的了。

一般來說,因為半透物件需要繪製前後的物理,所以overshading會較多。還有一種特殊情況如下:

注意到組成這個圓的三角形都是斜長的,而三角形其中一個頂點又都集中在圓心,那麼圓心所在的2*2quad的overshading就會非常多了。

更一般的情況是三角形面數影響overshading,因為三角形越密,重合的畫素就會越多,就越可能發生quad重複渲染。這也是為什麼要使用LOD的原因,近處的時候三角形面數較多,overshading次數增高,但離遠時,LOD切換成三角形面數較低的低模,overshading次數就會降低了。下圖是一個很好的示例:

GBuffer

GBuffer是延遲渲染才有的東西,它實際是渲染出了多張不同資訊量的2D圖片,然後用類似photoshop的方法來處理這些圖片。

使用UE4研究渲染必備的Renderdoc外掛,我們可以看到GBuffer的內容。

GBufferA快取了worldnormal,即模型在世界座標下的法向量,用以記錄朝向。

GBufferB快取了物理渲染的PBR引數,其中:

R分量:黑白mask標識哪部分是金屬 (全金屬則沒有漫反射,即固有色無影響)

G分量:黑白mask標識高光

B分量:黑白mask標識粗糙度(反映反射光是否有比較一致的方向性)

以B分量為例,如下圖:

顏色越白表示Roughness粗糙度越大,反射的方向性就越差;反之越黑則越光滑,反射方向性越好。

GBufferC快取了不帶光照的圖,這也就是viewmode裡面unlit的輸出了,可以認為呈現的是物體的固有色。

剩下的GBufferD、GBufferE、GBufferF都用作特殊的buffer,比如深度緩衝、標識半透物體的緩衝等。

最後合成的影象本質就是這些Buffer快取的2D影象經過某種影象演算法合成出來的,當場景簡單時,這些消耗反而會超過傳統的前向渲染,但如果場景複雜時,特別是多光源情況下,採用GBuffer的延遲渲染的消耗就會遠低於前向渲染,再複雜的場景也會簡化為2D的影象處理。

不過延遲渲染在生成這些2D快取影象時,需要消耗大量頻寬,因此不適用於移動端(不過現在基於tile的渲染可以緩解這方面的壓力)。

GBuffer也可以在編輯器視覺化,方法如下:

選擇後可以看到視口展現了多種功能的GBuffer:

Textures

在對畫素著色時,需要取樣其對應的紋理。因為記憶體與頻寬的限制,我們總是要對匯入的紋理進行各種各樣的壓縮。下面來說下紋理的壓縮格式,在編輯器裡隨便開啟一張texture:

這裡關注下紅框標識的兩個區域,一個是儲存格式(DXT1),另一個是儲存大小。這兩個參量是有關係的,DXT1/BC1用於不帶透明通道的有失真壓縮,而DXT5/BC5用於帶透明通道的有失真壓縮,也可以選擇無失真壓縮的

壓縮質量是有提升,但相應占用儲存空間也會變大:

視屏中特別強調了法線貼圖應該設定為DXT5/BC5,不然質量就會很差(會有明顯塊狀偽影)。

在遊戲中應該對關鍵物件採用高精度的紋理(比如操作的人物模型),其他地方可以採用低精度紋理(比如場景的石塊)。不當的使用紋理主要會帶來記憶體和頻寬的問題,而不是幀率,不過有時會因為載入造成畫面的延遲,這些要分辨清楚。

對於遊戲內使用的紋理,都會自動對其mipmap化。所謂mipmap,稱為多級漸進紋理,是自動按比例縮小原圖,如下圖所示:

為什麼要這樣做?是為了匹配視點遠近,需要使用的不同精度的貼圖。通常攝像機都是透視投影的,也即意味著近大遠小,遠處的物體佔用的畫素其實很少,這時如果仍用高精度的貼圖,那麼取樣UV覆蓋的範圍就會很少,取出來後看上去就像是噪點一般,如下圖所示:

左側採用了mipmap,右側沒有采用mipmap。引擎自動選擇了合適的mipmap精度來繪製,如下圖所示,每一種顏色都標識了不同的mipmap:

因為每級mipmap都是上一級長寬的一半,那就要求長寬必須是2的冪次方,才可以讓引擎正常生成多級的mipmap。如果匯入圖片不是2的冪次方,引擎不會報錯,只是不會生成mipmap了,如果這些圖片用於諸如UI這些地方,也是正確的。關鍵要區分應用的場合。

UE4渲染模組概述(三)---Pixel Shader & Material Rendering

下面就著手介紹畫素著色器(Pixel Shader)與材質渲染(Material Rendering),畫素著色器是材質的基礎,它的主要功能是進行逐畫素的著色。

例如,可以實現對畫片整體增加50%紅色分量的pixel shader。這個shader跑起來會遍歷2D影象中的每一個畫素點(只要記住是逐畫素的就行),並將它的紅色提升50%,例如下圖左上角前兩個畫素:

UE的shader使用的是HLSL語言,類似於下面這種:

float4 normal = mul(IN.Normal, ModelViewIT);
normal.w = 0;
normal = normalize(normal);
float4 light = normalize(LightVec);

我們可以在Engine/Shaders裡面看到很多.usf和.ush檔案裡面包含類似的程式碼段,這些是UE提供的材質模板,那如何使用這些模板呢?隨便選定一個材質,開啟材質編輯器,如下:

這種連線類似於藍圖,如果熟悉PBR的概念,也能大概猜出輸出結點的含義。

材質編輯器的本質仍然是HLSL程式碼,只是將之可視化了,它存在的意義是將使用者連線出的結果,轉換成HLSL程式碼插入到材質模板.usf檔案中。這是一種非常靈活的方式,用同一份材質模板,只改變材質編輯器裡面的連線,就能實現出千變萬化的pixel shader。

我們將材質編輯器中操作的材質稱之為母材質,既然是母親,說明它還可以繼續派生出多個子材質,這些子材質稱之為材質例項。我們在母材質中預留未定的可變引數,可以進一步在材質例項中賦上具體的值來最終確定pixel shader,最後將材質例項應用於具體的網格模型(Mesh)。

這裡將完整的材質應用流程總結如下圖:

.usf模板檔案中提供了基礎的材質變數和函式,這些不能改變,但同時也把可以變化的程式碼段留了空,這時開發同學就可以利用材質編輯器:

自動將圖形化的連線轉成HLSL程式碼插入到.usf檔案裡(print到.usf中的%s處):

形成母材質,但此時仍有引數變數未定義,可以通過建立材質例項去進一步特化這些引數:

這樣有具體表現力的材質例項就可以應用到具體的mesh中來了:

使用renderdoc也可以看到pixel shader裡面的內容:

選中Pixel Shader,在下面綠色橫線處就能看到當前使用的材質程式碼:

注意到這裡的程式碼與之前的HLSL不同,更像是組合語言,因為renderdoc裡面看到的已經是編譯後的shader了。對於世面上的沒有原碼的遊戲,如果掛renderdoc截幀,pixel shader看到都是這種彙編程式碼,不過經驗豐富的圖形工程師還是可以逆向推出一些邏輯來。

PBR

UE4的材質是基於物理渲染的(Physical Based Rendering)。PBR的優勢在於可以利用它在底層中建立相同的著色器系統,用於幾乎所有的渲染,而且預測材質外觀會更加容易。

PBR的資訊來源於之前提到的GBuffer,雖然提供了粗糙度、金屬色、高光三個GBuffer通道,但大部分情況只用金屬色和粗糙度兩個參量,下圖展示了這兩個引數的GBuffer分量(分別是金屬色R通道與粗糙度B通道):

Pixel Shader效能

需要注意下面幾點:

(1)一個材質有可引用的貼圖數量上限,比如16(經常只能用到13),在DX11可以通過共享取樣器做到128個貼圖上限。

(2)引用貼圖尺寸過大通常只會影響載入的遲延,而不會帶來幀率上的減少。

(3)Pixel shaders的影響特別大,因為我們會大量使用它們。

(4)Pixel shader對效能的影響與螢幕解析度有關,解析度越大時,影響越為嚴重。

第一個問題可以通過下面的方法設定共享取樣器:

為了解決第二個問題,我們常常在場景載入初期引入低解析度紋理,但這會帶來一種現象:玩家看到圖片先模糊而後慢慢變清晰。

解決第三個問題,需要多多留意材質編輯器中的輸出資訊,如下:

這裡會顯示指令的個數,通常是100~200次為合理範圍,像這種798的,說明Pixel Shader過於複雜,需要優化。還可以開啟shader complexity的視覺化視窗,如下:

視口中會出現一個紅綠條,綠色複雜度低,紅色表示複雜度高,白色表示極高。注意這裡同時累計了vertex shader與pixel shader的複雜度,英文字母vs所在處表示頂點著色器的複雜度,而ps則表示vs+ps後的整體複雜度。

shader複雜度與複雜材質佔螢幕空間的畫素比例有關係,比如:

當複雜材質佔的畫素少時,整體畫素複雜度仍落在綠色範圍,表示合理,但當複雜材質所佔畫素多時,如下圖:

就會發現整體畫素複雜度已經落在紅色區間了。所以視訊給出的建議是如果物件有可能離玩家視口很近,則要選用簡單的shader,而如果物件總是離玩家很遠時,可以用稍複雜的shader。

UE4渲染模組概述(四)---反射

在光滑的地面或牆面上需要渲染物體的反射資訊,實時計算很難實現 ,本文介紹UE使用的三種反射系統,這三種反射系統各有利弊,往往會將之混合起來使用。

第一種:反射捕獲(Reflection Capture)

它會在指定位置計算前後左右上下六個方向的反射資訊,形成一張靜態的立方體圖(Cubemap),這個過程是預計算的,所在實際跑起來的時候消耗很低,但也因為只是在特定位置計算的,所以當實際鏡頭與捕獲不同時,就會看到不合理的反射圖樣。

UE4使用球體反射捕獲物件(Sphere Reflection Capture Actor)來指定捕獲的位置,如下圖所示:

也有方形的版本(Box Reflection Capture Actor),如下圖所示,不過比較常用的還是放置球形捕獲Actor:

它本質上是捕獲一張360度的圖片,然後將圖片混合到模型上(如上圖所示的地圖和茶壺身上),但只在捕獲點是準確的,如果鏡頭移到別處,但混合的圖片沒有變化,就會出現下圖所示的穿幫問題:

可以看到地上混合的柱子倒影不再與真實柱子吻合。反射捕獲在很多遊戲大作中都有使用,那為什麼我們看不到這種詭異現象呢?這時因為同時還會混合其他反射處理方案(比如螢幕空間反射SSR),在某種程度上掩蓋了它的瑕疵。

Reflection Capture不精確,但它的預計算的優點對效能是非常友好的,通常我們build light的時候,就會去更新捕獲資訊,也可以在Build這裡單獨進行更新。

如果打包遊戲或者應用,它會將捕獲紋理烘焙進遊戲,這樣實際跑的時候,在一定範圍內(可在反射捕獲Actor上配置半徑)的任何畫素,都會檢查附近是否有反射捕獲,如果有,它就向反射捕獲Actor查詢立方體貼圖cubemap,將cubemap與物體本身貼圖進行混合。

如果相近區域內有兩個反射捕獲Actor,如下圖黃箭頭標識:

那麼重合的畫素就會分別融合兩張cubemap,但要注意的是,大量重疊的捕獲範圍會帶來效能問題,因為要在同一個畫素中計算多張cubemap融合的結果。

我們佈置反射場景的基本思路,是放置一個大型的反射捕獲Actor來大致覆蓋整個空間,然後再把許多較小的反射捕獲Actor放置在反射程度高或者需要精確反射的表面附近,儘量保證影響半徑不重合,如下圖所示:

當然也沒有必要刻意去規避重合,只要重合數量不要超過8個都還是可以接受的。另外,可以通過設定貼圖精度來改變反射圖樣的銳利/模糊程度。

天空光照能為整個遊戲世界提供低成本的備用反射捕獲,如下圖所示:

遊戲世界中任何附近沒有球體或立文體反射捕獲Actor的物件都會轉而使用天空光照反射,這對於大型開放式戶外環境很理想,因為我們不希望在場景中到處放置反射捕獲Actor。

第二種:平面捕獲(Planar Reflection)

這種在實際中用的比較少,因為它僅侷限於小範圍平面,也由於是實時計算,所以消耗要比反射捕獲大,但優勢是無論鏡頭如何移動,反射資訊總是正確的,如下圖所示:

第三種:螢幕空間反射(SSR)

這是UE4唯一預設生效的反射系統,它是實時計算的,因而位置上總是精確的,但生成的反射圖樣比較模糊,不如反射捕獲渲染的清晰。效能消耗介於反射捕獲與平面反射之間,SSR效果如下圖所示(研究前要關閉捕獲Actor的渲染,只開啟SSR):

它的優點同時也是缺點在於:只計算螢幕空間的反射。因為計算範圍受限,所以效能可以保障,但同時也由於只能採集到螢幕空間內的資料,所以只能繪製視口可見幾何的反射,如下圖所示:

我們知道柱子很長,在螢幕上方還有一截,但因為SSR只取了螢幕空間內的資訊,所以產生的反射資訊裡面就沒有螢幕以外的柱子了(紅圈內理應有柱子倒影,但實際沒有渲染)。

綜上,三種方法各有優缺點,實際使用中是將這三種系統混合在一起。三種方法的使用優先順序依次為SSR,平面反射,反射捕獲。只有當硬體或者其他效能撐不住的時候,才會使用預計算的反射捕獲,否則還是提供精確度相對較高的SSR和平面反射。

平面反射儘量不要使用,如果硬體不支援就要關掉SSR或者調整SSR的生成質量來平衡效能,方法是在控制檯中鍵入r.SSR.Quality + 數字,預設質量是3,數字越大,SSR的渲染效果就越好,但相應效能就會越差。

可以在RenderDoc裡面截幀分析反射資訊,下圖所示是螢幕空間反射SSR的渲染:

下圖是SSR成像的結果:

每一個點都是投射的反射射線,因為射線密度不夠,形成了較多的噪點,為了掩飾,所以SSR生成的反射圖看上去是模糊的。

UE4渲染模組概述(五)---靜態光照

同反射一樣,光照和陰影在實時渲染中是很難計算的。因此如方法論中介紹的,能預計算的就儘量預計算,能用空間換時間的就儘量換。靜態光的光照/陰影都是可以預計算的,動態光則需要實時計算。

本章討論的是靜態光照/靜態陰影。靜態光是指光源本身不會移動,且不會隨遊戲程序改變狀態。靜態光/陰影是在編輯器裡預計算的,它們被儲存於光圖(lightmap)裡。

使用光圖的優點如下:

(1)不需要執行時計算,節省效能;

缺點如下:

(1)需佔用不少記憶體

(2)預計算的時間較長,每移動一個物件,都需要重新build lightmap

(3)非常大的模型可能會沒有足夠的空間儲存lightmap

這裡看上去缺點很多,但都是預計算、以空間換時間必須要付出的代價,只要是執行時效能省下來了,就還是值得的。

lightmap光照貼圖本質上就是一張紋理,是一張烘焙有光照和陰影資訊的圖片,類似於紋理的UV取樣,我們也會對存於模型上的光照UV座標進行取樣,將取樣結果乘以固有色basecolor得到有光照的渲染效果。圖示如下:

在UE4中,會把許多光照貼圖打包到一起,類似於下面:

我們可以在worldsetting裡面看到這些打包好的光照貼圖,但不可修改它們。

我們用LightMass來生成光照貼圖,可以配置所需要的貼圖精度,分散在兩個地方:

(1)build光選項裡面的光照質量,如preview,high,productive

(2)worldsetting裡面的lightmass選項,如下圖:

對於一些想要高精度lightmap的地方,可以放置一個光照重要性體積(light importance volume),UE確保體積內的任何東西都有更高質量的光照 (如戰鬥區域),而體積外則使用低質量光照(如人物移動不到的遠景)。需要注意的是,光圖的精度主要影響的記憶體與檔案大小,而不是幀率。

對於光圖的製作/烘培過程,我們可以通過下面的方法來提升烘焙速度:

(1) 降低光圖解析度

(2) 減少燈光或場景內物件數量

(3) build時僅使用preview方式

(4)燈光設定較小的衰減半徑與光源半徑(即使光影響範圍小)

在編輯器裡,可以檢視光圖的密度:

選中後編輯器內的效果如下:

方格越小,顏色就越綠,這表示光照貼圖精度越高。對於一些鏡頭不常看到的地方(如空中的立柱),建議使用低細節度的光照貼圖即可。

光圖主要是針對直接光生成的光照紋理,對於因多次反射產生的間接光,UE是使用了一種稱之為Indirect Light Cache(間接光照快取)來進行處理。特別是對於移動中的物體(比如說遊戲角色),間接光照快取的光照資訊能彌補lightmap的不足。UE4採用了佈置取樣點方式來生成間接光照快取,如下圖所示:

灰白色的點即為取樣點,對於任何物體,都會找到離它最近的取樣點,查詢到它的亮度資訊,然後將取樣結果與物體自身顏色進行混合。可以選中物件,在下圖位置找到間接光的配置項:

下拉選單有三項可選:不使用取樣點,使用取樣點,使用取樣體積。使用取樣體積是指選用更多的取樣點,比如周圍5*5*5個點來計算間接光照。

間接光照快取是在build lightmap同時做的,取樣點會在lightmass importance volume範圍內生成,可能會超出一點點。需要注意的是,取樣點不是等密度的,一般來說地面的取樣點多於空中的取樣點,如果想要在空中也有較高密度的取樣點,可以建立一個Lightmass Character Indirect Detail Volume(Lightmass角色間接細節體積),如下:

在worldsetting這裡可以配置取樣點的生成密度:

配置好就可以在空間也獲得比較好的間接光質量了,如下圖:

關於靜態光的lightmap與Indirect lighting cache就介紹到這裡

UE4渲染模組概述(六)---動態光照

知道了UE4使用lightmap儲存靜態光的直接光分量,用取樣點儲存間接光分量。那麼對於位置變化或狀態變化的動態光源,又是如何處理的呢?

我們先從動態陰影入手,它是動態光照的重點部分。對效能有著非常大的影響。主要有四種類型的動態陰影。

第一種:常規動態陰影(Regular Dynamic Shadows)

這是最常用的動態陰影,我們在場景中設定一個移動式的動態光源(Movable),並將之配置為投射陰影(Cast Shadows),如下:

可以看到靜態網格體的邊角有著不合常理的銳利程度,如下:

第二種:逐物件陰影(Per Object Shadows)

我們在場景中擺放一個固定光照(Stationary)的光源,如下:

stationary的光照會混合使用靜態光照的lightmap與動態光照的實時計算,它建立的陰影如下:

可以看到陰影仍然清晰銳利,但要比純movable光源生成的陰影更自然。

第三種:級聯陰影圖(Cascaded Shadow Maps,CSM)

最經常遇到的CSM就是這個了,它是方向光(Directional light)的陰影生成方式。如果將生成距離調得很小的話,可以看到陰影逐漸生成的過程,如下圖:

CSM的特點就是會根據視錐體的遠近生成不同精度的影圖,如鏡頭當離得較遠時,可以看到陰影邊緣有一條鋸齒狀的線條,如下圖:

當相機逼近時,會用精度更高的影圖進行渲染,如下:

就幾乎沒有鋸齒了。CSM這樣做的目的儘可能降低效能影響,同時會保證好各級解析度之間的陰影有淹平滑的過度。

第四種:距離場陰影(Distance Field Shadows)

對於開闊的大世界,CSM也會力不從心,這時需要另一種系統,能夠處理長距離範圍的陰影,那就是距離場陰影。

為了投射陰影,我們需要知道點與點之間的距離 ,因而需要查詢幾體體之間的距離資訊,動態計算會很慢,如果有一種方法可以預先計算並存儲好距離資訊,那麼就可以大大加速這一過程。距離場陰影將距離資訊儲存於體積紋理中(Volume Texture),紋理的精度決定了生成陰影的細節與質量。體積紋理建立後看起來像這個樣子:

它將一張二維的平面紋理切成多個部分,上下堆疊,形成一塊立體區域,白色的位置告訴你物件的形狀,可以從這些資訊裡推斷出一個3D的物件,像下面這樣:

可以看出是一把椅子,本質上是一張紋理,只是以3D顯示出來。

距離場陰影可以通過下面方式進行啟用,在Project setting裡面選擇Rendering子項:

鉤上Generate Mesh Distance Fields就可以產生距離場陰影了,不過有一點要注意,因為是在編輯器裡預計算的,所以移動一小塊物件也會導致build很長時間的距離場陰影,會影響開發效率,可以在場景定型後再開啟預計算。build完成後,可以在編輯器裡視覺化距離場陰影,如下:

如果仔細觀察,可以看到它的細節並不好,如下面的欄杆所示:

以上介紹了渲染陰影的四個主要方法,下面來看下動態光是如何計算的。動態光源被渲染成球體,這個球類似於mask的作用,任何處於球內的點都會受到融合動態光shader的影響,舉個例子,在雕像附近有一盞動態光源:

從計算範圍來說,這盞動態光源就是這個球體:

球體內的任何畫素都相當於遮罩,白色區域覆蓋的畫素需要計算動態光影響,而黑色覆蓋的畫素則不需要。

實際光源有顏色和亮度,如下:

通過之前計算的深度,再加上這個光的顏色、強度,以及作用範圍,可以得到下面的圖:

再由GBuffer中快取的world normal,可以進一步優化光照效果:

至於動態光產生的陰影,需要光源到物體的深度資訊。可以類似於反射捕獲時用到的六面體Cubemap,以光源為捕獲點,渲染只有深度資訊的立方體貼圖,以下圖示了cubemap的兩面:

這樣我們就可以生成陰影貼圖shadowmap了,將之加入到前面的圖中,有:

最後一步與無動態光的圖片進行混合,就有了:

作為結尾,視訊中說到了一些效能優化的方面:

(1)確保動態光源的半徑儘可能小,這樣mask的區域就會小,特別在陰影的計算上可以省不少

(2)多個動態光源的儘量不要有重合的區域,因為重合區域內的畫素要分別計算不同光的影響,這一點可以通過編輯器的視覺化工具,檢視light complexity。如下:

與之前的視覺化工具類似,冷色調複雜度低,暖色調複雜度高。

(3)非必要情況下,還是優先考慮靜態光的lightmap(大世界除外,因為lightmap會超級佔記憶體)

(4)動態陰影非常非常費,因此可以關閉部分不重要光源的cast shadow選項,或者配置好動態光源的最大繪製距離(Max Draw Distance),超過這個距離引擎就會忽略該動態光源的計算。

以上就是動態陰影/動態光的基本原理了

UE4渲染模組概述(七)---半透與後處理

作為概述的最後一部分,主要介紹下半透和後處理。

半透

半透渲染的代表就是距離霧,所謂距離霧,就是說霧氣隨距離衰減。UE4有兩種型別的距離霧,一種是大氣霧,另一種是指數霧。同樣,這也是基於Pixel Shader的。距離霧的示例如下:

將霧的顏色與深度圖進行混合,就能產生距離霧的效果了:

對於半透物體的渲染來說,是延遲渲染不擅長的,一般來說有兩種解決方案:其一是延遲到較晚的階段處理,其二是這部分仍採用傳統前向渲染,然後兩者進行混合。

半透材質的渲染除了要繪製背後的畫素外,還需要進行渲染排序,這就非常費,而且材質覆蓋的畫素越多,就越費。可以用前幾章所說的shader complexity視覺化來觀察畫素的複雜度,比如說下面這個圖:

中間這一塊紅色的(表示複雜度很高)是透明的門,這些畫素會不斷疊加材質的著色器損耗,不過也有解決方案來減少半透效果的複雜度,比如用Masked代替Translucent,或者設定成不帶光照:

後處理

後處理位於渲染管線的末端,同樣也是依賴於Pixel Shader的,常見的後處理特效有:

LightBloom(泛光)

Depth of Field/Blurring(景深模糊)

Lensflares(鏡頭光暈)

Light Shafts(光斑)

Vignetee(暈影)

Tonemapping/Color correction(色調對映/顏色校正)

Exposure(曝光)

Motion Blur(運動模糊)

下面主要介紹泛光與景深,其他部分請參照原學習視訊:

(1)Bloom

泛光很簡單,首先需要查詢每個畫素的亮度,將亮度低於一定域值的畫素染成黑色,這樣對比度就會變高,如下圖:

再以某種方式模糊這張影象,例如可以縮小影象使之模糊,然後將之放大回原來解析度,就會得到很模糊的效果,如下圖:

最後疊加某種類似鏡片類塵之類的紋理,如:

就可以得到Bloom的效果了,如下圖:

(2)景深

把深度圖和模糊後的成像混在一起,模糊效果就會只出現在遠距離位置上了。

下圖是深度圖,注意是越遠越亮,越近越黑:

與下面模糊的原圖進行混合:

就能得到景深的效果了:

把泛光和景深的效果合到一起,就得到:

這部分內容就介紹到這裡,這樣有關渲染概述的篇章就到此作結了,還是推薦大家有時間去官網看下完整的學習視訊,相信一定還會有所收穫。

視訊所述內容對於渲染模組來說還是太粗了,最多隻能說是一個目錄而已,只有靜下心裡鑽研細節,多動手實際操練,才能成為一個渲染專家。

我自己後續會有更加細節的UE4渲染管線的分析,也會有其他UE4模組的研究,打算是儘量多貼示例圖來解釋演算法或邏輯流程,少貼大塊程式碼段,降低大家的學習成本。