Compute Shaders, particle system with quads, using Frag/Vert shader to draw
This is more or less the same as the last wall of code. However this time, we are using the Graphics draw to draw a quad, which consists of Four points, Two triangles. We need to adjust our buffer and create a new struct for this, as well as initialise our arrays in a specific way to ensure the quads are facing the correct direction.
As mentioned, our new Struct - Vertex stores a position, UV value and life. We're going to use the Compute Shader to calculate the postion of each particle- but then draw a quad at that position. We set the specific values for the vertices in the compute shader too. (the UVs don't change for each quad, so we set that value in the C# script)
//Triangle 1 - bot left, top left, top right
vertexBuffer[index].position.x = p.position.x - halfSize;
vertexBuffer[index].position.y = p.position.y - halfSize;
vertexBuffer[index].position.z = p.position.z;
vertexBuffer[index].life = p.life;
This is from the compute shader - we're taking the particle position, then using the value HalfSize (which is half the length of a quad edge) to get the position of one of the vertices. We store these calculated values in the vertex buffer.
Graphics.DrawProceduralNow(MeshTopology.Triangles, 6, numParticles);
This is from the C# script - it's the proecdural draw call that we use to draw the quad. Note we're using the type Triangles now, 6 vertices... instead of Point and 1 vertex.
int index = instance_id * 6 + vertex_id;
This is the main difference in the Frag/Vert shader - we're iterating through the vertex buffer using this index, which takes the instance id (which is the particle id)and multiplies it by 6 to access each set of vertices for our quad. Note that in the C# we make the vertex buffer 6 times as large as the particle buffer. - For each particle there are 6 verts. We only look at the vertex buffer in the Frag/Vert shader because we've calculated everything we need to make the quads in the Compute Shader.
C#
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
#pragma warning disable 0649
public class QuadParticles : MonoBehaviour
{
private Vector2 cursorPos;
// struct
struct Particle
{
public Vector3 position;
public Vector3 velocity;
public float life;
}
const int SIZE_PARTICLE = 7 * sizeof(float);
struct Vertex
{
public Vector3 position;
public Vector2 uv;
public float life;
}
const int SIZE_VERTEX = 6 * sizeof(float);
public int particleCount = 10000;
public Material material;
public ComputeShader shader;
[Range(0.01f, 1.0f)]
public float quadSize = 0.1f;
int numParticles;
int numVerticesInMesh;
int kernelID;
ComputeBuffer particleBuffer;//our particles are now quads....so...
ComputeBuffer vertexBuffer;//added to store vertices for each quad particle
int groupSizeX;
// Use this for initialization
void Start()
{
Init();
}
void Init()
{
// find the id of the kernel
kernelID = shader.FindKernel("CSMain");
uint threadsX;
shader.GetKernelThreadGroupSizes(kernelID, out threadsX, out _, out _);
groupSizeX = Mathf.CeilToInt((float)particleCount / (float)threadsX);
numParticles = groupSizeX * (int)threadsX;
// initialize the particles
Particle[] particleArray = new Particle[numParticles];
int numVertices = numParticles * 6;//quad has 6 vertices because 2 triangles
Vertex[] vertexArray = new Vertex[numVertices];
Vector3 pos = new Vector3();
int index;
for (int i = 0; i < numParticles; i++)
{
pos.Set(Random.value * 2 - 1.0f, Random.value * 2 - 1.0f, Random.value * 2 - 1.0f);
pos.Normalize();
pos *= Random.value;
pos *= 0.5f;
particleArray[i].position.Set(pos.x, pos.y, pos.z + 3);
particleArray[i].velocity.Set(0, 0, 0);
// Initial life value
particleArray[i].life = Random.value * 5.0f + 1.0f;
index = i * 6;
//Triangle 1-bottom-left,top-left,top,right//CLOCKWISE WINDING
vertexArray[index].uv.Set(0, 0);
vertexArray[index + 1].uv.Set(0, 1);
vertexArray[index + 2].uv.Set(1, 1);
//Triangle 2-bottom-left,top-right,bottom-right//CLOCKWISE WINDING TO BE FRONT FACING
vertexArray[index + 3].uv.Set(0,0);
vertexArray[index + 4].uv.Set(1,1);
vertexArray[index + 5].uv.Set(1,0);
}
// create compute buffers
particleBuffer = new ComputeBuffer(numParticles, SIZE_PARTICLE);
particleBuffer.SetData(particleArray);
vertexBuffer = new ComputeBuffer(numVertices, SIZE_VERTEX);
vertexBuffer.SetData(vertexArray);
// bind the compute buffers to the shader and the compute shader
shader.SetBuffer(kernelID, "particleBuffer", particleBuffer);
shader.SetBuffer(kernelID, "vertexBuffer", vertexBuffer);
shader.SetFloat("halfSize", quadSize * 0.5f);
material.SetBuffer("vertexBuffer", vertexBuffer);
}
void OnRenderObject()
{
material.SetPass(0);
Graphics.DrawProceduralNow(MeshTopology.Triangles, 6, numParticles);//drawing procedural triangles, 6verts per instance(quad), numParticles is the instance count - ie number of particles.
}
void OnDestroy()
{
if (particleBuffer != null){
particleBuffer.Release();
}
if(vertexBuffer != null)
{
vertexBuffer.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()
{
Vector3 p = new Vector3();
Camera c = Camera.main; //make sure your camera has the Main Camera tag enabled
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));
cursorPos.x = p.x;
cursorPos.y = p.y;
}
}
Compute Shader
#pragma kernel CSMain
// Particle's data
struct Particle
{
float3 position;
float3 velocity;
float life;
};
struct Vertex
{
float3 position;
float2 uv;
float life;
};
// Particle's data, shared with the shader
RWStructuredBuffer<Particle> particleBuffer;
RWStructuredBuffer<Vertex> vertexBuffer;
// Variables set from the CPU
float deltaTime;
float2 mousePosition;
float halfSize;
uint rng_state;
// http://www.reedbeta.com/blog/quick-and-easy-gpu-random-numbers-in-d3d11/
uint rand_xorshift()
{
// Xorshift algorithm from 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);
// reset the life of this particle
particleBuffer[id].life = 4;
particleBuffer[id].velocity = float3(0,0,0);
}
[numthreads(256, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID)
{
Particle p = particleBuffer[id.x];
// subtract the life based on deltaTime
p.life -= deltaTime;
float3 delta = float3(mousePosition.xy, 3) - p.position;
float3 dir = normalize(delta);
p.velocity += dir;
p.position += p.velocity * deltaTime;
particleBuffer[id.x] = p;
if (particleBuffer[id.x].life < 0){
respawn(id.x);
p = particleBuffer[id.x];
}
int index = id.x * 6;
//Triangle 1 - bot left, top left, top right
vertexBuffer[index].position.x = p.position.x - halfSize;
vertexBuffer[index].position.y = p.position.y - halfSize;
vertexBuffer[index].position.z = p.position.z;
vertexBuffer[index].life = p.life;
vertexBuffer[index+1].position.x = p.position.x - halfSize;
vertexBuffer[index+1].position.y = p.position.y + halfSize;
vertexBuffer[index+1].position.z = p.position.z;
vertexBuffer[index+1].life = p.life;
vertexBuffer[index+2].position.x = p.position.x + halfSize;
vertexBuffer[index+2].position.y = p.position.y + halfSize;
vertexBuffer[index+2].position.z = p.position.z;
vertexBuffer[index + 2].life = p.life;
//Triangle 2 -bot left, top right,bot right
vertexBuffer[index+3].position.x = p.position.x - halfSize;
vertexBuffer[index+3].position.y = p.position.y - halfSize;
vertexBuffer[index+3].position.z = p.position.z;
vertexBuffer[index + 3].life = p.life;
vertexBuffer[index + 4].position.x = p.position.x + halfSize;
vertexBuffer[index + 4].position.y = p.position.y + halfSize;
vertexBuffer[index + 4].position.z = p.position.z;
vertexBuffer[index + 4].life = p.life;
vertexBuffer[index + 5].position.x = p.position.x + halfSize;
vertexBuffer[index + 5].position.y = p.position.y - halfSize;
vertexBuffer[index + 5].position.z = p.position.z;
vertexBuffer[index + 3].life = p.life;
}
Frag/Vert Shader
Shader "Custom/QuadParticle" {
Properties
{
_MainTex("Texture", 2D) = "white" {}
}
SubShader {
Pass {
Tags{ "Queue"="Transparent" "RenderType"="Transparent" "IgnoreProjector"="True" }
Blend SrcAlpha OneMinusSrcAlpha
LOD 200
ZWrite Off
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
// Use shader model 3.0 target, to get nicer looking lighting
#pragma target 5.0
struct v2f{
float4 position : SV_POSITION;
float4 color : COLOR;
float2 uv: TEXCOORD0;
};
struct Vertex {
float3 position;
float2 uv;
float life;
};
StructuredBuffer<Vertex> vertexBuffer;
sampler2D _MainTex;
v2f vert(uint vertex_id : SV_VertexID, uint instance_id : SV_InstanceID)
{
v2f o = (v2f)0; //create empty point at 0
int index = instance_id * 6 + vertex_id; //where instance id will be our id.x and vertex id will be one of the quad's vertices
float lerpVal = vertexBuffer[index].life * 0.25;
o.color = fixed4(1-lerpVal+0.1,lerpVal+0.1,1,lerpVal);
o.position = UnityWorldToClipPos(float4(vertexBuffer[index].position,1));
o.uv = vertexBuffer[index].uv;//copy buffer's uv to the shader's vert uv (interpolates)
return o;
}
float4 frag(v2f i) : COLOR
{
fixed4 color = tex2D(_MainTex,i.uv)* i.color; //get color from texture using uv
return color;
}
ENDCG
}
}
FallBack Off
}
Comments
Post a Comment