什么是Compute Shader,以及它能用来干什么

首先根据Direct3D 11的说明文档,我们可以知道一个Compute Shader是一个能够利用泛式(通用)的内存访问(即输入和输出),来进行任意运算的可编程着色器。作为类比,我认为Compute Shader和Unity中的Job System很像,准备好一系列的输入,并设置好输出的目标,然后执行这个Compute Shader或者Job,就能得到想要的结果。GPU和多线程,我认为在某种程度上是异曲同工的。Job System有一个优势,就是快,而Compute Shader也有这个优势,而且还要更快!Compute shader背后的思想就是General-purpose computing on graphics processing units(GPGPU),即在图形处理单元上进行通用计算。

如何在Unity里面使用Compute Shader

在Unity中创建Compute Shader之后,会得到一个默认的Compute Shader,里面的代码是这样的:

// Each #kernel tells which function to compile; you can have many kernels
#pragma kernel CSMain

// Create a RenderTexture with enableRandomWrite flag and set it
// with cs.SetTexture
RWTexture2D<float4> Result;

[numthreads(8,8,1)]
void CSMain (uint3 id : SV_DispatchThreadID)
{
    // TODO: insert actual code here!

    Result[id.xy] = float4(id.x & id.y, (id.x & 15)/15.0, (id.y & 15)/15.0, 0.0);
}

需要执行Compute Shader的时候,需要获取该Compute Shader的引用,再调用它的Dispatch的方法:

//使用(256, 250, 1)的大小创建一个支持随机读写的四通道浮点数的RenderTexture
Vector3Int dispatchThreadCount = new Vector3Int(256, 250, 1);
RenderTextureDescriptor desc = new RenderTextureDescriptor
{
    dimension = TextureDimension.Tex2D,
    width = dispatchThreadCount.x,
    height = dispatchThreadCount.y,
    volumeDepth = dispatchThreadCount.z,
    graphicsFormat = UnityEngine.Experimental.Rendering.GraphicsFormat.R16G16B16A16_SFloat,
    msaaSamples = 1,
    enableRandomWrite = true,
};
renderTexture = new RenderTexture(desc);

//找到叫做"CSMain"的kernel
int kernel = computeShader.FindKernel("CSMain");
//获取numthreads,这里获取的值是(8, 8, 1)
computeShader.GetKernelThreadGroupSizes(kernel, out uint x, out uint y, out uint z);
//计算Dispatch Count,要注意先用浮点数计算,再向上取整
Vector3Int dispatchCount = new Vector3Int(Mathf.CeilToInt(dispatchThreadCount.x / (float)x),
                                        Mathf.CeilToInt(dispatchThreadCount.y / (float)y),
                                        Mathf.CeilToInt(dispatchThreadCount.z / (float)z));
computeShader.SetTexture(kernel, "Result", renderTexture);
computeShader.Dispatch(kernel, dispatchCount.x, dispatchCount.y, dispatchCount.z);

这样就能够根据Compute Shader里面的代码,在Render Texture上绘制出想要的效果了。最终绘制的效果是这样的: Default Compute Shader

Compute Shader里面的代码有什么含义

可以看到Compute Shader的#pragma kernel CSMain使用了一个叫做CSMain的函数作为kernel,就和普通shader中的vertex和fragment差不多。这个Compute Shader的输入和输出是同一张float4的2D贴图Result。RWTexture2D的RW(Read Write)表示这张贴图支持随机读写(unordered access view, UAV),这样在Compute Shader读取这张贴图的同时也能对这张贴图进行写入。这里需要注意一张可读写贴图,和两张贴图一张读一张写,在需要读取除当前像素外的像素信息并写入当前像素时,是会有一定区别的。再有就是CSMain中两条看不太懂的代码了[numthreads(8,8,1)]SV_DispatchThreadID。这里可以参考HLSL官方文档上的定义来进行理解: DispatchThreadID

通常用到的有SV_GroupID, SV_GroupThreadID, SV_GroupIndex, 和SV_DispatchThreadID,一般按我的想法,我还会额外传入一个_DispatchCount记录Compute Shader的Dispatch的数目。Compute Shader是这样运作的:假设我们执行了Dispatch(5, 3, 2)numthreads(10, 8, 3),那么一共会产生5 * 3 * 2 = 30个Thread Group,每个Thread Group会有一个SV_GroupID,范围是(0, 0, 0)到(4, 2, 1);每个Thread Group中会有10 * 8 * 3个Thread,每个Thread会有一个SV_GroupThreadID, SV_GroupIndexSV_DispatchThreadIDSV_GroupThreadID的范围是(0, 0, 0)到(9, 7, 2)。SV_GroupIndex是Thread在Thread Group内的序号,其值等于SV_GroupThreadID.z * numthreads.y * numthreads.x + SV_GroupThreadID.y * numthreads.x + SV_GroupThreadID.x,其范围是0到10 * 8 * 3 - 1,在涉及到groupshared memory的时候,往往会用到SV_GroupIndexSV_DispatchThreadID的范围是(0, 0, 0)到(5 * 10 - 1, 3 * 8 -1, 2 * 3 - 1),SV_DispatchThreadID的一个优点是对于任何一个Thread,它的SV_DispatchThreadID都是独一无二的,因此在很多情况下都可以利用SV_DispatchThreadID来进行数据操作;此外,在使用RWStructuredBuffer来作为输入输出的时候,往往会需要知道每个Thread Group在所有Thread Group中的序号,或者是每个Thread在所有Thread中的序号,我将其记为IndexGroupIndexThread,用来和自带的Index区分,IndexGroup的值等于SV_GroupID.z * _DispatchCount.y * _DispatchCount.x + SV_GroupID.y * _DispatchCount.x + SV_GroupID.x,范围是0到5 * 3 * 2 - 1,IndexThread的值等于IndexGroup * numthreads.z * numthreads.y * numthreads.x + SV_GroupIndex,范围是0到5 * 3 * 2 * 10 * 8 * 3 - 1。此外还要注意,由于DispatchCount有被向上取整的可能,所得到的如SV_DispatchThreadID可能会超过贴图、Buffer的大小,最好再传入一个_TextureSize来将SV_DisparchThreadID限制到合适的大小。

特别需要记住的是,Compute Shader中的numthreads、DispatchCount这些数值,跟输如输出的数据结构没有直接的关系,对于任意的数据结构,都能使用“任意”的numthreads和DispatchCount,但是我们需要在代码中指定这些数值和输入输出的数据结构的关系。比较常用的是使用IndexThread来读取写入RWStructuredBuffer,用SV_DispatchThreadID来读取写入贴图。