1use bytemuck::{Pod, Zeroable};
20use std::collections::HashMap;
21use wgpu::util::DeviceExt;
22
23pub const MAX_PARTICLES_PER_SYSTEM: u32 = 100_000;
25
26#[repr(C)]
29#[derive(Clone, Copy, Debug, Pod, Zeroable)]
30pub struct GpuParticle {
31 pub position_life: [f32; 4],
33 pub velocity_max_life: [f32; 4],
35 pub color: [f32; 4],
37 pub size_rotation: [f32; 4],
39}
40
41impl Default for GpuParticle {
42 fn default() -> Self {
43 Self {
44 position_life: [0.0, 0.0, 0.0, 0.0], velocity_max_life: [0.0, 0.0, 0.0, 1.0],
46 color: [1.0, 1.0, 1.0, 1.0],
47 size_rotation: [0.1, 0.1, 0.0, 0.0],
48 }
49 }
50}
51
52#[repr(C)]
54#[derive(Clone, Copy, Debug, Pod, Zeroable)]
55pub struct GpuEmitter {
56 pub position_shape: [f32; 4],
58 pub shape_params: [f32; 4],
60 pub direction_randomness: [f32; 4],
62 pub emission_config: [f32; 4],
64 pub lifetime_speed: [f32; 4],
66 pub size_config: [f32; 4],
68 pub start_color: [f32; 4],
70 pub mid_color: [f32; 4],
72 pub end_color: [f32; 4],
74}
75
76impl Default for GpuEmitter {
77 fn default() -> Self {
78 Self {
79 position_shape: [0.0, 0.0, 0.0, 0.0], shape_params: [0.0; 4],
81 direction_randomness: [0.0, 1.0, 0.0, 0.0], emission_config: [100.0, 0.0, 0.0, 1.0], lifetime_speed: [1.0, 2.0, 1.0, 2.0],
84 size_config: [0.1, 0.2, 0.0, 0.1],
85 start_color: [1.0, 1.0, 1.0, 1.0],
86 mid_color: [1.0, 1.0, 1.0, 0.5],
87 end_color: [1.0, 1.0, 1.0, 0.0],
88 }
89 }
90}
91
92#[repr(C)]
94#[derive(Clone, Copy, Debug, Pod, Zeroable)]
95pub struct GpuForce {
96 pub type_strength: [f32; 4],
99 pub direction_params: [f32; 4],
101}
102
103impl Default for GpuForce {
104 fn default() -> Self {
105 Self {
106 type_strength: [0.0, 0.0, 0.0, 0.0],
107 direction_params: [0.0, -9.8, 0.0, 0.0], }
109 }
110}
111
112#[repr(C)]
114#[derive(Clone, Copy, Debug, Pod, Zeroable)]
115pub struct GpuSimulationUniforms {
116 pub time_config: [f32; 4],
118 pub random_seed: [f32; 4],
120 pub force_config: [f32; 4],
122}
123
124impl Default for GpuSimulationUniforms {
125 fn default() -> Self {
126 Self {
127 time_config: [0.016, 0.0, MAX_PARTICLES_PER_SYSTEM as f32, 0.0],
128 random_seed: [0.0; 4],
129 force_config: [0.0; 4],
130 }
131 }
132}
133
134#[repr(C)]
136#[derive(Clone, Copy, Debug, Pod, Zeroable)]
137pub struct GpuRenderUniforms {
138 pub view_proj: [[f32; 4]; 4],
140 pub camera_pos_fov: [f32; 4],
142 pub camera_right_aspect: [f32; 4],
144 pub camera_up: [f32; 4],
146 pub viewport_config: [f32; 4],
148}
149
150impl Default for GpuRenderUniforms {
151 fn default() -> Self {
152 Self {
153 view_proj: [
154 [1.0, 0.0, 0.0, 0.0],
155 [0.0, 1.0, 0.0, 0.0],
156 [0.0, 0.0, 1.0, 0.0],
157 [0.0, 0.0, 0.0, 1.0],
158 ],
159 camera_pos_fov: [0.0, 0.0, 5.0, 0.8],
160 camera_right_aspect: [1.0, 0.0, 0.0, 1.0],
161 camera_up: [0.0, 1.0, 0.0, 0.0],
162 viewport_config: [800.0, 600.0, 0.0, 0.0],
163 }
164 }
165}
166
167#[derive(Clone, Debug)]
169pub struct ParticleViewport {
170 pub emitter: GpuEmitter,
172 pub forces: Vec<GpuForce>,
174 pub max_particles: u32,
176 pub camera_pos: [f32; 3],
178 pub camera_target: [f32; 3],
180 pub camera_up: [f32; 3],
182 pub fov: f32,
184 pub time: f32,
186 pub delta_time: f32,
188 pub bounds: [f32; 4],
190 pub blend_mode: u32,
192 pub playing: bool,
194}
195
196impl Default for ParticleViewport {
197 fn default() -> Self {
198 Self {
199 emitter: GpuEmitter::default(),
200 forces: Vec::new(),
201 max_particles: 10000,
202 camera_pos: [0.0, 0.0, 5.0],
203 camera_target: [0.0, 0.0, 0.0],
204 camera_up: [0.0, 1.0, 0.0],
205 fov: 0.8,
206 time: 0.0,
207 delta_time: 0.016,
208 bounds: [0.0, 0.0, 800.0, 600.0],
209 blend_mode: 0,
210 playing: true,
211 }
212 }
213}
214
215pub const PARTICLE_COMPUTE_SHADER: &str = r#"
217// ============================================================================
218// Blinc GPU Particle Compute Shader
219// ============================================================================
220
221struct Particle {
222 position_life: vec4<f32>, // xyz=position, w=life remaining
223 velocity_max_life: vec4<f32>, // xyz=velocity, w=max lifetime
224 color: vec4<f32>, // rgba
225 size_rotation: vec4<f32>, // current, start, end, rotation
226}
227
228struct Emitter {
229 position_shape: vec4<f32>, // xyz=position, w=shape type
230 shape_params: vec4<f32>, // shape-specific params
231 direction_randomness: vec4<f32>, // xyz=direction, w=randomness
232 emission_config: vec4<f32>, // rate, burst, spawn_acc, gravity_scale
233 lifetime_speed: vec4<f32>, // min_life, max_life, min_speed, max_speed
234 size_config: vec4<f32>, // start_min, start_max, end_min, end_max
235 start_color: vec4<f32>, // rgba - base (young particles)
236 mid_color: vec4<f32>, // rgba - middle age
237 end_color: vec4<f32>, // rgba - tip (old particles)
238}
239
240struct Force {
241 type_strength: vec4<f32>, // type, strength, 0, 0
242 direction_params: vec4<f32>, // xyz=dir/pos, w=extra
243}
244
245struct SimUniforms {
246 time_config: vec4<f32>, // dt, time, max_particles, active
247 random_seed: vec4<f32>, // 4 random seeds
248 force_config: vec4<f32>, // num_forces, 0, 0, 0
249}
250
251@group(0) @binding(0) var<storage, read_write> particles: array<Particle>;
252@group(0) @binding(1) var<uniform> emitter: Emitter;
253@group(0) @binding(2) var<uniform> uniforms: SimUniforms;
254@group(0) @binding(3) var<storage, read> forces: array<Force>;
255
256// Constants for emitter shapes
257const SHAPE_POINT: u32 = 0u;
258const SHAPE_SPHERE: u32 = 1u;
259const SHAPE_HEMISPHERE: u32 = 2u;
260const SHAPE_CONE: u32 = 3u;
261const SHAPE_BOX: u32 = 4u;
262const SHAPE_CIRCLE: u32 = 5u;
263
264// Constants for force types
265const FORCE_GRAVITY: u32 = 0u;
266const FORCE_WIND: u32 = 1u;
267const FORCE_VORTEX: u32 = 2u;
268const FORCE_DRAG: u32 = 3u;
269const FORCE_TURBULENCE: u32 = 4u;
270const FORCE_ATTRACTOR: u32 = 5u;
271const FORCE_RADIAL: u32 = 6u;
272
273// PCG random number generator
274fn pcg_hash(input: u32) -> u32 {
275 let state = input * 747796405u + 2891336453u;
276 let word = ((state >> ((state >> 28u) + 4u)) ^ state) * 277803737u;
277 return (word >> 22u) ^ word;
278}
279
280fn random_float(seed: ptr<function, u32>) -> f32 {
281 *seed = pcg_hash(*seed);
282 return f32(*seed) / 4294967295.0;
283}
284
285fn random_range(seed: ptr<function, u32>, min_val: f32, max_val: f32) -> f32 {
286 return min_val + random_float(seed) * (max_val - min_val);
287}
288
289fn random_unit_vector(seed: ptr<function, u32>) -> vec3<f32> {
290 let theta = random_float(seed) * 6.283185;
291 let z = random_range(seed, -1.0, 1.0);
292 let r = sqrt(1.0 - z * z);
293 return vec3<f32>(r * cos(theta), r * sin(theta), z);
294}
295
296// Get spawn position based on emitter shape
297fn get_spawn_position(seed: ptr<function, u32>) -> vec3<f32> {
298 let shape = u32(emitter.position_shape.w);
299 let base_pos = emitter.position_shape.xyz;
300
301 switch (shape) {
302 case SHAPE_POINT: {
303 return base_pos;
304 }
305 case SHAPE_SPHERE: {
306 let radius = emitter.shape_params.x;
307 let dir = random_unit_vector(seed);
308 let r = radius * pow(random_float(seed), 1.0/3.0);
309 return base_pos + dir * r;
310 }
311 case SHAPE_HEMISPHERE: {
312 let radius = emitter.shape_params.x;
313 var dir = random_unit_vector(seed);
314 dir.y = abs(dir.y); // Upper hemisphere
315 let r = radius * pow(random_float(seed), 1.0/3.0);
316 return base_pos + dir * r;
317 }
318 case SHAPE_CONE: {
319 let angle = emitter.shape_params.x;
320 let radius = emitter.shape_params.y;
321 let theta = random_float(seed) * 6.283185;
322 let r = radius * sqrt(random_float(seed));
323 return base_pos + vec3<f32>(r * cos(theta), 0.0, r * sin(theta));
324 }
325 case SHAPE_BOX: {
326 let half = emitter.shape_params.xyz;
327 return base_pos + vec3<f32>(
328 random_range(seed, -half.x, half.x),
329 random_range(seed, -half.y, half.y),
330 random_range(seed, -half.z, half.z)
331 );
332 }
333 case SHAPE_CIRCLE: {
334 let radius = emitter.shape_params.x;
335 let theta = random_float(seed) * 6.283185;
336 let r = radius * sqrt(random_float(seed));
337 return base_pos + vec3<f32>(r * cos(theta), 0.0, r * sin(theta));
338 }
339 default: {
340 return base_pos;
341 }
342 }
343}
344
345// Get spawn velocity based on direction and randomness
346fn get_spawn_velocity(seed: ptr<function, u32>) -> vec3<f32> {
347 let base_dir = normalize(emitter.direction_randomness.xyz);
348 let randomness = emitter.direction_randomness.w;
349
350 // Mix base direction with random direction
351 let random_dir = random_unit_vector(seed);
352 let dir = normalize(mix(base_dir, random_dir, randomness));
353
354 let speed = random_range(seed, emitter.lifetime_speed.z, emitter.lifetime_speed.w);
355 return dir * speed;
356}
357
358// Apply force to velocity
359fn apply_force(force: Force, pos: vec3<f32>, vel: vec3<f32>, dt: f32) -> vec3<f32> {
360 let force_type = u32(force.type_strength.x);
361 let strength = force.type_strength.y;
362 var new_vel = vel;
363
364 switch (force_type) {
365 case FORCE_GRAVITY: {
366 new_vel = vel + force.direction_params.xyz * dt;
367 }
368 case FORCE_WIND: {
369 let turbulence = force.direction_params.w;
370 new_vel = vel + force.direction_params.xyz * strength * dt;
371 }
372 case FORCE_VORTEX: {
373 let axis = normalize(force.direction_params.xyz);
374 let to_particle = pos;
375 let tangent = cross(axis, to_particle);
376 new_vel = vel + normalize(tangent) * strength * dt;
377 }
378 case FORCE_DRAG: {
379 new_vel = vel * (1.0 - strength * dt);
380 }
381 case FORCE_TURBULENCE: {
382 // 3D turbulence with per-particle variation
383 let freq = force.direction_params.w;
384 let t = uniforms.time_config.y;
385
386 // Use velocity magnitude as per-particle seed for variation
387 // This makes each particle get unique turbulence even at same position
388 let vel_seed = dot(vel, vec3<f32>(12.9898, 78.233, 37.719));
389 let particle_offset = fract(sin(vel_seed) * 43758.5453) * 6.28318; // 0 to 2*PI
390
391 // Position-based noise with per-particle phase offset
392 let p1 = pos * freq + vec3<f32>(t * 1.3 + particle_offset, t * 0.7, t * 1.1);
393 let p2 = pos * freq * 1.7 + vec3<f32>(t * 0.9, t * 1.4 + particle_offset, t * 0.6);
394
395 // Multi-octave noise
396 let n1 = sin(p1.x + particle_offset) * cos(p1.y) * sin(p1.z + t);
397 let n2 = cos(p1.y * 1.3 + particle_offset) * sin(p1.z * 0.8) * cos(p1.x + t * 0.7);
398 let n3 = sin(p1.z * 1.1) * cos(p1.x * 0.9 + particle_offset) * sin(p1.y + t * 1.2);
399
400 // Second octave
401 let m1 = sin(p2.x + particle_offset * 0.7) * cos(p2.z) * 0.5;
402 let m2 = cos(p2.y) * sin(p2.x + particle_offset * 1.3) * 0.5;
403 let m3 = sin(p2.z + particle_offset * 0.5) * cos(p2.y) * 0.5;
404
405 // Combine - allow more vertical variation for dancing flames
406 let turb = vec3<f32>(
407 n1 + m1,
408 (n2 + m2) * 0.5,
409 n3 + m3
410 );
411
412 new_vel = vel + turb * strength * dt;
413 }
414 case FORCE_ATTRACTOR: {
415 let attractor_pos = force.direction_params.xyz;
416 let to_attractor = attractor_pos - pos;
417 let dist = max(length(to_attractor), 0.1);
418 new_vel = vel + normalize(to_attractor) * strength / (dist * dist) * dt;
419 }
420 case FORCE_RADIAL: {
421 // Centering force - pulls particles toward the Y-axis (center line)
422 // Negative strength = pull toward center, positive = push away
423 let center = force.direction_params.xyz;
424 let to_center = vec3<f32>(center.x - pos.x, 0.0, center.z - pos.z);
425 let dist = max(length(to_center), 0.01);
426 // Linear falloff - stronger when further from center
427 new_vel = vel + to_center * strength * dt;
428 }
429 default: {}
430 }
431
432 return new_vel;
433}
434
435@compute @workgroup_size(64)
436fn cs_main(@builtin(global_invocation_id) global_id: vec3<u32>) {
437 let idx = global_id.x;
438 let max_particles = u32(uniforms.time_config.z);
439
440 if (idx >= max_particles) {
441 return;
442 }
443
444 var p = particles[idx];
445 let dt = uniforms.time_config.x;
446 let time = uniforms.time_config.y;
447
448 // Initialize random seed based on particle index and time
449 var seed = idx + u32(time * 1000.0);
450 seed = pcg_hash(seed + u32(uniforms.random_seed.x * 1000000.0));
451
452 // Check if particle is alive
453 if (p.position_life.w > 0.0) {
454 // Update particle
455 p.position_life.w -= dt;
456
457 if (p.position_life.w <= 0.0) {
458 // Particle died
459 p.position_life.w = 0.0;
460 } else {
461 // Apply forces
462 var vel = p.velocity_max_life.xyz;
463 let num_forces = u32(uniforms.force_config.x);
464 let gravity_scale = emitter.emission_config.w;
465
466 // Apply forces from the forces array
467 // Note: Gravity is handled via Force::Gravity in the forces array, not hardcoded
468 for (var i = 0u; i < num_forces; i++) {
469 let force = forces[i];
470 // For gravity forces (type 0), apply gravity_scale
471 if (u32(force.type_strength.x) == FORCE_GRAVITY) {
472 let gravity_dir = force.direction_params.xyz * gravity_scale;
473 vel = vel + gravity_dir * dt;
474 } else {
475 vel = apply_force(force, p.position_life.xyz, vel, dt);
476 }
477 }
478
479 p.velocity_max_life = vec4<f32>(vel, p.velocity_max_life.w);
480
481 // Update position
482 p.position_life = vec4<f32>(
483 p.position_life.xyz + vel * dt,
484 p.position_life.w
485 );
486
487 // Update color based on lifetime (3-color gradient)
488 // life_ratio: 1.0 = just born (start), 0.5 = mid-life (mid), 0.0 = dying (end)
489 let life_ratio = p.position_life.w / p.velocity_max_life.w;
490 if (life_ratio > 0.5) {
491 // Young particle: interpolate from start to mid
492 let t = (life_ratio - 0.5) * 2.0; // 0.5->1.0 maps to 0->1
493 p.color = mix(emitter.mid_color, emitter.start_color, t);
494 } else {
495 // Older particle: interpolate from mid to end
496 let t = life_ratio * 2.0; // 0->0.5 maps to 0->1
497 p.color = mix(emitter.end_color, emitter.mid_color, t);
498 }
499
500 // Update size based on lifetime
501 let start_size = p.size_rotation.y;
502 let end_size = p.size_rotation.z;
503 p.size_rotation.x = mix(end_size, start_size, life_ratio);
504 }
505 } else {
506 // Try to spawn new particle
507 let emission_rate = emitter.emission_config.x;
508 let burst_count = emitter.emission_config.y;
509
510 // Calculate spawn chance: combines continuous emission with burst
511 var spawn_chance = 0.0;
512 if (emission_rate > 0.0) {
513 spawn_chance = emission_rate * dt / f32(max_particles);
514 }
515 // For burst effects, use burst_count to determine spawn probability
516 if (burst_count > 0.0) {
517 spawn_chance = spawn_chance + burst_count / f32(max_particles);
518 }
519
520 if (random_float(&seed) < spawn_chance) {
521 // Spawn new particle
522 let pos = get_spawn_position(&seed);
523 let vel = get_spawn_velocity(&seed);
524 let lifetime = random_range(&seed, emitter.lifetime_speed.x, emitter.lifetime_speed.y);
525 let size = random_range(&seed, emitter.size_config.x, emitter.size_config.y);
526 let end_size = random_range(&seed, emitter.size_config.z, emitter.size_config.w);
527
528 p.position_life = vec4<f32>(pos, lifetime);
529 p.velocity_max_life = vec4<f32>(vel, lifetime);
530 p.color = emitter.start_color;
531 p.size_rotation = vec4<f32>(size, size, end_size, 0.0);
532 }
533 }
534
535 particles[idx] = p;
536}
537"#;
538
539pub const PARTICLE_RENDER_DT_SHADER: &str = include_str!("shaders/particle_render_dt.wgsl");
541
542pub const PARTICLE_RENDER_SHADER: &str = r#"
544// ============================================================================
545// Blinc GPU Particle Render Shader
546// ============================================================================
547
548struct Particle {
549 position_life: vec4<f32>,
550 velocity_max_life: vec4<f32>,
551 color: vec4<f32>,
552 size_rotation: vec4<f32>,
553}
554
555struct RenderUniforms {
556 view_proj: mat4x4<f32>,
557 camera_pos_fov: vec4<f32>,
558 camera_right_aspect: vec4<f32>,
559 camera_up: vec4<f32>,
560 viewport_config: vec4<f32>,
561}
562
563struct VertexOutput {
564 @builtin(position) position: vec4<f32>,
565 @location(0) uv: vec2<f32>,
566 @location(1) color: vec4<f32>,
567}
568
569@group(0) @binding(0) var<storage, read> particles: array<Particle>;
570@group(0) @binding(1) var<uniform> uniforms: RenderUniforms;
571
572@vertex
573fn vs_main(
574 @builtin(vertex_index) vertex_index: u32,
575 @builtin(instance_index) instance_index: u32,
576) -> VertexOutput {
577 var out: VertexOutput;
578
579 let p = particles[instance_index];
580
581 // Skip dead particles (move to clip space far away)
582 if (p.position_life.w <= 0.0) {
583 out.position = vec4<f32>(0.0, 0.0, 1000.0, 1.0);
584 out.uv = vec2<f32>(0.0);
585 out.color = vec4<f32>(0.0);
586 return out;
587 }
588
589 // Billboard quad vertices
590 let quad_verts = array<vec2<f32>, 6>(
591 vec2<f32>(-1.0, -1.0),
592 vec2<f32>( 1.0, -1.0),
593 vec2<f32>(-1.0, 1.0),
594 vec2<f32>( 1.0, -1.0),
595 vec2<f32>( 1.0, 1.0),
596 vec2<f32>(-1.0, 1.0),
597 );
598
599 let quad_uvs = array<vec2<f32>, 6>(
600 vec2<f32>(0.0, 1.0),
601 vec2<f32>(1.0, 1.0),
602 vec2<f32>(0.0, 0.0),
603 vec2<f32>(1.0, 1.0),
604 vec2<f32>(1.0, 0.0),
605 vec2<f32>(0.0, 0.0),
606 );
607
608 let local_pos = quad_verts[vertex_index];
609 let size = p.size_rotation.x;
610
611 // Calculate billboard orientation
612 let camera_right = uniforms.camera_right_aspect.xyz;
613 let camera_up = uniforms.camera_up.xyz;
614
615 // World position with billboard offset
616 let world_pos = p.position_life.xyz +
617 camera_right * local_pos.x * size +
618 camera_up * local_pos.y * size;
619
620 // Project to clip space
621 out.position = uniforms.view_proj * vec4<f32>(world_pos, 1.0);
622 out.uv = quad_uvs[vertex_index];
623 out.color = p.color;
624
625 return out;
626}
627
628@fragment
629fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
630 // Circular particle with soft edges
631 let center = vec2<f32>(0.5);
632 let dist = length(in.uv - center) * 2.0;
633
634 // Soft circle falloff
635 let alpha = 1.0 - smoothstep(0.8, 1.0, dist);
636
637 // Discard if too far from center
638 if (alpha < 0.01) {
639 discard;
640 }
641
642 return vec4<f32>(in.color.rgb, in.color.a * alpha);
643}
644"#;
645
646#[derive(Debug)]
648pub struct ParticleSystemGpu {
649 particle_buffer: wgpu::Buffer,
651 emitter_buffer: wgpu::Buffer,
653 sim_uniform_buffer: wgpu::Buffer,
655 render_uniform_buffer: wgpu::Buffer,
657 forces_buffer: wgpu::Buffer,
659 compute_pipeline: wgpu::ComputePipeline,
661 render_pipeline: wgpu::RenderPipeline,
663 compute_bind_group: wgpu::BindGroup,
665 render_bind_group: wgpu::BindGroup,
667 max_particles: u32,
669 time: f32,
671}
672
673impl ParticleSystemGpu {
674 pub fn new(
676 device: &wgpu::Device,
677 surface_format: wgpu::TextureFormat,
678 max_particles: u32,
679 ) -> Self {
680 let max_particles = max_particles.min(MAX_PARTICLES_PER_SYSTEM);
681
682 let particle_buffer = device.create_buffer(&wgpu::BufferDescriptor {
684 label: Some("Particle Buffer"),
685 size: (std::mem::size_of::<GpuParticle>() * max_particles as usize) as u64,
686 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
687 mapped_at_creation: false,
688 });
689
690 let emitter_buffer = device.create_buffer(&wgpu::BufferDescriptor {
692 label: Some("Emitter Buffer"),
693 size: std::mem::size_of::<GpuEmitter>() as u64,
694 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
695 mapped_at_creation: false,
696 });
697
698 let sim_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
700 label: Some("Simulation Uniforms"),
701 size: std::mem::size_of::<GpuSimulationUniforms>() as u64,
702 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
703 mapped_at_creation: false,
704 });
705
706 let render_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
708 label: Some("Render Uniforms"),
709 size: std::mem::size_of::<GpuRenderUniforms>() as u64,
710 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
711 mapped_at_creation: false,
712 });
713
714 let forces_buffer = device.create_buffer(&wgpu::BufferDescriptor {
716 label: Some("Forces Buffer"),
717 size: (std::mem::size_of::<GpuForce>() * 8) as u64,
718 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
719 mapped_at_creation: false,
720 });
721
722 let compute_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
724 label: Some("Particle Compute Shader"),
725 source: wgpu::ShaderSource::Wgsl(PARTICLE_COMPUTE_SHADER.into()),
726 });
727
728 let render_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
730 label: Some("Particle Render Shader"),
731 source: wgpu::ShaderSource::Wgsl(PARTICLE_RENDER_SHADER.into()),
732 });
733
734 let compute_bind_group_layout =
736 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
737 label: Some("Particle Compute Bind Group Layout"),
738 entries: &[
739 wgpu::BindGroupLayoutEntry {
741 binding: 0,
742 visibility: wgpu::ShaderStages::COMPUTE,
743 ty: wgpu::BindingType::Buffer {
744 ty: wgpu::BufferBindingType::Storage { read_only: false },
745 has_dynamic_offset: false,
746 min_binding_size: None,
747 },
748 count: None,
749 },
750 wgpu::BindGroupLayoutEntry {
752 binding: 1,
753 visibility: wgpu::ShaderStages::COMPUTE,
754 ty: wgpu::BindingType::Buffer {
755 ty: wgpu::BufferBindingType::Uniform,
756 has_dynamic_offset: false,
757 min_binding_size: None,
758 },
759 count: None,
760 },
761 wgpu::BindGroupLayoutEntry {
763 binding: 2,
764 visibility: wgpu::ShaderStages::COMPUTE,
765 ty: wgpu::BindingType::Buffer {
766 ty: wgpu::BufferBindingType::Uniform,
767 has_dynamic_offset: false,
768 min_binding_size: None,
769 },
770 count: None,
771 },
772 wgpu::BindGroupLayoutEntry {
774 binding: 3,
775 visibility: wgpu::ShaderStages::COMPUTE,
776 ty: wgpu::BindingType::Buffer {
777 ty: wgpu::BufferBindingType::Storage { read_only: true },
778 has_dynamic_offset: false,
779 min_binding_size: None,
780 },
781 count: None,
782 },
783 ],
784 });
785
786 let render_bind_group_layout =
788 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
789 label: Some("Particle Render Bind Group Layout"),
790 entries: &[
791 wgpu::BindGroupLayoutEntry {
793 binding: 0,
794 visibility: wgpu::ShaderStages::VERTEX,
795 ty: wgpu::BindingType::Buffer {
796 ty: wgpu::BufferBindingType::Storage { read_only: true },
797 has_dynamic_offset: false,
798 min_binding_size: None,
799 },
800 count: None,
801 },
802 wgpu::BindGroupLayoutEntry {
804 binding: 1,
805 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
806 ty: wgpu::BindingType::Buffer {
807 ty: wgpu::BufferBindingType::Uniform,
808 has_dynamic_offset: false,
809 min_binding_size: None,
810 },
811 count: None,
812 },
813 ],
814 });
815
816 let compute_pipeline_layout =
818 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
819 label: Some("Particle Compute Pipeline Layout"),
820 bind_group_layouts: &[&compute_bind_group_layout],
821 push_constant_ranges: &[],
822 });
823
824 let compute_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
825 label: Some("Particle Compute Pipeline"),
826 layout: Some(&compute_pipeline_layout),
827 module: &compute_shader,
828 entry_point: Some("cs_main"),
829 compilation_options: Default::default(),
830 cache: None,
831 });
832
833 let render_pipeline_layout =
835 device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
836 label: Some("Particle Render Pipeline Layout"),
837 bind_group_layouts: &[&render_bind_group_layout],
838 push_constant_ranges: &[],
839 });
840
841 let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
842 label: Some("Particle Render Pipeline"),
843 layout: Some(&render_pipeline_layout),
844 vertex: wgpu::VertexState {
845 module: &render_shader,
846 entry_point: Some("vs_main"),
847 buffers: &[],
848 compilation_options: Default::default(),
849 },
850 fragment: Some(wgpu::FragmentState {
851 module: &render_shader,
852 entry_point: Some("fs_main"),
853 targets: &[Some(wgpu::ColorTargetState {
854 format: surface_format,
855 blend: Some(wgpu::BlendState {
856 color: wgpu::BlendComponent {
857 src_factor: wgpu::BlendFactor::SrcAlpha,
858 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
859 operation: wgpu::BlendOperation::Add,
860 },
861 alpha: wgpu::BlendComponent {
862 src_factor: wgpu::BlendFactor::One,
863 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
864 operation: wgpu::BlendOperation::Add,
865 },
866 }),
867 write_mask: wgpu::ColorWrites::ALL,
868 })],
869 compilation_options: Default::default(),
870 }),
871 primitive: wgpu::PrimitiveState {
872 topology: wgpu::PrimitiveTopology::TriangleList,
873 strip_index_format: None,
874 front_face: wgpu::FrontFace::Ccw,
875 cull_mode: None, unclipped_depth: false,
877 polygon_mode: wgpu::PolygonMode::Fill,
878 conservative: false,
879 },
880 depth_stencil: None,
881 multisample: wgpu::MultisampleState::default(),
882 multiview: None,
883 cache: None,
884 });
885
886 let compute_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
888 label: Some("Particle Compute Bind Group"),
889 layout: &compute_bind_group_layout,
890 entries: &[
891 wgpu::BindGroupEntry {
892 binding: 0,
893 resource: particle_buffer.as_entire_binding(),
894 },
895 wgpu::BindGroupEntry {
896 binding: 1,
897 resource: emitter_buffer.as_entire_binding(),
898 },
899 wgpu::BindGroupEntry {
900 binding: 2,
901 resource: sim_uniform_buffer.as_entire_binding(),
902 },
903 wgpu::BindGroupEntry {
904 binding: 3,
905 resource: forces_buffer.as_entire_binding(),
906 },
907 ],
908 });
909
910 let render_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
911 label: Some("Particle Render Bind Group"),
912 layout: &render_bind_group_layout,
913 entries: &[
914 wgpu::BindGroupEntry {
915 binding: 0,
916 resource: particle_buffer.as_entire_binding(),
917 },
918 wgpu::BindGroupEntry {
919 binding: 1,
920 resource: render_uniform_buffer.as_entire_binding(),
921 },
922 ],
923 });
924
925 Self {
926 particle_buffer,
927 emitter_buffer,
928 sim_uniform_buffer,
929 render_uniform_buffer,
930 forces_buffer,
931 compute_pipeline,
932 render_pipeline,
933 compute_bind_group,
934 render_bind_group,
935 max_particles,
936 time: 0.0,
937 }
938 }
939
940 pub fn update(
942 &mut self,
943 queue: &wgpu::Queue,
944 encoder: &mut wgpu::CommandEncoder,
945 viewport: &ParticleViewport,
946 ) {
947 if !viewport.playing {
948 return;
949 }
950
951 self.time = viewport.time;
952
953 queue.write_buffer(
955 &self.emitter_buffer,
956 0,
957 bytemuck::bytes_of(&viewport.emitter),
958 );
959
960 let sim_uniforms = GpuSimulationUniforms {
962 time_config: [
963 viewport.delta_time,
964 viewport.time,
965 self.max_particles as f32,
966 0.0,
967 ],
968 random_seed: [
969 viewport.time * 12_345.679,
970 viewport.time * 98_765.43,
971 (viewport.time * 11_111.111).fract(),
972 (viewport.time * 22_222.223).fract(),
973 ],
974 force_config: [viewport.forces.len() as f32, 0.0, 0.0, 0.0],
975 };
976 queue.write_buffer(
977 &self.sim_uniform_buffer,
978 0,
979 bytemuck::bytes_of(&sim_uniforms),
980 );
981
982 let mut forces = [GpuForce::default(); 8];
984 for (i, force) in viewport.forces.iter().take(8).enumerate() {
985 forces[i] = *force;
986 }
987 queue.write_buffer(&self.forces_buffer, 0, bytemuck::cast_slice(&forces));
988
989 let workgroups = self.max_particles.div_ceil(64);
991 let mut compute_pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
992 label: Some("Particle Compute Pass"),
993 timestamp_writes: None,
994 });
995 compute_pass.set_pipeline(&self.compute_pipeline);
996 compute_pass.set_bind_group(0, &self.compute_bind_group, &[]);
997 compute_pass.dispatch_workgroups(workgroups, 1, 1);
998 }
999
1000 pub fn render<'a>(
1002 &'a self,
1003 queue: &wgpu::Queue,
1004 render_pass: &mut wgpu::RenderPass<'a>,
1005 viewport: &ParticleViewport,
1006 ) {
1007 let view = Self::look_at(
1009 viewport.camera_pos,
1010 viewport.camera_target,
1011 viewport.camera_up,
1012 );
1013 let aspect = viewport.bounds[2] / viewport.bounds[3];
1014 let proj = Self::perspective(viewport.fov, aspect, 0.1, 100.0);
1015 let view_proj = Self::mat4_mul(&proj, &view);
1016
1017 let forward = [
1019 viewport.camera_target[0] - viewport.camera_pos[0],
1020 viewport.camera_target[1] - viewport.camera_pos[1],
1021 viewport.camera_target[2] - viewport.camera_pos[2],
1022 ];
1023 let forward = Self::normalize(forward);
1024 let right = Self::cross(forward, viewport.camera_up);
1025 let right = Self::normalize(right);
1026 let up = Self::cross(right, forward);
1027
1028 let render_uniforms = GpuRenderUniforms {
1030 view_proj,
1031 camera_pos_fov: [
1032 viewport.camera_pos[0],
1033 viewport.camera_pos[1],
1034 viewport.camera_pos[2],
1035 viewport.fov,
1036 ],
1037 camera_right_aspect: [right[0], right[1], right[2], aspect],
1038 camera_up: [up[0], up[1], up[2], 0.0],
1039 viewport_config: [
1040 viewport.bounds[2],
1041 viewport.bounds[3],
1042 0.0, viewport.blend_mode as f32,
1044 ],
1045 };
1046 queue.write_buffer(
1047 &self.render_uniform_buffer,
1048 0,
1049 bytemuck::bytes_of(&render_uniforms),
1050 );
1051
1052 render_pass.set_pipeline(&self.render_pipeline);
1054 render_pass.set_bind_group(0, &self.render_bind_group, &[]);
1055 render_pass.draw(0..6, 0..self.max_particles); }
1057
1058 fn normalize(v: [f32; 3]) -> [f32; 3] {
1060 let len = (v[0] * v[0] + v[1] * v[1] + v[2] * v[2]).sqrt();
1061 if len > 0.0001 {
1062 [v[0] / len, v[1] / len, v[2] / len]
1063 } else {
1064 [0.0, 1.0, 0.0]
1065 }
1066 }
1067
1068 fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
1069 [
1070 a[1] * b[2] - a[2] * b[1],
1071 a[2] * b[0] - a[0] * b[2],
1072 a[0] * b[1] - a[1] * b[0],
1073 ]
1074 }
1075
1076 fn look_at(eye: [f32; 3], target: [f32; 3], up: [f32; 3]) -> [[f32; 4]; 4] {
1077 let f = Self::normalize([target[0] - eye[0], target[1] - eye[1], target[2] - eye[2]]);
1078 let r = Self::normalize(Self::cross(f, up));
1079 let u = Self::cross(r, f);
1080
1081 [
1082 [r[0], u[0], -f[0], 0.0],
1083 [r[1], u[1], -f[1], 0.0],
1084 [r[2], u[2], -f[2], 0.0],
1085 [
1086 -(r[0] * eye[0] + r[1] * eye[1] + r[2] * eye[2]),
1087 -(u[0] * eye[0] + u[1] * eye[1] + u[2] * eye[2]),
1088 f[0] * eye[0] + f[1] * eye[1] + f[2] * eye[2],
1089 1.0,
1090 ],
1091 ]
1092 }
1093
1094 fn perspective(fov: f32, aspect: f32, near: f32, far: f32) -> [[f32; 4]; 4] {
1095 let f = 1.0 / (fov * 0.5).tan();
1096 let nf = 1.0 / (near - far);
1097
1098 [
1099 [f / aspect, 0.0, 0.0, 0.0],
1100 [0.0, f, 0.0, 0.0],
1101 [0.0, 0.0, (far + near) * nf, -1.0],
1102 [0.0, 0.0, 2.0 * far * near * nf, 0.0],
1103 ]
1104 }
1105
1106 fn mat4_mul(a: &[[f32; 4]; 4], b: &[[f32; 4]; 4]) -> [[f32; 4]; 4] {
1107 let mut result = [[0.0f32; 4]; 4];
1108 for i in 0..4 {
1109 for j in 0..4 {
1110 result[i][j] =
1111 a[0][j] * b[i][0] + a[1][j] * b[i][1] + a[2][j] * b[i][2] + a[3][j] * b[i][3];
1112 }
1113 }
1114 result
1115 }
1116}
1117
1118pub struct ParticleManager {
1120 systems: HashMap<u64, ParticleSystemGpu>,
1121 next_id: u64,
1122}
1123
1124impl ParticleManager {
1125 pub fn new() -> Self {
1126 Self {
1127 systems: HashMap::new(),
1128 next_id: 0,
1129 }
1130 }
1131
1132 pub fn create_system(
1133 &mut self,
1134 device: &wgpu::Device,
1135 surface_format: wgpu::TextureFormat,
1136 max_particles: u32,
1137 ) -> u64 {
1138 let id = self.next_id;
1139 self.next_id += 1;
1140 let system = ParticleSystemGpu::new(device, surface_format, max_particles);
1141 self.systems.insert(id, system);
1142 id
1143 }
1144
1145 pub fn get_system(&self, id: u64) -> Option<&ParticleSystemGpu> {
1146 self.systems.get(&id)
1147 }
1148
1149 pub fn get_system_mut(&mut self, id: u64) -> Option<&mut ParticleSystemGpu> {
1150 self.systems.get_mut(&id)
1151 }
1152
1153 pub fn remove_system(&mut self, id: u64) {
1154 self.systems.remove(&id);
1155 }
1156}
1157
1158impl Default for ParticleManager {
1159 fn default() -> Self {
1160 Self::new()
1161 }
1162}