邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

Byreave發表於2023-08-17
篇一:基於淺水方程的水面互動

本文主要介紹一種基於淺水方程的水體互動演算法,在基本保持水體互動效果的前提下,實現了一種極簡的水面模擬和物體互動方法。

真實感的水體渲染在現今的遊戲中越來越被需要,除了光照和波形渲染之外,水體互動也是描述水體功能的重要組成部分。作為一個遊戲玩家,同時也作為一個遊戲開發者,每到一款遊戲中探索時,如果走到水中有較真實的互動效果,總會有種驚喜的感覺,並且也能提高遊戲樂趣。玩水誰不愛呢。

水體互動其實也可以分成很多個部分,本文主要討論的是水面互動,這種互動在遊戲中比較常見,也可以說是整個水體渲染中比較低垂的果實。

一些水體互動的例子

下面列舉了一些近年比較成功的真實感渲染遊戲的水體互動例子。其中大部分都是粒子、紋理和物理結合的方式。

• 老頭環 Elden Ring

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

• 刺客信條 英靈殿

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

• The Last of us Part I Remake

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

• The Last of us Part II

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

• 荒野大鏢客2

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

• Hogwarts: Legacy

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

也有一些看起來是沒有物理,僅憑藉紋理+粒子實現的互動效果,多用於彈道射擊水面等場景。在更久遠年代的遊戲中使用廣泛。列舉了一些近期的例子:

• Far Cry 6

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

• 荒野大鏢客2

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

• 外賣模擬器 Death Stranding

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

可以發現,大部分水面互動都是大同小異,因為遊戲對渲染實時性的要求,很多物理模擬步驟會被簡化很多,並且會有粒子效果等的幫助,來實現水面互動的真實性。這些互動場景有一些共同的特點,

• 只和水體表面的互動,深度不重要

• 可互動區域沒有明顯高度差(水平)

• 互動範圍有限,大部分圍繞角色周圍

這些特點基本忽略了三維的模擬,非常適合在遊戲中出現較多的,把水體作為單層網格渲染的情況。

那麼,有沒有一種能夠以很小渲染消耗來實現的物理模擬方法呢?本文接下來就會介紹一種基於淺水方程(Shallow Water/Wave Equation)的極簡模擬方法,並且整合在了最新的UE5.1版本中。

基於淺水方程的水面模擬方法

話不多說,先看在UE5.1中,使用基本素材做出的互動效果:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

水體的表達

對於被模擬的水面來說,遊戲中一般可互動的水面都是由一層單面mesh渲染,平面比較多,有坡度也不會太大(比如瀑布就不行)。這種情況我們可以用一張高度圖來表現模擬區域,其中水面高度細節可以透過高度圖的畫素值表達。這給我們在GPU上模擬水面物理帶來了便利,可以用貼圖(Texture)輕鬆的求解水面高度。

淺水方程

