Unity ComputeShader


最近一直想面對大規模程序時,如何提高運算速度,100個怪物循環100次沒有問題,但是處理的過程會特別龐大,所以考慮到使用多線程,unity的單線程,而unity自帶的dots系統也不知道什么時候成熟,不想造輪子所以jobsystem真心不想用,在網上偶然間看到了一個關於鳥群算法對Computeshader的使用,查閱了很多資料后終於暫時入門:簡單說就是在顯卡上扣出一部分性能給游戲的數值做運算。

首先轉載一張經典的圖和某位原作者的話:

個人理解:
numthreads 定義了一個三維的線程結構

如果我們在程序的Dispatch接口發送了(5,3,2)這樣的結構,就會生成5x3x2個線程組,其中每個組的線程結構由ComputeShader中的numthreads定義,圖中numthreads定義了10x8x3的三維結構,由此,我們可以分析4個HLSL關鍵詞的定義。
  • SV_GroupThreadID 表示該線程在該組內的位置
  • SV_GroupID 表示整個組所分配的位置
  • SV_DispatchThreadID 表示該線程在所有組的線程中的位置
  • SV_GroupIndex 表示該線程在該組內的索引

我自己的表述:

如果我們有一個長度為20的數組需要處理,為了同時處理者10000個數據,我們需要向顯卡申請至少10000個線程,此時就可以申請10000個線程,但是呢這10000個線程太大了,所以呢,把這10000個線程的布局成一個2維數組,但是這個二位數組還是太大,所以呢就把這個二維數組再次切割成一個1維數組,這樣整個表就切割成了一個3維數組;但是呢這個三維數組還是太大,以一個三維索引R范圍為單元,再次按照上面的方法再切割成一個三維數組G,結果就是三維單元R就是numthreads(x,y,z),而三維數組G就是Dispatch(x,y,z)。這里也利於理解,因為顯卡的處理單元過於龐大,已經超過了一個數組索引的范圍,或者顯卡的設計就是以層層陣列的方式制作的,因此這種超維度結構可以更好的索引到需要的資源,而它申請索引的時候完全是靠數字電路硬件獲取(學過計算機電子的都知道)。另外這樣分塊的好處將在下面的例子中提到。

所以再次對上面的索引做出解釋:

  • SV_GroupThreadID 表示該線程在該組內的位置----------即這個三維單元內的單元數組空間中索引
  • SV_GroupID 表示整個組所分配的位置----------即該線程所在的組在組空間中的索引
  • SV_DispatchThreadID 表示該線程在所有組的線程中的位置----------即不將整個三維單元分組的情況下的三維索引
  • SV_GroupIndex 表示該線程在該組內的索引----------即這個三維單元內的數組轉換成一個一維數組時所在的索引

 例子:

對一個結構體數組內的所有結構內的數乘以2:

C#代碼:

using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using UnityEngine;

public class MyFirstComputeShader : MonoBehaviour
{
    public ComputeShader computeShader = null;

    data[] inputDatas = new data[3];//輸入數組
    data[] outputDatas = new data[3];//結果數組

    struct data
    {
        public float a;
        public float b;
        public float c;
    }

    private void InitData()
    {
        inputDatas = new data[3];
        outputDatas = new data[3];

        UnityEngine.Debug.Log("---------------cpu輸入------------------------");
        for(int i = 0; i < inputDatas.Length; i++)
        {
            inputDatas[i].a = i * 3 + 1;
            inputDatas[i].b = i * 3 + 2;
            inputDatas[i].c = i * 3 + 3;
            UnityEngine.Debug.Log(inputDatas[i].a + "," + inputDatas[i].b + "," + inputDatas[i].c);
        }
    }

    private void ToComputeShader(ref data[] i,ref data[] o)
    {
        //data 數據里面float*3,而一個float的字節為4字節,所以3*4
        ComputeBuffer inputBuffer = new ComputeBuffer(i.Length, 12);
        ComputeBuffer outputBuffer = new ComputeBuffer(o.Length, 12);

        //拿到核心
        int k = computeShader.FindKernel("CSMain");

        inputBuffer.SetData(i);

        //寫入gpu
        computeShader.SetBuffer(k, "inputDatas", inputBuffer);
        computeShader.SetBuffer(k, "outputDatas", outputBuffer);

        //計算再輸出到cpu
        computeShader.Dispatch(k, 1, 1, 1);

        outputBuffer.GetData(o);

        UnityEngine.Debug.Log("---------------gpu輸出------------------------");
        for(int j = 0; j < o.Length; j++)
        {
            UnityEngine.Debug.Log(o[j].a + "," + o[j].b + "," + o[j].c);
        }

        //釋放內存
        inputBuffer.Release();
        outputBuffer.Release();
    }

    private void Update()
    {
        if (!Input.GetKeyDown(KeyCode.Space))
            return;

        Stopwatch sw = new Stopwatch();

        sw.Start(); //計時器開始
        
        InitData();
        ToComputeShader(ref inputDatas,ref outputDatas);

        sw.Stop();  //計時器結束
        UnityEngine.Debug.Log(sw.Elapsed); //開始到結束之間的時長

    }
}

computeShader代碼:

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

struct data
{
    float a;
    float b;
    float c;
};

//(cpu->gpu)
StructuredBuffer<data> inputDatas;
//(gpu->cpu)
RWStructuredBuffer<data> outputDatas;

//[numthreads(3,1,1)]
//void CSMain (uint3 id : SV_DispatchThreadID)
//{
//    //if (id > 2)return;
//    //計算一下
//    outputDatas[id.x].a = inputDatas[id.x].a * 2;
//    outputDatas[id.x].b = inputDatas[id.x].b * 2;
//    outputDatas[id.x].c = inputDatas[id.x].c * 2;
//}

[numthreads(3, 1, 1)]
void CSMain(uint id : SV_GroupIndex)
{
    //計算一下
    outputDatas[id].a = inputDatas[id].a * 2;
    outputDatas[id].b = inputDatas[id].b * 2;
    outputDatas[id].c = inputDatas[id].c * 2;
}

結果如下:

 

 因為我的數組長度是3,而numthreads(3,1,1),因此Dispatch(1,1,1),即申請了一個組,並且這個組內有3個線程。這里可以改進為numthreads(1,1,1),而dispatch(n,1,1),這樣n的大小就是總的線程數量。關於這個問題以及常常出現的誤區和某位仁兄討論過如下:

 

解決:

 

 其實本質就是我們申請到的是整個組區域中的所有線程,因此我們應該處理一下線程id超出的情況(貌似是難免的,當數組的不整齊的時候,就會有多余的線程出現),但是不知道為什么,顯卡處理程序的時候,索引值超過緩存的長度也不會報錯。

當要處理一個圖片的時候比如圖片大小是1024*1024,那么可以(512*2,512*2,1),即numthreads(2,2,1);dispatch(512,512,1);

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM