Unity Compute Shader - 5 Compute Shader buffer

准备工作

在上一章中,我们使用随机数在图片上生成了很多随机大小和位置的圆环,现在我们使用简单的代码让他们动起来

之前我们在 DrawCircleShader脚本中声明了 float time;,这里我们使用C#传入值,代码如下:

1
2
3
4
5
6
7
8
9
10
private void Update()
{
DispatchKernels();
}

private void DispatchKernels()
{
shader.SetFloat("time", Time.time);
...
}

这样,我们就得到了一个每一帧都会随机生成很多圆环的图片,效果如下:

虽然图片中的圆环每帧都会变化,但依然是杂乱无章的,而且速度非常快,并不太符合我们的预期,我们希望的是生成类似气泡感觉的图片

构建Buffer数据

为了可以生成气泡运动感觉的图片,我们这里就要用到Compute Shader Buffer,它可以让我们将任意的数据传递给Compute Shader中,并让其进行计算。我们先在 DrawCircle脚本中初始化所有环形的数据,例如圆心位置、运动方向和位置、半径,然后将这些数据在初始化时传输给 DrawCircleShader脚本,让其计算后绘制到图片上,这样就不会有环形杂乱无章随机的问题了,DrawCircle脚本代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
using UnityEngine;

public class DrawCircle : MonoBehaviour
{

//绘制环形中 X的调用次数
int count = 10;
//存储所有环形数据
private Circle[] circleData;
//存储向ComputeShader传输的数据
private ComputeBuffer buffer;

private void Start()
{
...

InitData();

InitShader();
}

//初始化要传输给Compute Shader的数据
private void InitData()
{
circleHandle = shader.FindKernel("Circles");
clearHandle = shader.FindKernel("Clear");

uint threadGroupSizeX;

//这里要获取circleHandle内核方法中,线程组内的线程数量
//这里我们只关心X的值,也就是 [numthreads(32, 1, 1)] 中,32的值,其他的就或略了
shader.GetKernelThreadGroupSizes(circleHandle, out threadGroupSizeX, out _, out _);
//根据线程数算出一共需要多少个环形,也就是数组的长度
int total = (int)threadGroupSizeX * count;

circleData = new Circle[total];

float _speed = 100.0f;
float _halfSpeed = _speed * 0.5f;
float _minRadius = 10.0f;
float _maxRadius = 30.0f;
float _radiusRange = _maxRadius - _minRadius;

for (int i = 0; i < total; i++)
{
circleData[i].origin.x = Random.value * texResolution;
circleData[i].origin.y = Random.value * texResolution;
circleData[i].velocity.x = (Random.value * _speed) - _halfSpeed;
circleData[i].velocity.y = (Random.value * _speed) - _halfSpeed;
circleData[i].radius = Random.value * _radiusRange + _minRadius;
}
}

private void InitShader()
{
...

//创建Compute Shader缓冲区大小
//其中(2 + 2 + 1)与DrawCircleShader中的struct circle对应
//两个float2和一个float,再乘以sizeof(float),就计算出了缓冲区中一个元素的大小
int _stride = (2 + 2 + 1) * sizeof(float);
buffer = new ComputeBuffer(circleData.Length, _stride);

//将创建好的ComputeBuffer数据传输给ComputeShader
buffer.SetData(circleData);
shader.SetBuffer(circleHandle, "circleBuffer", buffer);
}

private void DispatchKernels()
{
shader.Dispatch(circleHandle, count, 1, 1);
}
}

//向Compute Shader传输数据的结构体
public struct Circle
{
public Vector2 origin;//圆心位置
public Vector2 velocity;//运行方向和速度
public float radius;//半径
}

在上面的代码中,我们首先使用 struct Circle 结构体声明了一个数组,用来存储我们要传递的数据,这个数组的长度是线程组中X的值乘以调用次数,也就是 [numthreads(32, 1, 1)] 中的32与 count 的乘积
接下来我们创建了一些辅助变量将随机值控制在一定范围内,例如 _speed) - _halfSpeed; 中,如果没有 - _halfSpeed,那么所有环形的运动方向都只会是正方向。而 Random.value * _radiusRange + _minRadius 控制了圆环最小的尺寸
接下来我们在 InitShader 方法中构造了ComputeBuffer,其构造函数的第一个参数是缓冲区中元素的数量,也就是 circleData 的长度。第二个参数是每个元素的大小,这里我们使用了两个Vector2和一个float变量,其中Vector2中包含两个float,那么其大小就是5个float,也就是 (2 + 2 + 1) * sizeof(float)
最后,我们通过 SetDataSetBuffer 方法将我们创建好的数据传输给Compute Shader

接收Buffer数据并运算

DrawCircle 脚本中完成了数据的创建和传输,回到 DrawCircleShader 脚本中,我们需要接收Buffer数据并进行计算,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...

//接收buffer数据的结构
struct circle
{
float2 origin;
float2 velocity;
float radius;
};
//接收buffer数据的变量
StructuredBuffer<circle> circleBuffer;

...

[numthreads(32, 1, 1)]
void Circles(uint3 id : SV_DispatchThreadID)
{
int2 center = (int2) (circleBuffer[id.x].origin + circleBuffer[id.x].velocity * time);
int radius = (int) circleBuffer[id.x].radius;

...
}

这里,我们创建了一个与 DrawCircle 脚本相同的数据结构 struct circle,并且声明了一个用于接收buffer的变量,这样当 DrawCircle 脚本中运行 shader.SetBuffer(circleHandle, "circleBuffer", buffer); 语句时,数据就会保存到这个变量中了
接下来修改了计算环形圆心和半径的方法,之前是随机生成,现在是根据线程组中线程的ID,来确定每个环形的大小与位置
返回unity中运行,我们可以看到已经正常生成了环形,并且这些环形运行起来已经有一种气泡的感觉了,但是还有一个问题,当我们长时间运行的时候,会发现所有环形最后都到了图片的外面,图片中再也没有环形了,这里我们需要对所有环形的圆心做一些限制,代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
[numthreads(32, 1, 1)]
void Circles(uint3 id : SV_DispatchThreadID)
{
...

while (center.x > texResolution)
{
center.x -= texResolution;
}
while (center.x<0)
{
center.x += texResolution;
}
while (center.y > texResolution)
{
center.y -= texResolution;
}
while (center.y < 0)
{
center.y += texResolution;
}

...
}

经过修改,每个环形如果到了图片以外,就会自动回到图片中,最终效果如下:

至此,我们已经可以将自定义的数据从C#脚本中传输给Compute Shader进行计算并绘制,需要注意的是,创建和计算数据会比传输快很多,所以在构建数据的时候,尽量只保留最必须的。
在下一章中,我们将尝试获取Compute Shader计算完成后的数据,并在C#脚本中使用这些数据

相关链接

ComputeBufferConstructor