Shallow Water Equation,也可以叫Shallow Wave Equation,中文淺水方程,是一種流體力學模型,常用於模擬海洋與大氣層的流動,適用於分析水平方向的尺度遠大於垂直方向的尺度的流體自由液麵的流動。從之前的使用場景來看,大部分幾乎忽略了“垂直方向的尺度”,經過簡化後,非常適合在遊戲中實時使用。數學複雜的推導可以參見Wikipedia頁面(https://en.wikipedia.org/wiki/Shallow_water_equations),簡化過程可以在Games103課程(https://www.bilibili.com/video/BV12Q4y1S73g/?p=10)中學到。我們先忽略數學上的複雜性,直接擺出寫方程迭代程式碼需要的簡化結果:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

β:ViscosityConstant, α: SWE constant, hN(t-1): Height sum of the nearest four points

可以看到,在簡化過的模擬方程中,當前幀水面高度h(t)由前兩幀的高度h(t-1)和h(t-2)得到。總體程式碼實現並不複雜,由於我們的簡化加入了額外的Damping引數加快迭代,其它引數設定推薦β設定為1,α設定為0-0.5的值,Damping設定為0-1的值,以免出現模擬爆炸。

邊界條件

水體模擬的區域往往是有邊界的,無論是在小水窪的邊界,還是無邊界海洋中的石頭,都可以作為模擬的邊界資訊。對於邊界的處理往往有兩種方式,一種是不處理,水波會根據阻力慢慢消失,這樣以真實性為代價換來效能的提升,在開放的水域比較適用。第二種是把模擬的水波反彈回來,在游泳池之類的明顯邊界的水域比較適用。比如:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

模擬方程中對於第二種邊界處理方法的描述也相對簡單,叫Neumann boundary condition(https://en.wikipedia.org/wiki/Neumann_boundary_condition),簡單來說就是描述邊界上的導數,導數為0則在邊界上不會有變化。我們可以看到上述方程中有hN(t-1)項,既當前模擬點,假設為X,周圍4點的水面高度和。想要做到反彈水面,我們在查詢周圍4點水面高度時,如果4點中某一點Y在邊界上,把Y的高度設定為和X點高度相同即可。物理意義上,就是指阻止邊界點的水面高度交換,這樣在模擬過程中,水面波形就可以產生反彈的效果。可以看看下面的虛擬碼。

  1. <p>// Suppose Y is the point up to X. YIndex = XIndex + (0, 1);</p><p>
  2. </p><p>float XHeight = WaterPreHeight.Load(XIndex).x;</p><p>
  3. </p><p>float YHeight = WaterPreHeight.Load(YIndex).x;</p><p>
  4. </p><p>if (IsBoundary(YHeight))</p><p>
  5. </p><p>{</p><p>
  6. </p><p>YHeight = XHeight;</p><p>
  7. </p><p>}</p>
複製程式碼

模擬步驟

本文介紹的模擬方法大致分為如下幾步:

1. 收集和水面模擬區域產生互動的物體,並且渲染物體深度資訊到貼圖。

2. 疊加物體資訊到水面高度圖。

3. 模擬方程迭代。

4. 應用模擬結果的高度圖來渲染水面。

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

收集物體資訊

想要知道有哪些物體和水面互動有很多種辦法。我們的方法可以只作為參考。從效率上考慮,可以為模擬區域設定一個碰撞體,透過碰撞系統從GameThread得到所有的相交物體,然後提交到渲染執行緒,獲得物體的資訊,交給模擬迭代。上面提到,我們可以透過貼圖表達水面高度,我們也可以透過頂檢視的深度資訊來表達物體和水面互動的資訊。現在比較棘手的問題是,如何高效的渲染物體的深度。

在UE引擎中,我們可以透過SceneCaptureComponent來獲得水面物體深度,它支援只渲染場景中部分物體。但是在看過程式碼之後發現,SceneCapture需要走完整個渲染流程,哪怕我們只需要一個深度資訊,也需要付出渲染整個場景的代價。SceneCapture也無法定製渲染所用的Shader,當然可以用自定義後處理材質,但是這樣又會帶來新的效能消耗,所以可以想象我們需要一個更加簡單、高效的流程來完成物體資訊的收集。

在我們的實現中,我們實現了一個自定義的深度渲染pass,用來渲染指定物體的深度資訊。其原理和陰影貼圖渲染(ShadowDepth)類似,根據UE官方文件(https://docs.unrealengine.com/5.1/en-US/mesh-drawing-pipeline-in-unreal-engine/),

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

透過新增自定義的FParallelMeshDrawCommandPass來把收集到的物體渲染到自定義的DepthBuffer中。這種辦法的好處是可以自定義MeshProcessor,支援修改CullingMode和使用自定義的PS,這在後續獲得精確深度的步驟中有很重要的作用。

在切換到自定義的Pass實現後,獲取物體資訊這一步的耗時也明顯降低。從之前SceneRenderer使用的一整套渲染流程,變成了一些簡單的深度DrawCall的代價。

在獲取物體資訊上,除了運動的物體資訊,比如玩家角色、移動物體、子彈等,還需要繪製作為邊界物體的資訊。邊界物體資訊我們可以做進一步最佳化,比如每幀只渲染正在運動的物體,靜止的物體根據設定,判斷是否要渲染成邊界,而邊界資訊不需要每一幀更新,只有在邊界變化的時候按需求更新。邊界的資訊可以儲存成一個頂檢視深度圖,在後續步驟中重複使用。

對於深度圖,因為根據演算法原因,我們對於物體浸入了水面多少其實不敏感(垂直方向尺度非常小,所以叫做“淺”水方程),這樣就還可以進一步最佳化。新增一個ResolvePass,只使用R8格式儲存深度貼圖。這樣有利於後續步驟中貼圖讀取的開銷。

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

在UE Insight中可以看到,同時渲染9個移動的物體和角色,加上ResolvePass的開銷只需要0.08ms(雖然是在2080S上)。

疊加物體資訊到水面高度圖

為了使耗時較高的迭代Pass讀取貼圖次數減少,我們需要預處理我們的物體資訊。其中,我們在獲取物體資訊階段獲得了2個R8的貼圖(運動物體和邊界物體資訊)。我們需要將運動物體資訊+邊界資訊整合進水面高度圖。

對於運動物體資訊,我們需要將有物體浸入的水面排出一些水,模擬水面互動的過程。對於水面高度減少的多少,我們做了進一步簡化,只需要根據深度的減少就好(物理正確的結果需要迭代求解減少的高度,原理解釋參見Games103課程(https://www.bilibili.com/video/BV12Q4y1S73g/?p=10))。對於水面高度已經在物體深度之下的水面,我們不做處理。

對於邊界物體資訊,我們把水面高度設定成一個特殊值,比如float_max即可。虛擬碼如下:

  1. <p>if(bIsBoundary)</p><p>
  2. </p><p>{</p><p>
  3. </p><p>WaterHeight[SimulationIndex] = BOUNDARY_HEIGHT;</p><p>
  4. </p><p>}</p><p>
  5. </p><p>else</p><p>
  6. </p><p>{</p><p>
  7. </p><p>CapturedDepthVal = saturate(CapturedDepthVal);</p><p>
  8. </p><p>float CurrentWaterHeight = WaterHeight[SimulationIndex];</p><p>
  9. </p><p>// Object depth above water has no effect</p><p>
  10. </p><p>WaterHeight[SimulationIndex] = CapturedDepthVal == 0.0f ? CurrentWaterHeight : min(CurrentWaterHeight, - CapturedDepthVal);</p><p>
  11. </p><p>}</p>
複製程式碼

模擬方程迭代

有了上述物體資訊,我們需要完成方程的迭代。在傳統方法中,模擬方程並沒有最外層的Damping引數,需要在同一幀多次迭代,求解收斂值。這裡的多次迭代,包括了方程本身的迭代,和上文提到的對互動物體陷入水面後,水面高度應該變化多少的求解。方程本身的迭代我們可以透過多次執行CS求解,而水面高度變化因為不僅和物體陷入的深度相關,也和周圍水面的高度相關,所以需要用到共軛梯度法(PCG)求解。具體演算法可以參考Games103影片最後有關VirtualHeight的求解(https://www.bilibili.com/video/BV12Q4y1S73g?t=4855.3&p=10),和文末參考資訊中的原始論文。

在我們的實現中,簡化了多次迭代的過程(參考WaterLinePro外掛中的實現),每一幀只執行一次迭代,這樣在保證結果真實性的情況下有最好的效能。一次迭代的缺點就是波紋傳播的速度和遊戲的幀率相關,可以想象目前每幀一次迭代,根據方程波紋每幀只能影響周圍一個畫素,水面波紋資訊每幀只能傳遞一個畫素。幀率較高的應用或者對於傳播速度有調整,可以調整上述模擬方程中的α引數或者模擬範圍和模擬解析度的比例,來保證傳播速度符合理想。

對於Shader中的實現,我們有了2張前一幀和前兩幀的水面高度圖作為輸入,1張當前幀的水面高度圖作為輸出,就可以透過CS/全屏PS完成迭代。在實現過程中,可以透過ping-pong三張貼圖的方式,這樣不需要額外的複製操作就可以記錄水面高度歷史資訊。

模擬迭代比較直接,下面是虛擬碼:

  1. <p>// Boundary pixels.</p><p>
  2. </p><p>if(IsBoundary(PrevHeight))</p><p>
  3. </p><p>{</p><p>
  4. </p><p>// Skip iteration.</p><p>
  5. </p><p>return;</p><p>
  6. </p><p>}</p><p>
  7. </p><p>// SWE</p><p>
  8. </p><p>float NearHeight = UpPrevHeight + DownPrevHeight + LeftPrevHeight + RightPrevHeight;</p><p>
  9. </p><p>float OutHeight = Damping * (PrevHeight + (PrevHeight - PrevPrevHeight) + TravelSpeed * 0.5f * (NearHeight - PrevHeight * 4));</p><p>
  10. </p><p>// Store to output texture</p><p>
  11. </p><p>OutWaterHeight[CurrentIndex] = OutHeight;</p>
複製程式碼


應用模擬結果

完成每一幀的迭代後,我們就獲得了可以使用的水面高度貼圖。貼圖預覽結果如下:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法
左邊為移動物體資訊,右邊為迭代的水面高度

在我們的實現中,使用ENQUEUE_RENDER_COMMAND來完成對模擬步驟的提交,會在主渲染幀之前完成,這樣既不會破壞現有的渲染管線,也可以把當前幀的模擬結果直接使用到水體材質中,完成水體的Normal/Displacement計算等操作。

有了高度圖,我們可以在水體系統中得到Normal,並且應用到水體材質中。我們目前使用的自研PhotonWater系統透過提前的Normal/Displacement Pass來完成計算,結果很方便的就可以應用於水體。

值得注意的是,雖然在上述的遊戲示例中,大部分水面都有位移計算,但是部分情況我們只需要計算Norma即可(尤其是手機端)。不過有了水面Mesh的位移的確可以提高真實性。PhotonWater實現的基於CDLOD的水體Mesh Tessellation和Screen-Space Displacement Mapping可以比較高效地應用各種位移貼圖和波紋粒子效果。

獲取準確的物體互動資訊

至此,大部分水體模擬的實現細節就完成了。雖然還有很多可以改進的地方,我們還是可以先關注比較容易且效果好的改進。在上述示例的荒野大鏢客2的例子中,我們可以看到水面的互動只和馬的腿部產生。這種細節如果我們只用了正常渲染的頂檢視深度是不夠的,因為頂檢視會認為馬的身體遮擋了水面,從而把整個馬作為深度資訊傳入,就無法做到上述細節。

比較直觀的例子是一個倒立的圓錐體,如果圓錐體落入水面,我們只希望尖的地方和水體產生互動,而不做處理的方法會讓互動範圍變成圓錐的帽子,比如下圖:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

那麼怎麼做,才能讓圓錐的尖角和水面“碰撞”呢?

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

解決方法其實有很多,其中比較精確的辦法是透過自定義頂檢視的深度渲染,透過渲染互動物體Mesh的反面,然後對水面深度進行Clip,就可以獲得精確的反面深度資訊。實現層面就是在BuildMeshDrawCommand之前手動反轉CullMode。

  1. <p>ERasterizerCullMode MeshCullMode = ComputeMeshCullMode(MaterialResource, OverrideSettings);</p><p>
  2. </p><p>// Always render back faces</p><p>
  3. </p><p>MeshCullMode = MeshCullMode == CM_CCW ? CM_CW : CM_CCW;</p><p>
  4. </p><p>BuildMeshDrawCommmands(</p><p>
  5. </p><p>...,</p><p>
  6. </p><p>MeshCullMode,</p><p>
  7. </p><p>...</p><p>
  8. </p><p>);</p>
複製程式碼

在深度渲染的PS中(注意一般的深度渲染Pass不需要PS,這也是我們上文提到的自定義深度渲染的便捷點之一),我們傳入模擬位置的水面高度,不需要太精確,可以暫時假定模擬區域是一個平面即可。內容也可以非常簡單。

  1. <p>// CustomCapturePS.usf</p><p>
  2. </p><p>// Clip using water level, water level is already converted to device Z</p><p>
  3. </p><p>clip(WaterLevelVal - SvPosition.z);</p>
複製程式碼

另外一種解決辦法是使用一個替代(Proxy)Mesh渲染高度,而不是用真實物體渲染。部分遊戲,比如Hogwarts: Legacy游泳的場景,細節要求不需要太高,可以不渲染整個角色Mesh,而是渲染一個替代品。比如一個簡單的Sphere/Capsule代替人物,好處是顯著減少三角面,缺點是需要Artists手動放置到角色上,以防止穿幫。在馬匹的例子中,也可以用4個小球,Attach到馬腿上,並且忽略馬匹本身的Mesh,這樣也可以完成互動細節的渲染。

視訊記憶體和Shader消耗

本文介紹的方法使用了多個Buffer用於儲存模擬資料。其中Buffer的大小可以由使用者自定義,示例中使用的是1024x1024貼圖,用來渲染4096x4096世界大小範圍的水域。貼圖解析度可以根據效能需求調整,水域範圍也可以由實際應用決定。

• 3個水面高度圖 R16F * 3

• 深度資訊和邊界資訊 R8 * 2

• 渲染深度所用的DS * 1

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

模擬流程上用了這些Pass:

• 渲染運動物體(如果有)-CustomCapturePass

• 渲染邊界物體(如果有更新)

• Resolve深度資訊-PhotonWaterCopyCaptureDepth

• 疊加深度資訊到水體高度-ComputeWaves

• 迭代模擬方程-IterateSWE

在模擬1024X1024解析度的貼圖情況下,UnrealInsight顯示的消耗如下(2080S):

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

還有一些可以減少消耗的最佳化技巧,比如模擬區域沒有物體更新,可以過一段時間暫停更新,等有互動出現的時候再啟動,以免出現空轉的情況。

移動端也能用

在移動端,模擬過程類似,模擬效果來看可以達到和PC/Console差不多的效果。

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

關於移動端的耗時,筆者沒有統計各個Pass分開耗時,只能從開關模擬來看總體耗時,在三星S20手機上得到以下結果:

• 1024x1024的模擬解析度,模擬開銷大概3ms。

• 512x512的模擬解析度,模擬開銷大概在1ms以下。

在解析度降低的情況下,只要模擬區域的世界大小也等比例降低,得到的波紋細節是一致的,所以可以根據使用場景合理調節。在不需要準確波紋細節的情況下,256x256模擬解析度也是可以選擇的。

篇二:遊戲中的河道互動模擬方法

以上簡單介紹了淺水方程的簡化模型,以及應用它來進行水面互動。在湖泊、池塘、小水窪等水面區域,互動效果表現還算不錯。但是在遊戲中,河道的應用也非常多。那麼,在一個流動的河道中,我們怎麼模擬出水面互動效果呢?

下面主要介紹一種在河道模擬水面互動的方法,讓互動感更加真實。同樣是應用前文提到的淺水方程模擬框架,但是加入水域流速的概念,讓水花可以隨波逐流。

那麼,為什麼要在意這個細節呢?

首先是因為玩家的確會走到河裡面。如果我們還是使用不考慮水面流速的模擬方法,會得到這樣的效果:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

第一反應可能是還不錯,但是玩家一會兒就會反應過來,為什麼波紋不會被沖走,並且角色站在湍急的水流中,水流也應該要衝到角色身上。

從玩家角度出發,當然希望獲得更真實、沉浸的遊戲體驗。作為遊戲開發者,考慮到效能消耗,更希望以最小代價實現並提供更多具有真實感的遊戲體驗。畢竟我們都希望能對細節有更多的追求。

從抄作業的角度出發,筆者沒有發現很多可以參考的案例,歡迎大家可以在評論中補充。在大表哥2 (Red Dead Redemption 2)中,有一些流速對模擬有影響的體現:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

遊戲中的河道表現

一般來說,遊戲中的河道都是由正常水面+flowmap或者流向引數來完成“流動”的渲染。有單一流向的:

荒野大鏢客2:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

有需要細節流向的:

Cities Skylines:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

Uncharted:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

也有Unreal自帶的:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

和我們自研的Photon Water System,支援根據場景幾何資訊,自動生成流向圖(flowmap)用於渲染:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

不難發現,雖然渲染方法各有千秋,但是儲存流向資訊的方法基本都會用到流向圖(Flowmap)。比如在PhotonWater中,我們使用的Flowmap方向大致如下圖所示:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

如果是單一方向的河流,理論上也可以只用單一的方向向量。有了河流的方向,也為我們後續的模擬方法提供了基礎。

在淺水方程中加入流速影響

話不多說,先看看應用河流等流速影響之後的模擬效果。

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

也可以體現角色靜止和河道分流處流向不同的細節:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

擴充模擬流程

在前文提到的基於淺水方程的水面互動方法中,我們並沒有討論河流的模擬情況,並不是因為淺水方程不支援流速的求解,而是因為我們在上一篇文章中提到的方法進行了儘可能的簡化,導致忽略了速度場的求解,只單獨求解了壓力(高度)場。相反地,比如PhotonWater中的Flowmap求解正是基於淺水方程的,只不過是另外一個形態。

但是在實時的模擬方法中,我們希望儘可能減少效能消耗,所以我們最好避免使用“完整版”的淺水方程求解過程。在本文的實現中,我們使用了單獨的Flowmap Advection Pass來給模擬加入流速影響。模擬流程就變成了下圖所示。這樣的好處是Advection過程非常獨立,可以開關。缺點是這確實不是在“求解”速度,只是疊加一個外部的速度場,但是考慮到我們已經做了很多簡化,只要能達到可以接受的效果就行。

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

Advection

在物理模擬中經常用到的詞語,我的理解是把模擬的屬性(高度/速度/密度等)根據空間關係傳遞到下一幀。那麼在我們的模擬中,該怎麼根據Flowmap方向傳遞高度呢?雖然Advection在很多模擬過程中都有體現,我們試著從最基本出發理解一下。

最直觀的想法是,高度圖和流向圖在二維上是重疊的。所以我們可以把高度圖中每一個畫素中心的高度根據其對應的速度移動到目的點。如下圖所示(圖片參考自引用的影片 https://www.youtube.com/watch?v=qsYE1wMEMPA):

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

我們很快會發現一個顯而易見的問題,每個畫素移動後的位置都很可能不是畫素中心,這給我們基於畫素中心表示的高度圖疊加帶來了困難。我們當然可以讓移動後的位置根據雙線性插值來影響周圍4個點,但是疊加過程對於GPU的並行非常不友好。

另一種方法,也是模擬中經常被使用的方法,既以每個畫素中心為基點,根據當前速度,得到這一點之前所在的位置(回溯)。如下圖所示:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

我們根據左上角點速度,如藍色箭頭反向得到的位置,再取周圍四個點,由綠色箭頭的指向,就可以像Sample貼圖一樣,雙線性插值得到對應的高度。對高度圖中每個點做這個操作,就可以完成Advection,因為每個點的操作是獨立的,所以可以用GPU很好的並行。虛擬碼如下:

  1. int2 CurrentIndex = dispatchThreadId.xy;

  2. float2 FlowmapVector = GetFlowmapVec(CurrentIndex);

  3. // Get traced back indices by -FlowmapVector

  4. float2 TraceBackIndex = CurrentIndex - FlowmapVector * FlowmapStrength;

  5. // Handle boundary conditions inside the bilinear sample

  6. OutCurrentHeight[CurrentIndex] = BilinearSampleHeightsAtIndex(TraceBackIndex);

  7. // We need to update history height at the same time because the simulation depends on two frames of history data.

  8. OutHistoryHeight[CurrentIndex] = BilinearSampleHistoryHeightsAtIndex(TraceBackIndex);
複製程式碼

對流向場的要求

如果是固定方向的河流,對流向並沒有要求。但是如果使用了複雜流向的Flowmap,則對Flowmap的整體流向有一些要求。否則模擬過程很可能會出現不收斂而導致的爆炸。

我們的模擬區域一般是有限的,包括貼圖大小也是有限的,所以在有限的模擬區域中,我們要儘量保證區域中整體水量的不變。在上篇文章中,我們的方法有Volume Conservation的介紹,就不贅述。不過加上了Flowmap Advection之後,想要保證整體水量不變,我們需要整個流向場流入的水量和流出的水量是一致的。雖然我們上文的模擬方法加上了一個全域性的Damping,但是除非我們要犧牲模擬的真實感,加大Damping,否則隨著時間推移,我們的模擬遲早會因為Flowmap區域積累水量大於damping而爆炸。

河流渲染中,一些Flowmap可能是Artists手繪的,那我們該怎麼得到一個流入水量和流出水量相等的Flowmap呢?

這個看起來是個很高的要求,但是對於任意的速度場,我們都有兩個描述方法,散度(Divergence)和旋度(Curl)。二維中,可以如下圖表示:

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

不難發現,在流體速度場中,Curl表示速度的旋轉,Divergence表示水流流入和流出。對於一般的不可壓縮流體,我們希望任意的速度場的散度都是0,這代表了沒有任何水流入和流出,也就符合了我們模擬的需求。還好我們站在巨人的肩膀上,根據Helmholtz's theorem,任意的的速度場都可以表示成一個0散度和0旋度的場的和。

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

我們可以根據這個定律求出一個divergence-free的flowmap,這樣就可以滿足我們的需求了。對於求解過程,因為其實並不是本文的重點,計算也可以離線完成,直接使用計算結果用於渲染和模擬,可以參考這個影片中最後一節https://youtu.be/qsYE1wMEMPA?list=PLi3obdsHfCiIjjfoF43df61WR1aXNhNID&t=699的方法,或者在維基百科https://en.wikipedia.org/wiki/Helmholtz_decomposition瞭解推導過程。

在PhotonWater中,生成的Flowmap已經完成了這一步,所以不需要額外處理。

邊界條件

當根據速度回溯取樣畫素時,畫素位置可能超過邊界。邊界處理可以參考上文的兩種方式,反彈或者消散。對於無限邊界的模擬,我們需要像取樣貼圖一樣,Wrap得到高度值,這樣才能避免在貼圖的邊界時,高度資訊丟失。

效能參考

作為一個獨立的Pass,Flowmap Advection的效能消耗可以非常低。並且在沒有流速的湖泊和池塘等,我們可以方便的關閉這個Pass,節省效能開銷。

如我們第一篇文章介紹的一樣,在模擬解析度範圍內,有一個Flowmap的TextureSample,對當前高度和歷史高度進行雙線性差值高度圖取樣。對於固定流向的河流,我們甚至可以省去Flowmap,只用傳入一個速度作為引數就可以進一步壓縮效能消耗。在2080S上,該Pass GPU耗時大概為0.1ms,但是理論上根據實際應用場景(主要是如何獲得水流速度),還有進一步最佳化空間。

邊做遊戲邊划水: 基於淺水方程的水面互動、河道互動模擬方法

>>參考
1. Games103(https://www.bilibili.com/video/BV12Q4y1S73g/?p=10) -關於Surface Waves(SWE)詳細的簡化過程。
2. Waterline Pro Plugin(https://www.unrealengine.com/marketplace/en-US/product/waterline) -外掛中是純藍圖實現,並且使用SceneCapture,不過參考了一幀只迭代一次的方法。
3. Kass, Michael, and Gavin Miller. "Rapid, stable fluid dynamics for computer graphics." Proceedings of the 17th annual conference on Computer graphics and interactive techniques. 1990.
4. But How DO Fluid Simulations Work? (https://www.youtube.com/watch?v=qsYE1wMEMPA) 參考了本影片中的Advection部分。
5. 3B1B的旋度和散度介紹 (https://www.youtube.com/watch?v=rB83DpBJQsE)
6. PhotonWater的GDC演講 (https://www.youtube.com/watch?v=rB83DpBJQsE),介紹Flowmap的生成等。


來源:騰訊遊戲學堂

相關文章