Unity基礎—Computer Shader


Computer Shader是什么?

  Computer shader是一段運行在GPU上的一段程序。

什么時候用Computer shader?

  假如我們把一個cube當作單獨的點,用許多個(cube)點來組成一個變換矩陣。
  每幀cpu都需要對矩陣的點進行排序,批處理,將每個點位置復制給GPU,URP每幀需要執行兩次,DRP必須執行至少三遍。
  當100*100個點時,也許我們的cpu可以輕松應對,但如果我們想組成分辨率更高的圖形,1000 * 1000,一百萬個點時,CPU和GPU的工作量會大大的增加,從而失去流暢的體驗。
  而CS就是通過將工作轉移到GPU上,最大程度的減少CPU和GPU之前的通訊和數據傳輸量,從而提升渲染性能。總的來說,在需要高頻的重復計算時,我們使用CS;

創建一個計算着色器

Assets/Creats/Shader/Computer Shader,創建一個CS文件
打開文件可以看到如下

image

//第一個紅框中,聲明了一個kernel,相當於main函數。在一個cs文件里可以定義多個不同的kernel方法
#pragma kernel CSMain

//第二個紅框,定義前面聲明的CSMain函數
void CSMain(uint3 id:SV DispatchThreadID){};

在CSMain函數上面的numthreads(8,8,1)]是什么?我們需要了解一下線程組和線程的概念

線程組、線程

   當GPU執行CS時,會將其分成幾個組(線程組),安排它們獨立和並行運行。每個小組由多個線程組成。
image

最左邊的是一個dispatch,由它決定分成幾個線程組並行。如圖所示,圖中有3x2x3個thread groups(線程組)
中間的是一個thread group,由一個個線程組成,每個線程有自己的相對位置。圖中有4x4x2個線程,在我們上文提到的numthreads(8,8,1)],表示設置每個線程組的線程數8x8x1個;
最右邊的是單個線程。

需要注意的是,一個線程組中最大只支持1024個線程數

更近一步,看下圖
image
上半張圖是一個5x3x2的Dispatch,每個格子都代表着一個Thread Group
把坐標(2,1,0)的Thread Group打開,是一個10x8x3的Thread Group,每個格子里都是一個線程。
其中的幾個概念:
  SV_GroupThreadID:該線程在當前線程組中的坐標,如下半圖中箭頭指向坐標(7,5,0)
  SV_GroupID:該線程所在線程組在Dispatch的坐標(2,1,0);
  SV_DispatchThreadID:這是該線程全局唯一的ID,相當於在所有線程中該線程的坐標位置,算法為線程組大小*線程數大小+該線程坐標
  SV_GroupIndex:該線程在該線程組中的索引,即線程在這個線程組中排在第幾個位置;
  我們可以利用這些ID,定位我們的結構化緩沖區。

了解了這些概念,接下來我們可以做一個案例。通過計算着色器做一個動態的波浪矩陣;

1.首先創建一個C#文件,我們需要先創建組成矩陣的點,我們用Cube代替。

點的位置信息我們先不管,因為我們要交給計算着色器來計算。

 void Awake()
    {
        for (int i = 0; i < points.Length; i++) {
            points[i]= Instantiate(prefab);
            points[i].SetParent(transform);
        }
    }

2.接下來,我們需要一個緩沖區,用於給GPU計算的區域。通過new ComputeBuffer構造函數,第一個參數是我們要創建的緩沖區的長度,我們有一個矩陣的點 邊長*邊長的點的位置需要計算,所以我們第一個是resolution * resolution,第二個參數是每個點信息的內存大小,一個position是共有三個浮點數,所以是3 * 4個字節的大小;

  positions = new ComputeBuffer(resolution*resolution,12);

分配了緩沖區,我們還需要在disable的時候將緩沖區釋放

  private void OnDisable()
    {
        positions.Release();
        positions = null;
    }

3.還需要定義一個數組,用於存儲從GPU返回的位置信息。長度與我們的點數量是一樣的

   pointsArr = new Vector3[resolution * resolution];

