Skip to content

Instantly share code, notes, and snippets.

@adammyhre
Created May 3, 2026 10:40
Show Gist options
  • Select an option

  • Save adammyhre/b854362be51a2af55227ef91bc53e626 to your computer and use it in GitHub Desktop.

Select an option

Save adammyhre/b854362be51a2af55227ef91bc53e626 to your computer and use it in GitHub Desktop.
GPU Particles in Unity 6 using Compute Shaders and Render Graph
using UnityEngine;
public class ParticleController : MonoBehaviour {
[SerializeField] ComputeShader compute;
[SerializeField] Mesh mesh;
[SerializeField] Material material;
[SerializeField] int particleCount = 10000;
[SerializeField] float areaSize = 10f;
[SerializeField] float spawnDistanceFromCamera = 8f;
public struct Particle {
public Vector3 position;
public Vector3 velocity;
}
GraphicsBuffer particleBuffer;
int kCSMain;
uint threadGroupSizeX = 256;
int lastSimulationFrame = -1;
// Public access for the Render Pass
public ComputeShader Compute => compute;
public int Kernel => kCSMain;
public int DispatchGroupsX => Mathf.CeilToInt(particleCount / (float)Mathf.Max(1u, threadGroupSizeX));
public Material Material => material;
public Mesh Mesh => mesh;
public GraphicsBuffer ParticleBuffer => particleBuffer;
public int ParticleCount => particleCount;
public float AreaSize => areaSize;
public bool TryGetSimulationStep(out float deltaTime) {
if (lastSimulationFrame == Time.frameCount) {
deltaTime = 0f;
return false;
}
lastSimulationFrame = Time.frameCount;
deltaTime = Time.deltaTime;
return true;
}
protected void Start() {
transform.position = Camera.main.transform.position + Camera.main.transform.forward * spawnDistanceFromCamera;
if (compute == null) return;
kCSMain = compute.FindKernel("CSMain");
compute.GetKernelThreadGroupSizes(kCSMain, out threadGroupSizeX, out _, out _);
InitializeBuffers();
}
protected void OnDestroy() => particleBuffer?.Release();
void InitializeBuffers() {
particleBuffer = new GraphicsBuffer(GraphicsBuffer.Target.Structured, particleCount, sizeof(float) * 6);
Particle[] data = new Particle[particleCount];
for (int i = 0; i < particleCount; i++) {
data[i].position = Random.insideUnitSphere * 3f;
data[i].velocity = Random.insideUnitSphere;
}
particleBuffer.SetData(data);
}
public static ParticleController Active { get; private set; }
protected void OnEnable() {
Active = this;
}
protected void OnDisable() {
if (Active == this) Active = null;
}
}
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
public class ParticleRendererFeature : ScriptableRendererFeature {
public ParticleRenderPass pass;
public override void Create() {
pass = new ParticleRenderPass();
}
public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) {
if (pass != null) renderer.EnqueuePass(pass);
}
}
public class ParticleRenderPass : ScriptableRenderPass {
public ParticleRenderPass() {
renderPassEvent = RenderPassEvent.AfterRenderingTransparents;
}
class ParticlePassData {
public Material material;
public Mesh mesh;
public int particleCount;
public GraphicsBuffer particleBuffer;
public Vector3 objPosition;
public Vector3 objScale;
}
class ParticleComputePassData {
public ComputeShader compute;
public int kernel;
public int particleCount;
public float time;
public float deltaTime;
public int dispatchGroupsX;
public BufferHandle particleBuffer;
}
public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) {
var controller = ParticleController.Active;
if (controller == null || controller.Compute == null) return;
if (controller.Material == null || controller.Mesh == null || controller.ParticleBuffer == null) return;
if (controller.ParticleCount <= 0 || controller.DispatchGroupsX <= 0) return;
var particleBuffer = renderGraph.ImportBuffer(controller.ParticleBuffer);
bool simulateThisFrame = controller.TryGetSimulationStep(out float simulationDeltaTime);
if (simulateThisFrame) {
using (var builder = renderGraph.AddComputePass<ParticleComputePassData>("Particle System Compute", out var passData)) {
builder.UseBuffer(particleBuffer, AccessFlags.ReadWrite);
passData.compute = controller.Compute;
passData.kernel = controller.Kernel;
passData.particleCount = controller.ParticleCount;
passData.time = Time.time;
passData.deltaTime = simulationDeltaTime;
passData.dispatchGroupsX = controller.DispatchGroupsX;
passData.particleBuffer = particleBuffer;
builder.SetRenderFunc((ParticleComputePassData data, ComputeGraphContext cgContext) => {
cgContext.cmd.SetComputeIntParam(data.compute, "_ParticleCount", data.particleCount);
cgContext.cmd.SetComputeFloatParam(data.compute, "t", data.time);
cgContext.cmd.SetComputeFloatParam(data.compute, "dt", data.deltaTime);
cgContext.cmd.SetComputeBufferParam(data.compute, data.kernel, "Result", data.particleBuffer);
cgContext.cmd.DispatchCompute(data.compute, data.kernel, data.dispatchGroupsX, 1, 1);
});
}
}
using (var builder = renderGraph.AddRasterRenderPass<ParticlePassData>("Particle System Draw", out var passData)) {
var resourceData = frameData.Get<UniversalResourceData>();
builder.SetRenderAttachment(resourceData.activeColorTexture, 0, AccessFlags.Write);
builder.UseBuffer(particleBuffer, AccessFlags.Read);
passData.material = controller.Material;
passData.mesh = controller.Mesh;
passData.particleCount = controller.ParticleCount;
passData.particleBuffer = controller.ParticleBuffer;
passData.objPosition = controller.transform.position;
passData.objScale = controller.transform.localScale;
builder.SetRenderFunc((ParticlePassData data, RasterGraphContext rgContext) => {
data.material.SetBuffer("Result", data.particleBuffer);
data.material.SetVector("_ObjPos", data.objPosition);
data.material.SetVector("_ObjScale", data.objScale);
rgContext.cmd.DrawMeshInstancedProcedural(data.mesh, 0, data.material, 0, data.particleCount, null);
});
};
}
}
#pragma kernel CSMain
struct Particle {
float3 position;
float3 velocity;
};
int _ParticleCount;
float t;
float dt;
RWStructuredBuffer<Particle> Result;
[numthreads(256, 1, 1)]
void CSMain(uint3 id : SV_DispatchThreadID) {
if (id.x >= (uint)_ParticleCount) return;
Particle p = Result[id.x];
float r = (float)id.x / (float)_ParticleCount * 6.28f;
float3 targetVel = float3(sin(r + t), cos(r * 1.5 + t), sin(r * 0.8 + t * 1.2));
p.velocity = lerp(p.velocity, targetVel * 3.0, 0.08);
p.position += p.velocity * dt;
if (any(abs(p.position) > 10.0)) {
p.position = clamp(p.position, -10.0, 10.0);
p.velocity *= -0.7;
}
Result[id.x] = p;
}
Shader "Custom/ParticleSim3DRender"
{
Properties
{
_FastColor("Fast Particle Color", Color) = (1, 0, 0, 1)
_SlowColor("Slow Particle Color", Color) = (0, 0.5, 1, 1)
_Intensity("Lightning Intensity", Float) = 7
_Scale("Particle Scale", Float) = 0.4
}
SubShader
{
Tags
{
"RenderType" = "Transparent"
"RenderPipeline" = "UniversalPipeline"
"Queue" = "Transparent"
}
Blend One One
ZWrite Off
Cull Off
Pass
{
HLSLPROGRAM
#pragma vertex vert
#pragma fragment frag
#pragma target 4.5
#include "Packages/com.unity.render-pipelines.universal/ShaderLibrary/Core.hlsl"
struct Particle
{
float3 position;
float3 velocity;
};
struct Attributes
{
float4 positionOS : POSITION;
uint instanceID : SV_InstanceID;
};
struct Varyings
{
float4 position : SV_POSITION;
float4 color : COLOR;
};
// Particle data from Compute Shader
StructuredBuffer<Particle> Result;
CBUFFER_START(UnityPerMaterial)
half4 _FastColor;
half4 _SlowColor;
float _Intensity;
float _Scale;
float3 _ObjPos; // World position of the container object
float3 _ObjScale; // Scale of the container object
CBUFFER_END
Varyings vert(Attributes IN)
{
Varyings OUT;
Particle p = Result[IN.instanceID];
// Transform particle position from local simulation space to world space
float3 localPos = IN.positionOS.xyz * 0.1 * _Scale;
float3 worldPos = localPos * _ObjScale + p.position + _ObjPos;
OUT.position = TransformWorldToHClip(worldPos);
// Color based on velocity
OUT.color = lerp(_SlowColor, _FastColor, length(p.velocity) * 0.5);
return OUT;
}
half4 frag(Varyings IN) : SV_Target
{
return half4(IN.color.xyz * _Intensity, 1.0);
}
ENDHLSL
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment