Unity中的ComputeShader

一、简介


               和CPU Random MemoryAccesses(随机内存获取)不同,GPU是用平行架构处理大量的并行数据,例如vertex和fragment就是分开计算的。使用GPU并利用这种特性来进行非图形计算被称为GPGPU编程(General Purpose GPU Programming)。大量并行无序数据的少分支逻辑(少if)适合GPGPU,例如粒子间互不影响的粒子系统。GPGPU平台或接口有DirectCompute,OpenCL,CUDA等。

 

从此图可以看出 CPU和GPU之间的数据传输是瓶颈。故当使用GPGPU时,对Texture的逐像素处理不需要传回CPU,因而速度比较快。

   ComputeShader就是这样的可称为GPGPU编程的东西。顾名思义,它是一段shader程序。它有shader的特性,比如是在GPU上运行的,但它又是脱离于正常的渲染管线之外的,即不对渲染结果造成影响。这跟我们普通的shader不同。

         这种shader扩展名为.compute,是DX11的HLSL风格的shader,当然,也可以转为GLSL,使其能兼容OpenGL(目前还没测试,只在DX11上测试过)。

         ComputeShader目前能运行的图形库包括DX11、DX12、OpenGL4.3+、OpenGLES3.

    那么它的好处就是只要是涉及到大量的数学计算,并且是可以并行的没有很多分支的计算,都可以采用ComputeShader。它的缺点就是,如上图所示的,我们需要把数据从CPU传到GPU,然后GPU运算完之后,又从GPU传到CPU,这样的话可能会有点延迟,而且他们之间的传输速率也是一个瓶颈。

      不过,如果你真的有大量的计算需求的话,还是不要犹豫地使用ComputeShader,它能带来的性能提升绝对是值得的。

二、使用方法
    我们通过一个例子来逐渐地解释ComputeShader的用法。这个例子是在Unity平台下实现的,是计算两个相等大小的Vector3数组相加的结果,得到另一个大小一样的Vector3数组。

          我们先贴出ComputeShader的代码:

// Each #kernel tells which function to compile; you canhave many kernels
 
#pragma kernel CSMain
 
 
 
#define thread_group_x 2
 
#define thread_group_y 2
 
#define thread_x 2
 
#define thread_y 2
 
 
 
RWStructuredBuffer<float3> Result;
 
RWStructuredBuffer<float3> preVertices;
 
RWStructuredBuffer<float3> nextVertices;
 
//单个线程组包含的线程数
 
[numthreads(2,2,1)]
 
void CSMain (uint3 id : SV_DispatchThreadID)
 
{
 
int index = id.x + (id.y * thread_x * thread_group_x) +(id.z * thread_group_x * thread_group_y * thread_x * thread_y);
 
Result[index] = preVertices[index] + nextVertices[index];
 
}

          代码行数不多,我们一句句来。

          首先第一句,#pragma kernel CSMain 定义了该shader的入口函数。这也是C#文件访问该kernel的接口,下面会提到。当然,一个.compute文件可以包含多个kernel,只需要写多个#pragma kernel xxx即可。但是必须至少包含一个kernel,否则会报编译错误。

          接下来的四句#define 宏定义,跟C++没什么两样,至于它们的意思,前面说了,shader都是运行在多线程上的。而这里又多一个线程与线程组的概念。先按字面意思记一下,后面我们细讲。

         然后三句RWStructuredBuffer<float3>定义了三个buffer,pre和next是我们要从CPU传进来的,result是在GPU计算好之后,传回到CPU的。RW的意思是Read and Write.这里我们用的类型是float3,因为我们是计算两个Vector3数组的加法。也有很多别的类型可以套上去,就像泛型一样。也可以自定义结构体。比如:

   struct MyVector
   {
      float x;
      float y;
      float z;
  };
  RWStructuredBuffer<MyVector> data;

然后在C#端创建一个同等类型的结构体。

  [numthreads(2,2,1)] 这句话就比较重要,也能解释了上面的那四句宏定义。我们就来说说这个线程是怎么切分的。

     比如我们有16个数据需要进行计算,我们可以将它切分成几种形式。可以分成16*1*1,表示只有一个线程组,该线程组中只有一行共16个线程。

     也可以分成2*2*1*2*2*1,这个表示2*2*1个线程组,每个线程组有2*2*1个线程。我们用下图图例来看:

