unity compute shader学习(一)

入门篇

创建compute shader

unity资产库中,右键创建一个compute shader。

默认的shader:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 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);
}

AI注释:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 编译指令,指定CSMain为计算着色器的主函数
#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);
}

CSMain函数最终会在GPU执行。

一个ComputeShader中 至少要有一个kernel才能够被唤起 。声明方法即为:

1
#pragma kernel functionName

我们也可用它在一个ComputeShader里声明多个内核,可选择性地在 #pragma kernel 行后面添加要在编译该内核时定义的多个预处理器宏:

1
2
#pragma kernel KernelOne SOME_DEFINE DEFINE_WITH_VALUE=1337
#pragma kernel KernelTwo OTHER_DEFIN

使用多个 #pragma kernel 行时,请注意在 #pragma kernel 指令的同一行上不允许 // text 样式的注释,如果使用,会导致编译错误。

1
#pragma kernel functionName // 一些注释

定义线程(numthreads(8,8,1),意味着每个线程组包含8x8的线程网格来一起处理图像的一个小区块):

1
numthreads(X, Y, Z)

XYZ 表示每个线程组在各自维度上的线程数目。这三个数值决定了每个线程组具体包含多少个线程。它们乘积得出的结果是该线程组中总的线程数。

  • 这个数量决定了 GPU 上每次能并行运行多少个线程以及它们是如何组织的。
  • 计算任务在 Compute Shader 中通常被分割成许多小块来在多个线程中并行执行,每个小块由一个线程组处理。其中,每个线程组又包含了多个线程,这些线程会一起工作完成指定的任务。

这里的 Z 被设置成 1 是因为在大多数二维计算任务中,第三维并不需要。

numthreads 还会影响到内存共享策略,因为同一个 Thread Group 中的线程可以通过共享内存快速交换数据,这比跨 Thread Group 通信要高效得多。

    ps:也不是没有缺点,类似fragment shader,用线程块操作的时候无法获取到线程块以外的数据,那么会导致卷积算法出现接缝。

计算缓冲区compute buffer

ComputeShader程序通常需要将任意数据读取和写入内存缓冲区。

可以使用SystemInfo.supportsComputeShaders来查询是否支持缓冲区(具体与shader model的版本有关),这个函数的返回值是一个bool。

具体使用方法详见[官方文档](Unity - 脚本 API:ComputeBuffer (unity3d.com))。

最常用的是RWStructuredBuffer<任意类型的数据、结构体>,RWTexture2D

BeginWrite 开始对缓冲区执行写入操作
EndWrite 结束对缓冲区的写入操作
GetData 将数据值从缓冲区读取到数组中。数组只能使用可流通类型。
GetNativeBufferPtr 检索指向缓冲区的本机(基础图形 API)指针。
IsValid 如果此计算缓冲区有效,则返回 true,否则返回 false。
Release 释放计算缓冲区。
SetCounterValue 设置 append/consume buffer 的计数器值。
SetData 使用数组中的值设置缓冲区。

调用compute shader

可以在c#脚本中,使用Dispatch来调用compute shader。

1
public void Dispatch(int kernelIndex, int threadGroupsX, int threadGroupsY, int threadGroupsZ);

参数解析

参数 解析
内核索引 要执行的内核。单个计算着色器资产可以有多个内核入口点。通常是一个数字。
线程组X X 维度中的工作组数。
线程组Y Y 维度中的工作组数。
线程组Z Z 维度中的工作组数。

 可以使用 GetKernelThreadGroupSizes函数查询工作组大小,使用FindKernel查询kernel函数的序号。

1
int FindKernel(string name);
1
void GetKernelThreadGroupSizes(int kernelIndex, out uint x, out uint y, out uint z);

具体使用

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
//class
public ComputeShader computeShader;
private ComputeBuffer cBuffer;
//Start()
cBuffer = new ComputeBuffer(缓冲区的长度,每个元素的大小(以字节为单位));
cBuffer.SetData(一种数据,比如数组);//初始化计算缓冲区
computeShader.SetBuffer(0, "Compute Shader中定义好的计算缓冲区的名称", cBuffer); //将计算缓冲区设置为计算着色器
//Update()
//运行计算着色器,粒子的位置将在GPU上更新
computeShader.Dispatch(0, x, y, z);
//从GPU获取数据到CPU
cBuffer.GetData(一种数据,比如数组);//从GPU获取数据到CPU
//最后不要忘记实现获取到的数据到游戏中

在 Compute Shader 中,你可以定义多个缓冲区,并通过名字来引用它们。在 Compute Shader 的 kernel 函数中,你可以使用 ulong 类型的 __global 变量作为缓冲区,并通过名字来引用它们。例如:

1
2
3
4
5
6
7
8
RWStructuredBuffer<Particle> particleBuffer;

[numthreads(32, 1, 1)]
void main(uint3 id : SV_DispatchThreadID)
{
Particle p = particleBuffer[id.x];
// ...
}

在这个例子中,我们定义了一个名为 “particleBuffer” 的缓冲区,它是一个 Particle 类型的 RWStructuredBuffer,表示一个可读写的结构体缓冲区。然后,在 kernel 函数中,我们通过名字 particleBuffer 来引用这个缓冲区,并使用 [id.x] 来访问第 id.x 个元素。

在 C# 代码中,我们通过 computeShader.SetBuffer(0, "particleBuffer", cBuffer); 将 C# 中的 cBuffer 设置为 Compute Shader 中的 “particleBuffer” 缓冲区。这样,在 Compute Shader 中,我们就可以通过 “particleBuffer” 来访问 C# 中的数据了。

1