Created
May 3, 2026 10:40
-
-
Save adammyhre/b854362be51a2af55227ef91bc53e626 to your computer and use it in GitHub Desktop.
GPU Particles in Unity 6 using Compute Shaders and Render Graph
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | |
| }); | |
| }; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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