1use bytemuck::{Pod, Zeroable};
22use wgpu::util::DeviceExt;
23
24use crate::renderer::{ParticleMeshAlign, SpriteBlend, SpriteLitParams, SpriteSizeMode};
25
26#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
31pub struct GpuParticleSystemId(pub(crate) usize);
32
33impl GpuParticleSystemId {
34 pub fn index(self) -> usize {
36 self.0
37 }
38}
39
40#[derive(Debug, Clone)]
43#[non_exhaustive]
44pub struct GpuParticleSystemConfig {
45 pub capacity: u32,
49 pub render: ParticleRender,
51}
52
53impl Default for GpuParticleSystemConfig {
54 fn default() -> Self {
55 Self {
56 capacity: 10_000,
57 render: ParticleRender::default(),
58 }
59 }
60}
61
62#[derive(Debug, Clone)]
64#[non_exhaustive]
65pub enum ParticleRender {
66 Sprite {
68 texture_id: Option<u64>,
71 blend: SpriteBlend,
73 size_mode: SpriteSizeMode,
75 depth_write: bool,
77 lit: bool,
81 lit_params: SpriteLitParams,
83 normal_texture_id: Option<u64>,
85 },
86 Mesh {
91 mesh_id: u64,
93 texture_id: Option<u64>,
95 blend: SpriteBlend,
97 align: ParticleMeshAlign,
99 },
100}
101
102impl Default for ParticleRender {
103 fn default() -> Self {
104 ParticleRender::Sprite {
105 texture_id: None,
106 blend: SpriteBlend::AlphaBlend,
107 size_mode: SpriteSizeMode::ScreenSpace,
108 depth_write: false,
109 lit: false,
110 lit_params: SpriteLitParams {
111 roughness: 0.9,
112 normal_mode: crate::renderer::SpriteNormalMode::Spherical,
113 receive_shadows: false,
114 ambient_scale: 1.0,
115 },
116 normal_texture_id: None,
117 }
118 }
119}
120
121#[repr(C)]
125#[derive(Copy, Clone, Pod, Zeroable)]
126pub(crate) struct GpuParticle {
127 pub position: [f32; 3],
128 pub lifetime: f32, pub velocity: [f32; 3],
130 pub max_lifetime: f32, pub colour: [f32; 4],
132 pub size: f32,
133 pub spawn_seed: f32,
136 pub _pad: [f32; 2],
137}
138
139#[repr(C)]
141#[derive(Copy, Clone, Pod, Zeroable)]
142pub(crate) struct EmitParamsGpu {
143 pub spawn_min: [f32; 3],
144 pub spawn_kind: u32, pub spawn_max: [f32; 3],
146 pub spawn_radius: f32, pub vel_min: [f32; 3],
148 pub vel_kind: u32, pub vel_max: [f32; 3],
150 pub cone_half_angle: f32,
151 pub vel_axis: [f32; 3],
152 pub cone_min_speed: f32,
153 pub colour: [f32; 4],
154 pub spawn_count: u32,
155 pub capacity: u32,
156 pub rng_seed: u32,
157 pub size: f32,
158 pub lifetime_min: f32,
159 pub lifetime_max: f32,
160 pub cone_max_speed: f32,
161 pub _pad: f32,
162}
163
164pub(crate) const MAX_FORCES: usize = 8;
168
169#[repr(C)]
171#[derive(Copy, Clone, Pod, Zeroable)]
172pub(crate) struct GpuForce {
173 pub kind: u32, pub _pad: [u32; 3],
175 pub v0: [f32; 4], pub v1: [f32; 4], }
178
179#[repr(C)]
182#[derive(Copy, Clone, Pod, Zeroable)]
183pub(crate) struct SimParamsGpu {
184 pub dt: f32,
185 pub capacity: u32,
186 pub force_count: u32,
187 pub _pad: u32,
188 pub forces: [GpuForce; MAX_FORCES],
189}
190
191pub(crate) struct ParticleSystem {
193 pub capacity: u32,
194 pub render: ParticleRender,
195 pub particle_buf: wgpu::Buffer,
198 pub emit_counter_buf: wgpu::Buffer,
202 pub sim_bg: wgpu::BindGroup,
204 pub draw_bg: Option<wgpu::BindGroup>,
207 pub draw_bg_mesh: Option<wgpu::BindGroup>,
210 pub draw_lit_normal_bg: Option<wgpu::BindGroup>,
213 pub _draw_uniform_buf: Option<wgpu::Buffer>,
215 pub alive: bool,
217 pub frame_counter: u32,
221 pub spawn_accumulator: f32,
225}
226
227#[derive(Copy, Clone, Debug)]
229pub(crate) enum ParticleDrawRoute {
230 Sprite { lit: bool },
231 Mesh { mesh_id: u64 },
232}
233
234pub(crate) struct ParticleFrameData {
237 pub system_idx: usize,
239 pub blend: SpriteBlend,
241 pub hidden: bool,
243 pub route: ParticleDrawRoute,
245}
246
247impl crate::resources::ViewportGpuResources {
248 pub fn create_gpu_particle_system(
254 &mut self,
255 device: &wgpu::Device,
256 queue: &wgpu::Queue,
257 config: &GpuParticleSystemConfig,
258 ) -> GpuParticleSystemId {
259 self.ensure_particle_pipelines(device);
260
261 let capacity = config.capacity.max(1);
262
263 let particle_bytes_len = (capacity as usize) * std::mem::size_of::<GpuParticle>();
265 let zero_particles = vec![0u8; particle_bytes_len];
266 let particle_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
267 label: Some("gpu_particle_buf"),
268 contents: &zero_particles,
269 usage: wgpu::BufferUsages::STORAGE
270 | wgpu::BufferUsages::VERTEX
271 | wgpu::BufferUsages::COPY_DST,
272 });
273
274 let emit_counter_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
276 label: Some("gpu_particle_emit_counter"),
277 contents: bytemuck::bytes_of(&0u32),
278 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
279 });
280
281 enum DrawState {
285 Sprite {
286 texture_id: Option<u64>,
287 size_mode: SpriteSizeMode,
288 lit: bool,
289 lit_params: SpriteLitParams,
290 normal_texture_id: Option<u64>,
291 },
292 Mesh {
293 texture_id: Option<u64>,
294 align: ParticleMeshAlign,
295 },
296 }
297 let draw_state = match &config.render {
298 ParticleRender::Sprite {
299 texture_id,
300 blend: _,
301 size_mode,
302 depth_write: _,
303 lit,
304 lit_params,
305 normal_texture_id,
306 } => DrawState::Sprite {
307 texture_id: *texture_id,
308 size_mode: *size_mode,
309 lit: *lit,
310 lit_params: *lit_params,
311 normal_texture_id: *normal_texture_id,
312 },
313 ParticleRender::Mesh {
314 texture_id,
315 blend: _,
316 align,
317 mesh_id: _,
318 } => DrawState::Mesh {
319 texture_id: *texture_id,
320 align: *align,
321 },
322 };
323
324 let _ = queue; let sim_bgl = self
327 .particle_sim_bgl
328 .as_ref()
329 .expect("ensure_particle_pipelines failed to create sim BGL");
330 let sim_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
331 label: Some("gpu_particle_sim_bg"),
332 layout: sim_bgl,
333 entries: &[
334 wgpu::BindGroupEntry {
335 binding: 0,
336 resource: particle_buf.as_entire_binding(),
337 },
338 wgpu::BindGroupEntry {
339 binding: 1,
340 resource: emit_counter_buf.as_entire_binding(),
341 },
342 ],
343 });
344
345 let mut sprite_draw_bg: Option<wgpu::BindGroup> = None;
348 let mut mesh_draw_bg: Option<wgpu::BindGroup> = None;
349 let mut sprite_lit_normal_bg: Option<wgpu::BindGroup> = None;
350 let draw_uniform_buf: Option<wgpu::Buffer>;
351
352 match draw_state {
353 DrawState::Sprite {
354 texture_id,
355 size_mode,
356 lit,
357 lit_params,
358 normal_texture_id,
359 } => {
360 #[repr(C)]
361 #[derive(Copy, Clone, Pod, Zeroable)]
362 struct SpriteDrawUniform {
363 model: [[f32; 4]; 4],
364 world_space: u32,
365 has_texture: u32,
366 normal_mode: u32,
367 has_normal_map: u32,
368 ambient_scale: f32,
369 roughness: f32,
370 _pad0: u32,
371 _pad1: u32,
372 }
373 let normal_mode_u32 = match lit_params.normal_mode {
374 crate::renderer::SpriteNormalMode::Spherical => 0u32,
375 crate::renderer::SpriteNormalMode::Flat => 1u32,
376 crate::renderer::SpriteNormalMode::NormalMap => 2u32,
377 };
378 let uniform = SpriteDrawUniform {
379 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
380 world_space: matches!(size_mode, SpriteSizeMode::WorldSpace) as u32,
381 has_texture: texture_id.is_some() as u32,
382 normal_mode: normal_mode_u32,
383 has_normal_map: normal_texture_id.is_some() as u32,
384 ambient_scale: lit_params.ambient_scale,
385 roughness: lit_params.roughness,
386 _pad0: 0,
387 _pad1: 0,
388 };
389 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
390 label: Some("gpu_particle_sprite_draw_uniform"),
391 contents: bytemuck::bytes_of(&uniform),
392 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
393 });
394 let texture_view = match texture_id {
395 Some(id) if (id as usize) < self.textures.len() => {
396 &self.textures[id as usize].view
397 }
398 _ => &self.fallback_lut_view,
399 };
400 let draw_bgl = self
401 .particle_draw_bgl
402 .as_ref()
403 .expect("ensure_particle_pipelines failed to create draw BGL");
404 sprite_draw_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
405 label: Some("gpu_particle_draw_bg"),
406 layout: draw_bgl,
407 entries: &[
408 wgpu::BindGroupEntry {
409 binding: 0,
410 resource: uniform_buf.as_entire_binding(),
411 },
412 wgpu::BindGroupEntry {
413 binding: 1,
414 resource: wgpu::BindingResource::TextureView(texture_view),
415 },
416 wgpu::BindGroupEntry {
417 binding: 2,
418 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
419 },
420 wgpu::BindGroupEntry {
421 binding: 3,
422 resource: particle_buf.as_entire_binding(),
423 },
424 ],
425 }));
426 if lit {
427 let lit_bgl = self
428 .particle_sprite_lit_bgl
429 .as_ref()
430 .expect("ensure_particle_pipelines failed to create lit BGL");
431 let normal_view = match normal_texture_id {
432 Some(id) if (id as usize) < self.textures.len() => {
433 &self.textures[id as usize].view
434 }
435 _ => &self.fallback_normal_map_view,
436 };
437 sprite_lit_normal_bg =
438 Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
439 label: Some("gpu_particle_lit_normal_bg"),
440 layout: lit_bgl,
441 entries: &[
442 wgpu::BindGroupEntry {
443 binding: 0,
444 resource: wgpu::BindingResource::TextureView(normal_view),
445 },
446 wgpu::BindGroupEntry {
447 binding: 1,
448 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
449 },
450 ],
451 }));
452 }
453 draw_uniform_buf = Some(uniform_buf);
454 }
455 DrawState::Mesh { texture_id, align } => {
456 #[repr(C)]
457 #[derive(Copy, Clone, Pod, Zeroable)]
458 struct MeshDrawUniform {
459 align: u32,
460 has_texture: u32,
461 _pad0: u32,
462 _pad1: u32,
463 }
464 let align_u32 = match align {
465 ParticleMeshAlign::Identity => 0u32,
466 ParticleMeshAlign::Velocity => 1u32,
467 ParticleMeshAlign::Random => 2u32,
468 };
469 let uniform = MeshDrawUniform {
470 align: align_u32,
471 has_texture: texture_id.is_some() as u32,
472 _pad0: 0,
473 _pad1: 0,
474 };
475 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
476 label: Some("gpu_particle_mesh_draw_uniform"),
477 contents: bytemuck::bytes_of(&uniform),
478 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
479 });
480 let texture_view = match texture_id {
481 Some(id) if (id as usize) < self.textures.len() => {
482 &self.textures[id as usize].view
483 }
484 _ => &self.fallback_texture.view,
485 };
486 let mesh_bgl = self
487 .particle_mesh_draw_bgl
488 .as_ref()
489 .expect("ensure_particle_pipelines failed to create mesh draw BGL");
490 mesh_draw_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
491 label: Some("gpu_particle_mesh_draw_bg"),
492 layout: mesh_bgl,
493 entries: &[
494 wgpu::BindGroupEntry {
495 binding: 0,
496 resource: uniform_buf.as_entire_binding(),
497 },
498 wgpu::BindGroupEntry {
499 binding: 1,
500 resource: wgpu::BindingResource::TextureView(texture_view),
501 },
502 wgpu::BindGroupEntry {
503 binding: 2,
504 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
505 },
506 wgpu::BindGroupEntry {
507 binding: 3,
508 resource: particle_buf.as_entire_binding(),
509 },
510 ],
511 }));
512 draw_uniform_buf = Some(uniform_buf);
513 }
514 }
515
516 let system = ParticleSystem {
517 capacity,
518 render: config.render.clone(),
519 particle_buf,
520 emit_counter_buf,
521 sim_bg,
522 draw_bg: sprite_draw_bg,
523 draw_bg_mesh: mesh_draw_bg,
524 draw_lit_normal_bg: sprite_lit_normal_bg,
525 _draw_uniform_buf: draw_uniform_buf,
526 alive: true,
527 frame_counter: 0,
528 spawn_accumulator: 0.0,
529 };
530
531 if let Some(idx) = self
532 .particle_systems
533 .iter()
534 .position(|slot: &Option<ParticleSystem>| slot.as_ref().is_none_or(|s| !s.alive))
535 {
536 self.particle_systems[idx] = Some(system);
537 GpuParticleSystemId(idx)
538 } else {
539 self.particle_systems.push(Some(system));
540 GpuParticleSystemId(self.particle_systems.len() - 1)
541 }
542 }
543
544 pub fn drop_gpu_particle_system(&mut self, id: GpuParticleSystemId) {
547 if let Some(Some(s)) = self.particle_systems.get_mut(id.0) {
548 s.alive = false;
549 }
550 }
551
552 #[allow(dead_code)]
553 pub(crate) fn particle_system(&self, id: GpuParticleSystemId) -> Option<&ParticleSystem> {
554 self.particle_systems
555 .get(id.0)?
556 .as_ref()
557 .filter(|s| s.alive)
558 }
559
560 #[allow(dead_code)]
561 pub(crate) fn particle_system_mut(
562 &mut self,
563 id: GpuParticleSystemId,
564 ) -> Option<&mut ParticleSystem> {
565 self.particle_systems
566 .get_mut(id.0)?
567 .as_mut()
568 .filter(|s| s.alive)
569 }
570
571 pub(crate) fn ensure_particle_pipelines(&mut self, device: &wgpu::Device) {
574 if self.particle_sim_bgl.is_some() {
575 return;
576 }
577
578 let params_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
580 label: Some("gpu_particle_params_bgl"),
581 entries: &[wgpu::BindGroupLayoutEntry {
582 binding: 0,
583 visibility: wgpu::ShaderStages::COMPUTE,
584 ty: wgpu::BindingType::Buffer {
585 ty: wgpu::BufferBindingType::Uniform,
586 has_dynamic_offset: false,
587 min_binding_size: None,
588 },
589 count: None,
590 }],
591 });
592
593 let sim_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
595 label: Some("gpu_particle_sim_bgl"),
596 entries: &[
597 wgpu::BindGroupLayoutEntry {
598 binding: 0,
599 visibility: wgpu::ShaderStages::COMPUTE,
600 ty: wgpu::BindingType::Buffer {
601 ty: wgpu::BufferBindingType::Storage { read_only: false },
602 has_dynamic_offset: false,
603 min_binding_size: None,
604 },
605 count: None,
606 },
607 wgpu::BindGroupLayoutEntry {
608 binding: 1,
609 visibility: wgpu::ShaderStages::COMPUTE,
610 ty: wgpu::BindingType::Buffer {
611 ty: wgpu::BufferBindingType::Storage { read_only: false },
612 has_dynamic_offset: false,
613 min_binding_size: None,
614 },
615 count: None,
616 },
617 ],
618 });
619
620 let draw_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
622 label: Some("gpu_particle_draw_bgl"),
623 entries: &[
624 wgpu::BindGroupLayoutEntry {
625 binding: 0,
626 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
627 ty: wgpu::BindingType::Buffer {
628 ty: wgpu::BufferBindingType::Uniform,
629 has_dynamic_offset: false,
630 min_binding_size: None,
631 },
632 count: None,
633 },
634 wgpu::BindGroupLayoutEntry {
635 binding: 1,
636 visibility: wgpu::ShaderStages::FRAGMENT,
637 ty: wgpu::BindingType::Texture {
638 sample_type: wgpu::TextureSampleType::Float { filterable: true },
639 view_dimension: wgpu::TextureViewDimension::D2,
640 multisampled: false,
641 },
642 count: None,
643 },
644 wgpu::BindGroupLayoutEntry {
645 binding: 2,
646 visibility: wgpu::ShaderStages::FRAGMENT,
647 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
648 count: None,
649 },
650 wgpu::BindGroupLayoutEntry {
651 binding: 3,
652 visibility: wgpu::ShaderStages::VERTEX,
653 ty: wgpu::BindingType::Buffer {
654 ty: wgpu::BufferBindingType::Storage { read_only: true },
655 has_dynamic_offset: false,
656 min_binding_size: None,
657 },
658 count: None,
659 },
660 ],
661 });
662
663 let emit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
665 label: Some("particle_emit_shader"),
666 source: wgpu::ShaderSource::Wgsl(
667 include_str!(concat!(env!("OUT_DIR"), "/particle_emit.wgsl")).into(),
668 ),
669 });
670 let sim_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
671 label: Some("particle_sim_shader"),
672 source: wgpu::ShaderSource::Wgsl(
673 include_str!(concat!(env!("OUT_DIR"), "/particle_sim.wgsl")).into(),
674 ),
675 });
676
677 let compute_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
678 label: Some("particle_compute_layout"),
679 bind_group_layouts: &[¶ms_bgl, &sim_bgl],
680 push_constant_ranges: &[],
681 });
682
683 let emit_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
684 label: Some("particle_emit_pipeline"),
685 layout: Some(&compute_layout),
686 module: &emit_shader,
687 entry_point: Some("emit_main"),
688 compilation_options: wgpu::PipelineCompilationOptions::default(),
689 cache: None,
690 });
691
692 let sim_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
693 label: Some("particle_sim_pipeline"),
694 layout: Some(&compute_layout),
695 module: &sim_shader,
696 entry_point: Some("sim_main"),
697 compilation_options: wgpu::PipelineCompilationOptions::default(),
698 cache: None,
699 });
700
701 let sprite_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
703 label: Some("particle_sprite_shader"),
704 source: wgpu::ShaderSource::Wgsl(
705 include_str!(concat!(env!("OUT_DIR"), "/particle_sprite.wgsl")).into(),
706 ),
707 });
708
709 let draw_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
710 label: Some("particle_draw_layout"),
711 bind_group_layouts: &[&self.camera_bind_group_layout, &draw_bgl],
712 push_constant_ranges: &[],
713 });
714
715 let sample_count = self.sample_count;
716 let additive = wgpu::BlendState {
717 color: wgpu::BlendComponent {
718 src_factor: wgpu::BlendFactor::One,
719 dst_factor: wgpu::BlendFactor::One,
720 operation: wgpu::BlendOperation::Add,
721 },
722 alpha: wgpu::BlendComponent {
723 src_factor: wgpu::BlendFactor::One,
724 dst_factor: wgpu::BlendFactor::One,
725 operation: wgpu::BlendOperation::Add,
726 },
727 };
728 let premul = wgpu::BlendState {
729 color: wgpu::BlendComponent {
730 src_factor: wgpu::BlendFactor::One,
731 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
732 operation: wgpu::BlendOperation::Add,
733 },
734 alpha: wgpu::BlendComponent {
735 src_factor: wgpu::BlendFactor::One,
736 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
737 operation: wgpu::BlendOperation::Add,
738 },
739 };
740
741 let make_draw = |fmt: wgpu::TextureFormat, blend: wgpu::BlendState, label: &str| {
742 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
743 label: Some(label),
744 layout: Some(&draw_layout),
745 vertex: wgpu::VertexState {
746 module: &sprite_shader,
747 entry_point: Some("vs_main"),
748 buffers: &[],
749 compilation_options: wgpu::PipelineCompilationOptions::default(),
750 },
751 fragment: Some(wgpu::FragmentState {
752 module: &sprite_shader,
753 entry_point: Some("fs_main"),
754 targets: &[Some(wgpu::ColorTargetState {
755 format: fmt,
756 blend: Some(blend),
757 write_mask: wgpu::ColorWrites::ALL,
758 })],
759 compilation_options: wgpu::PipelineCompilationOptions::default(),
760 }),
761 primitive: wgpu::PrimitiveState {
762 topology: wgpu::PrimitiveTopology::TriangleList,
763 cull_mode: None,
764 ..Default::default()
765 },
766 depth_stencil: Some(wgpu::DepthStencilState {
767 format: wgpu::TextureFormat::Depth24PlusStencil8,
768 depth_write_enabled: false,
769 depth_compare: wgpu::CompareFunction::Less,
770 stencil: wgpu::StencilState::default(),
771 bias: wgpu::DepthBiasState::default(),
772 }),
773 multisample: wgpu::MultisampleState {
774 count: sample_count,
775 ..Default::default()
776 },
777 multiview: None,
778 cache: None,
779 })
780 };
781
782 let ldr = self.target_format;
783 let hdr = wgpu::TextureFormat::Rgba16Float;
784 let alpha = wgpu::BlendState::ALPHA_BLENDING;
785 self.particle_sprite_pipeline_alpha = Some(crate::resources::DualPipeline {
786 ldr: make_draw(ldr, alpha, "particle_sprite_alpha"),
787 hdr: make_draw(hdr, alpha, "particle_sprite_alpha"),
788 });
789 self.particle_sprite_pipeline_additive = Some(crate::resources::DualPipeline {
790 ldr: make_draw(ldr, additive, "particle_sprite_additive"),
791 hdr: make_draw(hdr, additive, "particle_sprite_additive"),
792 });
793 self.particle_sprite_pipeline_premultiplied = Some(crate::resources::DualPipeline {
794 ldr: make_draw(ldr, premul, "particle_sprite_premultiplied"),
795 hdr: make_draw(hdr, premul, "particle_sprite_premultiplied"),
796 });
797
798 let lit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
802 label: Some("gpu_particle_lit_bgl"),
803 entries: &[
804 wgpu::BindGroupLayoutEntry {
805 binding: 0,
806 visibility: wgpu::ShaderStages::FRAGMENT,
807 ty: wgpu::BindingType::Texture {
808 sample_type: wgpu::TextureSampleType::Float { filterable: true },
809 view_dimension: wgpu::TextureViewDimension::D2,
810 multisampled: false,
811 },
812 count: None,
813 },
814 wgpu::BindGroupLayoutEntry {
815 binding: 1,
816 visibility: wgpu::ShaderStages::FRAGMENT,
817 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
818 count: None,
819 },
820 ],
821 });
822
823 let lit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
824 label: Some("particle_sprite_lit_shader"),
825 source: wgpu::ShaderSource::Wgsl(
826 include_str!(concat!(env!("OUT_DIR"), "/particle_sprite_lit.wgsl")).into(),
827 ),
828 });
829
830 let lit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
831 label: Some("particle_draw_lit_layout"),
832 bind_group_layouts: &[&self.camera_bind_group_layout, &draw_bgl, &lit_bgl],
833 push_constant_ranges: &[],
834 });
835
836 let make_lit_draw = |fmt: wgpu::TextureFormat, blend: wgpu::BlendState, label: &str| {
837 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
838 label: Some(label),
839 layout: Some(&lit_layout),
840 vertex: wgpu::VertexState {
841 module: &lit_shader,
842 entry_point: Some("vs_main"),
843 buffers: &[],
844 compilation_options: wgpu::PipelineCompilationOptions::default(),
845 },
846 fragment: Some(wgpu::FragmentState {
847 module: &lit_shader,
848 entry_point: Some("fs_main"),
849 targets: &[Some(wgpu::ColorTargetState {
850 format: fmt,
851 blend: Some(blend),
852 write_mask: wgpu::ColorWrites::ALL,
853 })],
854 compilation_options: wgpu::PipelineCompilationOptions::default(),
855 }),
856 primitive: wgpu::PrimitiveState {
857 topology: wgpu::PrimitiveTopology::TriangleList,
858 cull_mode: None,
859 ..Default::default()
860 },
861 depth_stencil: Some(wgpu::DepthStencilState {
862 format: wgpu::TextureFormat::Depth24PlusStencil8,
863 depth_write_enabled: false,
864 depth_compare: wgpu::CompareFunction::Less,
865 stencil: wgpu::StencilState::default(),
866 bias: wgpu::DepthBiasState::default(),
867 }),
868 multisample: wgpu::MultisampleState {
869 count: sample_count,
870 ..Default::default()
871 },
872 multiview: None,
873 cache: None,
874 })
875 };
876
877 self.particle_sprite_lit_pipeline_alpha = Some(crate::resources::DualPipeline {
878 ldr: make_lit_draw(ldr, alpha, "particle_sprite_lit_alpha"),
879 hdr: make_lit_draw(hdr, alpha, "particle_sprite_lit_alpha"),
880 });
881 self.particle_sprite_lit_pipeline_additive = Some(crate::resources::DualPipeline {
882 ldr: make_lit_draw(ldr, additive, "particle_sprite_lit_additive"),
883 hdr: make_lit_draw(hdr, additive, "particle_sprite_lit_additive"),
884 });
885 self.particle_sprite_lit_pipeline_premultiplied = Some(crate::resources::DualPipeline {
886 ldr: make_lit_draw(ldr, premul, "particle_sprite_lit_premultiplied"),
887 hdr: make_lit_draw(hdr, premul, "particle_sprite_lit_premultiplied"),
888 });
889
890 let lit_fallback_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
891 label: Some("gpu_particle_lit_fallback_bg"),
892 layout: &lit_bgl,
893 entries: &[
894 wgpu::BindGroupEntry {
895 binding: 0,
896 resource: wgpu::BindingResource::TextureView(&self.fallback_normal_map_view),
897 },
898 wgpu::BindGroupEntry {
899 binding: 1,
900 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
901 },
902 ],
903 });
904
905 self.particle_sprite_lit_bgl = Some(lit_bgl);
906 self.particle_sprite_lit_fallback_bg = Some(lit_fallback_bg);
907
908 let mesh_draw_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
913 label: Some("gpu_particle_mesh_draw_bgl"),
914 entries: &[
915 wgpu::BindGroupLayoutEntry {
916 binding: 0,
917 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
918 ty: wgpu::BindingType::Buffer {
919 ty: wgpu::BufferBindingType::Uniform,
920 has_dynamic_offset: false,
921 min_binding_size: None,
922 },
923 count: None,
924 },
925 wgpu::BindGroupLayoutEntry {
926 binding: 1,
927 visibility: wgpu::ShaderStages::FRAGMENT,
928 ty: wgpu::BindingType::Texture {
929 sample_type: wgpu::TextureSampleType::Float { filterable: true },
930 view_dimension: wgpu::TextureViewDimension::D2,
931 multisampled: false,
932 },
933 count: None,
934 },
935 wgpu::BindGroupLayoutEntry {
936 binding: 2,
937 visibility: wgpu::ShaderStages::FRAGMENT,
938 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
939 count: None,
940 },
941 wgpu::BindGroupLayoutEntry {
942 binding: 3,
943 visibility: wgpu::ShaderStages::VERTEX,
944 ty: wgpu::BindingType::Buffer {
945 ty: wgpu::BufferBindingType::Storage { read_only: true },
946 has_dynamic_offset: false,
947 min_binding_size: None,
948 },
949 count: None,
950 },
951 ],
952 });
953
954 let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
955 label: Some("particle_mesh_shader"),
956 source: wgpu::ShaderSource::Wgsl(
957 include_str!(concat!(env!("OUT_DIR"), "/particle_mesh.wgsl")).into(),
958 ),
959 });
960
961 let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
962 label: Some("particle_mesh_draw_layout"),
963 bind_group_layouts: &[&self.camera_bind_group_layout, &mesh_draw_bgl],
964 push_constant_ranges: &[],
965 });
966
967 let make_mesh_draw = |fmt: wgpu::TextureFormat, blend: wgpu::BlendState, label: &str| {
968 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
969 label: Some(label),
970 layout: Some(&mesh_layout),
971 vertex: wgpu::VertexState {
972 module: &mesh_shader,
973 entry_point: Some("vs_main"),
974 buffers: &[crate::resources::types::Vertex::buffer_layout()],
975 compilation_options: wgpu::PipelineCompilationOptions::default(),
976 },
977 fragment: Some(wgpu::FragmentState {
978 module: &mesh_shader,
979 entry_point: Some("fs_main"),
980 targets: &[Some(wgpu::ColorTargetState {
981 format: fmt,
982 blend: Some(blend),
983 write_mask: wgpu::ColorWrites::ALL,
984 })],
985 compilation_options: wgpu::PipelineCompilationOptions::default(),
986 }),
987 primitive: wgpu::PrimitiveState {
988 topology: wgpu::PrimitiveTopology::TriangleList,
989 cull_mode: Some(wgpu::Face::Back),
990 ..Default::default()
991 },
992 depth_stencil: Some(wgpu::DepthStencilState {
993 format: wgpu::TextureFormat::Depth24PlusStencil8,
994 depth_write_enabled: false,
995 depth_compare: wgpu::CompareFunction::Less,
996 stencil: wgpu::StencilState::default(),
997 bias: wgpu::DepthBiasState::default(),
998 }),
999 multisample: wgpu::MultisampleState {
1000 count: sample_count,
1001 ..Default::default()
1002 },
1003 multiview: None,
1004 cache: None,
1005 })
1006 };
1007
1008 self.particle_mesh_pipeline_alpha = Some(crate::resources::DualPipeline {
1009 ldr: make_mesh_draw(ldr, alpha, "particle_mesh_alpha"),
1010 hdr: make_mesh_draw(hdr, alpha, "particle_mesh_alpha"),
1011 });
1012 self.particle_mesh_pipeline_additive = Some(crate::resources::DualPipeline {
1013 ldr: make_mesh_draw(ldr, additive, "particle_mesh_additive"),
1014 hdr: make_mesh_draw(hdr, additive, "particle_mesh_additive"),
1015 });
1016 self.particle_mesh_pipeline_premultiplied = Some(crate::resources::DualPipeline {
1017 ldr: make_mesh_draw(ldr, premul, "particle_mesh_premultiplied"),
1018 hdr: make_mesh_draw(hdr, premul, "particle_mesh_premultiplied"),
1019 });
1020 self.particle_mesh_draw_bgl = Some(mesh_draw_bgl);
1021
1022 self.particle_params_bgl = Some(params_bgl);
1023 self.particle_sim_bgl = Some(sim_bgl);
1024 self.particle_draw_bgl = Some(draw_bgl);
1025 self.particle_emit_pipeline = Some(emit_pipeline);
1026 self.particle_sim_pipeline = Some(sim_pipeline);
1027 }
1028
1029 pub(crate) fn run_particle_jobs(
1032 &mut self,
1033 device: &wgpu::Device,
1034 queue: &wgpu::Queue,
1035 items: &[crate::renderer::GpuParticleSystemItem],
1036 ) -> Vec<ParticleFrameData> {
1037 if items.is_empty() {
1038 return Vec::new();
1039 }
1040 self.ensure_particle_pipelines(device);
1041
1042 let emit_pipeline = self
1043 .particle_emit_pipeline
1044 .as_ref()
1045 .expect("particle pipelines should exist after ensure")
1046 .clone();
1047 let sim_pipeline = self
1048 .particle_sim_pipeline
1049 .as_ref()
1050 .expect("particle pipelines should exist after ensure")
1051 .clone();
1052 let params_bgl = self
1053 .particle_params_bgl
1054 .as_ref()
1055 .expect("particle params BGL")
1056 .clone();
1057
1058 let mut frame_data: Vec<ParticleFrameData> = Vec::with_capacity(items.len());
1059 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1060 label: Some("particle_compute_encoder"),
1061 });
1062
1063 for item in items {
1064 let idx = item.system_id.0;
1065 let (blend, route) = match self.particle_systems.get(idx).and_then(|s| s.as_ref()) {
1066 Some(s) if s.alive => match &s.render {
1067 ParticleRender::Sprite { blend, lit, .. } => {
1068 (*blend, ParticleDrawRoute::Sprite { lit: *lit })
1069 }
1070 ParticleRender::Mesh {
1071 blend, mesh_id, ..
1072 } => (*blend, ParticleDrawRoute::Mesh { mesh_id: *mesh_id }),
1073 },
1074 _ => continue,
1075 };
1076
1077 let hidden = item.settings.hidden;
1078
1079 let (
1081 capacity,
1082 particle_buf_binding,
1083 emit_counter_binding,
1084 sim_bg,
1085 spawn_count,
1086 frame_counter,
1087 ) = {
1088 let system = self.particle_systems[idx].as_mut().unwrap();
1089 let dt = item.time_step.max(0.0);
1090 system.spawn_accumulator += item.emitter.rate * dt;
1091 let spawn_count = system.spawn_accumulator.floor() as u32;
1092 system.spawn_accumulator -= spawn_count as f32;
1093 system.frame_counter = system.frame_counter.wrapping_add(1);
1094 let frame_counter = system.frame_counter;
1095 (
1096 system.capacity,
1097 system.particle_buf.as_entire_binding(),
1098 system.emit_counter_buf.as_entire_binding(),
1099 system.sim_bg.clone(),
1101 spawn_count,
1102 frame_counter,
1103 )
1104 };
1105 let _ = (particle_buf_binding, emit_counter_binding); let workgroups = capacity.div_ceil(64);
1108
1109 if spawn_count > 0 {
1111 queue.write_buffer(
1112 &self.particle_systems[idx]
1113 .as_ref()
1114 .unwrap()
1115 .emit_counter_buf,
1116 0,
1117 bytemuck::bytes_of(&spawn_count),
1118 );
1119
1120 let emit_params =
1121 build_emit_params(&item.emitter, capacity, spawn_count, frame_counter);
1122 let emit_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1123 label: Some("particle_emit_params"),
1124 contents: bytemuck::bytes_of(&emit_params),
1125 usage: wgpu::BufferUsages::UNIFORM,
1126 });
1127 let emit_params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1128 label: Some("particle_emit_params_bg"),
1129 layout: ¶ms_bgl,
1130 entries: &[wgpu::BindGroupEntry {
1131 binding: 0,
1132 resource: emit_uniform.as_entire_binding(),
1133 }],
1134 });
1135
1136 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1137 label: Some("particle_emit_pass"),
1138 timestamp_writes: None,
1139 });
1140 pass.set_pipeline(&emit_pipeline);
1141 pass.set_bind_group(0, &emit_params_bg, &[]);
1142 pass.set_bind_group(1, &sim_bg, &[]);
1143 pass.dispatch_workgroups(workgroups, 1, 1);
1144 }
1145
1146 let sim_params = build_sim_params(item.time_step, capacity, &item.forces);
1148 let sim_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1149 label: Some("particle_sim_params"),
1150 contents: bytemuck::bytes_of(&sim_params),
1151 usage: wgpu::BufferUsages::UNIFORM,
1152 });
1153 let sim_params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1154 label: Some("particle_sim_params_bg"),
1155 layout: ¶ms_bgl,
1156 entries: &[wgpu::BindGroupEntry {
1157 binding: 0,
1158 resource: sim_uniform.as_entire_binding(),
1159 }],
1160 });
1161
1162 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1163 label: Some("particle_sim_pass"),
1164 timestamp_writes: None,
1165 });
1166 pass.set_pipeline(&sim_pipeline);
1167 pass.set_bind_group(0, &sim_params_bg, &[]);
1168 pass.set_bind_group(1, &sim_bg, &[]);
1169 pass.dispatch_workgroups(workgroups, 1, 1);
1170 drop(pass);
1171
1172 frame_data.push(ParticleFrameData {
1173 system_idx: idx,
1174 blend,
1175 hidden,
1176 route,
1177 });
1178 }
1179
1180 queue.submit(std::iter::once(encoder.finish()));
1181 frame_data
1182 }
1183}
1184
1185fn build_emit_params(
1186 e: &crate::renderer::EmitterConfig,
1187 capacity: u32,
1188 spawn_count: u32,
1189 frame_counter: u32,
1190) -> EmitParamsGpu {
1191 use crate::renderer::{SpawnShape, VelocityDist};
1192
1193 let mut out = EmitParamsGpu {
1194 spawn_min: [0.0; 3],
1195 spawn_kind: 0,
1196 spawn_max: [0.0; 3],
1197 spawn_radius: 0.0,
1198 vel_min: [0.0; 3],
1199 vel_kind: 0,
1200 vel_max: [0.0; 3],
1201 cone_half_angle: 0.0,
1202 vel_axis: [0.0; 3],
1203 cone_min_speed: 0.0,
1204 colour: e.colour,
1205 spawn_count,
1206 capacity,
1207 rng_seed: frame_counter.wrapping_mul(0x9E3779B1),
1208 size: e.size,
1209 lifetime_min: e.lifetime.0,
1210 lifetime_max: e.lifetime.1,
1211 cone_max_speed: 0.0,
1212 _pad: 0.0,
1213 };
1214
1215 match e.spawn_shape {
1216 SpawnShape::Point(p) => {
1217 out.spawn_kind = 0;
1218 out.spawn_min = p;
1219 }
1220 SpawnShape::Box { min, max } => {
1221 out.spawn_kind = 1;
1222 out.spawn_min = min;
1223 out.spawn_max = max;
1224 }
1225 SpawnShape::Sphere { center, radius } => {
1226 out.spawn_kind = 2;
1227 out.spawn_min = center;
1228 out.spawn_radius = radius;
1229 }
1230 }
1231
1232 match e.initial_velocity {
1233 VelocityDist::Fixed(v) => {
1234 out.vel_kind = 0;
1235 out.vel_min = v;
1236 }
1237 VelocityDist::UniformBox { min, max } => {
1238 out.vel_kind = 1;
1239 out.vel_min = min;
1240 out.vel_max = max;
1241 }
1242 VelocityDist::UniformCone {
1243 axis,
1244 half_angle,
1245 min_speed,
1246 max_speed,
1247 } => {
1248 out.vel_kind = 2;
1249 out.vel_axis = axis;
1250 out.cone_half_angle = half_angle;
1251 out.cone_min_speed = min_speed;
1252 out.cone_max_speed = max_speed;
1253 }
1254 }
1255
1256 out
1257}
1258
1259fn build_sim_params(
1260 dt: f32,
1261 capacity: u32,
1262 forces: &[crate::renderer::ForceField],
1263) -> SimParamsGpu {
1264 use crate::renderer::ForceField;
1265
1266 let mut gpu_forces = [GpuForce {
1267 kind: 0,
1268 _pad: [0; 3],
1269 v0: [0.0; 4],
1270 v1: [0.0; 4],
1271 }; MAX_FORCES];
1272
1273 let n = forces.len().min(MAX_FORCES);
1274 for (i, f) in forces.iter().take(n).enumerate() {
1275 match *f {
1276 ForceField::Gravity(a) => {
1277 gpu_forces[i].kind = 0;
1278 gpu_forces[i].v0 = [a[0], a[1], a[2], 0.0];
1279 }
1280 ForceField::Drag(k) => {
1281 gpu_forces[i].kind = 1;
1282 gpu_forces[i].v0 = [k, 0.0, 0.0, 0.0];
1283 }
1284 ForceField::PointAttractor {
1285 position,
1286 strength,
1287 falloff,
1288 } => {
1289 gpu_forces[i].kind = 2;
1290 gpu_forces[i].v0 = [position[0], position[1], position[2], strength];
1291 gpu_forces[i].v1 = [falloff, 0.0, 0.0, 0.0];
1292 }
1293 }
1294 }
1295
1296 SimParamsGpu {
1297 dt,
1298 capacity,
1299 force_count: n as u32,
1300 _pad: 0,
1301 forces: gpu_forces,
1302 }
1303}