概括
計算著色器 (Compute Shader) 是一種獨立於渲染管線之外,用途也不僅限於著色計算的工具,
請注意,因為計算著色器已經屬於進階應用,所以
本文的內容一共用到兩種程式語言,由 CPU 運作的 C# (CSharp),以及由 GPU 運作的 HLSL (High Level Shader Language),閱讀程式時還請注意程式區塊的標題。文章範例使用的環境為 Unity 引擎,雖然著色器相關的內容大致是通用的,但實做時也請注意是否有環境差異的問題在。
那麼,正式開始前請先讓我介紹一下計算著色器的兩大重點:
並行運算
首先,假設我們想重複執行某項函式 10 次,在常規的 CPU 語言中可能會透過迴圈執行,根據給定的條件(如計數器)重複執行任務。以現在的例子便是根據執行次數 i
,由 0 開始直到執行完第 9 次後結束,且由於 CPU 語言是線性執行的,因此
cs
for(int i = 0; i < 10; i ++) { SomeFunction(i); } SomeFunction(int index) { //DoSomething }
但是在著色器這種 GPU 語言中,函式則會交由十個獨立的執行緒 (thread) 進行運算,讓 GPU 的大量核心
hlsl
[numthreads(10, 1, 1)] void SomeFunction (uint3 id : SV_DispatchThreadID) { //DoSomething }
這裡用一張簡單的示意圖展示運作差異。注意,這張圖只是表示兩者「運作方式的差異」,與處理效率無關。
不過,在計算著色器中,運作差異並不是真正的難點,對有編寫過材質著色器的人來說應該已經很熟悉並行這回事了。計算著色器與材質著色器的真正差異在於,計算著色器是
因此計算著色器的真正的難點是,在少了明確的目標指引以後我們只能靠自己判斷
資料傳遞
在常規的渲染著色器中,引擎管線會幫使用者處理完各種瑣碎的工作。但在計算著色器中,使用者擁有更大的權力指揮 GPU 幫助我們達成各種任務,因此相應的責任也產生了,我們必須
CPU 與 GPU 運作時使用的儲存空間不同,因此想執行任何的操作前也必須先將資料傳遞給 GPU。由於資料是在兩種不同環境中進行傳遞,
再加上著色器本身的除錯難度,計算著色器出錯時會讓使用者難以辨識究竟是資料傳遞出錯還是計算函式有誤。對於初次接觸計算著色器,還不熟悉除錯方法的新手來說也會是一大學習瓶頸。
腳本結構
以下是 Unity 中的預設 Compute Shader 腳本,這個章節就來逐步解析他的結構,了解一個完整的計算著色器是由哪些元素構成的。
hlsl
// 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.SetTexture RWTexture2D<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); }
計算核心
就如同名稱一樣,計算核心 (Kernel) 是計算著色器中的核心區塊,當執行計算著色器時,
著色器需要透過 #pragma kernel ___
宣告計算核心,就和宣告材質著色器的 vert
和 frag
一樣。計算核心的名稱可以自由定義,但
hlsl
#pragma kernel MyComputeKernelA #pragma kernel MyComputeKernelB #pragma kernel MyComputeKernelC void MyComputeKernelA (uint3 id : SV_DispatchThreadID) { // TODO: insert actual code here! } void MyComputeKernelB (uint3 id : SV_DispatchThreadID) { } void MyComputeKernelC (uint3 id : SV_DispatchThreadID) { }
多執行緒
在定義與實做了計算核心之後,接著便要指定他的[numthreads(x, y, z)]
分配這個核心需要的執行緒數量 “num” “threads”。
hlsl
[numthreads(10, 1, 1)] void CSMain (uint3 id : SV_DispatchThreadID) { // TODO: insert actual code here! }
numthreads()
提供的三個參數輸入分別代表維度軸 x, y, z
,計算著色器運作時便會透過函式的輸入 (uint3 id : SV___)
將執行緒 ID 等資料傳入計算函式中。轉換成 CPU 語言中的迴圈看起來應該會更直觀。
cs
Vector3 numthreads = new Vector3(10, 1, 1); void Threads() { for(int x = 0; x < numthreads.x; x ++) { for(int y = 0; y < numthreads.y; y ++) { for(int z = 0; z < numthreads.y; z ++) { CSMain(new Vector3(x, y, z)); } } } } void CSMain(Vector3 threadID) { // TODO: insert actual code here! }
但請注意!這只是比喻,計算著色器中不會真的像迴圈那樣跑,而是以並行的方式亂序執行,別忘了最一開始的示意圖。
至於為什麼會需要三個維度軸的執行緒的數量呢?Microsoft 官方文檔是這樣說的:numthreads
For example, if a compute shader is doing 4x4 matrix addition then numthreads could be set to numthreads(4,4,1) and the indexing in the individual threads would automatically match the matrix entries. The compute shader could also declare a thread group with the same number of threads (16) using numthreads(16,1,1), however it would then have to calculate the current matrix entry based on the current thread number.
簡單來說就是為了「方便」,視你要處理的資料結構而定,
當然並行時也同理,假如我現在要處理的是圖片資料,透過兩個維度軸直接對應至像素位置上,會比透過長寬換算來的方便,這就是為什麼計算著色器也允許分配多個軸向的執行緒數量。
執行緒組
計算著色器的運行時機是由使用者掌控的,因此在定義完計算核心與執行序數量後,還是需要手動呼叫 ComputeShader.Dispatch()
來「運行」計算著色器。
cs
public void Dispatch(int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ);
Dispath
函式一共接受四個參數輸入,第一個 kernelIndex
代表的是這次 Dispath 要使用的計算核心,如果著色器中有定義複數的核心,便可藉由FindKernel()
便可透過名稱尋找對應的計算核心。
cs
int knrnelIndex = compute.FindKernel("MyComputeKernel"); compute.Dispatch(knrnelIndex, x, y, z);
至於後面三個參數 threadGroupsX,Y,Z
,代表的是這次numthread
分配的是一個組裡面要有多少執行緒,而 threadGroups
則會指定一次運行要分配多少個執行緒組。
因此,當計算著色器運行時,最終會執行的次數會有 threadGroups * numthreads 次。同樣,替換 CPU 語言中的迴圈看起來會更直觀。但也再次提醒這只是比喻,而且組和組之間也會並行,並非上一個組完成才會進入下一個組。
cs
void ThreadGroups(int threadGroupsX, int threadGroupsY, int threadGroupsZ) { for(int x = 0; x < threadGroupsX; x ++) { for(int y = 0; y < threadGroupsY; y ++) { for(int z = 0; z < threadGroupsZ; z ++) { Threads(); } } } } void Threads() { //for numthread x y z => CSMain() } void CSMain() { }
每個執行緒的 SV_DispatchThreadID
便是當前的執行緒組 * 執行緒數量 + 當前的執行緒,不過這些 ID 的計算系統都會幫我們完成,所以使用者只要分配好需要的數量即可。
圖片引用自 microsoft HLSL 文檔,numthreads
至具體數量該怎麼分配呢?首先從(n, 1, 1)
,若要對圖片的二維像素陣列操作就用 (x, y, 1)
,或是要計算三維的體積網格就 (x, y, z)
。
執行緒的具體數量或比例則沒有明確規則,大概抓個介於 10 和資料總數 1% 以下的數值吧。假設陣列長度大約一萬,numthread 就分配為 (100, 1, 1)
,若圖片大小 2048 的話就分配 (20, 20, 1)
。
最後,組的數量可以透過
cs
compute.Dispatch(knrnelIndex, 1 + (array.Length / 100), 1, 1); compute.Dispatch(knrnelIndex, 1 + (image.width / 20), 1 + (image.height / 20), 1);
並且著色器訪問陣列時不會因為 index out of range 而出誤中斷,所以只需要確保分配的數量足夠即可。 (只有少數情況會因為數量過多導致結果錯誤,文章最後的範例會提到解決方法)
資料訪問
最後,計算著色器中最關鍵的部份,
假如我要將一張圖片重置為黑色,只需要透過執行緒 ID 對應至圖片的每個像素,並將黑色寫入像素即可。由於資料的維度為二維,因此訪問資料欄位時的索引輸入也是二維 Image[id.xy]
。
hlsl
RWTexture2D<float4> Image; [numthreads(8, 8, 1)] void ClearImage (uint3 id : SV_DispatchThreadID) { Image[id.xy] = float4(0, 0, 0, 1); }
在每個執行中緒訪問資料也不侷限於自己的 ID,視需求也
hlsl
RWTexture2D<float4> Image; [numthreads(8, 8, 1)] void BoxBlur (uint3 id : SV_DispatchThreadID) { float4 color = 0; for(int x = -1; x <= 1; x ++) { for(int y = -1; y <= 1; y ++) { color += Image[id.xy + float2(x, y)]; } } Image[id.xy] = color / 9; }
卷積矩陣的簡單範例,分別為無效果、方框模糊與高斯模糊。
圖片引用自 Kernel Convolution Wiki
資料傳遞
資料傳遞,計算著色器的第二項重點。一開始有提到過,CPU 與 GPU 運作時使用的儲存空間不同,因此在計算著色器執行任何的操作前都
只讀參數
與一般的渲染著色器一樣,計算著色器也可以傳入單一的參數用於計算。且同樣的,這些數值在整個著色器中是全域共享並且只讀的,包括不同計算核心與函式,通常
以繪圖系統為例就是畫布大小、筆刷位置、筆刷強度與顏色等參數。透過 SetVector()
, SetFloat()
等函式進行傳遞。
cs
compute.SetVector("_CanavsSize", canvasSize); compute.SetVector("_BrushPos", mousePosition); compute.SetFloat("_Intensity", intensity); compute.SetVector("_Color", color);
要使用這些參數的話,在著色器中也需要建立對應名稱的變數接收。
hlsl
int2 _CanavsSize; float2 _BrushPos; float _Intensity; float4 _Intensity;
緩衝資料
與只讀的全域參數不同,由於儲存空間的差異以及資料傳遞的成本,若想讓計算著色器
GPU 的資料儲存空間為緩衝區 (Buffer),在 Unity 中只需要透過 new ComputeBuffer()
即可建立一個新的 GPU 緩衝區,引擎會幫我們完成繁瑣的內部作業。
cs
ComputeBuffer buffer = new ComputeBuffer(count, stride, ComputeBufferType);
計算緩衝區的建構子需要接收三個參數輸入,第一個 count
代表緩衝區「最多」需要儲存多少元素,通常就是我們想傳遞的stride
為一個欄位的元素大小,通常就是想傳遞的sizeof(type)
取得。
而最後的 ComputeBufferType
則是緩衝區的類別,可以根據需求使用不同的類型。具體的類型有許多種,不過這裡先關注最常用的兩種即可:
-
ComputeBufferType.Structured
結構緩衝區,通常用於傳遞一般的陣列資料 ,讓計算著色器讀、寫其中的內容。 -
ComputeBufferType.Append
容器緩衝區,允許計算著色器對它「添加」元素 ,通常用於資料過濾。
假設我要將一個長度為 10000 的向量陣列傳入計算著色器,並對裡面的元素進行過濾的話,就會需要一個結構緩衝區與一個容器緩衝區,元素數量與陣列相同 (10000),元素大小為三個單精度浮點數。
cs
Vector3[] positions = new Vector3[10000]; sourceBuffer = new ComputeBuffer(positions.Length, sizeof(float) * 3, ComputeBufferType.Structured); filteBuffer = new ComputeBuffer(positions.Length, sizeof(float) * 3, ComputeBufferType.Append);
分配完儲存空間後,還需要SetData()
將想傳遞的資料存入緩衝區
cs
sourceBuffer.SetData(positions);
最後,只需要SetData()
的時候資料傳遞就已經完成了,這裡只是
cs
compute.SetBuffer(kernel, "sourceBuffer", sourceBuffer); compute.SetBuffer(kernel, "filteBuffer", filteBuffer);
計算著色器也需要對應的緩衝區變數接收才能使用這些資料。建立時需要透過 <T>
欄位指定資料型別,型別必須與建立緩衝區時的分配的元素大小 stride
一致。
hlsl
StructuredBuffer<float3> sourceBuffer; AppendStructuredBuffer<float3> filteBuffer;
若想讓緩衝區一次傳遞複合資料,也可以透過結構包裝多個變數。
hlsl
struct transform { float3 position; float3 rotation; float3 scale; }; StructuredBuffer<transform> transforms;
除此之外,結構緩衝區還有一種 RWStructuredBuffer<T>
,這種緩衝區會
貼圖資料
除了傳遞一維陣列的緩衝區以外,計算著色器也能接受圖片資料,SetTexture()
函式傳遞圖片至著色器中,一樣需要指定計算核心。
cs
compute.SetTexture(kernel, "image", image);
與所有類型的資料傳遞相同,圖片接收也需要建立對應的變數接收,並且還需要指定圖片的float, fixed3, half4
等等。
hlsl
Texture2D<fixed4> image;
除此之外,在計算著色器中訪問圖片像素資訊時也與渲染著色器的方法不同,Texture2D<T>
是透過像素座標sampler2D
的 uv 採樣函式 Tex2D()
。
hlsl
void CSMain (uint3 id : SV_DispatchThreadID) { fixed4 pixel = image[id.xy]; }
Texture2D<T>
為只讀的像素緩衝區,如果要允許寫入像素的話需要用 RWTexture2D<T>
。要注意的是讀寫貼圖只能傳入 RenderTexture
,原理和建立緩衝區時一樣,建立渲染貼圖時也會做分配空間的工作。
釋放空間
由於 GPU 儲存空間是相當珍貴的,所以在不需要緩衝區時也要記得將空間釋放。只要透過 Release()
函式執行即可。
cs
buffer.Release(); renderTexture.Release();
實作範例
最後,回到最一開始的問題,有什麼問題是能透過並行解決的,以及該怎麼透過並行解決問題?在概括與腳本結構的章節中有看到,無論是計算核心的編寫方法,還是代換成 C# 中迴圈的形式,他們都表現出了一個共同點:
cs
for(int i = 0; i < 10; i ++) { SomeFunction(i); } SomeFunction(int index) { }
hlsl
[numthreads(10, 1, 1)] void SomeFunction (uint3 id : SV_DispatchThreadID) { //DoSomething }
意思是,
最後的章節就透過各種範例,將文中提到的各項重點串起。問題拆分、資料傳遞、解決問題,逐步分析如何使用計算著色器,透過並行的方式達成任務。
回顧腳本
首先,在開始解決自己的問題前,先來回顧一次預設的腳本結構,分析它做了哪些事,傳遞了什麼資料,以及該怎麼使用這個計算著色器。
預設著色器宣告了一個計算核心,名稱叫做 CSMain (Compute Shader Main)。
hlsl
// Each #kernel tells which function to compile; you can have many kernels #pragma kernel CSMain
他只宣告了一個讀寫貼讀緩衝區,精度為 float,通道數量 4 個。代表這個計算著色器要處理的主要資料結構是圖片。
hlsl
// Create a RenderTexture with enableRandomWrite flag and set it // with cs.SetTexture RWTexture2D<float4> Result;
由於訪問資料的維度軸為二維(圖片、ㄋ像素陣列),因此執行緒數量的格式為 (x, y, 1)
。
hlsl
[numthreads(8,8,1)]
最後是預設的計算核心,名稱對應一開始宣告的 CSMain
。透過多個執行緒對應到圖片緩衝區 Result
的每個像素上,同時利用像素座標的數值(也就是 id
)進行計算,並將計算結果寫入像圖片緩衝區。(先忽略計算式的原理,那不是這裡的重點)
hlsl
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); }
回到 C# 處,來看看如何使用這個預設著色器。首先要尋找著色器中定義的計算核心 CSMain
。
cs
int kernel = compute.FindKernel("CSMain");
接著,為了提供讀寫貼圖需要的圖片資料,需要建立一個 RenderTexture,並傳入計算著色器的 Result 當中。
cs
resultTex = new RenderTexture(1024, 1024, 0, RenderTextureFormat.Default); resultTex.enableRandomWrite = true; resultTex.Create(); compute.SetTexture(kernel, "Result", resultTex);
最後,調用著色器執行指定的計算核心。由於著色器中指定的執行序數量為 8,因此執行時必須將執行緒組的數量分配至圖片大小除以 8 才夠。
cs
compute.Dispatch(kernel, 1 + (resultTex.width / 8), 1 + (resultTex.height / 8), 1);
運作結果如下,這是一個能繪製分型的計算著色器。
陣列計算
看完了預設的著色器,現在輪到我們應用這些知識嘗試解決自己的問題。一步一步來,首先是:
1. 要解決什麼問題
將陣列中每個元素的數值 + n
2. 要怎麼傳遞資料
首先是全域只讀的參數,也就是要增加的數值 n。透過 SetInt()
函式將數值傳入計算著色器。
cs
int addition; compute.SetInt("_Addition", addition);
接著是要透過計算著色器處理的資料。建立一個 ComputeBuffer 分配需要 GPU 儲存空間,將要進行操作的陣列資料存入緩衝區,並指定給計算著色器。
cs
int[] array; ComputeBuffer buffer = new ComputeBuffer(array.Length, sizeof(int), ComputeBufferType.Structured); buffer.SetData(array); compute.SetBuffer(kernel, "valuesBuffer", buffer);
3. 要怎麼解決問題
將問題拆分為相似的片段,透過重複執行的方式解決問題。在這個例子中便是以多個執行緒分別對應到陣列的所有元素上,並各自執行 + n 的動作。
hlsl
int _Addition; RWStructuredBuffer<int> valuesBuffer; void AddValueKernel (uint3 id : SV_DispatchThreadID) { buffer[id.x] = buffer[id.x] + _Addition; }
由於資料維度為一維陣列,因此執行序數量的格式為 (n, 1, 1)
。
hlsl
[numthreads(10, 1, 1)]
最後,呼叫計算著色器執行計算,執行緒組的數量為陣列數量除以 10。
cs
compute.Dispatch(kernel, 1 + (array.Length / 10), 1, 1);
4. 要怎麼使用資料
運算完畢後,透過 GetData 取得緩衝區資料,用於檢視運行結果。
cs
int[] result = new int[array.Length]; buffer.GetData(result);
資料過濾
第二個範例,透過計算著色器進行資料過濾。首先:
1. 要解決什麼問題
對陣列的元素進行過濾,找出位於指定範圍中的向量元素。
2. 要怎麼傳遞資料
首先是兩個只讀的全域向量,用於作為過濾範圍的最小與最大值。使用 SetVector()
函式進行傳遞。
cs
Vector2 rangeMin, rangeMax; compute.SetVector("_RangeMin", rangeMin); compute.SetVector("_RangeMax", rangeMax);
接著是要透過計算著色器處理的資料。由於我們像要對元素進行過濾,因此需要建立兩個計算緩衝區,一個為 StructuredBuffer
用於AppendBuffer
。
cs
Vector2[] array; ComputeBuffer sourceBuffer = new ComputeBuffer(array.Length, sizeof(float) * 2, ComputeBufferType.Structured); ComputeBuffer resultBuffer = new ComputeBuffer(array.Length, sizeof(float) * 2, ComputeBufferType.Append); sourceBuffer.SetData(array); compute.SetBuffer(kernel, "sourceBuffer", sourceBuffer); compute.SetBuffer(kernel, "resultBuffer", resultBuffer);
除此之外,使用計算著色器過濾元素時,可能因為執行序數量過多而
cs
compute.SetInt("_ElementCount", array.Length);
3. 要怎麼解決問題
將問題拆分為相似的片段,在這個範例中便是透過執行緒 ID 讀取各自欄位上的資料,並將符合條件的元素加入結果緩衝區中。透過 Append
函式即可將元素存入緩衝區。
hlsl
float2 _RangeMin, _RangeMax; RWStructuredBuffer<float2> sourceBuffer; AppendStructuredBuffer<float2> resultBuffer; void FilteKernel (uint3 id : SV_DispatchThreadID) { float2 element = sourceBuffer[id.x]; if(element.x < _RangeMin.x) return; if(element.y < _RangeMin.y) return; if(element.x > _RangeMax.x) return; if(element.y > _RangeMax.y) return; resultBuffer.Append(element); }
為了避免將非預期的元素也存入緩衝區,可以
hlsl
int _ElementCount; void FilteKernel (uint3 id : SV_DispatchThreadID) { if(id.x >= _ElementCount) return; // codes ... }
執行緒的數量和上個範例相同,因為資料維度為一維陣列,所以執行序數量的格式為 (n, 1, 1)
。
hlsl
[numthreads(10, 1, 1)]
最後,呼叫著色器執行計算。
cs
compute.Dispatch(kernel, 1 + (array.Length / 10), 1, 1);
4. 要怎麼使用資料
運算完成後,透過 GetData()
取得緩衝區資料,用於檢視效果。
cs
Vector2[] result = new Vector2[array.Length]; resultBuffer.GetData(result);
要注意的是緩衝區在建立時,欄位數量是根據「可能的最大值」建立的,即使 AppendBuffer 當中沒有「添加」那麼多元素,他的
除錯建議
由於著色器本身的除錯難度,再加上兩個環境之間的資料傳遞問題,計算著色器的除錯過程也是相當令人頭疼的。範例的最後就提供幾項除錯時的指標供各位參考,一步步縮小可能的問題範圍。
-
檢查資料有沒有正確寫入緩衝區
在buffer.SetData()
的環節中是否錯誤?資料有成功傳入緩衝區嗎?資料有沒有傳遞進正確的緩衝區?原始資料本身是正確的嗎? -
檢查緩衝區有沒有分配給著色器
在compute.SetBuffer()
的環節是否正常?是否有將緩衝區指定給計算著色器?指定時的計算核心正不正確?緩衝區的名稱是否匹配? -
檢查著色器讀取資料有沒有正確
計算函式訪問緩衝區資料時是否出錯?有沒有訪問到正確的緩衝區?資料欄位的 ID 是否正確? -
檢查著色器寫入資料有沒有正確
計算函式輸出結果的過程是否正常?有沒有將結果寫入緩衝區? 有沒有寫入到正確的緩衝區中? -
檢查著色器計算函式有沒有正確
最後,當上述檢查都確認過以後,問題可能就出在著色器的計算函式本身了。由於各種需求的實際差異甚大,這裡就比較難提供建議了,但請放心,與資料傳遞相比這是最好除錯的部份了~
更多例子
上面用了兩個簡單的例子展示如何編寫自己的計算著色器,但要注意這並不是真正「應用」計算著色器時會使用的做法。由於 CPU 與 GPU 間的資料傳遞成本高昂以及運行時機等問題,通常不會像範例中透過 GetData()
將資料取回 C#,而是
例如傳入 Graphics.DrawMeshInstancedIndirect 讓 Unity 進行 GPU Instance,或是透過計算著色器將結果繪製到 RenderTexture 中,再利用 ImageEffectShader 渲染到畫面上。
或者將它視為一種「開發工具」也是可以的,使用計算著色器製作出輔助工具,在編輯器狀態下
Conway’s Game of Life
康威生命遊戲,以網格為空間單位,每個單位格都是一個細胞,而回合則為這個世界的時間單位,在每個回合中細胞都會
引用自 Conway’s Game of Life Wiki
具體遊戲規則可以參考 Wiki。
GPU Slime Simulations
透過計算著色器模擬大量的單位,並讓這些單位
引用自 Coding Adventure: Ant and Slime Simulations
參考影片 Coding Adventure: Ant and Slime Simulations
GPU Culling
與 GPU Instance 搭配使用的技術,透過計算著色器進行視錐剃除,
圖片引用自 Unity中使用ComputeShader做视锥剔除
更多細節可以參考此篇文章 Unity中使用ComputeShader做视锥剔除(View Frustum Culling)。
GPU Ray Tracing
將環境、物件與材質等資料傳入計算著色器,
引用自 GPU Ray Tracing in Unity
參考資料 GPU Ray Tracing in Unity, Coding Adventure: Ray Marching
感謝閱讀
在知道了 GPU Instance 和 GPU Culling 兩項技術後,我也接觸到計算著色器這項工具,並正式踏入 GPU 並行的世界了。為了學計算著色器我查了不少資料研究,但總覺的很多內容都不夠直觀,不然就是一口氣跳到太深的內容(像是直接教 RayTracing 的文章),以至於我花了不少時間試錯後才得出一些基礎但相當重要的結論。
於是,在幾個月的實做研究後,我嘗試用自己的理解重新解釋了一次計算著色器,將學習時注意到的各項重點分享給各位,希望能提供有興趣的人參考方向!
有任何建議和想法都歡迎提出討論,如果喜歡文章內容的話也請幫我點一下 Like Button :D
個人網站留言功能尚未製作,如果需要留言還請移駕至巴哈文章的留言板 Orz
參考資料
Ronja’s tutorials, Compute Shader
Getting Started with Compute Shaders in Unity
Coding Adventure: Compute Shaders
Check if a ComputeShader.Dispatch() command is completed on GPU before doing second kernel dispatch
hlsl CG compute shader Race Condition
Parallel Computer Architecture and Programming, Spring 2018: Schedule (感謝巴友 美遊ちゃん 提供)