騰訊魔方技術專家:開放世界中的水體渲染和模擬
2021年11月22日-24日,由騰訊遊戲學堂舉辦的第五屆騰訊遊戲開發者大會(Tencent Game Developers Conference,簡稱TGDC)線上上舉行。本屆大會以“Five by Five”為主題,邀請了海內外40多位行業嘉賓,從主論壇、產品、技術、藝術、獨立遊戲、市場及遊戲社會價值7大專場共同探討遊戲產業趨勢和多元價值,以開發者視角與需求為出發點,助力遊戲行業良性發展,探索遊戲的更多可能性。
我是騰訊魔方工作室群的陳家銘,今天和我的搭檔、天美工作室群的胡有為一起,分享大面積深度可互動的水體模擬和渲染技術Aqua。
1. Aqua專案簡介
我和有為來自兩個部門,是什麼原因使我們一起合作研究這個水體方案呢?因為我們參加了騰訊遊戲舉辦的開源計劃Tech Future,目的是加強內部技術交流,推進前沿遊戲技術開發。Aqua就是其中一個專案,它的的目標是研究可互動的水體模擬和渲染技術,由來自不同部門、崗位的小夥伴們,經過一年時間的業務時間開發,做出了一些成果,在本次大會分享。
水體的模擬和渲染一直以來都是經常被研究的課題,比如育碧《刺客信條》的海洋,頑皮狗《神秘海域》系列中的水體,虛幻引擎 4.26版本釋出的水體系統。
虛幻引擎的水體系統,和我們的目標比較接近,是在編輯時首先以曲線定義好各種水體範圍,然後在執行時透過一個四叉樹來構造水體的Mesh進行繪製。這個系統的功能很強大,但是跟我們希望做的全動態水體有點差異, 比如說:我們希望可以做到下雨的時候造成洪水,把整個場景淹沒。又或者玩家可以透過技能,隨意把水放到任何一個地方。
所以,Aqua團隊希望在這方面有所突破,開發了一套方案,並且製作了一個 tech demo。demo 裡,可以看到的水體範圍是1km x 1km, 而模擬的精度能達到 25cm, 渲染精度能做到6.5cm,玩家可以像剛才提到的用技能在場景中噴水,並且把篝火弄滅,同時噴出來的水也能留在場景裡面。除了一些圖形效果外,我們也實現了像載具、浮力等需要跟CPU溝通的一些特性,主要是為了驗證方案的實用性。也可以在demo裡看到,下雨會導致水位上升,然後洪水把場景淹沒的一個事件。還有,除了可以模擬湖泊外,還可以模擬河流、海洋。
2.1 模擬:多層級水體模擬
Aqua背後的原理是怎樣的呢?先講講 Aqua 的模擬演算法選形。在遊戲裡面常見的水體演算法有以下幾:
PBD/SPH:透過粒子來最真實地模擬水體動態,一般都需要一個很巨大數量的粒子。所以當我們應用在大面積水體裡面,比如說幾百米,不論是模擬或者渲染要解決的效能問題都是非常困難。
Wave Particles:它的思路是用一個個粒子來代表一個波浪(Wave),然後把所有波浪都轉成高度圖來進行渲染,好處是模擬效果很可控,但是較難去模擬水體容量的改變。
網格法:最傳統的,雖然它不能像 SPH 一樣可以支援水躍 (Hydraulic jump),但大部份水體的特性都能夠模擬出來,而且它有很好的特性和伸縮性。考慮到我們目標是要支援好幾百米以上的超大範圍,Aqua 最終選擇了網格法作為方案基礎。
但是不能簡單用很高解析度的網格進行模擬,因為記憶體、計算量在一般的 GPU 上都會扛不住。因此我們借鑑了Clip mapping 的原理來進行多層級模擬。思路是把整個模擬範圍以不同精度的網格來覆蓋,去掉一些肉眼看不到的細節來換取效能;每層 Clip 的中心大概與相機對齊,水體離相機越近便會有越高的精度。我們從面積最大的一層開始,之後就把圖中紅色的邊緣部份同步給下一層;就能把 Clip 外部的重要影響傳遞進去。
整個系統的流程大概這樣:每一層 clip 都會分為“資料收集”和“模擬”兩個階段,首先我們會收集clip範圍內的模擬輸入,包括場景或者地形的高度圖,主要是用來跟水體進行遮擋或者碰撞。此外,還會收集一系列額外的模擬輸入,同時轉成貼圖,以便給後面的 GPU 模擬使用。
接著,我們會利用 Compute Shader 來進行模擬的解算;我們實現了兩種網格演算法,分別是 LBM 和 Pipe Water, 這個我們在後面會有進一步的去分享。為了避免水體的速度太快而超出 CFL 條件,我們還要考慮 sub-stepping 的支援,透過縮小 delta time 來提升模擬的穩定性。完成了一級 clip 的模擬後,我們會進行邊緣狀態的傳遞。如此類推,當所有 clips 完成模擬之後,會把結果匯出到一個TextureArray或者TextureAtlas裡面,再交給渲染模組進行繪製。
剛才提到我們需要收集場景或者是地形的高度圖,實際上是透過虛幻的 SceneCapture功能來達成。使用者預先定義好模擬在世界空間最小和最大的高度,當相機在水平移動超過一個閾值之後,我們會在最高點,從上往下拍一張深度圖,再把深度反轉,加上最低點的高度,就能得到場景朝上一面的高度。
另外,我們還要收集一系列額外的模擬輸入,像水的容量或者速度的影響,這裡的難點是如何把各種各樣的影響統一成固定的輸入,以便給 GPU 模擬使用。
模擬演算法的相關內容:到目前為止,我們實現了PipeWater和LBM兩種網格演算法,由於時間關係,細節參考下圖所列的相關文章。雖然兩個演算法的公式都不太一樣,但都是在求格子某幾個特定方向的水體流量 F_i,比如說 Pipe 是求上下左右 4個方向,而 LBM 是求 D2Q9 中的9 個方向。模擬的 CS 會透過當前幀的 F_i , 加上與相鄰格子的高度差、黏度、引力等等引數來算出下一幀的 F_i ,這樣不停迭代。模擬 CS 會用 structured buffer 來儲存 F_i, 把中間資料,像容量資料速度匯出給渲染使用。
在開始的時侯,我們提到方案中有一個邊緣狀態傳遞的操作,所謂的邊緣狀態就是指每一層 Clip 邊界上的 F_i。在每一層 Clip 開始模擬之前,我們都必須要從上一層把對應這一層的邊界 2 個格子寬度的 F_i 拷過來。這樣我們就可以把 clip 範圍以外的影響傳遞到這一層的 clip 裡面。以下面兩個影片為例,第一個影片演示了狀態傳遞關閉,水就流不進場境中心的區域。而第二個影片裡因為開啟了狀態傳遞,水就能正常流到場景中心裡面了。
雖然我們用了多層級的策略來增加模擬的範圍,但面對好幾公里的超大場景,需要用另外一個技巧來增量去擴充套件模擬範圍。我們把這個方法稱成Scroll Update或者是 Sliding Window。首先每層 Clip 各自與相機對齊中心,這樣當相機平移後就讓 clip 的所覆蓋範圍也會自動跟隨著去更新。由於覆蓋範圍有所更變,我們需要把更新前的 F_i, 按世界座標拷到新的格子裡,相機位置會按格子大小進行 snapping, 避免因為複製F_i時候造成跳變。為了避免太頻密的更新,我們會等相機超過多個格子才觸發更新。在移動方向出現 Cache miss 區域, 我們可以從面積較大的一級Clip 去讀取 F_i,提高穩定性。
跟一般的物理模擬一樣,當水體流動速度太快,就很容易超出了 CFL 條件就會導致數值溢位的情況 (下圖爆炸的水體)。這個在模擬精度提高的時候特別容易出現,因為模擬公式中的delta x相對地降低了, 速度就很容易超出Cmax的限制。所以只能透過 sub stepping 來降低 delta t 來解決,簡單說就是在一幀裡面多跑好幾次的模擬。但這樣對效能不太友好,因為計算量直接翻了好幾倍。我們觀察到,在多層級的方案中每層 Clip 的 delta x 都會增加了一倍,這就意味著Sub-step 次數可以減半。透過這樣的最佳化,跟原來不分層級來比較,可以降低大概 ~90% 左右的計算量。
最後,我們看看多層級模擬的效果比較:左圖只有一級,而右圖是分了兩級來進行模擬,可以看到效果都是很接近的。而從我們 tech demo 的資料裡面去看到, 多層級模擬不論在記憶體和效能上都有明顯提升。
2.2 模擬:作用器系統
接下來交給有為,介紹水體模擬的作用器系統和渲染部分。接下來會向大家介紹我們開發作用器系統的目的和挑戰,以及如何解耦模擬,以及如何自定義自己的作用器材質,最後我們會展示一些Demo實際案例。
其實不論是LBM模擬演算法還是Pipe模擬演算法,本質上都是在計算水體高度場和速度場,假如沒有其他的因素干擾,模擬結果最終會趨向平穩。
但是,如果在計算的過程中加入一些外部影響的話,就可以得到一些互動式的結果,比如角色在河裡游泳產生的水波,以及下雨產生的漣漪,海風吹起的海浪等等,這些都是互動模擬,那我們如何實現這些互動模擬呢?答案就是作用器。
我們曾經想過讓作用器直接參與模擬ComputeShader的計算,但是這樣會導致一個問題:作用器種類是非常多的,每個作用器都有自己的演算法實現,這導致很難直接在模擬階段透過一個統一的函式入口去直接影響模擬結果。這會使得模擬的shader變得及其複雜,也不方便以後的管理和維護。
於是我們使用了一個解耦手段,就是將所有作用器的結果都輸出到作用圖中,對於所有的作用器,即便他們的演算法實現不同,但他們都輸出統一的結果,就是剛體入水深度、速度和體積,然後我們會把這些結果分別儲存在作用圖的RGBA通道里面。然後我們就可以在LBM模擬或者是Pipe模擬的時候去取樣這個RT作用圖,分別將RGBA通道資料取出來去影響各個模擬引數,完成互動模擬過程。
這裡分享一個比較重要的內容,就是我們的作用器系統框架,該系統首先會每幀收集當前區域內有效的作用器,然後同時更新這些作用器Gameplay。然後收集它們產生的Instance資訊,包括位置、大小、自定義資料等。同時會收集所有作用器可能訪問的uniform Buffer。包括上一幀的模擬高度場、速度場、場景高度,以及自定義貼圖資料等。然後透過Draw Instance方式,將每個作用器以相同材質進行合批渲染到四邊形的方式繪製到RT上,最終每個作用器統一的輸出體積,XY方向速度和剛體資料四個結果,分別儲存到貼圖的RGBA通道。那麼對不同的模擬演算法可以訪問這個RT,對模擬結果加上外部影響。因為RT上輸出的是統一的物理結果,所以,不論哪種模擬演算法都可以使用,而無須關心作用器的類別,達到解耦的目的。
那如何讓使用者快速開發屬於他自己的作用器呢?最初的設想是,讓TA或者程式能像開發材質一樣,在材質編輯器中透過連線的方式就可以實現自己的作用器演算法,我們稱之為作用器材質。如圖所示,目前開放專案中已經透過自定義的方式實現了一部分常用作用器材質。
同時為了豐富高度自定義內容,我們也封裝了很多材質函式,以供開發者使用,比如訪問每個作用器自定義的資料,uniform buffer資料,高度場,速度場貼圖資料等。都能在材質編輯器中拖入使用。可以看到下面的截圖,它是一個水源作用器的實現過程。
大量不同作用器材質的使用會導致drawcall數量增加的問題,那麼我們如何解決這個問題呢?剛剛的分享當中我已經提到了,我們這裡也會對相同材質的作用器進行合批處理。我們可以看到截圖裡面,在開放專案中,我們大量使用了作用器去模擬水源,波浪等,但最終的drawcall呼叫次數只有3次。
這裡我們擷取了開放專案中部分的作用器效果示例。可以看到藉助作用器框架,可以方便的自定義實現多種Affector Material與水體進行互動的Gameplay玩法。
3.1 渲染:基於GPU Driven的水體渲染
這個主題我們會向大家分享以往的傳統做法,然後我們如何創造性地應用CDLOD來實現GPU驅動的水體網格渲染,包括如何在GPU上構建四叉樹,如何實現遮擋剔除、超高網格密度、頂點變形等。傳統的水體網格渲染,我們都知道我們一般會使用一個平面網格,加一個高度圖來還原水面的波浪運動。在移動端甚至不需要真實的波浪,而是僅僅透過法線和UV動畫來模擬波浪。
實現演算法就是我們去還原波浪的高度以及還原頂點的位置,很簡單,我們只需要在VertexShader中取樣高度圖,來還原頂點的Z座標值。並透過取樣鄰近高度場資料來重新計算每個頂點的切線空間,最後把頂點的切線空間插值到光柵化階段。
在實現GPU驅動之前,我們也調研了UE4.26 的水體外掛,其中截幀分析了海洋的例子,可以看到,Unreal為了表現不同的海洋網格密度,它使用了6種不同的網格拓撲結構。雖然用了同一個水體材質,並且instance方式繪製,但也產生了6DrawCall。而且,UE4.26的水體分為海洋,河流和湖泊不同的水體Actor,這也會導致DrawCall數量的增加。另外UE4.26的水體是以CPU的方式驅動,而我們的目的是做GPU Driven。
最終我們在方案的選擇上,我們採用了CDLOD的技術,CDLOD全稱是基於連續距離的細節等級。它本來是用來解決大世界地形渲染最佳化問題的,我們利用這次機會,將它很好的實踐到了大型水體渲染當中。CDLOD它有很多優點,它具有變化平滑,不會產生裂縫,Lod之間等級差不超過1的特點,並且非常適合做基於四叉樹節點的剔除。
在GPU上面去構建四叉樹的演算法原理其實就是一個自頂而下的過程,從根節點開始,每個根節點是ComputeShader中的一個執行緒。如果當前節點在遞迴的這一級,又在下一級,它就確定了這個Quad。如果即在當前遞迴的這一級,又在下一級,那麼意味著子節點需要繼續遞迴到更低階別去確定,如此迴圈往復最終遞迴到0級。在GPU上構建去四叉樹會遇到比較多的問題,我們遇到的第一個問題,首先在ComputeShader上面是不能使用遞迴函式的,它有遞迴函式使用的限制。
然後,我們如果想跳過這個限制,比如在shader程式碼中寫大量的巢狀迴圈來解決遞迴函式問題,又會使得shader變得超級複雜,也會將暫存器耗盡。而且,一般的構建過程當中,遞迴複雜度會隨著每一級遞減,也就是說,根節點那一級他是最複雜的,也是最耗時的,0級是最不耗時的,如果我們把所有的級別都用統一的shader程式碼,就不能很好的平均每個執行緒執行時間。
為了解決這個問題,我們採用了分批次執行ComputeShader,每一級構建都會有自己對應的ComputeShader變種。每次使用上一次構建未確定的節點的輸出作為這次的輸入,這樣使得每一次構建都只關心它當前這一級,從而大大降低了shader複雜度,平均了執行緒間的執行時間。
在構建的過程當中,我們也會先判斷四叉樹節點是不是應該被剔除,以減少Instance數量。透過取樣水面高度的模擬結果,我們可以構建一個節點內的Quad內的Min/Max Height資訊,從而構建一個WorldSpace下面的立方體。我們的剔除分為兩種,一種是視錐剔除,另外一種是HZB剔除。視錐剔除會判斷立方體的8頂點是否在裁剪空間內,而HZB則根據立方體的螢幕空間尺寸選擇合適的Z-buffer mipmap做深度判斷進行剔除。
對於構建好的節點,如果直接渲染的話,其密度是很低的,那麼我們如何去構建一個高密度的網格呢?如下圖所示,顏色相同的區域具有相同的網格密度。這裡我們使用了2種網格密度,預先生成了2種面片mesh,第二面片的頂點數是第一個的1/4。
以全尺寸32和半尺寸16為例,我們去拿32和16去填充非邊界和邊界的四叉樹節點,透過這種方法,我們僅使用2種拓撲結構就可以表達多種網格密度,DrawCall數量最大為2。
最終渲染的時候,最關鍵的就是要確定哪些頂點是需要變形,我們會為每一級設定一個當前這一級對應的節點尺寸大小的變形區域,處於區域內的頂點將進行變形。因為之前按照2的冪次方規則,使用了2種不同的面片進行網格密度填充,所以這些面片的頂點位置是奇數的是需要變形的。
另外我們也總結了構建解析度和該解析度下對應下的構建消耗時間對照表,無論場景多大,我們的構建四叉樹花銷的消耗時間只與構建解析度有關,這樣可以穩定效能上限。構建解析度和網格密度可以隨意搭配,根據實際專案需要進行配置,達到效能和效果的平衡。在目前的開源專案中,以512的構建解析度+32的網格密度作為預設配置,如果想要效能更好,可以選擇256 +16甚至更低。但是這樣會導致網格精度會下降,可能會出現鋸齒、走樣。
最後我們來看下實際Demo中的效果,角色近處始終維持比較高的網格密度,而遠處比較稀疏的網格會隨著角色攝像機移動而進行平滑過渡。
3.2 渲染:基於Height Blend的動態地表淺水
接下來介紹如何改進UE4原生高度混合演算法,並在此基礎上實現靜態淺水,然後結合模擬結果進一步實現了動態淺水。最後我們將展示在遊戲Gameplay過程中的實際效果。
開放世界除了海洋,湖泊,河流這些常規水體。淺水也是一個很重要的能體現環境細節表達,特別是池塘窪地,雨後積水的路面。UE4的地形渲染,我們知道都是多層Layer進行Blend的結果。其中的基於高度的混合,可以讓地表表現出縫隙或者低矮處混合。於是,我們設想在Landscape中透過插入一個淺水層,與其他層去做Height Blend,以表達淺水效果。
先來分析下UE原生的heightblend演算法,這個演算法很簡單,它將每層的權重乘以高度值,然後累加,再歸一化百分比,最後混合每個層的結果。這個演算法很簡單,但是它導致不同layer在整個權重區間都在Blend,效果顯得很髒。
Layer weight 和 height map value 數值空間在[0,1]範圍,這個範圍很小,不是真實物理單位,它會丟失計算精度。並且在接縫處無法實現均值過渡,以及無法控制過渡閾值。
下圖右側是這個演算法的原生效果。改進的做法,第一步把高度值對映到真實物理空間,然後乘以對應的權重,接下來我們會篩選一個權重最大的層,也就是最高的層。然後將每層權重與最高層的差值除以一個過渡閾值,這樣就得到一個平滑過渡的權重,最後將這個平滑過渡的權重去做歸一化百分比,最後混合每層得結果。這個演算法的優點在於,首先將在[0,1]的height map value 對映到實際物理單位下,擴大了計算精度。在接縫的地方會是一個趨向於平均實現了均值過渡,以及可以控制過渡閾值。我們可以看到右邊的截圖是改進後的效果,可以看到基本達到了做淺水的要求。
另外很重要的是,我們不希望淺水的HeightBlend過程被固定死在C++程式碼中,而是開放讓美術或者TA能在材質編輯器中透過連線的方式去進行編輯。UE預設的地表材質節點,Layer Blend是無法解耦淺水和其他層的混合操作的。所以,我們開發了自己的材質節點叫做Height Blend。可以看到截圖裡面,我們自己開發的這個節點除了輸出其他層的混合結果以外,會將淺水層的權重資訊單獨輸出,這樣我們便能在材質中控制最終淺水的混合效果。
我們把最終的淺水混合操作都封裝在了一個材質函式中,之前我們有提到,我們開發了一個自己的地表材質節點,它會將淺水層的權重單獨輸出,我們拿到這個權重後,就可以分別去混合淺水的顏色,法線和PBR資訊了。法線和PBR的混合比較簡單 就用了比較常規線性插值,而顏色方面我們則考慮了乾燥地表向潮溼區域過渡的效果,以及潮溼區域向淺水過渡的效果。
剛才講的其實都是靜態淺水,進一步增加動態淺水,我們又加入了LBM的模擬結果,下圖左側是加入了LBM結果後的效果。我們把LBM的模擬結果去影響高度混合的高度值,可以看到它與周圍環境產生了互動效果,並且真實反應了HeightBlend。然後,我們也測試了Pipe模擬和Rain Affector一起作用的效果,可以看到除了表面漣漪以外,還可以看到下圖右側,積水體積是不一樣的,從而真實反應了模擬對積水體積的影響。
最後,是一些Gameplay中的有趣應用。可以看到主角在遊戲世界中可以任意釋放技能,這個技能可以在地表上殘留一層淺水。淺水和地表形成高度混合的侵蝕效果,並且淺水會隨時間慢慢吸收掉。接下來由家銘繼續分享。
3.3 模擬結果應用
剛才瞭解到水體基礎繪製的原理,現在會進一步講解模擬結果是如何應用到更多水體的細節效果裡。首先是水體表面的細節法線,有了細節法線,水流動的感覺更加強烈了;而這個效果也不難去實現的,我們只需要利用水體的速度場當作是 flowmap 去取樣Detail Normal的就可以了。由於模擬出來的速度是在世界空間裡面定義的,我們需要進行一個簡單的 縮放 和 Clamp的操作。另外我們也需要考慮到速度為零的時侯把細節法線 Flatten。
還有就是泡沫效果, 跟其他方案透過分析水體高度圖的 Jacobian 不一樣。我們發現水體出現碰撞的地方通常都會有比較高的旋度,由於我們是二維的速度場,所以算出來的旋度是一個純量。因此我們可以把旋度視為左邊的公式的一個2維 旋度的計算方法, 在裡面f 就是速度是x座標 ; 而g就是速度的y 座標; 而k就是一個c軸,可以忽略。我們再一次把速度場當成是 flow map 去取樣泡沫的貼圖,再乘以遮罩之後就能得到下圖效果。
除了水體表面的效果外,水體周邊的物體也會因為沾到水而改變材質顏色。為了計算沾水的程度,我們會利用一個 double buffering 的技巧。首先我們把當前幀 Buffer1 的水體高度拷到 Buffer2 裡面, 然後讀取上一幀Buffer2時候儲存的高度,做一個縮減,再跟新的水體高度取 Max 再放回Buffer 1裡面 所記錄的水體高度就可以算出一個沾水權重。在渲染場景物體的時侯,我們會利用物體的世界座標的C去減去Buffer 1 所記錄的水體高度就能算出一個沾水權重。物體計算光照的時候PBR 引數,就利用這個權重從乾和溼兩組 Preset裡面去插值出來。
還有是一個GPU 粒子與水體互動的例子。在影片裡面演示的落葉效果不僅可以漂浮在水上,也可以隨著波浪而旋轉。這個效果的 粒子系統裡面,每個粒子可以分為:掉落、漂浮還有消失三個狀態。掉落時粒子會以 場景高度 和 水體高度 判斷它是否已經掉落到水裡面,如果是就切換到漂浮狀態。漂浮時就簡單的以水體高度作為它在世界空間裡面的高度;還加上之前提到的旋度來控制葉子旋轉。假如粒子是掉落到地上它就會變成一個消失,就是簡單的把它設成一個透明,而後在等好幾幀讓粒子系統讓它回收到池子裡面就可以了。
再分享兩個與水體高度有關的後處理效果;首先是 WaterLine ,這個效果會出現在水面跟水底交接的邊緣部分。而它的思路就是在螢幕空間去找出接近水面的畫素,再插值成WaterLine的顏色。利用投影距陣,我們可以計算 Near Plane 上每畫素的世界座標, 再用這座標去取樣水體高度, 這樣我們就可以算出 Near Plane 對應畫素與水面的距離,再把這個距離透過 falloff的函式 算出一個插值的權重就能得出左邊截圖的WaterLine 效果。
延伸剛才 Water Line 的思路,我們可以算出在視線上不同距離的世界座標, WP利用WP我們就可以透過一些噪聲 或者是水體壓場模擬出該點的一些Scattering的亮度。再以WP取樣水體的高度,我們就可以知道這一點在水下深度,再放到一個 falloff的函式里面就可以算出因為這個水深而做成的散射的縮減。這樣,我們在視線上 Ray March 4 個點,同時算出它散射的一個積分,就能得到影片裡的 Light Shafts 效果。
最後要分享的是跟玩法裡面有緊密關係的浮力效果。有別於之前介紹的,浮力是在 CPU 上計算再給到物理引擎的剛體上。根據公式,浮力跟物體浸在水中的體積有關,所以我們需要在 CPU 訪問物體位置對應的水深來算出它浸入體積。我們需要一個有效的機制從 GPU 裡讀取模擬結果,因此 Aqua 實現了一個延遲一幀的 GPU 讀取功能。大概的流程是我們會在每幀都先記錄所需要查詢的位置,之後合批發給一個 CS 從Texture Atlas 中讀取模擬結果並儲存到一張解析度很低的 Staging RT 裡。等 CS Dispatch 完成之後我們才進行 read back, 最後在透過下一幀 C++ 回撥來通知查詢結果。這樣就是可以儘量降低GPU 讀取所帶來的頻寬消耗和延遲。有了水深,我就可以計算浮力。在 tech demo 裡的浮木就是利用這個 GPU 讀取框架來計算木頭兩端的浮力,再以AddImpluseAtLocation 應用到木頭的剛體上。此外,當木頭每一端檢查到它是浸在水裡,我們會用讀取到的水體速度在這一端在給一道力;這樣就能做出剛體隨水流漂浮的一個效果了。
今天我們為大家分享了 Aqua 團隊所開發的開放世界水體方案,裡面包括了多層級模擬系統、作用器系統、GPU Driven 的 CDLOD 水體渲染;還有淺水效果渲染,以及各樣的模擬結果應用。在經歷了一年不長也不短的開發,Aqua 支援了不少功能和特性,但當中也有不完美或者可以續繼最佳化的地方。比如說:我們目前還沒有做音效的支援;又或者是方案還沒可以支援像地穴這種複雜地形的水體模擬。希望後面我們有機會再繼續最佳化和打磨 Aqua, 讓它變得更盡善盡美。
Q & A
問:水體模擬的網格精度如何控制?
陳家銘:這個問題很好,在我們開發tech demo 的過程裡面發現,其實以25釐米來代表一個格子的精度來模擬,不論在效能或者效果上都能達到一個比較好的平衡。由於在我們的demo 裡面,我們是分了三層 clips 來進行模擬;所以對應的精度就大概是25cm,50cm 以及是1m左右。當然,我們也可以按著遊戲的一個需求來提升模擬精度,但是當精度每提升一倍,就代表著substep 次數要增加一次,主要是為避免超出 CFL 條件導致了數值爆炸的情況出現。
問:水體渲染的網格是固定大小還是自適應?
胡有為:剛才我的分享當中也提到了,就是說我們的網格大小,其實是由兩個決定的,第一是構建解析度,第二是網格密度的設定,我們現在是以512的構建解析度+32的設定去決定生成網格大小的,當然您也可以選擇其他的,比如說256+16。目前這個配置是在執行前在專案中設定好的,我們並沒有實現說在執行時runtime的時候去改變這個,當然我們理論上也是可以做到的。
問:水體光照渲染是如何避免走樣的呢?GPU Driven如何適配到移動端?
胡有為:這個問題我們之前也遇到了,就是在水面很遠的地方,我們確實也出現了一些鋸齒、就是走樣。那我們如何解決這個問題的呢?我們透過將逐頂點的切線空間的還原變成逐畫素的,就是在畫素著色器裡面去還原切線空間,同時我們還原切線空間的時候,其實可以不用取樣鄰近畫素的高度場,而是跨多個鄰近畫素,比如說兩個、三個,可以讓它的切線空間再次平滑。
目前在移動端去實現GPU Driven還是有點困難,因為我們目前大家都知道移動端的硬體架構,對於ComputeShader來說目前停留在API的支援階段,移動端的架構並沒有做出升級或者改變,所以說在移動端去適配GPU Driven還是比較困難,如果要在移動端去使用CDLOD,可能還是要去回滾到CPU驅動的方式,我們把在GPU上面去構建的演算法,把它移植到CPU端,然後像變換大小這些資訊可以用Vertex Streams頂點流的方式去做到instance渲染。自定義的資料就只能透過烘焙到貼圖或者更新到貼圖的方式,因為移動端目前是對struct buff的支援是不太好的。
來源:騰訊遊戲學堂
我是騰訊魔方工作室群的陳家銘,今天和我的搭檔、天美工作室群的胡有為一起,分享大面積深度可互動的水體模擬和渲染技術Aqua。
1. Aqua專案簡介
我和有為來自兩個部門,是什麼原因使我們一起合作研究這個水體方案呢?因為我們參加了騰訊遊戲舉辦的開源計劃Tech Future,目的是加強內部技術交流,推進前沿遊戲技術開發。Aqua就是其中一個專案,它的的目標是研究可互動的水體模擬和渲染技術,由來自不同部門、崗位的小夥伴們,經過一年時間的業務時間開發,做出了一些成果,在本次大會分享。
水體的模擬和渲染一直以來都是經常被研究的課題,比如育碧《刺客信條》的海洋,頑皮狗《神秘海域》系列中的水體,虛幻引擎 4.26版本釋出的水體系統。
虛幻引擎的水體系統,和我們的目標比較接近,是在編輯時首先以曲線定義好各種水體範圍,然後在執行時透過一個四叉樹來構造水體的Mesh進行繪製。這個系統的功能很強大,但是跟我們希望做的全動態水體有點差異, 比如說:我們希望可以做到下雨的時候造成洪水,把整個場景淹沒。又或者玩家可以透過技能,隨意把水放到任何一個地方。
所以,Aqua團隊希望在這方面有所突破,開發了一套方案,並且製作了一個 tech demo。demo 裡,可以看到的水體範圍是1km x 1km, 而模擬的精度能達到 25cm, 渲染精度能做到6.5cm,玩家可以像剛才提到的用技能在場景中噴水,並且把篝火弄滅,同時噴出來的水也能留在場景裡面。除了一些圖形效果外,我們也實現了像載具、浮力等需要跟CPU溝通的一些特性,主要是為了驗證方案的實用性。也可以在demo裡看到,下雨會導致水位上升,然後洪水把場景淹沒的一個事件。還有,除了可以模擬湖泊外,還可以模擬河流、海洋。
2.1 模擬:多層級水體模擬
Aqua背後的原理是怎樣的呢?先講講 Aqua 的模擬演算法選形。在遊戲裡面常見的水體演算法有以下幾:
PBD/SPH:透過粒子來最真實地模擬水體動態,一般都需要一個很巨大數量的粒子。所以當我們應用在大面積水體裡面,比如說幾百米,不論是模擬或者渲染要解決的效能問題都是非常困難。
Wave Particles:它的思路是用一個個粒子來代表一個波浪(Wave),然後把所有波浪都轉成高度圖來進行渲染,好處是模擬效果很可控,但是較難去模擬水體容量的改變。
網格法:最傳統的,雖然它不能像 SPH 一樣可以支援水躍 (Hydraulic jump),但大部份水體的特性都能夠模擬出來,而且它有很好的特性和伸縮性。考慮到我們目標是要支援好幾百米以上的超大範圍,Aqua 最終選擇了網格法作為方案基礎。
但是不能簡單用很高解析度的網格進行模擬,因為記憶體、計算量在一般的 GPU 上都會扛不住。因此我們借鑑了Clip mapping 的原理來進行多層級模擬。思路是把整個模擬範圍以不同精度的網格來覆蓋,去掉一些肉眼看不到的細節來換取效能;每層 Clip 的中心大概與相機對齊,水體離相機越近便會有越高的精度。我們從面積最大的一層開始,之後就把圖中紅色的邊緣部份同步給下一層;就能把 Clip 外部的重要影響傳遞進去。
整個系統的流程大概這樣:每一層 clip 都會分為“資料收集”和“模擬”兩個階段,首先我們會收集clip範圍內的模擬輸入,包括場景或者地形的高度圖,主要是用來跟水體進行遮擋或者碰撞。此外,還會收集一系列額外的模擬輸入,同時轉成貼圖,以便給後面的 GPU 模擬使用。
接著,我們會利用 Compute Shader 來進行模擬的解算;我們實現了兩種網格演算法,分別是 LBM 和 Pipe Water, 這個我們在後面會有進一步的去分享。為了避免水體的速度太快而超出 CFL 條件,我們還要考慮 sub-stepping 的支援,透過縮小 delta time 來提升模擬的穩定性。完成了一級 clip 的模擬後,我們會進行邊緣狀態的傳遞。如此類推,當所有 clips 完成模擬之後,會把結果匯出到一個TextureArray或者TextureAtlas裡面,再交給渲染模組進行繪製。
剛才提到我們需要收集場景或者是地形的高度圖,實際上是透過虛幻的 SceneCapture功能來達成。使用者預先定義好模擬在世界空間最小和最大的高度,當相機在水平移動超過一個閾值之後,我們會在最高點,從上往下拍一張深度圖,再把深度反轉,加上最低點的高度,就能得到場景朝上一面的高度。
另外,我們還要收集一系列額外的模擬輸入,像水的容量或者速度的影響,這裡的難點是如何把各種各樣的影響統一成固定的輸入,以便給 GPU 模擬使用。
模擬演算法的相關內容:到目前為止,我們實現了PipeWater和LBM兩種網格演算法,由於時間關係,細節參考下圖所列的相關文章。雖然兩個演算法的公式都不太一樣,但都是在求格子某幾個特定方向的水體流量 F_i,比如說 Pipe 是求上下左右 4個方向,而 LBM 是求 D2Q9 中的9 個方向。模擬的 CS 會透過當前幀的 F_i , 加上與相鄰格子的高度差、黏度、引力等等引數來算出下一幀的 F_i ,這樣不停迭代。模擬 CS 會用 structured buffer 來儲存 F_i, 把中間資料,像容量資料速度匯出給渲染使用。
在開始的時侯,我們提到方案中有一個邊緣狀態傳遞的操作,所謂的邊緣狀態就是指每一層 Clip 邊界上的 F_i。在每一層 Clip 開始模擬之前,我們都必須要從上一層把對應這一層的邊界 2 個格子寬度的 F_i 拷過來。這樣我們就可以把 clip 範圍以外的影響傳遞到這一層的 clip 裡面。以下面兩個影片為例,第一個影片演示了狀態傳遞關閉,水就流不進場境中心的區域。而第二個影片裡因為開啟了狀態傳遞,水就能正常流到場景中心裡面了。
雖然我們用了多層級的策略來增加模擬的範圍,但面對好幾公里的超大場景,需要用另外一個技巧來增量去擴充套件模擬範圍。我們把這個方法稱成Scroll Update或者是 Sliding Window。首先每層 Clip 各自與相機對齊中心,這樣當相機平移後就讓 clip 的所覆蓋範圍也會自動跟隨著去更新。由於覆蓋範圍有所更變,我們需要把更新前的 F_i, 按世界座標拷到新的格子裡,相機位置會按格子大小進行 snapping, 避免因為複製F_i時候造成跳變。為了避免太頻密的更新,我們會等相機超過多個格子才觸發更新。在移動方向出現 Cache miss 區域, 我們可以從面積較大的一級Clip 去讀取 F_i,提高穩定性。
跟一般的物理模擬一樣,當水體流動速度太快,就很容易超出了 CFL 條件就會導致數值溢位的情況 (下圖爆炸的水體)。這個在模擬精度提高的時候特別容易出現,因為模擬公式中的delta x相對地降低了, 速度就很容易超出Cmax的限制。所以只能透過 sub stepping 來降低 delta t 來解決,簡單說就是在一幀裡面多跑好幾次的模擬。但這樣對效能不太友好,因為計算量直接翻了好幾倍。我們觀察到,在多層級的方案中每層 Clip 的 delta x 都會增加了一倍,這就意味著Sub-step 次數可以減半。透過這樣的最佳化,跟原來不分層級來比較,可以降低大概 ~90% 左右的計算量。
最後,我們看看多層級模擬的效果比較:左圖只有一級,而右圖是分了兩級來進行模擬,可以看到效果都是很接近的。而從我們 tech demo 的資料裡面去看到, 多層級模擬不論在記憶體和效能上都有明顯提升。
2.2 模擬:作用器系統
接下來交給有為,介紹水體模擬的作用器系統和渲染部分。接下來會向大家介紹我們開發作用器系統的目的和挑戰,以及如何解耦模擬,以及如何自定義自己的作用器材質,最後我們會展示一些Demo實際案例。
其實不論是LBM模擬演算法還是Pipe模擬演算法,本質上都是在計算水體高度場和速度場,假如沒有其他的因素干擾,模擬結果最終會趨向平穩。
但是,如果在計算的過程中加入一些外部影響的話,就可以得到一些互動式的結果,比如角色在河裡游泳產生的水波,以及下雨產生的漣漪,海風吹起的海浪等等,這些都是互動模擬,那我們如何實現這些互動模擬呢?答案就是作用器。
我們曾經想過讓作用器直接參與模擬ComputeShader的計算,但是這樣會導致一個問題:作用器種類是非常多的,每個作用器都有自己的演算法實現,這導致很難直接在模擬階段透過一個統一的函式入口去直接影響模擬結果。這會使得模擬的shader變得及其複雜,也不方便以後的管理和維護。
於是我們使用了一個解耦手段,就是將所有作用器的結果都輸出到作用圖中,對於所有的作用器,即便他們的演算法實現不同,但他們都輸出統一的結果,就是剛體入水深度、速度和體積,然後我們會把這些結果分別儲存在作用圖的RGBA通道里面。然後我們就可以在LBM模擬或者是Pipe模擬的時候去取樣這個RT作用圖,分別將RGBA通道資料取出來去影響各個模擬引數,完成互動模擬過程。
這裡分享一個比較重要的內容,就是我們的作用器系統框架,該系統首先會每幀收集當前區域內有效的作用器,然後同時更新這些作用器Gameplay。然後收集它們產生的Instance資訊,包括位置、大小、自定義資料等。同時會收集所有作用器可能訪問的uniform Buffer。包括上一幀的模擬高度場、速度場、場景高度,以及自定義貼圖資料等。然後透過Draw Instance方式,將每個作用器以相同材質進行合批渲染到四邊形的方式繪製到RT上,最終每個作用器統一的輸出體積,XY方向速度和剛體資料四個結果,分別儲存到貼圖的RGBA通道。那麼對不同的模擬演算法可以訪問這個RT,對模擬結果加上外部影響。因為RT上輸出的是統一的物理結果,所以,不論哪種模擬演算法都可以使用,而無須關心作用器的類別,達到解耦的目的。
那如何讓使用者快速開發屬於他自己的作用器呢?最初的設想是,讓TA或者程式能像開發材質一樣,在材質編輯器中透過連線的方式就可以實現自己的作用器演算法,我們稱之為作用器材質。如圖所示,目前開放專案中已經透過自定義的方式實現了一部分常用作用器材質。
同時為了豐富高度自定義內容,我們也封裝了很多材質函式,以供開發者使用,比如訪問每個作用器自定義的資料,uniform buffer資料,高度場,速度場貼圖資料等。都能在材質編輯器中拖入使用。可以看到下面的截圖,它是一個水源作用器的實現過程。
大量不同作用器材質的使用會導致drawcall數量增加的問題,那麼我們如何解決這個問題呢?剛剛的分享當中我已經提到了,我們這裡也會對相同材質的作用器進行合批處理。我們可以看到截圖裡面,在開放專案中,我們大量使用了作用器去模擬水源,波浪等,但最終的drawcall呼叫次數只有3次。
這裡我們擷取了開放專案中部分的作用器效果示例。可以看到藉助作用器框架,可以方便的自定義實現多種Affector Material與水體進行互動的Gameplay玩法。
3.1 渲染:基於GPU Driven的水體渲染
這個主題我們會向大家分享以往的傳統做法,然後我們如何創造性地應用CDLOD來實現GPU驅動的水體網格渲染,包括如何在GPU上構建四叉樹,如何實現遮擋剔除、超高網格密度、頂點變形等。傳統的水體網格渲染,我們都知道我們一般會使用一個平面網格,加一個高度圖來還原水面的波浪運動。在移動端甚至不需要真實的波浪,而是僅僅透過法線和UV動畫來模擬波浪。
實現演算法就是我們去還原波浪的高度以及還原頂點的位置,很簡單,我們只需要在VertexShader中取樣高度圖,來還原頂點的Z座標值。並透過取樣鄰近高度場資料來重新計算每個頂點的切線空間,最後把頂點的切線空間插值到光柵化階段。
在實現GPU驅動之前,我們也調研了UE4.26 的水體外掛,其中截幀分析了海洋的例子,可以看到,Unreal為了表現不同的海洋網格密度,它使用了6種不同的網格拓撲結構。雖然用了同一個水體材質,並且instance方式繪製,但也產生了6DrawCall。而且,UE4.26的水體分為海洋,河流和湖泊不同的水體Actor,這也會導致DrawCall數量的增加。另外UE4.26的水體是以CPU的方式驅動,而我們的目的是做GPU Driven。
最終我們在方案的選擇上,我們採用了CDLOD的技術,CDLOD全稱是基於連續距離的細節等級。它本來是用來解決大世界地形渲染最佳化問題的,我們利用這次機會,將它很好的實踐到了大型水體渲染當中。CDLOD它有很多優點,它具有變化平滑,不會產生裂縫,Lod之間等級差不超過1的特點,並且非常適合做基於四叉樹節點的剔除。
在GPU上面去構建四叉樹的演算法原理其實就是一個自頂而下的過程,從根節點開始,每個根節點是ComputeShader中的一個執行緒。如果當前節點在遞迴的這一級,又在下一級,它就確定了這個Quad。如果即在當前遞迴的這一級,又在下一級,那麼意味著子節點需要繼續遞迴到更低階別去確定,如此迴圈往復最終遞迴到0級。在GPU上構建去四叉樹會遇到比較多的問題,我們遇到的第一個問題,首先在ComputeShader上面是不能使用遞迴函式的,它有遞迴函式使用的限制。
然後,我們如果想跳過這個限制,比如在shader程式碼中寫大量的巢狀迴圈來解決遞迴函式問題,又會使得shader變得超級複雜,也會將暫存器耗盡。而且,一般的構建過程當中,遞迴複雜度會隨著每一級遞減,也就是說,根節點那一級他是最複雜的,也是最耗時的,0級是最不耗時的,如果我們把所有的級別都用統一的shader程式碼,就不能很好的平均每個執行緒執行時間。
為了解決這個問題,我們採用了分批次執行ComputeShader,每一級構建都會有自己對應的ComputeShader變種。每次使用上一次構建未確定的節點的輸出作為這次的輸入,這樣使得每一次構建都只關心它當前這一級,從而大大降低了shader複雜度,平均了執行緒間的執行時間。
在構建的過程當中,我們也會先判斷四叉樹節點是不是應該被剔除,以減少Instance數量。透過取樣水面高度的模擬結果,我們可以構建一個節點內的Quad內的Min/Max Height資訊,從而構建一個WorldSpace下面的立方體。我們的剔除分為兩種,一種是視錐剔除,另外一種是HZB剔除。視錐剔除會判斷立方體的8頂點是否在裁剪空間內,而HZB則根據立方體的螢幕空間尺寸選擇合適的Z-buffer mipmap做深度判斷進行剔除。
對於構建好的節點,如果直接渲染的話,其密度是很低的,那麼我們如何去構建一個高密度的網格呢?如下圖所示,顏色相同的區域具有相同的網格密度。這裡我們使用了2種網格密度,預先生成了2種面片mesh,第二面片的頂點數是第一個的1/4。
以全尺寸32和半尺寸16為例,我們去拿32和16去填充非邊界和邊界的四叉樹節點,透過這種方法,我們僅使用2種拓撲結構就可以表達多種網格密度,DrawCall數量最大為2。
最終渲染的時候,最關鍵的就是要確定哪些頂點是需要變形,我們會為每一級設定一個當前這一級對應的節點尺寸大小的變形區域,處於區域內的頂點將進行變形。因為之前按照2的冪次方規則,使用了2種不同的面片進行網格密度填充,所以這些面片的頂點位置是奇數的是需要變形的。
另外我們也總結了構建解析度和該解析度下對應下的構建消耗時間對照表,無論場景多大,我們的構建四叉樹花銷的消耗時間只與構建解析度有關,這樣可以穩定效能上限。構建解析度和網格密度可以隨意搭配,根據實際專案需要進行配置,達到效能和效果的平衡。在目前的開源專案中,以512的構建解析度+32的網格密度作為預設配置,如果想要效能更好,可以選擇256 +16甚至更低。但是這樣會導致網格精度會下降,可能會出現鋸齒、走樣。
最後我們來看下實際Demo中的效果,角色近處始終維持比較高的網格密度,而遠處比較稀疏的網格會隨著角色攝像機移動而進行平滑過渡。
3.2 渲染:基於Height Blend的動態地表淺水
接下來介紹如何改進UE4原生高度混合演算法,並在此基礎上實現靜態淺水,然後結合模擬結果進一步實現了動態淺水。最後我們將展示在遊戲Gameplay過程中的實際效果。
開放世界除了海洋,湖泊,河流這些常規水體。淺水也是一個很重要的能體現環境細節表達,特別是池塘窪地,雨後積水的路面。UE4的地形渲染,我們知道都是多層Layer進行Blend的結果。其中的基於高度的混合,可以讓地表表現出縫隙或者低矮處混合。於是,我們設想在Landscape中透過插入一個淺水層,與其他層去做Height Blend,以表達淺水效果。
先來分析下UE原生的heightblend演算法,這個演算法很簡單,它將每層的權重乘以高度值,然後累加,再歸一化百分比,最後混合每個層的結果。這個演算法很簡單,但是它導致不同layer在整個權重區間都在Blend,效果顯得很髒。
Layer weight 和 height map value 數值空間在[0,1]範圍,這個範圍很小,不是真實物理單位,它會丟失計算精度。並且在接縫處無法實現均值過渡,以及無法控制過渡閾值。
下圖右側是這個演算法的原生效果。改進的做法,第一步把高度值對映到真實物理空間,然後乘以對應的權重,接下來我們會篩選一個權重最大的層,也就是最高的層。然後將每層權重與最高層的差值除以一個過渡閾值,這樣就得到一個平滑過渡的權重,最後將這個平滑過渡的權重去做歸一化百分比,最後混合每層得結果。這個演算法的優點在於,首先將在[0,1]的height map value 對映到實際物理單位下,擴大了計算精度。在接縫的地方會是一個趨向於平均實現了均值過渡,以及可以控制過渡閾值。我們可以看到右邊的截圖是改進後的效果,可以看到基本達到了做淺水的要求。
另外很重要的是,我們不希望淺水的HeightBlend過程被固定死在C++程式碼中,而是開放讓美術或者TA能在材質編輯器中透過連線的方式去進行編輯。UE預設的地表材質節點,Layer Blend是無法解耦淺水和其他層的混合操作的。所以,我們開發了自己的材質節點叫做Height Blend。可以看到截圖裡面,我們自己開發的這個節點除了輸出其他層的混合結果以外,會將淺水層的權重資訊單獨輸出,這樣我們便能在材質中控制最終淺水的混合效果。
我們把最終的淺水混合操作都封裝在了一個材質函式中,之前我們有提到,我們開發了一個自己的地表材質節點,它會將淺水層的權重單獨輸出,我們拿到這個權重後,就可以分別去混合淺水的顏色,法線和PBR資訊了。法線和PBR的混合比較簡單 就用了比較常規線性插值,而顏色方面我們則考慮了乾燥地表向潮溼區域過渡的效果,以及潮溼區域向淺水過渡的效果。
剛才講的其實都是靜態淺水,進一步增加動態淺水,我們又加入了LBM的模擬結果,下圖左側是加入了LBM結果後的效果。我們把LBM的模擬結果去影響高度混合的高度值,可以看到它與周圍環境產生了互動效果,並且真實反應了HeightBlend。然後,我們也測試了Pipe模擬和Rain Affector一起作用的效果,可以看到除了表面漣漪以外,還可以看到下圖右側,積水體積是不一樣的,從而真實反應了模擬對積水體積的影響。
最後,是一些Gameplay中的有趣應用。可以看到主角在遊戲世界中可以任意釋放技能,這個技能可以在地表上殘留一層淺水。淺水和地表形成高度混合的侵蝕效果,並且淺水會隨時間慢慢吸收掉。接下來由家銘繼續分享。
3.3 模擬結果應用
剛才瞭解到水體基礎繪製的原理,現在會進一步講解模擬結果是如何應用到更多水體的細節效果裡。首先是水體表面的細節法線,有了細節法線,水流動的感覺更加強烈了;而這個效果也不難去實現的,我們只需要利用水體的速度場當作是 flowmap 去取樣Detail Normal的就可以了。由於模擬出來的速度是在世界空間裡面定義的,我們需要進行一個簡單的 縮放 和 Clamp的操作。另外我們也需要考慮到速度為零的時侯把細節法線 Flatten。
還有就是泡沫效果, 跟其他方案透過分析水體高度圖的 Jacobian 不一樣。我們發現水體出現碰撞的地方通常都會有比較高的旋度,由於我們是二維的速度場,所以算出來的旋度是一個純量。因此我們可以把旋度視為左邊的公式的一個2維 旋度的計算方法, 在裡面f 就是速度是x座標 ; 而g就是速度的y 座標; 而k就是一個c軸,可以忽略。我們再一次把速度場當成是 flow map 去取樣泡沫的貼圖,再乘以遮罩之後就能得到下圖效果。
除了水體表面的效果外,水體周邊的物體也會因為沾到水而改變材質顏色。為了計算沾水的程度,我們會利用一個 double buffering 的技巧。首先我們把當前幀 Buffer1 的水體高度拷到 Buffer2 裡面, 然後讀取上一幀Buffer2時候儲存的高度,做一個縮減,再跟新的水體高度取 Max 再放回Buffer 1裡面 所記錄的水體高度就可以算出一個沾水權重。在渲染場景物體的時侯,我們會利用物體的世界座標的C去減去Buffer 1 所記錄的水體高度就能算出一個沾水權重。物體計算光照的時候PBR 引數,就利用這個權重從乾和溼兩組 Preset裡面去插值出來。
還有是一個GPU 粒子與水體互動的例子。在影片裡面演示的落葉效果不僅可以漂浮在水上,也可以隨著波浪而旋轉。這個效果的 粒子系統裡面,每個粒子可以分為:掉落、漂浮還有消失三個狀態。掉落時粒子會以 場景高度 和 水體高度 判斷它是否已經掉落到水裡面,如果是就切換到漂浮狀態。漂浮時就簡單的以水體高度作為它在世界空間裡面的高度;還加上之前提到的旋度來控制葉子旋轉。假如粒子是掉落到地上它就會變成一個消失,就是簡單的把它設成一個透明,而後在等好幾幀讓粒子系統讓它回收到池子裡面就可以了。
再分享兩個與水體高度有關的後處理效果;首先是 WaterLine ,這個效果會出現在水面跟水底交接的邊緣部分。而它的思路就是在螢幕空間去找出接近水面的畫素,再插值成WaterLine的顏色。利用投影距陣,我們可以計算 Near Plane 上每畫素的世界座標, 再用這座標去取樣水體高度, 這樣我們就可以算出 Near Plane 對應畫素與水面的距離,再把這個距離透過 falloff的函式 算出一個插值的權重就能得出左邊截圖的WaterLine 效果。
延伸剛才 Water Line 的思路,我們可以算出在視線上不同距離的世界座標, WP利用WP我們就可以透過一些噪聲 或者是水體壓場模擬出該點的一些Scattering的亮度。再以WP取樣水體的高度,我們就可以知道這一點在水下深度,再放到一個 falloff的函式里面就可以算出因為這個水深而做成的散射的縮減。這樣,我們在視線上 Ray March 4 個點,同時算出它散射的一個積分,就能得到影片裡的 Light Shafts 效果。
最後要分享的是跟玩法裡面有緊密關係的浮力效果。有別於之前介紹的,浮力是在 CPU 上計算再給到物理引擎的剛體上。根據公式,浮力跟物體浸在水中的體積有關,所以我們需要在 CPU 訪問物體位置對應的水深來算出它浸入體積。我們需要一個有效的機制從 GPU 裡讀取模擬結果,因此 Aqua 實現了一個延遲一幀的 GPU 讀取功能。大概的流程是我們會在每幀都先記錄所需要查詢的位置,之後合批發給一個 CS 從Texture Atlas 中讀取模擬結果並儲存到一張解析度很低的 Staging RT 裡。等 CS Dispatch 完成之後我們才進行 read back, 最後在透過下一幀 C++ 回撥來通知查詢結果。這樣就是可以儘量降低GPU 讀取所帶來的頻寬消耗和延遲。有了水深,我就可以計算浮力。在 tech demo 裡的浮木就是利用這個 GPU 讀取框架來計算木頭兩端的浮力,再以AddImpluseAtLocation 應用到木頭的剛體上。此外,當木頭每一端檢查到它是浸在水裡,我們會用讀取到的水體速度在這一端在給一道力;這樣就能做出剛體隨水流漂浮的一個效果了。
今天我們為大家分享了 Aqua 團隊所開發的開放世界水體方案,裡面包括了多層級模擬系統、作用器系統、GPU Driven 的 CDLOD 水體渲染;還有淺水效果渲染,以及各樣的模擬結果應用。在經歷了一年不長也不短的開發,Aqua 支援了不少功能和特性,但當中也有不完美或者可以續繼最佳化的地方。比如說:我們目前還沒有做音效的支援;又或者是方案還沒可以支援像地穴這種複雜地形的水體模擬。希望後面我們有機會再繼續最佳化和打磨 Aqua, 讓它變得更盡善盡美。
Q & A
問:水體模擬的網格精度如何控制?
陳家銘:這個問題很好,在我們開發tech demo 的過程裡面發現,其實以25釐米來代表一個格子的精度來模擬,不論在效能或者效果上都能達到一個比較好的平衡。由於在我們的demo 裡面,我們是分了三層 clips 來進行模擬;所以對應的精度就大概是25cm,50cm 以及是1m左右。當然,我們也可以按著遊戲的一個需求來提升模擬精度,但是當精度每提升一倍,就代表著substep 次數要增加一次,主要是為避免超出 CFL 條件導致了數值爆炸的情況出現。
問:水體渲染的網格是固定大小還是自適應?
胡有為:剛才我的分享當中也提到了,就是說我們的網格大小,其實是由兩個決定的,第一是構建解析度,第二是網格密度的設定,我們現在是以512的構建解析度+32的設定去決定生成網格大小的,當然您也可以選擇其他的,比如說256+16。目前這個配置是在執行前在專案中設定好的,我們並沒有實現說在執行時runtime的時候去改變這個,當然我們理論上也是可以做到的。
問:水體光照渲染是如何避免走樣的呢?GPU Driven如何適配到移動端?
胡有為:這個問題我們之前也遇到了,就是在水面很遠的地方,我們確實也出現了一些鋸齒、就是走樣。那我們如何解決這個問題的呢?我們透過將逐頂點的切線空間的還原變成逐畫素的,就是在畫素著色器裡面去還原切線空間,同時我們還原切線空間的時候,其實可以不用取樣鄰近畫素的高度場,而是跨多個鄰近畫素,比如說兩個、三個,可以讓它的切線空間再次平滑。
目前在移動端去實現GPU Driven還是有點困難,因為我們目前大家都知道移動端的硬體架構,對於ComputeShader來說目前停留在API的支援階段,移動端的架構並沒有做出升級或者改變,所以說在移動端去適配GPU Driven還是比較困難,如果要在移動端去使用CDLOD,可能還是要去回滾到CPU驅動的方式,我們把在GPU上面去構建的演算法,把它移植到CPU端,然後像變換大小這些資訊可以用Vertex Streams頂點流的方式去做到instance渲染。自定義的資料就只能透過烘焙到貼圖或者更新到貼圖的方式,因為移動端目前是對struct buff的支援是不太好的。
來源:騰訊遊戲學堂
相關文章
- 騰訊魔方《洛克王國》開放世界技術詳解
- 騰訊和祖龍最有野心的開放世界MMO浮出了水面
- 模擬 easywechat 開發騰訊的 ocr 包
- 騰訊天美3A專案熱招 打造開放世界、射擊專案
- 騰訊“充值”庫洛遊戲,衝著開放世界專案《鳴潮》?遊戲
- 騰訊遊戲學院專家:PBR渲染模型的理論及具體應用遊戲模型
- 西山居和騰訊聯手打造的「開放世界」,去哪兒了?
- ros(2) 模擬slam定位和高斯渲染通訊ROSSLAM
- 騰訊光子《黎明覺醒》技術美術負責人:如何製作超真實的開放世界?
- <開放世界>專題整理
- 開放性和故事性可以並存嗎? 騰訊專家團隊分享遊戲敘事技巧遊戲
- 騰訊和西山居聯手研發開放世界,但辦公室已搬空
- 【福利】騰訊WeTest專有云解決方案,限時開放招募體驗官
- 獨家專訪丨騰訊、 Roblox中國、以及MetaverseMetaverse
- 雙開《GTA》和《模擬市長》:騰訊玩自動駕駛的清奇腦洞自動駕駛
- 首期Techo Day騰訊技術開放日,628等你!
- 叮!Techo Day 騰訊技術開放日如約而至!
- 《魔獸世界》作為開放世界類遊戲應該能自由飛行嗎?遊戲
- 過程化技術:打造「開放世界」的秘密
- Insomniac分享:開放世界VR遊戲《Stormland》背後的藝術和洞察VR遊戲ORM
- javascript模擬鳥群使用cax和threejs渲染引擎JavaScriptJS
- T4 級老專家:AIOps 在騰訊的探索和實踐AI
- 省頻寬、耗電小,騰訊遊戲學院專家解析手遊渲染架構遊戲架構
- 無盡的探索:遊戲中的開放世界遊戲
- 專訪深職院XR專家 | 實時雲渲染賦能虛擬模擬實訓,打造5G+XR智慧教育平臺
- CORNERSTONE對話騰訊&華為敏捷專家敏捷
- 傳騰訊天美將打造虛幻5跨平臺3A開放世界大作
- 騰訊遊戲學院專家:UE高階效能剖析技術之RHI遊戲
- 超自然開放世界設計:怪物、場景和遊戲體驗遊戲
- 【招聘資訊】騰訊雲資料庫高階專家資料庫
- 魂系列、惡魔城和開放世界——關卡結構理論和非線性流程設計方法淺析
- 騰訊王巨巨集:雲與開源共生共榮|共建開放協作的技術標準
- 《原神》開放世界的思考
- L2-021 點贊狂魔【模擬】
- 騰訊遊戲學院專家:手遊開發,該如何做好Android記憶體優化?遊戲Android記憶體優化
- 媒體專訪 | 開源開放,OceanBase 的“成人禮”
- 央視攜騰訊打造首個數實融合虛擬音樂世界節目體驗
- 軟體調優方法有哪些?看看飛騰技術專家怎麼說 | 龍蜥技術