准备工作 在上一章中,我们使用随机数在图片上生成了很多随机大小和位置的圆环,现在我们使用简单的代码让他们动起来
之前我们在 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)
最后,我们通过 SetData
和 SetBuffer
方法将我们创建好的数据传输给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