戰神4

中的風力場模擬

這次帶來的分享的主題是,聖莫妮卡工作室他們在戰神4中關於GPU模擬風力場。

演講者Rupert Renard 12年遊戲行業開發經驗,參與過戰神4,塞爾達傳說

,質量效益3等大作。

0.What Wind is Used For

風在遊戲中能帶來什麼?

通常情況線下,風可以是一個簡單的正弦波,影響物體的擺動,但是為了創造一個更生動的世界,戰神4寫了一套完善的風力系統,可以作用的對象包括 粒子 頭髮 樹葉 皮毛 聲音系統 布料

這裡的頭髮,樹葉,皮毛,被集成在另一個系統中,子系統中處理局部空間的擾動。

風的強弱可以烘托環境氛圍,營造氣氛。

1.CPU Origins

CPU上流體模擬的傳統方法,03年GDC上有一篇經典文章 「Real-Time Fluid Dynamics for Games」,主要是在不影響流體表現的前提線下,通過簡化流體表達方程提高計算速度。

那篇論文提到了方法,計算過程主要是 通過密度(density)添加力(add force),結合流體本身的擴散(diffuse),達到流體的效果(move),解決 boundary issues(邊界問題),會在bound box 外面再包一層,論文地址

但是戰神他們覺得現在都9012了,為啥不用一些更先進的其他方法去嘗試呢

2.Wind Tiers

風的類型和級別

三種風的類型:

  • Static Wind
  • Dynamic Wind
  • Counter Wind

靜態風:靜態風是一個全局的風,均勻地應用於場景中的所有物體。它可以隨著時間的推移而改變,也可以隨著玩家在世界各地的移動而改變。有時會用scrolling noise texture 來做靜態風。

動態風:動態風是他們的重點,作用範圍是在玩家周圍形成一個3D立體的空間,並隨著玩家的移動而移動的。

逆風:逆風是其實是一個機制,用來模擬在風中移動的物體,是否受到風的影響。 如果一個物體的運動速度和方向與靜態風或動態風大致相同,就會抵消風的作用,並給出物體不受風影響的表現。

SampleWind(object) := StaticWind + DynamicWind[object.position] - object.velocity

公式也比較好理解。

風的影響的採樣公式 = 全局靜態風一個vector3 + 動態風場中物體位置的風采樣 - 物體的移動速度vector3

3.Dynamic Wind Details

動態風詳解

用32x16x32 的三維紋理來存, 每立方米 一個紋理單位。 為了在GPU上快速方便的模擬風的計算,選擇了標準的三維紋理volume,而沒有使用層次化的volume。

戰神的動態風場在玩家周圍也足夠大,能包含斧頭扔出去的距離。所以他們的動態風場xz是比y大一倍的。

使用每幀5次的迭代,沒有什麼特別願意,只是剛好找到了一個比較balance的值。

風的產生設計了不同類型的「發動機」,用來給風場注入速度。

戰神裡面的Advection 對流提供了,正向和反向的2種,他們強烈建議別圖便宜只搞一種,後面會說原因。

他們嘗試過用壓強來模擬風場,但是他們的美術不喜歡,而且壓強有個弊端,就是不能是負的。但是壓強他們也做了,把壓強做為一個額外的使用參數

4.Storage

每個屬性都有單獨的三維紋理,x的速度,y的速度,z的速度

關於三維紋理的切片方向也有講究,他們選擇的是xz軸的切片。(據說,這樣做在計算的更高效,因為很多時候風的流向都是水平運動)

5.Diffusion

風的擴散。

隨時間推移,某個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

風力發動機

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。

7.Advection

平流(或者叫水平對流)

是基於速度傳遞能量的過程,發生在紋理和紋理之間,可以用來傳播速度屬性。

通過平流 來傳播速度,模擬能量的流動。

處理平流可以處理diffusion擴散一樣,按軸進行分離,減少等待時間。

但是會存在一個問題,在做迭代的時候, 正向和反向的會同時對數據讀寫,寫入數據的時候發生數據爭搶。多線程的時候可能同時有不同的線程在往texel紋理中寫數據。

8.Spin Compare & Exchange

交換比較

多線程運作的時候,紋理寫入,因為內存可見性的原因,不是原子運算,可能最後返回的結果有偏差。

舉個簡單的例子開多線程 執行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

9.Atomic Add

定長操作。上一頁提交浮點型在比較的時候,硬體沒法對浮點型進行原子運算。所以戰神換了個思路,損失一定精度,轉化成定長的浮點(或者說是定點),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));
}
}

10.Scheduling

調度演算法以及耗時。

風力模擬,在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導出的部分耗時增多,演講者說可能是紋理定址的問題。

11.Timing-Full Frame

這張圖展示了戰神 一幀繪製各部分的耗時,其實wind的耗時所佔的比重很小,上面下那是的是非同步並行的耗時,

12.Debugging

戰神他們團隊的debug工具真是完善,

  • 可視化2D的切面
  • 可以用向量更改wind的屬性
  • 粒子發射器
  • 鎖定volume位置
  • volume中風力的採樣和顯示

可視化3D volume風場和2D的風 切面是最直接有效的。可以很方便美術去布風,看效果,也方便程序去debug。圖中綠色的是一個directional motor(定向風發動機)。

奎爺的斧頭也是一個Moving Motor,扔出去之後也會產生風影響周圍的環境。

採樣方法也分了2種:一種是均勻時間間隔,在固定距離間隔的位置採樣,然後繪製矢量圖標,表示風力;另一種是直接用矢量圖標表示風力,越密集表示風強度越大。

13.Wind Customers

因為模擬的結果,不僅僅是GPU用,CPU布料和聲音的系統也會用到,CPU和GPU通信又不叫耗,戰神用了一個比較能接受的方法,把速度屬性xyz,存成RGB16的 double buffer texture。保證流暢性,CPU上布料和聲音系統讀取的是上一幀GPU返回的結果。

14.Beaufort scale

蒲福風級,Beaufort風力等級。就是幾級風力等級。

可以參考wiki Beaufort風力等級

0到12的等級,0代表沒有風,12代表颶風的力量。比如能聽到樹葉的嗦嗦的聲音,差不多是風力2級,地面差不多2m/s。

小樹搖擺,差不多5級風,地面9m/s。 這等於把遊戲中的風和現實世界關聯上了,這樣更具有真實性。

15.Conclusion

  • 風的模擬使遊戲更加生動
  • 高性能和低消耗也是可以實現的
  • 正向平流和反向一定要同事使用
  • 好的debug工具可以事半功倍

相比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


推薦閱讀:
相关文章