Compute Shaders, a basic particle system drawn with a vertex fragment shader

A post for reference really...
Not a lot of magic here - except we're using a Graphic procedural - a point- to draw particles.
To do this we calculate the position of the particles in the compute shader, then a standard vert-frag shader assigned to a material reads from the gpu buffer and draws the points accordingly. We're also getting the mouse position on screen converted to World Space & having the particles follow that coordinate.

 The C# is as follows, careful with the comments i've left...blogger's formatting is a bit wonky -

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

#pragma warning disable 0649

public class particle_dave : MonoBehaviour
{

    private Vector2 cursorPos;

    // struct of a particle, fairly simple attributes
    struct Particle
    {
        public Vector3 position;
        public Vector3 velocity;
        public float life;
    }

    const int SIZE_PARTICLE = 7 * sizeof(float); //define the size of the particle struct

    public int particleCount = 1000000;
    public Material material;
    public ComputeShader shader;
    [Range(1, 10)]
    public int pointSize = 2;

    int kernelID;
    ComputeBuffer particleBuffer;

    int groupSizeX;


    // Use this for initialization
    void Start()
    {

        Init();
    }

    void Init()
    {
        // initialize the particles
        Particle[] particleArray = new Particle[particleCount];
        
        for (int i = 0; i < particleCount; i++)
        {
            //TO DO: Initialize particle
            Vector3 v = new Vector3();
            v.x = Random.value * 2 - 1.0f;
            v.y = Random.value * 2 - 1.0f;
            v.z = Random.value * 2 - 1.0f;
            v.Normalize();
            v *= Random.value * 0.5f;
            particleArray[i].position.x = v.x;
            particleArray[i].position.y = v.y;
            particleArray[i].position.z = v.z + 3; //offset away from camera

            particleArray[i].velocity.x = 0;
            particleArray[i].velocity.y = 0;
            particleArray[i].velocity.z = 0;
            particleArray[i].life = Random.value * 5.0f + 1.0f;
        }
            // find the id of the kernel
        kernelID =shader.FindKernel("CSParticle");
        // create compute buffer
        particleBuffer = new ComputeBuffer(particleCount, SIZE_PARTICLE); //make buffer based on count * size

        particleBuffer.SetData(particleArray);//copy array data to buffer

 
        

        uint threadsX=256;
       // shader.GetKernelThreadGroupSizes(kernelID, out threadsX, out _, out _); //get x thread size
        groupSizeX = Mathf.CeilToInt((float)particleCount / (float)threadsX); //calculate group size as int

        // bind the compute buffer to the shader and the compute shader
        shader.SetBuffer(kernelID, "particleBuffer", particleBuffer);
        material.SetBuffer("particleBuffer", particleBuffer); //passing buffer to material. this is new - we share the buffer between gpu and the vert frag shader

        material.SetInt("_PointSize", pointSize);

    }

    void OnRenderObject()
    {
        material.SetPass(0); //set first pass
        Graphics.DrawProceduralNow(MeshTopology.Points, 1, particleCount);//draw procedural now can draw different types of geo. params are typically :type of topology(points,tris,quads,lines,linestrips),vertex count for one of these (point would be 1),instance count is how many
    }

    void OnDestroy()
    {
        if (particleBuffer != null)
            particleBuffer.Release();
    }

    // Update is called once per frame
    void Update()
    {

        float[] mousePosition2D = { cursorPos.x, cursorPos.y };

        // Send datas to the compute shader
        shader.SetFloat("deltaTime", Time.deltaTime);
        shader.SetFloats("mousePosition", mousePosition2D);

        // Update the Particles
        shader.Dispatch(kernelID, groupSizeX, 1, 1);

    }

    void OnGUI() //here is where we find the mouse position
    {
        Vector3 p = new Vector3();
         Camera c = Camera.main;//this requires a camera to have the Main Camera tag in the inspector active... The script will complain and fail if this is absent.

         Event e = Event.current;
        Vector2 mousePos = new Vector2();

        // Get the mouse position from Event.
        // Note that the y position from Event is inverted.
        mousePos.x = e.mousePosition.x;
        mousePos.y = c.pixelHeight - e.mousePosition.y;

        p = c.ScreenToWorldPoint(new Vector3(mousePos.x, mousePos.y, c.nearClipPlane + 14));// z = 3.

        cursorPos.x = p.x;
        cursorPos.y = p.y;

    }
}

 

