前言
這次讀的是2023年SIG中,來自High Moon Studios的CTO——Stephan Etienne
的一篇分享。(這個工作室是動視旗下的一間工作室)
雖然我沒玩過《使命的召喚》系列,但是在我的印象裡這個系列無論好玩與否,基本是畫面和運行性能都很頂格的遊戲。而大地形系統,作為一個戰場中“打底”的模塊,要做到近處精細,遠處自動LOD,同時兼顧剔除與分塊;所以雖然其實每個3D引擎幾乎都有自己的地形系統,但細看下來又有一些細節上的取捨和不同。
本文還是以翻譯原文PPT頁及解說稿為主,打星號的部分則是我個人的補充。雖然頁數不算多,但是由於每頁文字量比較大,篇幅原因還是會拆成上下兩篇。
正文
*這次的正文比較整體,或者說分節比較碎,基本一兩篇就是一個小課題。當然,內容量還是比較足的。而且這篇分享配圖相對比較清晰,討論的課題從圖上把握的話其實比較明白了。
大地形如何工作
超大地形是一組地形表面的集合。
在這個截圖中,每個紫色的矩形是一個地形表面。每個紅色的區塊是一個興趣點,有著各自單獨的地形表面。
有這麼多地形表面的其中一個原因是——我們在同一個地圖上有很多並行的開發製作;另一個原因是我們想有豐富的細節。
地形表面有著一些限制——它們不能被縮放或旋轉;它們也需要是方形(拼合)的,這能使代碼變得簡單高效。
地形中標紅的部分呈現出並非方形,並且經過了旋轉的狀態,這是通過cut out體積來實現的——後續會介紹到。
幾何體
地形技術對於四叉樹有著深度的使用。
每個地形表面有一顆四叉樹,在每個節點上,我們存儲了渲染必須的頂點和索引緩衝;不需要被選擇的節點則不用加載頂點和索引數據。
網格簡化
一個地形表面包含一個
高度圖(height map)和一個四叉樹(quad tree)。四叉樹的每一個節點,是一個地形補丁(patch)。
在最低級別,地形補丁使用程序生成的頂點,並且所有頂點被放置在一個傳統柵格(grid)中;在分支(branches)處,我們進行網格簡化。網格簡化器在每個頂點輸出2個float值,分別是X和Y,而Z通過採樣高度圖被重構。
這使我們得以用少量的頂點來維持顯著的細節。我們得到了視覺質量以及性能,因為GPU更傾向處理大三角形而不是小三角形(*這裡和之前一篇可見性緩衝又聯動了)。
地形補丁也會引用它們的低精度網格。在攝像機遠離網格時,頂點會向低精度的一側部分做插值(合併)。
當相機離網格足夠遠(sufficiently)時,所有頂點最終都摺疊到最低精度的網格——我們就將其(整體)切換到低精度網格。
cut out貼圖
當製作地形時,藝術家們通常使用cutout體積來標記不希望生成地形的區域。
這有時是因為低分辨率遠景的地形中需要讓出(性能)空間給其它位置,在可遊玩的區域提供更緊密的地形網格;這也用於在地形上挖洞,以開闢山洞或是放置建築。
cutout體積在每個地形上會烘焙到一個紋理上,它在內存方面非常高效,每個頂點只需要1bit。一個字節(byte)中編碼了8個頂點——2行各4個頂點。
材質層級
一個地形表面由一組材質構成,我們稱為地形層級。傳統的地形層級可能是草地、沙地、泥土等。
在我們原始的地形技術實現中,每個地形層有一個單獨的alpha遮罩。
我們只支持32層地形層,但所有的alpha遮罩仍然需要大量的內存。(*memory,英文不區分,但其實基本是說顯存)
同時shader的開銷也是不可控的,多數像素可能只使用了1層,但也沒有辦法阻止一個像素使用全部32層來渲染。除此之外,32作為限制也是一個過低的值,我們希望提高它。
*這裡主要是說想把分層做得動態一些,在舊的結構裡32層即使沒有用到也有很多需要計算的部分。
逐頂點材質(OMPV)
在Black Ops Cold War(黑色行動冷戰)中,Treyarch將地形層遮罩技術替換成了一項新技術——逐頂點材質,簡稱OMPV。
OMPV使用一個索引映射圖,我們在圖中展示為紅色——它為每個像素存儲一個字節數據;以及一個顏色映射圖,展示在索引映射圖的左側,為每個像素存儲RGB值。
通過OMPV,層遮罩就不再需要了,而由於顏色是採用BC1格式壓縮的,這能顯著的節省內存。
對於每個像素,索引映射圖記錄了最重要的的層,而顏色映射圖為該層提供了染色(tint)的數據。
*注意這裡是映射(map)數據的貼圖,不是材質貼圖。後面會混合具體的材質紋理。
OMPV分析
通過OMPV,假設要為T紋素著色,shader代碼中會收集周圍4個頂點的索引和顏色。
我們用每個頂點中定義的貢獻度(contribution)計算alpha值。而紋素越靠近一個頂點,則該頂點對應的alpha值就越大。
每個頂點的索引指向一個地形層。由於每個頂點可能會引用同一層,我們可以通過累加alpha值來加速重複層的處理過程。
例如,假設所有4個頂點恰好引用了同一層——而這也是很常見的情況,這樣相比於採樣同一材質4次,我們將只採樣一次就夠了。
基於JT在他的GDC演講中介紹的“定義不清”(ill defined)的問題,我們將alpha值乘2,之後再收縮到0至1的範圍內。(*精度問題)
之後我們通過藝術家製作的顯示紋理(reveal map)來混合所有層,這樣他們也可以對混合的過程有控制力。
並不是所有層都被同樣處理。所有層會基於索引排序,有著最低索引的層作為基礎層不會被合併——它始終有著1的alpha值。
扭曲
與節省性能相對的,OMPV也有其自身的問題。(*原文是has its fair share of,公平份額的issue)
第一個問題是,通過之前介紹的方法,繪製的結果看起來是“像素化”(pixelated)的。從右側上方的圖中可以明顯看出。
為解決這一問題,我們通過一個全局256X256的扭曲貼圖來扭曲UV——如圖所示。
藝術家可以逐層控制扭曲的幅度和頻率。這是一個映射層面的概念,因為UV在索引和顏色映射被採樣前就被扭曲了。
Tile隱藏
不管是否用OMPV,當一個地形層被用於地圖上大畫幅(原文是swath ,直譯是刈幅)的部分時,平鋪視覺故障就變得顯眼了。解決這一問題需要用到我們稱為tile隱藏的技術。
在每一個頂點,我們基於世界空間位置為頂點計算了一個隨機旋轉。這個旋轉是0-15之間的一個整形值,從角度轉化成弧度。
這種方式能有效掩蓋平鋪視覺故障,但由於添加的隨機旋轉,我們不能混合重複的地形層了——除非它們有著相同的旋轉值。
在上方的圖中,由於大部分頂點都會引用草地材質,通常著色一個像素只需要採樣草地材質一次。
在執行了隱藏步驟後,由於4個頂點可能有不同旋轉值,著色一個像素可能需要採樣4次,每次使用不同的UV。不過為了這種視覺提升付出的開銷是值得的。
遠景連續UV
另一個問題是當同一個紋理被用於地圖上大畫幅時,最終所有最高的mip中的細節塌陷成了一個單色。這個問題的解決方法被稱為遠景連續UV(Vista Uvs)。
使用Vista Uv,我們計算了第二套UV的集合,和原始的UV類似——除了縮小UV以放大被採樣的紋理。我們重新採樣了漫反射係數(albedo)和法線(normal)以計算這些宏觀的貢獻度。
我們在常規貢獻度和宏觀貢獻度之間插值,基於攝像機距離來計算最終的貢獻度。
在某些類型的素材上——例如岩石,Vista Uv能在遠景出產生很好的細節表現。
OMPV的問題
OMPV的另一個問題是材質之間過渡的邊界太明顯。這裡(圖中)我們展示了從泥土到沙子的過渡發生了什麼。
圍繞地形的內容工作
目前為止我們採用的方案是藝術家手動調整過渡材質。這樣效果不錯,但會帶來很大的手動工作量。
而由於每次過渡都是一組全新的紋理,這也會增加紋理streaming系統的壓力,並消耗寶貴的內存。
多層地形材質(MLTM)
目前我們為這個問題實現了一套方案,被稱為多層地形材質(Multi-Layered Terrain Materials),縮寫為MLTM。
一個多層地形材質接收2個地形材質的輸入,以及一個用於混合兩種材質的閾值(threshold)。它對應的reveal map被用於計算混合效果。
實際上這也類似Photoshop製作的過渡效果,但不需要額外內存開銷。
所有混合過程都是運行時執行的。這確實會增加shader的開銷,但由於這種材質只用於(兩種材質的)過渡計算,因此是能接受的。
*這裡說的幾個點都是相對前一頁,藝術家手動調整製作過渡紋理的情況。
虛擬紋理
虛擬紋理是在運行時被用來模擬(emulated)超大紋理的一種處理。
類似虛擬內存為計算機顯著地在物理內存的基礎上增加內存一樣,虛擬紋理能使本來特別大(超過尺寸限制)的紋理變得可行。
這個模擬的關鍵是2類紋理:
- 首先,有一個物理紋理(physical texture)以管理超大紋理的頁——一個頁對應模擬紋理的一小塊方形區域,或它的mip。一個頁通常有256X256像素,但它也可以是你需要的任何尺寸。
- 其次,有一個間接紋理(indirection texture)存儲足夠的信息,以便GPU能重定向採樣物理紋理中存儲的紋理數據。其中的一個像素將指向物理紋理中的一個頁。
例如,一個4K的物理紋理包含256x256頁,再乘以2K的間接紋理就能覆蓋512K的紋理。
虛擬紋理通常也被稱為程序化(procedural)的虛擬紋理,因為最終的模擬紋理是程序化組合材質及其它繪製單元的結果。
在本次分享中,程序化這個詞被省略了,但它實際上是很重要而值得被記住的——因為最終的紋理是一系列複雜的混合處理的結果,並不存在於硬盤上。
*現在VT已經不止用於處理超大尺寸紋理的問題了,但最初針對超大尺寸的這個思路確實是約翰卡馬克提出的。
自適應虛擬紋理(AVT)
虛擬紋理對於模擬不符合GPU尺寸要求的紋理是很有效的。
然而,其數量級對於模擬更龐大的紋理還是不足。
例如,512K的虛擬紋理只能覆蓋大約3平方英里、每英寸(inch)25像素的地表。
對於更大的虛擬紋理,育碧公司的Ka Chen在2015年的GDC上介紹了自適應虛擬紋理(Adaptive Virtual Texturing)的方案。之後我會把它縮寫成AVT。
AVT可以模擬最多包含24層mip的紋理。以每英寸25像素作為精度時,這足以覆蓋10.6平方英里的區域。
作為算法的基礎,世界被切分為210英尺(feet)左右的很多部分(sectors);每個部分通過一個虛擬圖像(virtual image)來管理——這部分實質上就是傳統的VT。
虛擬圖像的尺寸是動態的,它會隨攝像機距離的不同而變化——這就是它被稱為自適應的原因。
虛擬圖像
一個虛擬圖像有16個mip。通過它們,我們能覆蓋210平方英尺,每英寸25像素精度的區域。
只有靠近攝像機的sector是被激活的,我們只支持最多255個sector。
也有一個默認的sector覆蓋整個世界,而它總是使用16mip的虛擬圖像。
屏幕左上方的方形區域展示了這種方案的間接紋理——它只有512X512像素。
- 默認的sector是灰綠色的,它佔據了左上角。虛擬圖像在這個間接紋理中為它們各自分配空間。
- 從圖中可以看出虛擬圖像中的哪一部分圖樣(scheme)是負責為哪一部分地形著色的。當我們靠近一個sector,你可以看到它的虛擬圖像變得更大;當遠離時,它對應的虛擬圖像就會縮小。
*這裡作者演示了一段視頻,文稿中雖然沒有但從顏色對應能理解作者的意思。
虛擬圖像四叉樹
一個虛擬圖像的四叉樹(quad tree)比起普通的四叉樹更復雜,因為它要追蹤GPU中使用的頁。
當展開四叉樹的分支時,部分子節點可能沒有對應的頁,這時它們需要指向父節點的頁。圖中展示了這一過程。
例如,在深度1,根節點的4個子節點需要被展開,以創建棕色的頁;另外3個子節點則直接指向父節點(以此類推)。
每一幀,這些頁可能變為準備好的狀態,或相反地變為無效的狀態。
當一個新的頁被合成時,四叉樹直到下一幀才更新。這是因為我們無法承受GPU中使用了合成中的頁的風險。(*基於多線程的原因)
當一頁不再被請求時,它會進入緩存中進行計時。它在四叉樹中被(邏輯)刪除了,但內存中它會存在至少3幀。這對於GPU不會使用回收中的頁已經綽綽有餘(more than enough)了。
間接紋理
CPU版本的四叉樹對於GPU來說太複雜了。(*這裡我理解是這個算法對於偏並行計算的GPU不友好)
作為替代,我們把四叉樹烘焙到一個間接紋理上。每一幀,遊戲會計算前一幀和當前幀的四叉樹的區別。
這一步會被轉換成指令,並在一個compute shader中執行,它負責更新這個間接紋理。
The picture here shows what the CPU quad trees looks like from the perspective of the GPU.
圖中展示了從GPU的角度來看CPU中的四叉樹的狀態。
動態尺寸
當攝像機移動到和sector足夠近,遊戲會決定啟用新的mip並添加。
它首先會從主間接紋理中分配更大的子間接紋理,之後會用每幀填充間接紋理同樣的算法填充它。
當攝像機移動得足夠遠,相反的處理會被執行。最終mip會被從間接紋理中移除。
間接紋理的紋素
一個32bit的間接紋素(texel)提供了物理頁中的座標。它也指出了頁在四叉樹中的mip級別。
所有這些信息在從sector的Uv定位到物理頁Uv的過程中都是需要的——這裡的代碼片段展示了這一轉換過程。
動作中的間接紋理
我們也開發了工具以幫助可視化和debug間接紋理——如圖所示。
每個地形的方塊代表了間接紋素的32bit,並被展現為一個albedo顏色——而通常它們會被以物理頁(實際的紋理)填充。
地形的渲染在prepass和opaque pass中都執行了。
During the prepass, terrain writes to the depth buffer and to a stencil bit reserved for super terrain. We also write the geometric normal to a g-buffer.
在prepass階段,地形寫入深度緩衝(depth buffer),對於超大地形還會寫入一個模板bit。我們也會將幾何法線寫入gBuffer。
During the opaque pass, terrain is deferred rendered using a full screen quad. Every pixels that have the stencil bit set, sample from the virtual texture to shade the current pixel.
在不透明著色階段,地形以延遲渲染的方式通過一個全屏的quad來渲染。每個設置了模板bit的像素,從虛擬紋理中採樣來為當前像素著色。
*這模板bit的具體數據結構作者沒有介紹,但基本是和之前介紹的虛擬紋理索引有關。
虛擬紋理的一個核心面向就是GPU驅動(driven)。
在opaque pass中,它既為每個像素著色,也計算可用的情況下會使用的物理頁。這些信息被存儲在一個回饋buffer中(feedback buffer)——它在3幀之後被CPU讀取。
它的分辨率是240X136像素,不過我們也在研究在低端硬件上使用更小分辨率的可能。
基於性能方面的考慮,這部分也沒有線程同步(thread synchronization)的過程。
*這裡的feedback buffer主要是起到紋理計算與分派的作用。
在opaque pass階段,每個地形像素存儲一個32bit的值到feedback buffer中。
它寫入了虛擬紋理的mip級別,像素的sector ID以及間接紋理四叉樹節點中的四叉樹座標。
其中最困難的部分是mip級別。它是通過查找UV的梯度(gradients)來計算的,而圖中的代碼片段展示了這一過程。
*簡單分析下,這裡用偏導數(ddx,ddy)計算了UV(textcoords就是紋理座標,縮寫就是UV)的變化率,來確定應該採用的lod或者說mip級別。如果不是虛擬紋理,這一過程一般已經封裝在紋理採樣的函數中了。
在GPU寫入feedback buffer完成後,CPU從中讀取。在其精度是240X136時,這意味著要處理32000個值。
- 首先,我們計算所有唯一的值。
- 之後,我們將子節點的計數傳遞給它們的父節點。這是為了確保當子節點沒有足夠的hits(內存命中)時,期望父節點能有(對應的內存數據)。
- 最後我們按照流行度(popularity)來排序。我們希望最先請求的頁是最需要合成的頁。
我們對sector的根頁做了偏移(bias),因此如果一個sector是全部可見的,至少有一頁空間是留給這個sector的。我們也基於相機速度對高分辨率的頁做了偏移。
由於feedback buffer是通過非同步的線程來更新的,因此它可能會有很多噪聲(noisy)。我們通過引入一個閾值來解決這一問題,因為我們不希望影響頁的合成。
有時,接近閾值的頁會有一幀超過這個閾值,而下一幀又低於這個閾值。因此,我們也需要搶救(rescue)已經可見的頁,以避免視覺故障;我們也從不請求(緩存中)超過1/3的總數的頁,以避免視覺擾動(churn)。
儘管前面已經介紹了很多(數據和優化細節),遊戲中仍然會時不時地看到低分辨率的虛擬紋理。(*這裡指預期外的一些視覺故障情況)
例如,如果你的攝像機面向一個方向,之後快速朝向側面,使屏幕渲染內容的請求填滿了緩存區域,之後再做180度旋轉看向身後,新的頁就會經過幾幀才能添加到緩存——這種情況就顯得很明顯。
解決方案是為每個sector選擇最接近的中等分辨率的頁,並將它們添加到請求頁的列表中。
我們經常進行攝像機位置的瞬移(teleport),或很快的攝像機切換——例如從天空切換到玩家實際出生(spawn)的位置。遊戲會提前告知攝像機瞬移的目標位置,因而我們可以提前stream合適的資源。(*通過帶有預判的加載來解決快速切換問題)。
AVT技術也肩負著創建中等分辨率頁的任務。
Virtual texturing is essentially a cache. Typically, you only want to populate caches with up-to-date data. If the VT wants to composite a page, we need to make sure all the mips that are needed for that page are loaded before the page is composited.
虛擬紋理實際上是一個緩存。通常來說,你只希望緩存中填充合用的數據。如果VT希望合成一個頁,我們需要確認相關的mip都已經被加載了。
不幸的是,在存取速度很慢的硬件上,可能需要數秒來加載需要的mip。這會導致視覺上的BUG,攝像機會很快用盡VT的空間,而整個地形看起來就像PS遊戲(那麼糊)。
我們發現更好的方式是在頁被請求是立刻去合成,但標記它是低分辨率的。
15幀之後,如果它還沒有準備好,我們會重新合成一次,並假定我們有足夠的帶寬來執行。
之後這一延遲會加倍成30幀,我們會再次嘗試合成它——直到這個頁被標記為準備完成,我們會最後一次合成它(作為合適的分辨率)。
*這裡更多是在加載很慢的設備上的一種取捨,即性能實在不行的時候如何儘量看起來舒服一點。這裡實際上說的就是VT方式下進行了從低精度mip開始的加載。
結語
讀的上半部分更多是基於虛擬紋理的介紹,這項技術提出已經有相對不短的時間了,但對於硬件還是有一定基本要求的。
值得一提的是,虛擬紋理也不僅僅用來處理超大紋理的問題,例如虛幻引擎中的SVT和RVT,雖然不完全是一回事,但是其中有很多映射和緩存思想是相似的。同時VT也是渲染調用上的一種優化,例如能減少材質綁定的過程,調整渲染批次等。
而VT似乎也是卡馬克大神在遊戲領域提出的最後一項有前瞻性的技術,可惜對應的遊戲《Rage》表現就一般了。
下週會繼續更新這篇文章的下篇,看看更多地形合成上的細節。
最後是資料鏈接:
國外一篇比較好的Virtual Texture總結介紹
Large-Scale Terrain Rendering in Call of Duty 的PPTX