Compute Shader 功能測試
Compute Shader 可以在通常的渲染管線之外執行,執行一些大量的通用計算(GPGPU algorithms),因此可以聯想到把一些大量相互之間沒有關聯的計算轉移到GPU中進行,以減輕CPU的工作量。
Compute Shader 例項
#pragma kernel FillWithRed
RWTexture2D<float4> res;
[numthreads(1,1,1)]
void FillWithRed (uint3 dtid : SV_DispatchThreadID)
{
res[dtid.xy] = float4(1,0,0,1);
}
以上是一個簡單的Compute Shader,大概解釋一下就是,
#pragma kernel FillWithRed
聲明瞭主函式叫 FillWithRed,有點類似VF Shader的#pragma vertex vert
RWTexture2D<float4> res;
聲明瞭一個可讀寫的Texture2D物件,RW是Read Write的縮寫,這個物件用來儲存Compute Shader的計算結果,這裡計算結果是float4型別的資料。在C#程式碼中對應的也要有一個可讀寫的Texture物件,一般情況是定義一個RenderTexture,通過ComputeShader.SetTexture
方法把RenderTexture和RWTexture2D關聯起來。Compute Shader 執行結束後,C#中的RenderTexture也相應被改變。[numthreads(1,1,1)]
宣告XYZ三個維度執行緒組中的執行緒數量,即 thread number per group。對應的C#程式碼中在呼叫Compute Shader時也會指定XYZ三個維度的執行緒組的數量,即 group number。這行程式碼意思是xyz執行緒組上都只有1個執行緒。res[dtid.xy] = float4(1,0,0,1);
儲存計算結果到Texture2D物件的畫素中,這裡所有畫素都儲存同一個數值float4(1,0,0,1),那麼在C#中如果讀取對應的RenderTexture物件應該會得到一張純紅色的圖片。注意這裡的dtid.xy
並不是紋理座標,範圍不是[0,1],dtid.xy
Compute Shader 的使用
計算結果儲存到Texture中
Compute Shader 大概能做的事情已經很清晰了,現在就來實際試用下,先從簡單一點的開始,剛才例項裡的shader只是給所有畫素儲存了同一個float4數值,並沒有進行什麼計算,這樣並不符合Compute Shader的名號,所以這裡加一點簡單的計算,實現在Compute Shader中給一個貼圖設定顏色,然後在C#中把這張圖設定到一個Cube上。
效果如圖:
C# 部分:
m_rt = new RenderTexture(Width, Height, 0, RenderTextureFormat.ARGB32);
m_rt.enableRandomWrite = true;
m_rt.Create();
computeShader.SetTexture(kernelIndex, "ResultTex", m_rt);
// 在Shader中需要用到X維和Y維的資料作為座標去讀取和設定Texture2D的畫素,因此需要給X維和Y維的thread group設定數值,Z維的thread group數量為1即可 //
computeShader.Dispatch(kernelIndex, 32, 32, 1);
Shader 部分:
#pragma kernel CSMain_Texture
RWTexture2D<float4> ResultTex;
[numthreads(32,32,1)]
void CSMain_Texture (uint3 id : SV_DispatchThreadID)
{
float r = (id.x > 256 && id.x < 768 && id.y > 256 && id.y < 768) ? 1 : 0;
float b = 1 - r;
ResultTex[id.xy] = float4(r, 0, b, 1);
}
以上程式碼中需要注意的地方有:
- 建立的RenderTexture尺寸為1024*1024
- C#中的
computeShader.Dispatch(kernelIndex, 32, 32, 1);
結合Shader中的[numthreads(32,32,1)]
,可以計算出 X維度一共有 3232個執行緒,Y維也是3232個執行緒,這樣就能保證id.xy
的範圍可以在[0,1024]之間,能夠確保所有畫素點都被設定了顏色 - 需要設定
m_rt.enableRandomWrite = true
並且一定要執行create方法m_rt.Create();
,否則Shader執行結束畫素也不會被修改
計算結果儲存到Buffer中
相比於把計算結果儲存到一張Texture中,可能把計算結果儲存到一個Buffer中會更靈活些,因為可以在Buffer中儲存你自定義的結構體(struct),操作是這樣:
- 在C#中頂一個結構體,其中聲明瞭希望放到Compute Shader中取計算的資料,比如位置資訊 float4 pos,matrix4x4 之類的
- 定義一個ComputeBuffer物件,用來把構體資料傳遞給Shader
- 呼叫
ComputeShader.SetBuffer
方法把ComputeBuffer物件傳遞給指定的Kernel,並指定thread group num - 在Compute Shader中也定義這樣一個結構體,不必名稱一樣,但是結構體中資料的形式必須和C#中的保持一致,即資料型別應該是Shader中對應的型別,比如 float4,float4x4之類的。這篇文章 裡的描述可能更能準確的表達意思:
We also need to define this data type inside our shader, but HLSL doesn’t have a Matrix4x4 or Vector3 type. However, it does have data types which map to the same memory layout.
- Compute Shader中同時定義一個
RWStructuredBuffer<Data>
物件,即可讀寫的buffer,其中<Data>
就是上一步中定義的結構體 - 然後根據主函式的引數作為索引,對
RWStructuredBuffer<Data>
物件進行操作,在C#中通過ComputeBuffer.GetData(Array);
方法獲取Shader的計算結果,用於後續使用
說完了大致流程下面開始具體實現一下,這個測試要實現這樣一個功能:定義100個物體,在C#端構造好100個matrix4x4矩陣(包含位置和縮放),然後傳給Compute Shader,在Compute Shader中完成矩陣和向量的計算,然後在C#端獲取計算結果,把位置和縮放設定給100個物體。
效果圖:
Sounds cool hum? Let’s do this.
C# 主要程式碼:
// 初始化m_dataArr //
InitDataArr();
m_comBuffer = new ComputeBuffer(m_dataArr.Length, sizeof(float) * Stride);
m_comBuffer.SetData(m_dataArr);
computeShader.SetBuffer(kernelIndex, "ResultBuffer", m_comBuffer);
// 在Shader中只需要用到X維的資料作為陣列索引,因此只需要給X維的thread group設定數值,Y維和Z維的thread group數量為1即可 //
computeShader.Dispatch(kernelIndex, 32, 1, 1);
// 初始化傳給GPU的資料 //
void InitDataArr()
{
if (m_dataArr == null)
{
m_dataArr = new DataStruct[MaxObjectNum];
}
const int PosRange = 10;
for (int i = 0; i < MaxObjectNum; i++)
{
m_dataArr[i].pos = new Vector4(0, 0, 0, 1);
m_dataArr[i].scale = Vector3.one;
Matrix4x4 matrix = Matrix4x4.identity;
// 位移資訊 //
matrix.m03 = (Random.value * 2 - 1) * PosRange;
matrix.m13 = (Random.value * 2 - 1) * PosRange;
matrix.m23 = (Random.value * 2 - 1) * PosRange;
// 縮放資訊 //
matrix.m11 = Random.value * 2 + 1; // 從[0,1]對映到[1,3] //
matrix.m22 = Random.value * 2 + 1;
matrix.m33 = Random.value * 2 + 1;
m_dataArr[i].matrix = matrix;
}
}
Shader 程式碼:
#pragma kernel CSMain_Buffer
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> ResultTex;
struct Data
{
float4 pos;
float3 scale;
float4x4 matrix_M;
};
[numthreads(16,1,1)]
void CSMain_Buffer (uint3 id : SV_DispatchThreadID)
{
ResultBuffer[id.x].pos = mul(ResultBuffer[id.x].matrix_M, ResultBuffer[id.x].pos);
ResultBuffer[id.x].scale = mul((float3x3)ResultBuffer[id.x].matrix_M,
ResultBuffer[id.x].scale);
}
C#中執行緒組數量為 32,computeShader.Dispatch(kernelIndex, 32, 1, 1);
,Shader中X執行緒組中執行緒數量是 16,[numthreads(16,1,1)]
,32*16 = 512,而我們只有100個物體,所以其實X組裡設定4個執行緒就可以滿足需求,4*32=128,大於100,即寫成[numthreads(4,1,1)]
也可以完成任務。
完整程式碼
最後把兩部分結合到一起
C#部分:
using System;
using UnityEngine;
using Random = UnityEngine.Random;
public class ComputeShaderTest : MonoBehaviour
{
public ComputeShader computeShader;
public EMethod method;
public Transform prefab;
// KernelName //
private const string KernelName_Texture = "CSMain_Texture";
private const string KernelName_Buffer = "CSMain_Buffer";
// 方式1要用到的變數 //
private RenderTexture m_rt;
private const int Width = 1024;
private const int Height = 1024;
private Material m_material;
private Transform m_object;
// 方式2要用到的變數 //
private const int MaxObjectNum = 100;
private ComputeBuffer m_comBuffer;
private DataStruct[] m_dataArr;
private Transform[] m_objArr;
private Material[] m_materialArr;
public enum EMethod : int
{
RenderTexture = 0, // 方式1: 使用 RenderTexture 來儲存結算結果 //
ComputerBuffer = 1, // 方式2: 使用 ComputeBuffer 來儲存結算結果 //
}
struct DataStruct
{
public Vector4 pos;
public Vector3 scale;
public Matrix4x4 matrix;
}
private const int Stride = sizeof(float) * (4 + 3 + 16);
void Start()
{
switch (method)
{
case EMethod.RenderTexture:
m_object = Instantiate(prefab);
m_object.position = Vector3.zero;
m_object.localScale = Vector3.one*5;
MeshRenderer render = m_object.GetComponent<MeshRenderer>();
if (render != null)
{
m_material = render.material;
}
break;
case EMethod.ComputerBuffer:
GameObject parent = new GameObject("Parent");
parent.transform.position = Vector3.zero;
// 初始化物體陣列 //
m_objArr = new Transform[MaxObjectNum];
for (int i = 0; i < MaxObjectNum; i++)
{
Transform obj = Instantiate(prefab);
obj.transform.SetParent(parent.transform);
obj.transform.localPosition = Vector3.zero;
obj.transform.localScale = Vector3.one;
m_objArr[i] = obj;
}
break;
}
//uint x = 0;
//uint y = 0;
//uint z = 0;
獲取的是shader檔案中的數值, 即 [numthreads(X, X, X)] 中的數值 //
//computeShader.GetKernelThreadGroupSizes(kernelIndex, out x, out y, out z);
//Debug.LogFormat("x = {0}, y = {1}, z = {2}", x, y, z);