Unity — Compute Shader 基礎認識

Witte Hsieh
9 min readJan 26, 2018

--

本篇文章是參考了Unity官方手冊以及幾篇相關教學文章後,紀錄實作和理解的過程,希望對於想學習Compute Shader的人提供些許幫助。需要的前置知識是對Unity的物件有基本了解(知道GameObject、Component、Material、Renderer是什麼大概就夠了)。

首先我們會概略地介紹GPU、Shader是如何加速複雜場景的顯示,接下來介紹Compute Shader與一般Shader的相異之處以及用途。然後進入主題,這邊會列舉兩個ComputeShader的程式範例。

甚麼是GPU以及Shader?

以下用一張圖解釋CPU和GPU的差別:

CPU vs GPU (可能有看七龍珠的才懂…)

CPU擁有強大的運算能力,可執行計算的核心數量卻很少;而GPU擁有數百至數千(可能更多)個運算核心,這些運算核心是針對矩陣或向量運算特別設計的,無法處理過於複雜的邏輯。GPU可以處理大量簡單且各自獨立的運算。例如,要計算十萬個粒子要隨著風向往哪裡飛,CPU必須執行100000次運算;而GPU如果有1000個計算核心,能同時計算1000個粒子的運動,只需要計算100000/1000 = 100次運算。相較之下,可以看出在這種狀況下,GPU有著非常強大的運算能力。

我們如何使喚GPU做事呢?答案就是Shader。Shader是一段程式碼,定義了每一個單一GPU計算核心要做的事情。Shader共分為Surface Shader、Vertex and Fragment Shader、Compute Shader三種,前兩種主要功能是加速各種3D物件或是螢幕特效的顯示,在此不詳述,畢竟本篇的主角是Compute Shader嘛。

Compute Shader

如上所說,只要能拆解成彼此獨立的運算,都可以交由Compute Shader來做。等等!首先你的電腦必須支援才行,你可以在C#內利用下述程式碼來確認是否支援喔。

SystemInfo.supportsComputerShaders

使用步驟

  1. 建立Compute Shader,副檔名是.compute。
新增一個Compute Shader

2. 撰寫呼叫Compute Shader的C#程式,在Monobehaviour裡設定貼圖或者是緩衝區給Compute Shader進行運算。

3. 在Unity的Inspector內指定Compute Shader給Monobehaviour。

把Compute Shader指定給Monobehaviour的變數

接下來會看到C#和Computer Shader實際上是怎麼互相合作的。他們之間可以透過數種方式溝通,以下介紹其中兩種:

對貼圖進行讀寫的Compute Shader

範例描述:使用Compute Shader照著Thread在Dispatch內的ID在貼圖上填色。主要可以看到Compute Shader怎麼讀寫貼圖。

執行結果

程式碼部分:

Monobehaviour

(1) 宣告ComputeShader的參考,在UnityEditor內可以把ComputeShader Asset拖入。

(2) 從ComputeShader中定義的kernel名稱挑選出要執行的kernel函式。

(3) 把貼圖設定給ComputeShader內定義的貼圖參考。

(4) 呼叫ComputeShader的Kernel開始執行,傳入的參數是Compute Shader內定義的ThreadGroup的資料格式numthreads(可以參考下面Compute Shader程式解說的(4))。

Compute Shader

(1) 定義了這個Compute Shader要使用的Kernel Function,至少要定義一個(或以上)。

(2) 宣告了Compute Shader要存讀的貼圖資料,是一個由float4(RGBA)構成的陣列。

(3) 宣告了一個Thread Group,在執行時會把運算分派給每個Thread去執行。在此表示一個Thread Group內有8x8x1個Thread。

(4) Compute Shader主要的處理函式,每個Thread會依序執行此函式,傳入參數是SV_DispatchThreadID,一個三維向量;指出這個Thread在所有Dispatch的Thread中的ID(白話:一堆要做的事情裡面的第幾件事)。

Thread Group的具體結構和Dispatch的邏輯可以參考下圖:

原圖:https://msdn.microsoft.com/en-us/library/windows/desktop/ff471442(v=vs.85).aspx

對緩衝進行讀寫的Compute Shader

範例描述:使用一個自定義的結構存取position、rotation、scale等等資訊,並且把這些資料存在一個Compute Shader可讀寫的Buffer內,讓Compute Shader計算每個物件的移動和旋轉等等。主要可以看到C#和Compute Shader各自怎麼讀寫ComputeBuffer物件。

執行結果

程式碼部分:

Monobehaviour

(1) 宣告ComputeBuffer的參考,之後可以透過此變數讀寫傳遞給Compute Shader的緩衝區。

(2) 初始化緩衝區,第一個參數是緩衝區裡的元素數量。第二個參數是每一個元素的大小,單位是Byte。在這個範例裡,緩衝區的元素結構是自行定義的PBuffer,內含一個float和三個Vector3,一個float是4 bytes,一個Vector3有三個float,因此大小是:4x1 + (4x3) x 3 = 40 bytes。也可以寫成 sizeof(float) + (sizeof(float)*3)*3。

(3) 透過buffer參考取得緩衝區內的資料,傳入的參數是事先宣告好的陣列,資料會存到該陣列中。透過buffer的參考把資料存進緩衝區。

(4) 透過Shader參考設定Compute Shader內的deltaTime參數。

(5) 透過Shader參考把ComputeBuffer變數指定給Compute Shader中的RWStructuredBuffer。

(6) 總共有16個元素必須計算,ThreadGroup的結構是(2, 2, 1),Dispatch時必須讓16個元素都被計算到,(16 = 4 x 4),因此輸入參數是(4/2, 4/2, 1) = (2, 2, 1)。

Compute Shader

(1) 自行定義的結構,需要與C#中傳入結構相同。

(2) 宣告可讀寫的緩衝區,內容可以從C#傳入。

(3) 可從C#傳入的參數。

(4) 如下圖所示:Dispatch分配了2x2x1個GroupThread,每個GroupThread有 2 x 2 x 1 個Thread,邏輯上Dispatch內Thread所構成的結構是 4 x 4 x 1。因此當SV_DispatchThreadID為 ( 2 , 1, 0) 時,代表的是在一個 4 x 4 x 1 的矩陣裡,第 ( 2, 1, 0)個元素,換算成一維陣列的index則是 2 + 1 x 4 + 0 x 4 x 4 = 6,公式如下:

threadId.x +threadId.y * dispatch.x * numthread.x +threadId.z * dispatch.x * numthread.x * dispatch.y * numthread.y
Dispatch、numthreads和DispatchThreadID之間的關係

結語

利用Compute Shader大幅增加平行運算的效能,網路上很容易查到非常酷炫的程式碼範例可以下載。在上述第二個範例中,其實還有個問題:就是我們雖然用很快速的方式計算每個物件的位置,但是在每次Update時,會從Buffer把所有物件的位置讀出來,如果有超大量的物件,這就會造成CPU的負擔。因此下一步,就是要讓盡量減少GPU和CPU的溝通囉!這就不在”基礎”介紹的範圍啦XD

參考來源

http://blog.csdn.net/wolf96/article/details/44132411

https://docs.unity3d.com/Manual/ComputeShaders.html

--

--