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文件
打開文件可以看到如下

//第一個紅框中,聲明了一個kernel,相當於main函數。在一個cs文件里可以定義多個不同的kernel方法
#pragma kernel CSMain
//第二個紅框,定義前面聲明的CSMain函數
void CSMain(uint3 id:SV DispatchThreadID){};
在CSMain函數上面的numthreads(8,8,1)]是什么?我們需要了解一下線程組和線程的概念
線程組、線程
當GPU執行CS時,會將其分成幾個組(線程組),安排它們獨立和並行運行。每個小組由多個線程組成。

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

上半張圖是一個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。我們需要用對應的變量存儲起來。變量命名是和前面的標識符獲取的屬性名對應的;

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

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/;
