一直以來都想試着自己翻譯一些東西,現在發現翻譯真的很不容易,如果你直接把作者的原文按照英文的思維翻譯過來,你會發現中國人讀起來很是別扭,但是如果你想完全利用中國人的語言方式來翻譯,又怕自己理解的不到位,反而與作者的願意相悖。所以我想很多時候,國內的譯者也是無奈吧,下次再看到譯作也會抱着一些感同身受的態度去讀。這是我第一次翻譯整篇文章,能力有限,望見諒,翻譯不好的地方也希望大家指出來。
其實ComputeShader在Unity中出現已經有蠻長的一段時間了,因為自己一直對Shader比較感興趣,所以最近也在嘗試着學習ComputeShader,從國外的論壇上看到討論說明已經有很多人在用了,但是在國內幾乎沒有什么實際應用。而且Unity官方文檔一向的模棱兩可原則,幾乎在文檔上看不到太多有用信息,只能去Google了一下,就發現了這篇文章,這篇文章對ComputeShader講的並不是很深入,而且我覺得他英文原文有些地方講述的也並不是十分清楚,不過對於ComputeShader的基本的一些描述都講到了,作為一個參考材料是非常不錯的。
原文鏈接:http://kylehalladay.com/blog/tutorial/2014/06/27/Compute-Shaders-Are-Nifty.html
下面是原文翻譯:
我很喜歡頂點V&F Shaders(就是咱們常用的頂點片元着色器—譯者注)的簡單。它們只做一件事情(把頂點和顏色處理后顯示到屏幕上),並且它們做的非常出色.但是有時候,這種簡單會讓你感覺到被限制了,你可能發現你自己正盯着一連串發生在CPU上的矩陣運算,拼命的想怎么才能把它們存在圖片里呢!
或許只有我才擔心這個吧.但是不管怎樣,Compute Shader 解決了這個問題,而且它們用起來非常的簡單,所以呢,我今天就像來講一講一些關於Compute Shader的基礎.首先我會帶你來瀏覽一下Unity為你自動生成的Compute Shader代碼.然后再以一個利用數據結構緩沖區(structured buffer of data)為例子的Compute Shader作為結束.
Compute Shader可以被用來控制粒子的位置
Compute Shader到底是什么?
簡單來說,Compute Shader 就是一段運行在 GPU上的程序,這段程序並不需要用來處理網格數據或者是紋理數據的,它是工作在OpenGL或者DirectX的內存空間中的(不像OpenCL那樣擁有自己的內存空間),它們可以輸出緩沖數據或者紋理並且在多個執行的線程間共享內存。
現在Unity只支持 DX11(DirectX 11) Compute Shaders,但是如果你把OpenGL更新到4.3版本,我們這些mac 粉也是有希望使用到它的。
這也意味着,這會是迄今為止第一個 只針對Windows平台的 教程.所以如果你現在使用不到windows的機器,下面這些東西可能就對你沒什么幫助了。
那它們擅長什么呢?(又不擅長什么呢)
兩個詞:數學和並行.任何一個涉及到對數據集中的每一個元素都進行一同樣一系列操作(不包含條件分支)的問題都非常適合它。而且操作集合越大,你從GPU中獲得的回報就越多。
條件分支會嚴重影響效率,因為GPU並不非常擅長處理這種情況。但是這和寫V&F Shader並沒有太大區別,所以如果你對V&F Shader有些經驗的話,你編寫它們也不會遇到太多麻煩。
這里還有一個還有延遲的問題,把數據從GPU內存傳遞回CPU是要花費些時間的 ,這可能會成為你利用Compute Shader的一個瓶頸。不過你也可以通過優化內核程序(kernels)讓它們工作在盡量少的緩沖數據上來一定程度上減少傳遞耗費的時間。但是這問題還是不可能被完全避免。
明白了沒?好,讓我們開始吧
既然我們是利用DX的,那Compute Shader 就要用HLSL語法來寫。這和其他的Shader 語言沒有太大區別。如果你寫過Cg或者GLSL的話,那就沒有什么問題了(其實這也是我第一次寫HLSL)。
你首先要做的事情是創建一個ComputeShader,Unity的Project面板中已經有這一項了,所以這一步應該是很簡單,如果你打開這個新創建的問價,你就會看到下面這些自動生成的代碼(為了簡潔,我把注釋都刪掉了).
1 #pragma kernel CSMain 2 3 RWTexture2D<float4> Result; 4 5 [numthreads(8,8,1)] 6 void CSMain (uint3 id : SV_DispatchThreadID) 7 { 8 Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0); 9 }
這段代碼真的是非常適合用來理解Compute Shader啊,下面我們來一行一行的看吧:
1 #pragma kernel CSMain
這里定義了程序的入口點(相當於Compute Shader的main函數).一個Compute Shader文件可以有多個這樣的方法聲明,你可以在腳本里調用任何你需要的一個。
1 RWTexture2D<float4> Result;
這聲明了一個變量,Shader會利用這個變量包含的數據來工作,因為我們不初始利用網格數據來工作的,所以你必須明確的聲明你的Compute Shader要讀或者寫哪些數據。數據類型前面的”RW”表明了這個變量是可讀可寫的。
1 [numthreads(8,8,1)]
這一行指定了當前Compute Shader要產生的線程組的大小。GPU通過創建同時運行的多個線程,擁有大規模並行處理能力。線程組規定了如何組織這些生成的線程。在上面的代碼中,我們指定了我們希望每個線程組包含64個線程。就像一個二維數組一樣。
決定線程組的最優大小是一個很復雜的問題,這和你的目標硬件有很大的關系。一般來講,把你的GPU當成一個流處理器的集合,其中的每一個處理器都能同時執行X個線程。一個處理器一次運行一個線程組,所以理想的情況,你希望你的線程組包含X個線程來充分利用處理器。我設置的值只是我根據我自己的情況,所以比起我給你們提供關於設置最優值的意見,你們還是自己去Google吧(然后在Twitter分享:D)
剩下的Shader代碼都是比較正常的了,內核程序函數根據運行的線程的ID來決定它應該處理哪一個像素.然后把一些數據寫到緩沖里面去。簡單么?
真的來運行Shader
因為Compute Shader 不是利用網格數據來運行的,很顯然我們不能把它掛在一個Mesh上讓他運行。Compute Shader 需要在腳本中裝配和調用,像下面一樣:
1 public ComputeShader shader; 2 3 void RunShader() 4 { 5 int kernelHandle = shader.FindKernel("CSMain"); 6 7 RenderTexture tex = new RenderTexture(256,256,24); 8 tex.enableRandomWrite = true; 9 tex.Create(); 10 11 shader.SetTexture(kernelHandle, "Result", tex); 12 shader.Dispatch(kernelHandle, 256/8, 256/8, 1); 13 }
這段代碼有很多需要說明的地方,首先是在創建一個RenderTexture之前設置一下他的enableRandomWrite屬性。這讓你的Compute Shader擁有對這張紋理的寫權限。如果不設置這個標記,在你的Shader中就不能把這個貼圖作為寫入目標。
接下來我們需要指定一下我們想調用Compute Shader中的哪個內核程序,FindKernel 方法需要一個字符串參數作為名字,這個名字可以Shader中相關內核程序的任意一個。就像我們在Shader一開頭寫的那個一樣。一個Compute Shader在單個文件里可以擁有多個內核程序。
ComputeShader.SetTexture這個調用讓我們可以把Shader需要的數據從CPU內存傳遞到GPU內存。才兩個內存之前傳遞數據會給我們的程序引入延遲,程序效率被降低的程度正比於你需要傳遞的數據大小。由於這個原因。如果你打算每一幀都運行你的Shader,你最好認真的優化一下你真正需要操作多少數據。
傳遞給Dispatch方法的三個整數定義了,我們想生成的線程組數量。回想一下,每一個線程組的大小是通過Compute Shader中的numthreads來指定的,所以在上面的例子中,我們一共生成的線程數量如下:
32*32個線程組*64(每個線程組中線程的數量) = 65536個線程。
這樣下來最后就相當於一個線程對應我們要處理的RenderTexture中的一個像素,也就是內核程序的一次調用只處理一個像素。
現在我們知道了如何編寫ComputeShader和如何處理紋理內存,讓我們看看我們可以用這些東西做什么。
結構緩沖(Structured Buffers)真是個好東西
處理紋理數據和我們之前的V&F Shader有點像,這讓我並不是感到很激動。是時候去解放我們的GPU了,讓它可以利用任何的數據。是的,這是可以做到的,就像它聽起來的一樣好。
結構緩沖就是一個只包含一種數據類型的數據序列,你可以創建一個存儲float類型的結構緩沖,或者一個int類型的,但是不能創建一個同時存儲float和int的。你可以像下面一樣在ComputeShader中聲明結構緩沖:
1 StructuctedBuffer<float> floatBuffer; 2 RWStructuredBuffer<int> readWriteIntBuffer;
讓結構緩沖真正有趣的其實是它可以用來存儲struct類型的數據。我們會在第二個例子中進行說明。
對於我們的的例子,我們打算給我們的ComputeShader傳遞一些頂點數據,同時再為每個頂點數據傳遞一個我們想對他進行何種變換的矩陣。我們可以通過分別創建兩個緩沖來實現(一個用來存Vector3數據,一個用來存Matrix4X4數據)。我們很容易把兩種結構抽象成一種點/矩陣對結構,那我們就這么辦吧。
在我們的C#腳本中,我們定義了數據類型如下:
1 struct VecMatPair 2 { 3 public Vector3 point; 4 public Matrix4x4 matrix; 5 }
同樣我們也得在Shader中定義相應的結構,但是HLSL沒有提供Matrix4x4或者Vector3類型。不過它有和他們擁有同樣內存結構的數據類型。我們的Shader最后看起來會像下面一樣:
1 #pragma kernel Multiply 2 3 struct VecMatPair 4 { 5 float3 pos; 6 float4x4 mat; 7 }; 8 9 RWStructuredBuffer<VecMatPair> dataBuffer; 10 11 [numthreads(16,1,1)] 12 void Multiply (uint3 id : SV_DispatchThreadID) 13 { 14 dataBuffer[id.x].pos = mul(dataBuffer[id.x].mat, float4(dataBuffer[id.x].pos, 1.0)); 15 }
注意我們的線程組現在被組織成一個一維序列。線程組被設定成什么維度對性能是沒有影響的,所以你可以選一個最適合你的程序的設定。
在腳本里面創建一個一個結構緩沖和我們前面創建紋理的那個例子有些不同。對於一個緩沖,你需要指定緩沖中每一個元素占用的字節大小,對於我們的例子中的struct,它占用的字節大小實際上就是我們用到的float數量(Vector用3個,Matrix用16個)乘以每個float占用的字節大小(4字節)。創建過程如下:
1 public ComputeShader shader; 2 3 void RunShader() 4 { 5 VecMatPair[] data = new VecMatPair[5]; 6 //INITIALIZE DATA HERE 7 8 ComputeBuffer buffer = new ComputeBuffer(data.Length, 76); 9 int kernel = shader.FindKernel("Multiply"); 10 shader.SetBuffer(kernel, "dataBuffer", buffer); 11 shader.Dispatch(kernel, data.Length, 1,1); 12 }
現在我們需要把修改完的數據重新按照腳本中可以使用的格式傳遞回去,不想上面處理RenderTexture的例子。結構緩沖需要被明確的從GPU內存傳遞回CPU。以我的經驗,這是你在使用ComputeShader時候會遇到的最影響性能的點。我目前發現的唯一緩解它的辦法就是優化你的緩沖,從而讓它在不影響你使用的情況下盡量的小,並且只有在你真的非常需要的時候才從shader中向外傳遞數據。
把數據傳遞會給CPU的代碼非常的簡單,你需要做的就是用一個相同類型的緩沖區接收它。我們修改上面的腳本讓它把shader計算的結果回傳到第二個序列中,如下:
1 public ComputeShader shader; 2 3 void RunShader() 4 { 5 VecMatPair[] data = new VecMatPair[5]; 6 VecMatPair[] output = new VecMatPair[5]; 7 8 //INITIALIZE DATA HERE 9 10 ComputeBuffer buffer = new ComputeBuffer(data.Length, 76); 11 int kernel = shader.FindKernel("Multiply"); 12 shader.SetBuffer(kernel, "dataBuffer", buffer); 13 shader.Dispatch(kernel, data.Length, 1,1); 14 buffer.GetData(output); 15 }
這就是全部內容了,你應該去看看profiler,確切的感受一下從GPU傳遞數據給CPU需要耗費多少時間.但是我發現,如果你用你的Compute Shader處理一個足夠大的數據集的時候,這些代價是值得的。
如果你有任何問題(或者指出本文的錯誤),在twitter上給我發個信息,我不會給你寫Shader,但是我很願意指導你,Happy shading!
尊重他人智慧成果,歡迎轉載,請注明作者esfog,原文地址 http://www.cnblogs.com/Esfog/p/Translation_BeginStart_ComputeShader.html