Awake的代碼就是這些

   void Awake()
    {
        //位置緩沖區 在這里第一個參數是我們存放的矩陣點的數量 
        positions = new ComputeBuffer(resolution * resolution, 12);
        //從GPU返回的位置信息
        pointsArr = new Vector3[resolution * resolution];
        //點的實例數組
        points = new Transform[resolution * resolution];
        //創建點;
        for (int i = 0; i < points.Length; i++) {
            points[i]= Instantiate(prefab);
            points[i].SetParent(transform);
        }
    }

1.我們要GPU幫我們算出一個波浪矩陣的信息,那么總得給它傳遞一些信息數據才行。
要想要一個動態波浪的矩陣,隨着Time時間變化,Time這個信息我們需要傳過去。邊長,只有知道了邊長,GPU才知道我們的矩陣是什么構造,怎么波動。還需要給它把位置緩沖區傳過去,畢竟它需要靠這個給我們返回計算結果。我們通過它們的標識符進行傳遞。

   //獲得着色器屬性的存儲標識符
   static readonly int positionsId = Shader.PropertyToID("_Positions"),
   resolutionId = Shader.PropertyToID("_Resolution"),
   timeId = Shader.PropertyToID("_Time");
   void Update()
    {
        float time = Time.time;
	   //給着色器傳遞當前時間
        _ComputeShader.SetFloat(timeId, time);
	   //給着色器傳遞當前邊長
        _ComputeShader.SetInt(resolutionId, resolution);
	   //給着色器傳遞位置緩沖區
        _ComputeShader.SetBuffer(kernel, positionsId, positions);
    }

2.萬事具備,開始分派線程組,執行內核函數。線程組的分派也有些門道,比如我們現在是8080的矩陣,6400個點。而我們的一個線程組設置的是[8,8,1],那就是88*1=64點;那么怎么說也得把讓這些點有足夠的線程數用。那就是6400/64=100個組。如果多了幾個點,6500個點呢,那只能再把組數加上去。總之總組數,需要讓點夠用。但是也不能分配太多,否則會造成性能浪費。至於分配的組的形式,不管是[2,50,1],還是[100,1,1],怎么方便怎么分配;

	//獲取內核函數的索引
   kernel = _ComputeShader.FindKernel("CSMain");
    //分派線程組,執行內核函數
   _ComputeShader.Dispatch(kernel, resolution/8, resolution/8, 1);

3.現在GPU並發執行了它的內核函數,但是我們怎么獲取它計算的結果呢;我們通過GetData獲取緩沖區的數據,並將它復制給你傳進去的參數PointArr,我們開頭定義的用來存儲從GPU返回的位置信息的數組,最后根據返回的信息,將點位置進行更新即可

	  //從位置緩沖區獲取結果 將結果復制給pointsArr
        positions.GetData(pointsArr);
        for (int i = 0; i < pointsArr.Length; i++)
        {
            //將各個點的位置更新
            points[i].localPosition = pointsArr[i];
        }

再看看計算着色器是怎么運作的
1.剛剛從C#,也就是CPU段傳過來了哪些信息呢。時間_Time,邊長_Resolution,位置緩沖區_Positions。我們需要用對應的變量存儲起來。變量命名是和前面的標識符獲取的屬性名對應的;
image

    RWStructuredBuffer <float3> _Positions;
	float _Time;
	uint _Resolution;

2.有了這些數據我們可以開始在內核函數內計算 需要的位置信息;[numthreads(8, 8, 1)],根據前面的概念解釋,我們知道這是一個線程組的規格,也就是88的一個二維矩形為一個線程組。
我們通過一個id參數,后面加我們需要獲取的類型SV_DispatchThreadID,獲取到當前線程在所有線程中的三維坐標,因為我們是單個線程組和dispatch設置的都是二維坐標,所以呈現在我們面前的總線程應該是一個(線程組.x
dispatch.x)(線程組.ydispath.y)的二維矩形。而我們的點矩陣被總線程二維的
包含。下圖,我們假設線程組我設為[2,2,1],我們的邊長是5,所以把dispath設為[3,3,1],即9個線程組,這樣才可以完整覆蓋我們所有需要計算的點。但是有一行和一列是我們矩陣不需要的點,所以我們把這一行一列除外。即做了一個判斷,僅在id.x < _Resolution && id.y < _Resolution作為有效的點位置。
image

