利用GPGPU計算大規模群落模擬行為
0x00 前言
在今年6月的Unite Europe 2017大會上 Unity 的CTO Joachim Ante演示了未來Unity新的編程特性——C# Job系統,它提供了編寫多線程代碼的一種既簡單又安全的方法。Joachim通過一個大規模群落行為模擬的演示,向我們展現了最新的Job系統是如何充分利用CPU多核架構的優勢來提升性能的。
但是吸引我的並非是C# Job如何利用多線程實現性能的提升,相反,吸引我的是如何在現在還沒有C# Job系統的Unity中實現類似的效果。
在Ante的session中,他的演示主要是利用多核CPU提高計算效率來實現大群體行為。那麼我就來演示一下,如何利用GPU來實現類似的目標吧。利用GPU做一些非渲染的計算也被稱為GPGPU——General-purpose computing on graphics processing units,圖形處理器通用計算。
0x01 CPU的限制
為何Joachim 要用這種大規模群落行為的模擬來宣傳Unity的新系統呢?
其實相對來說複雜的並非邏輯,這裡的關鍵詞是「大規模」——在他的演示中,實現了20,000個boid的群體效果,而更牛逼的是幀率保持在了40fps上下。
事實上自然界中的這種群體行為並不罕見,例如大規模的鳥群,大規模的魚群。
在搜集資料的時候,我還發現了一位優秀的水下攝影師、加利福尼亞海灣海洋計劃總監octavio aburto的個人網站上的一些讓人驚嘆的作品。
圖片來自Octavio Aburto
圖片來自Octavio Aburto
而要在計算機上模擬出這種自然界的現象,乍看上去似乎十分複雜,但實際上卻並非如此。查閱資料,可以發現早在1986年就由Craig Reynolds提出了一個邏輯簡單,而效果很贊的群體模擬模型——而作為這個群體內的個體的專有名詞boid(bird-oid object,類鳥物)也是他提出的。簡單來說,一個群體內的個體包括3種基本的行為:
- Separation:顧名思義,該個體用來規避周圍個體的行為。
- Alignment:作為一個群體,要有一個大致統一的前進方向。因此作為群體中的某個個體,可以根據自己周圍的同伴的前進方向獲取一個前進方向。
- Cohesion:同樣,作為一個群體肯定要有一個向心力。否則隊伍四散奔走就不好玩了,因此每個個體就可以根據自己周圍同伴的位置信息獲取一個向中心聚攏的方向。
以上三種行為需要同時加以考慮,才有可能模擬出一個接近真實的效果。
Vector3 direction = separation+ alignment + (cohesion - boid.position).normalized;
可以看出,這裡的邏輯並不複雜,但是麻煩的問題在於實現這套邏輯的前提是每個個體boid都需要獲取自己周圍的同伴信息。
因此最簡單也最通用的方式就是每個boid都要和群落中的所有boid比較位置信息,獲取二者之間的距離,如果小於閾值則判定是自己周圍的同伴。而這種比較的時間複雜度顯然是O( )。因此,當群體是由幾百個個體組成時,直接在cpu上計算時的表現還是可以接受的。但是數量一旦繼續上升,效果就很難保證了。當然,在Unity中我們還可以利用它的物理組件來獲取一個boid個體周圍的同伴信息:
Physics.OverlapSphere(Vector3 position, float radius, int layerMask);
這個方法會返回和自己重疊的對象列表,由於unity使用了空間劃分的機制,所以這種方式的性能要好於直接比較n個boid之間的距離。
但是即便如此,cpu的計算能力仍然是一個瓶頸。隨著群體個體數量的上升,性能也會快速的下降。
0x02 GPU的優勢
既然限制的瓶頸在於CPU面對大規模個體時的計算能力的不足,那麼一個自然的想法就是將這部分計算轉移到更擅長大規模計算的GPU上來進行。
CPU的結構複雜,主要完成邏輯控制和緩存功能,運算單元較少。與CPU相比,GPU的設計目的是儘可能的快速完成圖像處理,通過簡化邏輯控制並增加運算單元實現了高性能的並行計算。
利用GPU的超強計算能力來實現一些渲染之外的功能並非一個新的概念,早在十年前nvidia就為GPU引入了一個易用的編程介面,即CUDA統一計算架構,之後微軟推出了DirectCompute——它隨DirectX 11一同發布。
和常見的vertex shader和fragment shader類似,要在GPU運行我們自己設定的邏輯也需要通過shader,不過和傳統的shader的不同之處在於,compute shader並非傳統的渲染流水線中的一個階段,相反它主要用來計算原本由CPU處理的通用計算任務,這些通用計算常常與圖形處理沒有任何關係,因此這種方式也被稱為GPGPU——General-purpose computing on graphics processing units,圖形處理器通用計算。
利用這些功能,之前由CPU來實現的計算就可以轉移到計算能力更強大的GPU上來進行了,比如物理計算、AI等等。
而Unity的Compute Shader十分接近DirectCompute,最初Unity引入Compute Shader時僅僅支持DirectX 11,不過目前的版本已經支持別的圖形API了。詳情可以參考:Unity - Manual: Compute shaders。
在Unity中我們可以很方便的創建一個Compute Shader,一個Unity創建的默認Compute Shader如下所示:
// Each #kernel tells which function to compile; you can have many kernels#pragma kernel CSMain// Create a RenderTexture with enableRandomWrite flag and set it// with cs.SetTextureRWTexture2D<float4> Result;[numthreads(8,8,1)]void CSMain (uint3 id : SV_DispatchThreadID){ // TODO: insert actual code here! Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);}
這裡我先簡單的介紹一下這個Compute Shader中的相關概念,首先在這裡我們指明了這個shader的入口函數。
#pragma kernel CSMain
之後,聲明了在compute shader中操作的數據。
RWTexture2D<float4> Result;
這裡使用的是RWTexture2D,而我們更常用的是RWStructuredBuffer(RW在這裡表示可讀寫)。
之後是很關鍵的一行:[numthreads(8,8,1)]
這裡首先要說一下Compute Shader執行的線程模型。DirectCompute將並行計算的問題分解成了多個線程組,每個線程組內又包含了多個線程。
[numthreads(8,8,1)]的意思是在這個線程組中分配了8x8x1=64個線程,當然我們也可以直接使用
[numthreads(64,1,1)]
因為三維線程模型主要是為了方便某些使用情景,和性能關係不大,硬體在執行時仍然是把所有線程當做一維的。
至此,我們已經在shader中確定了每個線程組內包括幾個線程,但是我們還沒有分配線程組,也沒有開始執行這個shader。
和一般的shader不同,compute shader和圖形無關,因此在使用compute shader時不會涉及到mesh、material這些內容。相反,compute shader的設置和執行要在c#腳本中進行。
this.kernelHandle = cshader.FindKernel("CSMain"); ...... cshader.SetBuffer(this.kernelHandle, "boidBuffer", buffer); ...... cshader.Dispatch(this.kernelHandle, this.boidsCount, 1, 1); buffer.GetData(this.boidsData); ......
在c#腳本中準備、傳送數據,分配線程組並執行compute shader,最後數據再從GPU傳遞迴CPU。
不過,這裡有一個問題需要說明。雖然現在將計算轉移到GPU後計算能力已經不再是瓶頸,但是數據的轉移此時變成了首要的限制因素。而且在Dispatch之後直接調用GetData可能會造成CPU的阻塞。因為CPU此時需要等待GPU計算完畢並將數據傳遞迴CPU,所以希望日後Unity能夠提供一個非同步版本的GetData。
最後將行為模擬的邏輯從CPU轉移到GPU之後,模擬10,000個boid組成的大群組在我的筆記本上已經能跑在30FPS上下了。
完整的項目可以到這裡到這裡下載:
chenjd/Unity-Boids-Behavior-on-GPGPUref:
【1】wikipedia-Boids
【2】Craig Reynolds【3】Compute Shader Overview【4】Compute shaders各位如果覺得有趣的話,歡迎點個贊。
-EOF-
最後打個廣告,歡迎支持我的書《Unity 3D腳本編程》歡迎大家關注我的公眾號慕容的遊戲編程:chenjd01
推薦閱讀:
※「千萬別學我!」之遊戲開發者的囧事(下)
※Indie Figure: 用 PICO-8 開發遊戲原型的 Benjamin Soulé
※現在大紅大紫的《羞辱》曾經差點兒給霉運毀掉
※遊必有方 Vol.11 NYU Practice 系列活動現場彙報(下)
※PBR製作流程在手游美術方面的應用現狀?