这样的话,总的线程数也有16个。当然,也还可以分成2*2*2*2*1*1,这里就不画图了,用系统自带的画图工具不是很方便。这里我们是以2*2*1*2*2*1为例。

       numthreads的意思就是单个线程组中线程数有多少,并且是怎么分割的。它指定的就是上图中右下的格子。那么线程组的分割又是在哪里指定的呢?这是在C#代码里指定的,等下面我们会说它怎么使用。

       然后是函数的签名。这里说一下参数uint3 id :SV_DispatchThreadID 。它是指该线程所在的总的线程结构中的索引,还是以上面这个例子来说。如果我们把单个线程组的线程都压缩到这个线程组中,那么就会成为一个4*4的线程方格矩阵。所以id的取值范围就是(0,0,0)~(3,3,0).

         这里因为z一直都是0,所以比较好算一点,那么如果一个线程组中的线程数为2*2*2呢,即总的线程数为32呢,这个id又是什么取值范围?

       其实id的取值范围为(0,0,0)~(threadx*thread_groupx-1,thready*thread_groupy-1,threadz*thread_groupz-1)其中numthreads指定的就是(threadx,thready,threadz).(thread_groupx,thread_groupy,thread_groupz)是在C#脚本里指定的。

      到这里我们就说完了shader的代码。接下来我们开始C#的代码解析:

using UnityEngine;
public class TestCalcMesh:MonoBehaviour
{
   public ComputeShader calcMeshShader;
   private ComputeBuffer preBuffer;
   private ComputeBuffer nextBuffer;
   private ComputeBuffer resultBuffer;
   public Vector3[] array1;
   public Vector3[] array2;
   public Vector3[] resultArr;
   public int length = 16;
   private int kernel;
   // Use this for initialization
   void Start ()
   {
       array1 = new Vector3[length];
       array2 = new Vector3[length];
       resultArr = new Vector3[length];
       for (int i = 0; i < length; i++)
       {
            array1[i] = Vector3.one;
            array2[i] = Vector3.one * 2;
       }
       InitBuffers();
       kernel = calcMeshShader.FindKernel("CSMain");
       calcMeshShader.SetBuffer(kernel, "preVertices", preBuffer);
       calcMeshShader.SetBuffer(kernel, "nextVertices", nextBuffer);
       calcMeshShader.SetBuffer(kernel, "Result", resultBuffer);
   }
 
 
   void InitBuffers()
    {
       preBuffer = new ComputeBuffer(array1.Length, 12);
       preBuffer.SetData(array1);
       nextBuffer = new ComputeBuffer(array2.Length, 12);
       nextBuffer.SetData(array2);
       resultBuffer = new ComputeBuffer(resultArr.Length, 12);
    }
 
   // Update is called once per frame
   void Update ()
   {
   if(Input.GetKeyDown(KeyCode.KeypadEnter))
       {
            calcMeshShader.Dispatch(kernel,2,2,1);
            resultBuffer.GetData(resultArr);
            //do something with resultArr.
            resultBuffer.Release();
       }
}
 
   void OnDestroy()
   {
       preBuffer.Release();
       nextBuffer.Release();
   }
}

其实C#的代码能说的地方不多,只要用Visual Studio的智能感知就知道大概的意思了。

首先是变量定义部分,定义了一个Compute Shader,还有三个Compute Buffer,Compute Buffer就是在shader计算时传入传出数据的缓存。可以存储任何的数据类型,对应shader中的RWStruceturedBuffer.因为我们没有指定传入传出的类型,所以在初始化这个buffer的时候,我们需要指定stride的值,在这个例子中,stride=12,因为Vector3有3个float值,每个float值是4个字节。所以这里stride的值是指单个类型的字节数。

然后就是ComputeShader.SetBuffer函数,这个其实比较好理解。

比较重要的是ComputeShader.Dispatch函数,它就是指定线程组是如何划分的,即指定(thread_groupx,thread_groupy,thread_groupz)。

最后记得在使用完之后,将Buffer释放掉。

 关于ComputeShader的使用方法大概就是这些。当然如果深入下去还有很多的问题,以后待补充。

三、注意事项

1、numthreads的数值是有大小限制的,具体如下表所示:

Compute ShaderMaximum ZMaximum Threads (X*Y*Z)
cs_4_x1768
cs_5_0641024

你可以选中一个ComputeShader,然后点击Show CompiledShader,里面有说明是哪个版本。即是cs_4_x还是cs_5_0.

2、DX和OpenGL是有不同的布局规则的,如果将compute shader自动转换成GLSL的compute shader的话,用的是std430标准。因此如果是用float3数组的话,在DX里是会自动对齐位数的,而在OpenGL里的话,则会强制转成float4.所以就有可能产生兼容的问题,但是标量、二维、四维的数组就不会有这个问题。所以在使用float3数组时,需要注意一下。

3、main函数里,索引不能是常数,会报编译错误。比如

int a = 0;

Result[a] = preVertices[0] +nextVertices[0];

它这样的规则可能是为了线程的安全性,毕竟ComputeShader是多线程的。不能写入一个常数索引里,估计是担心多线程会有数据的冲突问题。

4、Don't use get data in real time! (took me 2months to find the reason, which I can explain another time if you like)

Instead:

Make an array of compute buffers, aminimum of 2: one for read, one for write.

Do a rw structure in compute, filledwith junk data to be overridden in the compute

On the next frame (this is import - Isuggest doing an enum to 'waitforenfoframe' yield )

Lastly copy the Contents of the writebuffer into read.

Then use these cloned buffers withwhatever you need - get data does work on static buffers in this case.

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值