As you can see we not only set the buffer for the Compute Shader, but also for the Material - this is so the vert/frag shader can look at the Compute Shader's buffer.  In the Compute Shader we have a random number generator function based on exclusive or bit-shifting. We use it to generate a random position vector that a particle respawns at when it eventually reaches the end of its life.
Within the main kernel we update the particle's life, position based on it's direction and a new direction for it to head toward - based on the mouse position. We write the updated info into the particlebuffer.

The Compute Shader is as follows -

#pragma kernel CSParticle


struct Particle
{
    float3 position;
    float3 velocity;
    float life;
};

RWStructuredBuffer<Particle> particleBuffer; //create read-write structured buffer of type Particle

float deltaTime;//variables set by C# script
float2 mousePosition;

uint rng_state;

uint rand_xorshift()
{
    //George Marsaglia's paper
    rng_state ^= (rng_state << 13);
    rng_state ^= (rng_state >> 17);
    rng_state ^= (rng_state << 5);
    return rng_state;
}

void respawn(uint id)
{
    rng_state = id;
    float tmp = (1.0 / 4294967296.0);
    float f0 = float(rand_xorshift()) * tmp - 0.5;
    float f1 = float(rand_xorshift()) * tmp - 0.5;
    float f2 = float(rand_xorshift()) * tmp - 0.5;
    float3 normalF3 = normalize(float3(f0, f1, f2)) * 0.8f;
    normalF3 *= float(rand_xorshift())*tmp;
    particleBuffer[id].position = float3(normalF3.x+mousePosition.x,normalF3.y + mousePosition.y, normalF3.z + 3.0);
    particleBuffer[id].life = 4;
    particleBuffer[id].velocity = float3(0, 0, 0);
      
}

[numthreads(256, 1, 1)]
void CSParticle(uint3 id : SV_DispatchThreadID)
{
    Particle particle = particleBuffer[id.x];//read from buffer
    particle.life -= deltaTime; //deltatime from c#

    float3 delta = float3(mousePosition.x,mousePosition.y, 3) - particle.position;//z set to 3
    float3 dir = normalize(delta);

    particle.velocity += dir;
    particle.position += particle.velocity*deltaTime;
    particleBuffer[id.x] = particle;//write to buffer
    if (particle.life < 0) respawn(id.x); //when life ends, respawn using random function above
}

Finally is the vert/frag shader used for the drawing of points
Notice, that like in the C# and compute shader, we must define the Particle Struct. We only need a Structured Buffer here, as we are only reading -no writing.

UnityObjectToClipPos -Transforms a point from object space to the camera’s clip space in homogeneous coordinates. We do this for each point we draw. We're also setting some RGBA values based on the particle life. Red increases as the particle ages, Green decreases, Blue is a static value & Alpha is the same as green so the point fades away.
 

Shader "Custom/particle_dave"
{
    Properties
    {
        _PointSize("Point size",Float) = 5.0
    }

        SubShader
    {
        Pass{
        Tags { "RenderType" = "Opaque" }
        LOD 200
        Blend SrcAlpha one

        CGPROGRAM

        #pragma vertex vert
        #pragma fragment frag

        uniform float _PointSize;
        #include "UnityCG.cginc"

        #pragma target 5.0

    struct Particle {
        float3 position;
        float3 velocity;
        float life;

        };
    
    StructuredBuffer<Particle> particleBuffer;//create buffer

    struct v2f {
        float4 position : SV_POSITION;
        float4 color: COLOR;
        float life : LIFE;
        float size : PSIZE;


    };

    v2f vert(uint vertex_id: SV_VertexID, uint instance_id : SV_InstanceID)
    {
        v2f o = (v2f)0;//create empty point

        //Color
        float life = particleBuffer[instance_id].life;
        float lerpVal = life * 0.25;

        o.color = fixed4(1 - lerpVal + 0.1, lerpVal + 0.1, 1, lerpVal);

        o.position = UnityObjectToClipPos(float4(particleBuffer[instance_id].position, 1));
        o.size = _PointSize;

        return o;


    }

    float4 frag(v2f i):COLOR
    {

        return i.color;
    }

    ENDCG

                                              
        }
    }
    FallBack Off
}


Comments

Popular posts from this blog

Unity's "new" input system and Keijiro's Minis midi stuff

setting VFX graph properties using C#