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
Post a Comment