使用示例_在Unity中使用ComputeShader示例
寫這篇的緣由是最近老師給了一個UE4的工程,是一個海水模擬的Demo,實現了二十年前一篇paper的算法,paper的地址是:
http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.161.9102&rep=rep1&type=pdf?citeseerx.ist.psu.edu不過由于我還不夠熟悉UE4,加之UE4各種版本滿天飛,不同插件基于不同版本開發實在混亂。。。老師給的工程跑不起來,但至少看看代碼還是能知道在干啥的,于是嘗試在Unity里復現一下這個工程。這篇文章主要介紹Unity Compute Shader的用法流程,而不是IFFT模擬海水的算法,因為受限于貧瘠的數學水平,目前我還沒理解這個算法到底在干什么。。。
閱讀此文前你應該知道
- 基本的Unity操作及編寫C#腳本
- 一種Shader語言,GLSL / HLSL / Unity 的Shaderlab也行
閱讀此文后你會了解
- 在Unity中使用Compute Shader的流程
- 使用Compute Shader時要注意的問題
此文環境
- Unity 2019.4.11f1
- Windows 10
什么是Compute Shader
這要從GPU開始說起。GPU相比于CPU的一個特點是它特化了向量、矩陣和浮點運算能力,以眾多核心數和大顯存來實現極高的并行程度從而完成光柵化流程渲染。因此,我們可以利用GPU的能力做一些別的事情,不是要它的完整光柵化流程,而是它的高性能浮點和向量運算能力。由此誕生了Compute Shader,即專注于運算的Shader。
使用Compute Shader的基本流程
要使用Compute Shader的能力,樸素的想法讓我們關注三件事情:
- 如何進行數據傳輸,這包括把我要進行運算的數據傳給Compute Shader和取出運算結果
- 如何編寫Computer Shader里的具體運算指令
- 如何用C#腳本控制Compute Shader運行
下面就來分別講述這三個事情
數據傳輸
如果你寫過普通的shader,就知道傳統光柵化管線的shader往往是對輸入的texture等數據進行操作,結果輸出一個fixed4顏色。在Compute Shader中也是一樣。要運算的數據要存放在texture、float等數據結構中傳入shader,但輸出是一張texture。因此,每個compute shader至少需要一張輸出texture(不然你寫這個shader干啥?)。然后,如果你需要輸入的話,也至少需要一張輸入texture。將texture與Compute Shader中的變量綁定的方法,是ComputeShader.SetTexture(string shader中texture的變量名, Texture 要綁定的貼圖)。以貼圖方式輸入的數據相當于是每個線程不同的數據。對于那些各個線程相同的輸入數據,也就是一些uniform的數據(texture其實也是uniform的,只不過每個線程從中取出自己需要的那個像素而已),使用ComputeShader.SetFloats之類的方法。具體的代碼將在下個段落里一起說明,因為有些需要結合Compute Shader的結構才能說明白。
編寫ComputeShader
直接在資源里新建一個Compute Shader,里面會包含一段官方示例代碼。不過這里我們使用下面這些代碼解釋一下各個部分是做什么的(代碼都是完整的,只是劈開一段一段放而已)。
// Each #kernel tells which function to compile; you can have many kernels #pragma kernel CSMain一上來是一個#pragma kernel 指令,這個指令指示一個類似main函數一樣的東西。之后我們就會通過這個指令來在C#腳本中控制Compute Shader的運行。
uniform float4 width_height_Lx_Lz;Texture2D <float4> InputTexture; RWTexture2D<float4> OutputSurface0;接下來就聲明一些自己需要的變量,例如代碼中聲明了一個flaot4,一個InputTexture,一個outputTexture。這個input就是我們給shader輸入的要計算的數據,output就是計算結果;我們將在腳本中獲取它們的內容。
[numthreads(32, 32, 1)] void CSMain(uint3 ThreadId : SV_DispatchThreadID) { float2 pos = ThreadId.xy; //screen space position // 一些具體運算OutputSurface0[pos] = float4(1,0,0,0); }接下來是main函數,這里的函數名字叫CSMain。名字你可以自定義,只要和 #pragma kernel 命令匹配即可,就像平常寫光柵化shader時 #pragma vertex vert 一樣。但注意到這里有一個[numthreads(32, 32, 1)]的命令。這個命令是用來指示每個組里有多少線程運行的。那么什么是分組呢?畫個圖會比較好理解一些:
比如上面這張texture,假設它像素數量是16*16,我們要對它進行分組,每個組里分派一些線程計算。比如我們將它分成4*4共16個組,那么每個組里就有4*4個像素點,所以我們在numthreads命令里就要寫4*4*1(numthreads控制的是一個組內線程分配情況)。分組方式與每個組里的線程數不必相同,這里都是4*4僅是舉例巧合。在上面給出的Shader代碼中,numthread(32,32,1) 是因為后面我們會看到C#代碼中傳過來的輸入輸出texture都是512*512的,所以可知共分了8*8=64組(每組中有32*32個線程,因此就有(512 / 32) * (512 / 32)個組)。這里要注意的是,如何分組沒有限制,但要結合GPU的核心數進行設計以最大化效率,且Compute Shader中numthreads的三個參數乘積不能大于1024。
而語義SV_DispatchThreadID就是當前線程對應的整張texture中的像素位置(而不是它在自己組內的位置)。因此上面這段代碼的意思就是將輸出中的所有位置都填寫上(1,0,0,0)這個向量。還有一些其他的語義可以使用,比如SV_DispatchIndex就是當前的線程在哪個組里。
使用C# 控制腳本運行
代碼如下:
// get the handle of the main function in computer shaderint kernel = H0Generation.FindKernel("CSMain");第一步是獲得操縱Compute Shader的句柄。這里H0Generation是一個ComputeShader類型的變量(就是上面展示的那段代碼),你通過public聲明后在Inspector中綁定也好什么方法,反正你把你的ComputeShader和腳本中的ComputeShader變量綁定到一起,然后調用FindKernel函數,參數就是你剛才在Compute Shader里#pragma kernel 的參數,也就是你Compute Shader的main函數的名字。
// H0Generation is called every frame, so release the texture of last frameif (H0Output)H0Output.Release();// declare a new rt for this frame with a size of 512 * 512 and ARGB32H0Output = new RenderTexture(512, 512, 1, RenderTextureFormat.ARGB32);// enable write otherwise it cannot be modified in the shaderH0Output.enableRandomWrite = true;// actually create the rtH0Output.Create();接下來我們創建一張輸出貼圖。H0Output是一個RenderTexture類型的變量,我們把它聲明成512*512,格式是ARGB32。這些參數你都可以隨你需要設定,只要保證texture的尺寸、分組數量和每個組內線程數量匹配就可以了。注意這里關鍵的一點是要設置 RenderTexture.enableRandomWrite=true,你的Compute Shader才有向紋理中寫入數據的權限。
// bind rt and some uniform variables with those in the shaderH0Generation.SetTexture(kernel, "OutputSurface0", H0Output);H0Generation.SetTexture(kernel, "InputTexture", GaussTexture);H0Generation.SetFloats("width_height_Lx_Lz", new float[] { width_height_Lx_Lz.x, width_height_Lx_Lz.y, width_height_Lx_Lz.z, width_height_Lx_Lz.w });接下來我們進行數據綁定。直接用SetXXX的函數將Shader中參數的名字和C#中的變量綁定起來即可,注意SetTexture命令需要指定操縱Compute Shader的句柄。這里就需要解釋一下,上面一直將Kernel比作main函數,但main函數只能由一個,而Compute Shdaer里的kernel是可以有多個的。因此我們可以聲明多個函數#pragma kernel,然后再C#中取得相應的句柄,這樣設置texture時就會給對應的函數分配它該訪問的texture,接下來執行Compute Shader時也使用這個句柄去運行對應的函數。
// run the compute shader, wtih 32 * 32 groups. this is the maximum, it cannot exceeds 1024H0Generation.Dispatch(kernel, 512 / 32, 512 / 32, 1);接下來就是執行了。Dispatch函數將執行你傳入的句柄對應的Shader中的函數,后面傳入的三個數據,就是你對Texture分組的方式。就像上一節提到過的,因為我們這張texture是512*512的,而且Compute Shader里的numthreads定義的每組線程數量是(32, 32, 1),所以我們這里的分組數就應該是 (512/32, 512/32, 1)。運行完后,Compute Shader的運算結果就會存在你指定的output里面啦!接下來你就可以像訪問普通texture那樣獲取里面的數據了!
這里我直接把輸出的texture賦給一個材質,然后用材質在世界中渲染一個平面來查看效果;感覺這種方式還是十分方便的(果然printf是最經典的調試方法了)。使用的Compute Shader中有時間變量,所以圖案會一直改變,運行效果如下:
知乎視頻?www.zhihu.com就到這里啦?
總結
以上是生活随笔為你收集整理的使用示例_在Unity中使用ComputeShader示例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 怎么调节电机启动值_发电机组的几个使用规
- 下一篇: python dataframe是什么_