gizmo_renderer/gpu_particles/
system.rs1use wgpu::util::DeviceExt;
2
3use super::pipeline::{create_particle_pipelines, ParticlePipelines};
4use super::types::*;
5
6pub struct GpuParticleSystem {
7 pub max_particles: u32,
8 pub particles_buffer: wgpu::Buffer,
9 pub params_buffer: wgpu::Buffer,
10 pub pipelines: ParticlePipelines,
11 pub quad_vertex_buffer: wgpu::Buffer,
12 pub active_particles: u32,
13 pub ring_head: std::sync::atomic::AtomicU32,
14}
15
16impl GpuParticleSystem {
17 pub fn new(
18 device: &wgpu::Device,
19 max_particles: u32,
20 global_bind_group_layout: &wgpu::BindGroupLayout,
21 output_format: wgpu::TextureFormat,
22 ) -> Self {
23 let mut initial_particles = Vec::with_capacity(max_particles as usize);
24 for _ in 0..max_particles {
25 initial_particles.push(GpuParticle {
26 position: [0.0, 0.0, 0.0],
27 life: 999.0, velocity: [0.0, 0.0, 0.0],
29 max_life: 0.1,
30 color: [0.0, 0.0, 0.0, 0.0],
31 size_start: 0.0,
32 size_end: 0.0,
33 _padding: [0.0; 2],
34 });
35 }
36
37 let particles_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
38 label: Some("GPU Particles Buffer"),
39 contents: bytemuck::cast_slice(&initial_particles),
40 usage: wgpu::BufferUsages::STORAGE
41 | wgpu::BufferUsages::VERTEX
42 | wgpu::BufferUsages::COPY_DST,
43 });
44
45 let params = ParticleSimParams {
46 dt: 0.0,
47 global_gravity: 0.0,
48 global_drag: 0.0,
49 _padding: 0.0,
50 };
51 let params_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
52 label: Some("GPU Particle Params Buffer"),
53 contents: bytemuck::cast_slice(&[params]),
54 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
55 });
56
57 let quad_vertices: [[f32; 2]; 4] = [[-0.5, -0.5], [0.5, -0.5], [-0.5, 0.5], [0.5, 0.5]];
58
59 let quad_vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
60 label: Some("Particle Quad Vertex Buffer"),
61 contents: bytemuck::cast_slice(&quad_vertices),
62 usage: wgpu::BufferUsages::VERTEX,
63 });
64
65 let pipelines = create_particle_pipelines(
66 device,
67 global_bind_group_layout,
68 output_format,
69 ¶ms_buffer,
70 &particles_buffer,
71 );
72
73 Self {
74 max_particles,
75 particles_buffer,
76 params_buffer,
77 pipelines,
78 quad_vertex_buffer,
79 active_particles: max_particles,
80 ring_head: std::sync::atomic::AtomicU32::new(0),
81 }
82 }
83
84 pub fn update_params(&self, queue: &wgpu::Queue, dt: f32) {
85 let params = ParticleSimParams {
86 dt,
87 global_gravity: 9.81,
88 global_drag: 0.8,
89 _padding: 0.0,
90 };
91 queue.write_buffer(&self.params_buffer, 0, bytemuck::cast_slice(&[params]));
92 }
93
94 pub fn spawn_particles(&self, queue: &wgpu::Queue, new_particles: &[GpuParticle]) {
95 if new_particles.is_empty() {
96 return;
97 }
98
99 let count = new_particles.len() as u32;
100 let mut head = self
101 .ring_head
102 .fetch_add(count, std::sync::atomic::Ordering::Relaxed)
103 % self.max_particles;
104
105 let mut remaining = count;
106 let mut offset = 0;
107
108 while remaining > 0 {
109 let to_write = remaining.min(self.max_particles - head);
110 let slice = &new_particles[offset as usize..(offset + to_write) as usize];
111
112 queue.write_buffer(
113 &self.particles_buffer,
114 (head as usize * std::mem::size_of::<GpuParticle>()) as wgpu::BufferAddress,
115 bytemuck::cast_slice(slice),
116 );
117
118 head = (head + to_write) % self.max_particles;
119 offset += to_write;
120 remaining -= to_write;
121 }
122 }
123
124 pub fn compute_pass(&self, encoder: &mut wgpu::CommandEncoder, active_particles: u32) {
125 if active_particles == 0 {
126 return;
127 }
128 let mut cpass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
129 label: Some("Particle Compute Pass"),
130 timestamp_writes: None,
131 });
132 cpass.set_pipeline(&self.pipelines.compute_pipeline);
133 cpass.set_bind_group(0, &self.pipelines.compute_bind_group, &[]);
134 let workgroups = active_particles.div_ceil(64);
135 cpass.dispatch_workgroups(workgroups, 1, 1);
136 }
137
138 pub fn render_pass<'a>(
139 &'a self,
140 rpass: &mut wgpu::RenderPass<'a>,
141 global_bind_group: &'a wgpu::BindGroup,
142 active_particles: u32,
143 ) {
144 if active_particles == 0 {
145 return;
146 }
147 rpass.set_pipeline(&self.pipelines.render_pipeline);
148 rpass.set_bind_group(0, global_bind_group, &[]);
149 rpass.set_vertex_buffer(0, self.quad_vertex_buffer.slice(..));
150 rpass.set_vertex_buffer(1, self.particles_buffer.slice(..));
151 rpass.draw(0..4, 0..active_particles);
152 }
153
154 pub fn spawn_explosion(
156 &self,
157 queue: &wgpu::Queue,
158 center: [f32; 3],
159 count: u32,
160 base_color: [f32; 4],
161 force: f32,
162 ) {
163 let mut new_particles = Vec::with_capacity(count as usize);
164 for _ in 0..count {
165 let u: f32 = rand::random();
166 let v: f32 = rand::random();
167 let theta = u * 2.0 * std::f32::consts::PI;
168 let phi = (2.0 * v - 1.0).acos();
169 let r = force * (0.5 + 0.5 * rand::random::<f32>());
170
171 let vx = r * phi.sin() * theta.cos();
172 let vy = r * phi.sin() * theta.sin() + force * 0.5; let vz = r * phi.cos();
174
175 let life = 0.5 + rand::random::<f32>() * 1.5;
176
177 new_particles.push(GpuParticle {
178 position: center,
179 life,
180 velocity: [vx, vy, vz],
181 max_life: life,
182 color: base_color,
183 size_start: 0.1 + rand::random::<f32>() * 0.2,
184 size_end: 0.0,
185 _padding: [0.0; 2],
186 });
187 }
188 self.spawn_particles(queue, &new_particles);
189 }
190}