Unreal Virtual Texture 原始碼導讀

侑虎科技發表於2020-07-20

上一篇《淺談Virtual Texture》主要是對理論知識的介紹,本篇開始對Unreal Virtual Texture的原始碼做一個導讀。

內容包括Virtual Texture的流程和一些技術實現細節,預設你已經對Virtual Texture有一定的認識,如對技術概念有疑惑,可以先看上篇。本文會先從整體出發,介紹Unreal實現的大概內容和流程,以及結構關係,然後再深入到細節,儘量還原Unreal的設計。

一、Unreal 實現的內容

首先,先給Unreal的Virtual Texture的實現給一個大體上的介紹。Unreal是基於Software Virtual Texture,並未涉及Hardware的內容,實現了Procedural Virtual Texture,Unreal叫Runtime Virtual Texture,並未實現Adaptive Procedural Texture。地址對映使用了indirection texture的page和MIP level的對映方式。Texture Filtering方面實現了Bi-linear Filtering、Anisotropic Filtering和Tri-linear Filtering,Bi-linear Filtering是基於border來實現的,Tri-linear Filtering則是利用TAA的一個實現,這個實現是其特有的,Anisotropic Filtering則是自己計算AnisoBias來實現的。

Feedback Rendering是跟GBuffer同時的,也就是結果會延遲一幀,解析度可以用VIRTUAL_TEXTURE_FEEDBACK_FACTOR來控制,是UAV來操作的。Transcode方面使用的是Crunch,也就是壓縮方式是DXT,由於Unreal的磁碟上的紋理是uasset,所以沒有其他通用image格式的壓縮了。

二、Unreal Virtual Texture流程

Unreal Virtual Texture的基本流程跟Software Virtual Texture是一致的,主要的邏輯在FVirtualTextureSystem的Update函式裡。

可以看到,FeedBackAnalysisTask和GatherRequestsTask是多執行緒做的。雖然是多執行緒,這邊是同步等待執行的,因為後面的執行依賴前面的結果。在這些流程中,SubmitRequests這個流程會比較複雜,我從中再抽取出與Physical Texture載入相關的流程,Steaming Virtual Texture的載入過程:

可以看到,這裡包含了Streaming Load的部分和Transcode的部分。Runtime Virtual Texture就比較簡單了,因為是實時生成的:

包含了渲染Mesh,壓縮和重新編碼紋理的過程。這裡糾正官方視訊中的一個錯誤,視訊中說Runtime Virtual Texture只會渲染一次,這是不對的,它會實時渲染不存在Physical Texture中的Tile,為什麼移動物體不會使得Physical Texture更新,是因為Tile已經存在於Physical Texture上了,只有噹噹前的Tile被替換出去,才會發生再次渲染更新Tile。

三、Unreal Virtual Texture 結構

Unreal設計了一個非常漂亮的結構,使得整個系統能夠優雅的合作執行。

從資料結構方面主要分為兩大塊,就是上圖的左上方部分和右上方部分。FVirtualTextureSpace用來管理Virtual Texture部分,FVirtualTexturePhysicalSpace用來管理Physical Texture部分,中間的FVirtualTextureSystem則負責具體的行為邏輯,串聯起兩邊。左下角的Producer部分是負責製造Physical Texture Tiles的,右下角的IVirtualTextureFinalizer部分則是負責將Tiles “拷貝”到Physical Texture的確定位置上。

如果只是想大概瞭解一下Unreal的實現,到這裡就可以結束了,後文會是比較瑣碎的實現細節。

四、FeedBack Rendering

Unreal的FeedBack Rendering的實現是和BasePass渲染同時的,使用一個RWBuffer來實現不同解析度的輸出,VIRTUAL_TEXTURE_FEEDBACK_FACTOR引數來調整解析度。具體程式碼在VirtualTextureCommon.ush的FinalizeVirtualTextureFeedback中,這個在每個需要生成Feedback buffer的pixel shader的末尾呼叫。FinalizeVirtualTextureFeedback需要一個FVirtualTextureFeedbackParams.Request,這個需要找一個Material,然後看它生成的HLSL,會找到是如下方法得到的:

VTPageTableResult Local1 = TextureLoadVirtualPageTableBias(VIRTUALTEXTURE_PAGETABLE_0, VTPageTableUniform_Unpack(Material.VTPackedPageTableUniform[0*2], Material.VTPackedPageTableUniform[0*2+1]), Parameters.SvPosition.xy, Parameters.VirtualTextureFeedback, 0 + LIGHTMAP_VT_ENABLED, Parameters.TexCoords[0].xy, VTADDRESSMODE_WRAP, VTADDRESSMODE_WRAP, View.MaterialTextureMipBias);