[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	//獲取當前索引
	uint index=id.x + id.y * _Resolution;
	float3 position;
	//獲取的id.x
	position.x = id.x;
	position.z = id.y;
	//根據x的位置和時間的變化,讓y的位置變化起伏
	position.y = sin(PI * (position.x/10 + _Time));
	if (id.x < _Resolution && id.y < _Resolution) {
		_Positions[index] = position;
	}
}

效果圖

image

C#完整代碼


using UnityEngine;
public class WaveRect : MonoBehaviour
{
    //綁定一個計算着色器
    [SerializeField]
    ComputeShader _ComputeShader = default;
    //我們的cube實例
    [SerializeField]
    Transform prefab = default;
    //定義矩陣邊長 配置成可控制的范圍10-100;
    [SerializeField,Range(10,100)]
    int resolution = 10;
    //儲存我們實例的數組
    Transform[] points;
    //定義結構化緩沖區 用於給計算着色器 計算我們需要的點 的位置
    ComputeBuffer positions;
    //獲得着色器屬性的存儲標識符
    static readonly int positionsId = Shader.PropertyToID("_Positions"),
    resolutionId = Shader.PropertyToID("_Resolution"),
    timeId = Shader.PropertyToID("_Time");
    //存放由計算着色器也就是Gpu返回的點位置信息
    private Vector3[] pointsArr;
    void Awake()
    {
        //位置緩沖區 在這里第一個參數是我們存放的矩陣點的數量 
        positions = new ComputeBuffer(resolution * resolution, 12);
        //從GPU返回的位置信息
        pointsArr = new Vector3[resolution * resolution];
        //點的實例數組
        points = new Transform[resolution * resolution];
        //創建點;
        for (int i = 0; i < points.Length; i++) {
            points[i]= Instantiate(prefab);
            points[i].SetParent(transform);
        }
    }
    int kernel;
     void Update()
    {
        float time = Time.time;
        //給着色器傳遞當前時間
        _ComputeShader.SetFloat(timeId, time);
        //給着色器傳遞當前邊長
        _ComputeShader.SetInt(resolutionId, resolution);
        //給着色器傳遞位置緩沖區
        _ComputeShader.SetBuffer(kernel, positionsId, positions);
        kernel = _ComputeShader.FindKernel("CSMain");
        //分派線程組,執行內核函數
        int count = Mathf.CeilToInt(resolution / 8);
        _ComputeShader.Dispatch(kernel, count, count, 1);
        //從位置緩沖區獲取結果 將結果復制給pointsArr
        positions.GetData(pointsArr);
        for (int i = 0; i < pointsArr.Length; i++)
        {
            //將各個點的位置更新
            Debug.Log(i +"====="+ pointsArr[i]);
            points[i].localPosition = pointsArr[i];
        }
    }

    private void OnDisable()
    {
        positions.Release();
        positions = null;
    }
}

CS完整代碼

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

// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWStructuredBuffer <float3> _Positions;
float _Time;
uint _Resolution;
[numthreads(8, 8, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
	//獲取當前索引
	uint index=id.x + id.y * _Resolution;
	float3 position;
	//獲取的id.x
	position.x = id.x;
	position.z = id.y;
	//根據x的位置和時間的變化,讓y的位置變化起伏
	position.y = sin(PI * (position.x/10 + _Time));
	if (id.x < _Resolution && id.y < _Resolution) {
		_Positions[index] = position;
	}
}

歡迎批評指正。原文博客http://xmxw.top/index.php/2021/04/19/unitybasic-computer-shader/;


免責聲明!

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



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