戰神4中的風力場模擬
這次帶來的分享的主題是,聖莫妮卡工作室他們在戰神4中關於GPU模擬風力場。
演講者Rupert Renard 12年遊戲行業開發經驗,參與過戰神4,塞爾達傳說,質量效益3等大作。
風在遊戲中能帶來什麼?
通常情況線下,風可以是一個簡單的正弦波,影響物體的擺動,但是為了創造一個更生動的世界,戰神4寫了一套完善的風力系統,可以作用的對象包括 粒子 頭髮 樹葉 皮毛 聲音系統 布料
這裡的頭髮,樹葉,皮毛,被集成在另一個系統中,子系統中處理局部空間的擾動。
風的強弱可以烘托環境氛圍,營造氣氛。
CPU上流體模擬的傳統方法,03年GDC上有一篇經典文章 「Real-Time Fluid Dynamics for Games」,主要是在不影響流體表現的前提線下,通過簡化流體表達方程提高計算速度。
那篇論文提到了方法,計算過程主要是 通過密度(density)添加力(add force),結合流體本身的擴散(diffuse),達到流體的效果(move),解決 boundary issues(邊界問題),會在bound box 外面再包一層,論文地址
但是戰神他們覺得現在都9012了,為啥不用一些更先進的其他方法去嘗試呢
風的類型和級別
三種風的類型:
靜態風:靜態風是一個全局的風,均勻地應用於場景中的所有物體。它可以隨著時間的推移而改變,也可以隨著玩家在世界各地的移動而改變。有時會用scrolling noise texture 來做靜態風。
動態風:動態風是他們的重點,作用範圍是在玩家周圍形成一個3D立體的空間,並隨著玩家的移動而移動的。
逆風:逆風是其實是一個機制,用來模擬在風中移動的物體,是否受到風的影響。 如果一個物體的運動速度和方向與靜態風或動態風大致相同,就會抵消風的作用,並給出物體不受風影響的表現。
SampleWind(object) := StaticWind + DynamicWind[object.position] - object.velocity
公式也比較好理解。
風的影響的採樣公式 = 全局靜態風一個vector3 + 動態風場中物體位置的風采樣 - 物體的移動速度vector3
動態風詳解
用32x16x32 的三維紋理來存, 每立方米 一個紋理單位。 為了在GPU上快速方便的模擬風的計算,選擇了標準的三維紋理volume,而沒有使用層次化的volume。
戰神的動態風場在玩家周圍也足夠大,能包含斧頭扔出去的距離。所以他們的動態風場xz是比y大一倍的。
使用每幀5次的迭代,沒有什麼特別願意,只是剛好找到了一個比較balance的值。
風的產生設計了不同類型的「發動機」,用來給風場注入速度。
戰神裡面的Advection 對流提供了,正向和反向的2種,他們強烈建議別圖便宜只搞一種,後面會說原因。
他們嘗試過用壓強來模擬風場,但是他們的美術不喜歡,而且壓強有個弊端,就是不能是負的。但是壓強他們也做了,把壓強做為一個額外的使用參數
每個屬性都有單獨的三維紋理,x的速度,y的速度,z的速度
關於三維紋理的切片方向也有講究,他們選擇的是xz軸的切片。(據說,這樣做在計算的更高效,因為很多時候風的流向都是水平運動)
風的擴散。
隨時間推移,某個cell會對周圍的cell產生影響。可以理解成是使流體模擬達到平衡的一種機制。它被用來在相鄰的cell之間傳遞能量。
這來的擴散就用到了 double的buffer,2個buffer交替存數據。
驗證之後發現將速度屬性分成三個單獨的三維紋理,計算的時候尤其高效。
是因為如果不分離的話,在計算風的迭代的時候,xyz三個方向全部計算完成之後,才能進行下次的迭代。但其實這三個方向的計算,發生在不同迭代軸上,是互不影響的
看著2張圖就能看出來。
先解釋下VGPR。
AMD GCN 計算單元中, 一個GCN計算單元(CU),包含四個SIMDs(單指令流多數據流),每一個包含一個包含32位的VGPRs(矢量通用寄存器)的64KB寄存器文件
著色器最終處理每個線程的帶寬更少,這是因為每個線程的數據更少,這意味著每個線程的VGPR更少,這意味著更好的佔用潛力。 對於希望非同步運行的著色器,更少的VGPRs也是非常好的選擇
好處是,GPU在計算迭代的時候,更少的等待時間。
對於希望非同步運行的shader,更少的VGPRs也是非常好的選擇
風力發動機
6種不同類型的Motors Directional 平行風 (類似unity WindZone里的Directional) Omni 全向風 (類似unity里的Spherial) Vortex 旋渦,沿某個軸產生風 Moving 運動發動機,錐形,可以理解成發動機在運動,產生風場是錐形擴散的 Cylinder 圓柱的上下面可以大小不一樣 Pressure 直接就是壓強
當時面臨的一個挑戰就是不同類型的Motor混合時候,互相作用
// 平行風,out返回float3的velocity void ApplyMotorDirectional(in float3 cellPosWS, uniform MatorDirectional motorDirectional, in out float3 velocityWS) { // 計算cell到motor的距離 float distanceSq = lengthSq(cellPosWS - motorDirectional.posWS); // 距離的平方小於motor的作用範圍,加上速度 // force = direction * strength * deltaTime if(distanceSq < motorDirectional.force) velocityWS += motorDirectional.force; }
// 全向風,作用朝四面八方,輻射出去,存在作用半徑radius void ApplyMotorOmni(in float3 cellPosWS, uniform MotorOmni motorOmni, in out float3 velocityWS) { // force = strength * deltaTime float3 differenceWs = cellPosWS - motorOmni.posWS; float distanceSq = lengthSq(differenceWs); // 速度受到作用半徑和距離的影響 if(distanceSq < motorOmni.radiusSq) velocityWS += motorOmni.force * rsqrt(distanceSq) * differenceWs }
// 螺旋風 void ApplyMotorVortex(in float3 cellPosWS, uniform MotorVortex motorVortex, in out float3 velocityWS) { // force = strength * deltaTime float3 differenceWs = cellPosWS - motorVortex.posWS; float distanceSq = lengthSq(differenceWs); // 速度受到作用半徑和螺旋風軸向叉乘的影響 if(distanceSq < motorVortex.radiusSq) velocityWS += motorVortex.force * cross(motorVortex.axis, rsqrt(distanceSq) * differenceWs) }
和unity里的WindZone做一個簡單對比, unity中3D 在 Game Object > 3D Object > Wind Zone,提供了wind zone,但是類型較為單一,只提供了Direction和Spherical兩種,差不多等價於戰神里的 Directional 和 Omni。
unity主要還是用壓強來實現的,暴露的參數,Main(強度)近似戰神Motor類型中的force。
平流(或者叫水平對流)
是基於速度傳遞能量的過程,發生在紋理和紋理之間,可以用來傳播速度屬性。
通過平流 來傳播速度,模擬能量的流動。
處理平流可以處理diffusion擴散一樣,按軸進行分離,減少等待時間。
但是會存在一個問題,在做迭代的時候, 正向和反向的會同時對數據讀寫,寫入數據的時候發生數據爭搶。多線程的時候可能同時有不同的線程在往texel紋理中寫數據。
交換比較
多線程運作的時候,紋理寫入,因為內存可見性的原因,不是原子運算,可能最後返回的結果有偏差。
舉個簡單的例子開多線程 執行i++ 1000次,最後返回的結果不一定是1000,是一樣的原因。
簡單解釋下,多線程線程之間的共享變數存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變數的副本。
這裡用到的解決方法是多線程裡面常見的CompareExchange,函數差不多是這樣的 CompareExchange(Double, Double, Double)
void SpinCompareExchange(uniform RWTexture3D<float> rwTex, uniform unit3 rwTexSize, in unit3 coord, in float value) { if(all(coord < rwTexSize)) { float curVal = 0; for(;;) { float oldVal, newVal = curVal + value; InterlockedCompareExchange(rwTex[coord], curVal, newVal, oldVal); if (curVal == oldVal) break; curVal = oldVal } } }
把目標操作數(第1參數所指向的內存中的數)與一個值(第3參數)比較,如果相等,則用另一個值(第2參數)與目標操作數(第1參數所指向的內存中的數)交換
整個操作過程是鎖定內存的,其它處理器不會同時訪問內存,從而實現多處理器環境下的線程互斥。
這個地方可以直接參考MSDN compareExchange函數的api
定長操作。上一頁提交浮點型在比較的時候,硬體沒法對浮點型進行原子運算。所以戰神換了個思路,損失一定精度,轉化成定長的浮點(或者說是定點),16.16,上下都保證一定的精度。
也叫fixed-point number,定點數的計算效率是比浮點數更高的
wiki上解釋是:
定點數類型的值其實就是個整數,需要額外做比例進位,進多少位需要根據具體的定點數類型決定。例如 1.23 使用 1/1000 比例的定點數表示時是 1230;1,230,000 使用 1000 比例的定點數表示也是 1230。與浮點數不同,相同類型的定點數中所有值的縮放係數都是一致的,在計算過程中也保持不變。
縮放係數通常是 10 或 2的冪,前者方便人類讀寫,後者易於高效計算。不過有時也會使用其它比例,例如可以用 1/3600 的比例的定點數來表示以小時為單位的時間值,可以精確到秒。
定點數的最大值,可以通過將其內部所使用的整數的最大值乘以縮放係數求得,最小值同理。
浮點數的精度是有尾數位決定的,正常單精度float,雙精度double的小數位如下:
float: 1bit(符號位) 8bits(指數位) 23bits(尾數位) double: 1bit(符號位) 11bits(指數位) 52bits(尾數位)
他們把小數位按16位算精度,轉換成int,在做原子加的操作。
// float轉int要乘的宏 # define FXDPT_SIZE(1<<16) void AtomicAdd(uniform RWTexture3D<int> rwTex, uniform unit3 twTexSize, in unit3 coord, in float value) { if(all(coord < rwTexSize)) { InterlockedAdd(rwTex[coord], (int)(value * FXDPT_SIZE)); } }
調度演算法以及耗時。
風力模擬,在GPU管線上是第一次做的事,模擬差不多耗時0.1ms。
模擬的過程本身也是非同步的,和渲染物體,渲染粒子,並行。
藍色表示擴散,紅色是Motor相關,橘色是正向對流初始化的一些過程,黃色開始計算正向對流,後面是反向平流,最後紫色導出,方便gpu和cpu訪問。
整個過程VGPR和SGPR都很低,適合併行運算。
diffuse進行了5次,耗時43.2ms,平均一次8ms,這是因為使用了分離軸的技巧(3個軸互不影響分離計算)。這種方法,如果使用更大的Volume,性能的提高會更加明顯。
後面他們覺得forward平流和reverse評論在setup的階段,可以用buffer來記錄之前的數據,空間換時間,差不多又節約了10ms左右。
他們還做了一個實驗,把每個軸的size擴大一倍,等於整體體積擴大了8倍。
實驗結果可以看出來,最後一列是耗時,可以看出差不多都是7,8倍的樣子,說明耗時和數據量基本線性相關,只有export導出的部分耗時增多,演講者說可能是紋理定址的問題。
這張圖展示了戰神 一幀繪製各部分的耗時,其實wind的耗時所佔的比重很小,上面下那是的是非同步並行的耗時,
戰神他們團隊的debug工具真是完善,
可視化3D volume風場和2D的風 切面是最直接有效的。可以很方便美術去布風,看效果,也方便程序去debug。圖中綠色的是一個directional motor(定向風發動機)。
奎爺的斧頭也是一個Moving Motor,扔出去之後也會產生風影響周圍的環境。
採樣方法也分了2種:一種是均勻時間間隔,在固定距離間隔的位置採樣,然後繪製矢量圖標,表示風力;另一種是直接用矢量圖標表示風力,越密集表示風強度越大。
因為模擬的結果,不僅僅是GPU用,CPU布料和聲音的系統也會用到,CPU和GPU通信又不叫耗,戰神用了一個比較能接受的方法,把速度屬性xyz,存成RGB16的 double buffer texture。保證流暢性,CPU上布料和聲音系統讀取的是上一幀GPU返回的結果。
蒲福風級,Beaufort風力等級。就是幾級風力等級。
可以參考wiki Beaufort風力等級
0到12的等級,0代表沒有風,12代表颶風的力量。比如能聽到樹葉的嗦嗦的聲音,差不多是風力2級,地面差不多2m/s。
小樹搖擺,差不多5級風,地面9m/s。 這等於把遊戲中的風和現實世界關聯上了,這樣更具有真實性。
相比2003的方法,戰神把流體風的模擬放到了GPU上,更好的發揮了硬體GPU並行計算的性能,有更高的質量。
後面就是他安利大家去聽他同事Sean的 Interactive Wind and Vegetation in God of War,講植被和風交互,會詳細介紹客戶端表現,包括在Vertex Shader上風是怎麼影響物體的,還有雙高度場的草場交互。
最後談一下,自己的一點點小感想。
戰神他們的對風力場的創新,是相當於把原來一直CPU上做的一件事件,放到了GPU上,做到了並行高效的效果。
多線程部分,並行的一些思想,原子操作等等。
計算的時候用定點數,相比浮點數,提交運算效率
最後他們的debug工具真是完善呀,開發效率高,調試效率高。實名羨慕...
1.Wind Simulation in God of War
2.Introduction to Fixed Point Number Representation(berkeley.cs61c)
3.Real-Time Fluid Dynamics for Games