資料編碼是放在了一個32bit uint裡了,|4bit pageid|4bit level|12bit pagex|12bit pagey|。對於半透明物體和單畫素多Page的情況,是使用了一個跟pixelpos、depth、FrameNumber相關的隨機值來解決的:

const float AlphaThreshold = frac( PseudoRandom(PixelPos) + // Random  value in 0-1 on 128 x 128 pixel grid

SvPosition.w + // Add in depth so we pick different thresholds on different depths

(FrameNumber / (float)VIRTUAL_TEXTURE_FEEDBACK_FACTOR) // Add in framenumber for extra jitter so the pseudorandom pattern changes over time

);

上一篇文章提到過,這種方案理論會出現同一個Pixel引起反覆載入的情況。

五、FVirtualTextureSpace

FVirtualTextureSpace代表的是相同FVTSpaceDescription的一個空間,這個空間包括多個FAllocatedVirtualTexture,然後需要提出一個Unreal的新概念——Layer。一個FVirtualTextureSpace下有多層Layer,Layer之間是同UV的,這樣可以減少同UV的VT的地址轉換。FVirtualTextureSpace還包括VirtualTexture和PageTable相關內容,以及處理PageTable的更新。

5.1 Virtual Texture Allocating
Unreal的Virtual Texture實現不是像傳統的VT,一個邏輯VT對應上一個已經存在的大的Texture,而是會將幾個Texture合併到一個Virtual Texture上,這裡的地址的分配由一個Allocator來實現。這個Allocator的演算法有點像Buddy Allocator,只不過是二維的。

首先,先將Virtual Texture的大小Ceil到二次冪的正方形大小,然後在Allocator中申請。假如大小不夠了,會呼叫Grow方法,在小於閾值的情況下增倍總大小;如果夠,會嘗試逐漸分割大小,直到大小合適,下面是一個比較簡單的例子:

5.2 FTexturePageMap
這個類是負責一個Layer的Page Table,包括Page Table的資料結構和Map Page的操作。Virtual Address在程式碼裡面為vAddress,它的編碼方式是Morton Order。

這個編碼有很多好處,在Update Page Table中,需要有一個很重要的操作,就是當我們得到需要更新的Tile後,我們需要不僅僅更新這個Tile,還需要更新對應的低MIP Level對應的位置的Tile,這樣可以減少Texture Poping。這裡就需要快速找到與當前更新Tile的所謂子Tiles,它維護了一個叫SortedKeys的資料,這裡面的key是編碼過後的vAddress和Mip。

用上面的圖編碼(實際是U32的),如果要找到與vAddress為000001,Mip為1的子Tiles。首先對vAddress操作一下,000001 << (vDimensions * Mip) = 000100。這裡vDimensions這裡我們預設為2,因為Unreal是支援體紋理的,所以有可能為3。然後再計算一個Mask,~0u << (vDimensions * vLogSize) = 00100,就可以發現使用Mask對左上第二個Quad操作,地址就等於vAddress了,這樣就找到了它的所有子Tile。

其實,原理上說,Morton Order可以快速構建四叉樹,而我們的MipMap其實結構上就是一個四叉樹。這裡的相關程式碼在,ExpandPageTableUpdateMasked和ExpandPageTableUpdatePainters上,這兩個方法都是用來做MipMap的Tiles的更新的。

兩者的區別是,前者會找出原本低Mip的需要更新的Tile,並剔除掉;後者則是用painters演算法來保證正確性,也就是每個Tile可能會被繪製多次,使用者可以根據情況選擇,一個GPU友好,一個CPU友好。

5.3 PageMap Update
通過Feedback Analysis我們得到需要Update的Tile,然後再通過上面說到的Expand函式補充好MipMap的Page,根據上面的結果就可以開始Update PageMap了。Unreal的做法為,為每個Layer,每個MipMap,Draw需要更新的Page的數量的Instance。然後在VS中改Pixel Position和計算Page的值,具體程式碼在FVirtualTextureSpace的ApplyUpdates中,Shader在PageTableUpdate.usf裡。

六、FVirtualTexturePhysicalSpace

FVirtualTexturePhysicalSpace主要內容是Physical Texture的GPU資源和FTexturePagePool。本身的邏輯比較少,多數邏輯在FTexturePagePool中。

6.1 FTexturePagePool
這個類主要和FTexturePageMap一起負責Mapping的相關邏輯。它的主體是Physical Texture,主要負責Physical Texture Tiles的分配,Physical Address在程式碼裡為pAddress,它的編碼方式是X優先的展開到一維。它裡面有幾個資料結構,其中之一是個二叉堆,這個是它的核心資料結構,是用來實現Physical Textured的Tils的LRU演算法的。所有的Tiles的地址會在這個二叉堆中,當申請分配的時候,會得到堆頂的地址,每次操作也會更新這個二叉堆,保證堆頂是最舊被使用到的。還有一個FPageEntry的陣列,這是以pAddress為Index,儲存所有Physical Texture Tiles,對應還有一個方便用FPageEntry找pAddress的HashTable。還有一個FPageMapping的陣列,前面的NumPages的內容索引後面的列表,除了最後一個是存了FreeList,其他存的是每個pAddress的Mapping資訊。

6.2 FVirtualTextureProducer
這個負責對Physical Texture Tiles的製造,一個FVirtualTexturePhysicalSpace對應一個FVirtualTextureProducer,主要的邏輯在IVirtualTexture的介面中,包括兩個流程,一個是RequestPageData,這個流程主要是負責Tiles的載入過程。一個是ProducePageData,這個流程主要負責更新需要最終拷貝到Physical Texture的Tiles的列表。

Runtime VT的實現比較簡單,因為它是實時生成的,只是將需要生成哪些Tiles記錄下來就可以了。這裡再補充一下Stream VT的RequestPageData,除了上文提到過的載入流程。在RequestPageData流程中,有一個會根據平臺來做決策的方案,就是會判斷是否支援Persistent Mapped Buffers,這個技術可以Map一次,一直保留Map返回的指標,由於Streaming的原因,這個指標確實有一直到載入完才使用的情況。可惜這個在手機和PC平臺是不支援的,甚至相關方法在開源的Unreal中是空實現,只有主機版本才有。開源的版本中的實現是申請了一份臨時CPU Buffer,先將載入的放到這個臨時Buffer中,在後續流程中再將這份記憶體拷貝到Physical Texture上,這個就是IVirtualTextureFinalizer的工作。

6.3 IVirtualTextureFinalizer
這個介面負責,將FVirtualTextureProducer整理好的資料最終拷貝到Physical Texture上。Runtime Virtual Texture的流程上文已經提及,就是那三個Pass。Streaming Virtual Texture的流程用到上面Producer提供的那份臨時Buffer。這裡由於Physical Texture被設定成了TexCreate_ShaderResource,也就是CPU是不可寫的,需要有一箇中間Staging Texture,先把Buffer拷貝到這個中間Staging Texture,再從這個Staging Texture拷貝到Physical Texture上。

6.4 Virtual Texture Filtering
上文已經提到了,Unreal是支援Bi-linear Filtering、Anisotropic Filtering、Tri-linear Filtering的,如何計算座標這裡就不說了,可以看上篇文章,這裡說一下Unreal是如何實現這些Filtering的。Bi-linear Filtering就是用Border來解決的,Anisotropic Filtering的實現是Unreal軟計算了Anisotropic的偏移,具體演算法在MipLevelAniso2D裡,然後通過SampleGrad方法傳上dUVdx、dUVdy,讓硬體完成Anisotropic Filtering。

Tri-linear Filtering的實現比較Trick,它是用一個噪聲去讓Mip Level在一個範圍內變化,引數是位置和幀數,這樣就會讓一個畫素的取樣值在一定時間範圍內是變化的,配合上TAA來實現了Tri-linear Filtering。


上文所說的一切,還需要配合Unreal的易用而穩定的多執行緒框架,記憶體管理機制,Streaming系統等等。我只是簡單介紹了一些點,管中窺豹,Unreal對Virtual Texture的實現,需要引擎大量的基底,而在上面又是每行程式碼的精益求精。讀Unreal的程式碼往往如沐春風,每讀一段就感慨他們對技術的執著,以及與他們的差距。

本文的目的是一個導讀性質,如果感興趣,建議大家自己去看看原始碼。進行下一步的使用、優化和定製修改。


文末,再次感謝李兵的分享,如果您有任何獨到的見解或者發現也歡迎聯絡我們,一起探討。(QQ群:793972859)

作者主頁:https://www.zhihu.com/people/li-bing-77-8U,作者也是 Sparkle活動參與者,UWA歡迎更多開發朋友加入U Sparkle開發者計劃,這個舞臺有你更精彩!

相關文章