在寫shader的時(shí)候糖儡,我們通常會(huì)創(chuàng)建一個(gè)unlit shader或者standard surface shader伐坏,但是,至少在我的工作中休玩,從來沒有創(chuàng)建過compute shader著淆,這個(gè)東西是干嘛用的呢劫狠?帶著這個(gè)疑問,我們今天來一探究竟永部。
談起compute shader独泞,我們要先了解一個(gè)概念,叫GPGPU(General-purpose computing on graphics processing units)苔埋。根據(jù)Wikipedia的介紹
它是利用處理圖形任務(wù)的圖形處理器來計(jì)算原本由中央處理器處理的通用計(jì)算任務(wù)懦砂。這些通用計(jì)算任務(wù)通常與圖形處理沒有任何關(guān)系。
那么组橄,專門為圖形任務(wù)所設(shè)計(jì)的圖形處理器是如何怎么能處理通用計(jì)算任務(wù)的呢荞膘?這不是在搶CPU的飯碗么?的確玉工,早期的顯卡是沒有這種功能的羽资。在很久很久以前,那時(shí)老黃還沒成立NVIDIA遵班,顯卡市場(chǎng)還是Voodoo稱霸的時(shí)候屠升,顯卡里有兩種單元,一種專門處理頂點(diǎn)狭郑,叫做vertex unit腹暖,另一種專門處理像素,稱作pixel unit翰萨。然而脏答,隨著渲染場(chǎng)景越來越復(fù)雜(想想游戲史,是不是游戲看起來越來越逼真了亩鬼?)殖告,這種模式不利于負(fù)載均衡,而且這時(shí)候人們也希望顯卡能做一些通用計(jì)算的需求辛孵,圖形渲染不再是唯一的需求丛肮,所以GPGPU在這時(shí)開始浮出水面。到了2006年底及2007年初魄缚,老黃拿出了他的GeForce 8800 GTX,AMD也拿出了Radeon HD 2800焚廊,unified shaders的時(shí)代來臨了冶匹。什么叫unified shaders?之前專門處理頂點(diǎn)的vertex unit我們需要為它寫vertex shader咆瘟,專門處理像素的pixel unit我們需要為它寫pixel shader嚼隘,到了unified shaders時(shí)代,不管vertex shader也好袒餐,pixel shader也好飞蛹,顯卡都會(huì)用一種unit來處理谤狡,這時(shí),GPGPU變成了可能卧檐。
現(xiàn)在回到compute shader墓懂,它是一種運(yùn)行在顯卡上卻不在普通渲染管線上的程序,利用它可以做大型并行的GPGPU算法霉囚,以此來獲得比CPU快很多倍的計(jì)算能力捕仔。不過在獲得這種能力之前,我們先來看看如何使用這種shader盈罐。
首先是C#腳本榜跌,用于掌控全局
public Texture inputTex;
public ComputeShader computeShader;
public RawImage image;
void Start(){
RenderTexture t = new RenderTexture(inputTex.width,inputTex.height,24);
t.enableRandomWrite = true;
t.Create();
image.texture = t;
image.SetNativeSize();
int kernel = computeShader.FindKernel("Gray");
computeShader.SetTexture(kernel,"inputTexture",inputTex);
computeShader.SetTexture(kernel,"outputTexture",t);
computeShader.Dispatch(kernel,inputTex.width / 8, inputTex.height / 8,1);
}
其次是compute shader,所有的計(jì)算都放在這里
// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel Gray
Texture2D inputTexture;
// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> outputTexture;
[numthreads(8,8,1)]
void Gray (uint3 id : SV_DispatchThreadID)
{
float r = inputTexture[id.xy].r;
float g = inputTexture[id.xy].g;
float b = inputTexture[id.xy].b;
float res = r * 0.299 + g * 0.587 + b * 0.114;
outputTexture[id.xy] = float4(res,res,res,1);
}
這里我實(shí)現(xiàn)的功能是將一張彩色圖轉(zhuǎn)成灰階圖盅粪,雖然體現(xiàn)不出GPU的強(qiáng)大并行計(jì)算能力钓葫,但能起到如何使用compute shader的作用O(∩_∩)O~
首先我們?yōu)橐敵龅幕译A圖準(zhǔn)備一個(gè)地方,名叫t
票顾,t的屬性enableRandomWrite
必須為true础浮,這樣才能將數(shù)據(jù)寫入。然后通過FindKernel
方法找kernel库物,里面?zhèn)魅氲膕tring就是compute shader中#pragma kernel Gray定義的名字Gray霸旗,當(dāng)然你想起什么名字就起什么名字,可以隨便改的戚揭,不過相應(yīng)的void Gray (uint3 id : SV_DispatchThreadID)
這個(gè)地方的名字要一致诱告。然后用SetTexture
方法將數(shù)據(jù)設(shè)置好,由于inputTexture
是讀取數(shù)據(jù)用的民晒,所以對(duì)應(yīng)在compute shader里面的變量類型是只讀型的Texture2D精居,而outputTexture
的輸出數(shù)據(jù)用的,所以用讀寫型的RWTexture2D潜必。有朋友可能要問了靴姿,RWTexture2D這個(gè)怎么沒見過啊。原來磁滚,unity中的compute shader是遵循DirectX 11語法的佛吓,所以這個(gè)RWTexture2D是HLSL里的類型,詳見微軟文檔https://docs.microsoft.com/en-us/windows/win32/direct3dhlsl/sm5-object-rwtexture2d垂攘。
最后到了調(diào)用Dispatch
方法的時(shí)候维雇,也意味著整段程序都設(shè)置完畢,要開始工作了晒他。從文檔里可以看出需要傳四個(gè)參數(shù)進(jìn)去吱型,分別是kernelIndex、threadGroupsX陨仅、threadGroupsY津滞、threadGroupsZ铝侵,這些是什么意思呢?
kernelIndex很好解釋触徐,就是剛剛FindKernel
方法返回的值咪鲜。其余的參數(shù),從淺層次上講锌介,就是要讓threadGroupsX * numthreads.x = 圖片寬嗜诀,threadGroupsY * numthreads.y = 圖片高,threadGroupsZ大部分時(shí)間下都是1孔祸。這里的numthreads就是compute shader里的[numthreads(8,8,1)]
隆敢。
另外有個(gè)限制條件是在shader model 5的平臺(tái)下numthreads.x *numthreads.y * numthreads.z <= 1024,numthreads.z <= 64崔慧,(在shader model 4.5的平臺(tái)下這個(gè)數(shù)字是768拂蝎,numthreads.z <=1,再往下的shader model則不支持compute shader了)惶室。
還有要注意的是由于架構(gòu)問題温自,一個(gè)線程組里有幾個(gè)線程需要結(jié)合硬件,NVIDIA的架構(gòu)下最好是32的倍數(shù)個(gè)線程皇钞,AMD架構(gòu)下最好為64的倍數(shù)個(gè)線程悼泌。
更進(jìn)一步,我們來看微軟文檔里的一張圖夹界。
threadGroupsX馆里、threadGroupsY、threadGroupsZ代表著你要開多少組線程可柿,每個(gè)線程組里面有多少個(gè)線程是由numthreads里的參數(shù)決定的鸠踪。拿我寫的這段代碼舉例,Dispatch
的時(shí)候開了128 * 128 * 1組的線程組复斥,每組線程組里面有8 * 8 * 1個(gè)線程营密,128 * 8 = 1024,這里我用的圖片的長和寬都是1024目锭,即每個(gè)線程都在處理圖片上的某一個(gè)像素评汰。void Gray (uint3 id : SV_DispatchThreadID)
這邊的id即為每個(gè)線程的index。那如果numthreads設(shè)為[numthreads(64,4,1)]
痢虹,那么Dispatch
的時(shí)候可以設(shè)為Dispatch(kernel,inputTex.width / 64, inputTex.height / 4,1);
键俱。
文檔圖片2中還提到了SV_GroupThreadID
,SV_GroupID
,SV_GroupIndex
,這些也是用來索引線程的世分,具體關(guān)系看彥霖大佬的圖就明白了。
Group ID 一看就懂 :
Group Thread ID 一看就懂 :
Group Index 一看就懂 :
接下來我們來看如何用compute shader進(jìn)行簡單計(jì)算任務(wù)缀辩,而不是處理貼圖臭埋。代碼如下
CS腳本
public ComputeShader csBuffer;
ComputeBuffer buffer;
struct MyInt{
public int val;
public int index;
};
void Start()
{
CSFib();
}
public void CSFib(){
MyInt[] total = new MyInt[32];
buffer = new ComputeBuffer(32,8);
int kernel = csBuffer.FindKernel("Fibonacci");
csBuffer.SetBuffer(kernel,"buffer",buffer);
csBuffer.Dispatch(kernel,1,1,1);
buffer.GetData(total);
for (int i = 0; i < total.Length; i++)
{
Debug.Log(total[i].val);
}
}
private void OnDestroy() {
buffer.Release();
}
compute shader
#pragma kernel Fibonacci
struct MyInt{
int val;
int index;
};
RWStructuredBuffer<MyInt> buffer;
int Fib(int n){
int a = 0;
int b = 1;
int res = 0;
for(int i=0;i<n;i++){
res = a + b;
a = b;
b = res;
}
return a;
}
[numthreads(32,1,1)]
void Fibonacci (uint3 id : SV_DispatchThreadID)
{
buffer[id.x].val = Fib(id.x);
buffer[id.x].index = id.x;
}
菲波那切數(shù)列我想大家都應(yīng)該知道吧踪央?那么用GPU來算菲波那切數(shù)列就是以上代碼在實(shí)現(xiàn)的內(nèi)容了。為了讓大家看看在compute shader中如何使用自定義結(jié)構(gòu)體瓢阴,我舍棄了int類型而使用了自定義結(jié)構(gòu)體MyInt畅蹂,其中val存菲波那切數(shù)列中每一項(xiàng)的值,index存的是值所對(duì)應(yīng)的索引荣恐。
這里多出來了一個(gè)ComputeBuffer類型的buffer液斜,用于存儲(chǔ)計(jì)算得到的值,可以看到后面要用
GetData
方法把數(shù)值從GPU里面拿出來叠穆。在new這個(gè)ComputeBuffer的時(shí)候我們需要傳入兩個(gè)參數(shù)少漆,從文檔上來看第一個(gè)是count,我需要輸出32個(gè)菲波那切數(shù)列硼被,就填32示损;第二個(gè)是stride,代表每個(gè)元素的長度嚷硫,由于自定義結(jié)構(gòu)體MyInt有兩個(gè)int類型的屬性检访,所以這里的stride為8。其余內(nèi)容通過上面的講解仔掸,我想應(yīng)該不難理解了脆贵。
當(dāng)然,compute shader能做的遠(yuǎn)遠(yuǎn)不止這些起暮,來看看大佬們把compute shader玩出了什么花樣卖氨。
還有很多很多應(yīng)用在這就不一一列舉了焙矛,以及,以上的我都無力實(shí)現(xiàn)残腌,這么菜讓大家見笑了(⊙o⊙)…
參考
Introduction to compute shaders
【風(fēng)宇沖】Shader:二十八ComputeShaders
Unity 使用 GPGPU 計(jì)算村斟,使用 ComputeShader 將圖片轉(zhuǎn)成灰階圖
numthreads
Unity 3D : ComputeShader 全面詳解
Compute Shader次世代優(yōu)化方案