Compute Shader, read from calculated buffer

 Use case - get the GPU to calculate a bunch of X,Y,Z coordinates for an arbitrary number of instanced prefabs in Unity. 

On the Unity C# side, we need :

To specify a compute shader, a handle for the compute shader, a buffer that we'll be writing to in the shader, a prefab, the number of prefabs we want, an array for the instanced prefabs, an array for the coordinate data. Any extra variables we want to pass over, eg time.

On the compute shader side we need :

To specify a read-write buffer, instead of a texture that we created in the previous post. The buffer will be of float3 type, which is how HLSL calls a vector3. That's actually it...

Here's the Compute Shader code -


#pragma kernel boxMove

RWStructuredBuffer<float3> yesBlad;
float time;

[numthreads(64,1,1)]
void boxMove (uint3 id : SV_DispatchThreadID)
{
    float xpos = (float)id.x;
    float ypos = sin(id.x+time)*50;
    float zpos = cos(id.x +time)* 50;
    float3 pos = { xpos,ypos,zpos };
    
    yesBlad[id.x] = pos;
    
 }


So, we see here our kernel is called boxMove and I'm calculating the xyz coordinates using the id.x as the x value, and sin(id.x) and cos(id.x) with time for y & z. Also notice how we assign xpos-zpos to the float3 pos, using { }. Also also, notice you can call what we usually see as "Result", whatever you like, as long as you match whatever it's called in the C# script. Here I've called the buffer yesBlad.

Other thing to note - sin & cos take an argument in Radians. This puzzled me a bit when I wrote and equivalent C#  script for the behaviour & used Rad2Deg haha. All rad all rad all raaaaaaaaaad.

The corresponding C# script -


using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class test_positions : MonoBehaviour
{
    public ComputeShader shader;//our shader goes here
    int kernelHandle;//handle var
    uint threadGroupSizeX;
    int groupSizeX;

    ComputeBuffer resultBuffer;
    Vector3[] outputData;
    Transform[] boxArray;
    [SerializeField]
    public int boxCount = 50;

    public GameObject box;

    // Start is called before the first frame update
    void Start()
    {
        kernelHandle = shader.FindKernel("boxMove");//get handle
        shader.GetKernelThreadGroupSizes(kernelHandle, out threadGroupSizeX, out _, out _);
        groupSizeX = (int)((boxCount + threadGroupSizeX - 1) / threadGroupSizeX);//calculate dispatch size for correct num of threads

        resultBuffer = new ComputeBuffer(boxCount, sizeof(float) * 3);
        shader.SetBuffer(kernelHandle, "yesBlad", resultBuffer);//write to resultBuffer
        outputData = new Vector3[boxCount]; //output array created

        boxArray = new Transform[boxCount];
        for(int i = 0; i < boxCount; i++)
        {
            boxArray[i] = Instantiate(box,transform).transform;
        }

    }

    // Update is called once per frame
    void Update()
    {
        shader.SetFloat("time", Time.time);//pass time to shader
        shader.Dispatch(kernelHandle, groupSizeX, 1, 1);//dispatch
        resultBuffer.GetData(outputData);
        for(int i=0; i < boxCount; i++)
        {
            boxArray[i].localPosition = outputData[i];
        }
    }

    void OnDestroy()
    {
        resultBuffer.Dispose();

    }

}

 

Things of note - 

The prefab used is user specified. I just made a cube, you can use what you like. That's what the "Public Gameobject box" is in the inspector.

We only deal with the X component of the thread size because we're working in one dimension - 64. I've set the shader's buffer to have the size of boxCount x the size of 3 floats, because that's the size of the data we're playing with. Everything is initialised in the void Start(). You could put this in another function and call it from Start, if you wanted.
In Update(), we pass the time into the shader, using the shader.SetFloat method.
We also retrieve data from the buffer and put it into "outputData", our array of vector3. This is then assigned to the localPosition of each instanced prefab.

Last thing - OnDestroy() is some cleanup, it marks the buffer as ok to use/delete/whatever. Unity might complain if you do not do this!

Comments

Popular posts from this blog

setting VFX graph properties using C#