前言
ComputeShader是如今比较流行的一种技术,例如之前的《天刀手游》,还有最近大火的《永劫无间》,在分享技术的时候都有提到它。
本着不学习就可能失业的压力,就来学一下,虽然好像已经晚了好几年了=。=。
Unity官方对ComputeShader的介绍如下:https://docs.unity3d.com/Manual/class-ComputeShader.html
ComputeShader和其他Shader一样是运行在GPU上的,但是它是独立于渲染管线之外的。我们可以利用它实现大量且并行的GPGPU算法,用来加速我们的游戏。
在Unity中,我们在Project中右键,即可创建出一个ComputeShader文件:
生成的文件属于一种 Asset 文件,并且都是以 .compute 作为文件后缀的。
我们来看下里面的默认内容:
#pragma kernel CSMain
RWTexture2D<float4> Result;
[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}
本文的主要目的就是让和我一样的萌新能够看懂这区区几行代码的含义,学好了基础才能够看更牛逼的代码嘛。如果看完还看不懂,那就是我写的不好了!
语言
Unity使用的是DirectX 11的HLSL语言,会被自动编译到所对应的平台。
kernel
然后我们来看看第一行:
#pragma kernel CSMain
CSMain其实就是一个函数,在代码后面可以看到,而 kernel 是内核的意思,这一行即把一个名为CSMain的函数声明为内核,或者称之为核函数。这个核函数就是最终会在GPU中被执行。
一个ComputeShader中至少要有一个kernel才能够被唤起。声明方法即为:
#pragma kernel functionName
我们也可用它在一个ComputeShader里声明多个内核,此外我们还可以再该指令后面定义一些预处理的宏命令,如下:
#pragma kernel KernelOne SOME_DEFINE DEFINE_WITH_VALUE=1337 #pragma kernel KernelTwo OTHER_DEFINE
我们不能把注释写在该命令后面,而应该换行写注释,例如下面写法会造成编译的报错:
#pragma kernel functionName // 一些注释
RWTexture2D
接着我们再来看看第二行:
RWTexture2D<float4> Result;
看着像是声明了一个和纹理有关的变量,具体来看一下这些关键字的含义。
RWTexture2D中,RW其实是Read和Write的意思,Texture2D就是二维纹理,因此它的意思就是一个可以被compute shader读写的二维纹理。如果我们只想读不想写,那么可以使用Texture2D的类型。
我们知道纹理是由一个个像素组成的,每个像素都有它的下标,因此我们就可以通过像素的下标来访问它们,例如:Result[uint2(0,0)]。
同样的每个像素会有它的一个对应值,也就是我们要读取或者要写入的值。这个值的类型就被写在了<>当中,通常对应的是一个rgba的值,因此是float4类型。通常情况下,我们会在ComputeShader中处理好纹理,然后在FragmentShader中来对处理后的纹理进行采样。
这样我们就大致理解这行代码的意思了,声明了一个名为Result的可读写二维纹理,其中每个像素的值为float4。
在Compute Shader中可读写的类型除了RWTexture以外还有RWBuffer和RWStructuredBuffer,后面会介绍。
numthreads
然后是下面一句(很重要!):
[numthreads(8,8,1)]
又是num,又是thread的,肯定和线程数量有关。没错,它就是定义一个线程组(Thread Group)中可以被执行的线程(Thread)总数量,格式如下:
numthreads(X, Y, Z)
其中 X*Y*Z 的值即线程的总数量,例如 numthreads(4, 4, 1) 和 numthreads(16, 1, 1) 都代表着有16个线程。那么为什么不直接使用 numthreads(num) 这种形式定义,而非要分成X,Y,Z这种三维的形式呢?看到后面自然就懂其中的奥秘了。
每个核函数前面我们都需要定义numthreads,否则编译会报错。
其中X,Y,Z三个值也并不是也可随便乱填的,比如来一刀 X=99999 暴击一下,这是不行的。它们在不同的版本里有如下的约束:
Compute Shader 版本 |
Z的最大取值 |
最大线程数量(X*Y*Z) |
---|---|---|
cs_4_x |
1 |
768 |
cs_5_0 |
64 |
1024 |
在Direct11中,可以通过ID3D11DeviceContext::Dispatch(X,Y,Z)方法创建X*Y*Z个线程组,一个线程组里又会包含多个线程(数量即numthreads定义)。注意顺序,先numthreads定义好每个核函数对应线程组里线程的数量,再用Dispatch定义用多少线程组来处理这个核函数。其中每个线程组内的线程都是并行的,不同线程组的线程可能同时执行,也可能不同时执行。一般一个GPU同时执行的线程数,在1000-10000之间。
接着我们用一张示意图来看看线程与线程组的结构,如下图: