1use bytemuck::{Pod, Zeroable};
22use wgpu::util::DeviceExt;
23
24use crate::renderer::{ParticleMeshAlign, SpriteBlend, SpriteLitParams, SpriteSizeMode};
25
26#[derive(Default)]
30pub(crate) struct ParticleResources {
31 pub(crate) systems: Vec<Option<ParticleSystem>>,
33 pub(crate) sim_bgl: Option<wgpu::BindGroupLayout>,
35 pub(crate) params_bgl: Option<wgpu::BindGroupLayout>,
37 pub(crate) draw_bgl: Option<wgpu::BindGroupLayout>,
39 pub(crate) emit_pipeline: Option<wgpu::ComputePipeline>,
41 pub(crate) sim_pipeline: Option<wgpu::ComputePipeline>,
43 pub(crate) sprite_pipeline_alpha: Option<crate::resources::DualPipeline>,
45 pub(crate) sprite_pipeline_additive: Option<crate::resources::DualPipeline>,
46 pub(crate) sprite_pipeline_premultiplied: Option<crate::resources::DualPipeline>,
47 pub(crate) sprite_lit_pipeline_alpha: Option<crate::resources::DualPipeline>,
49 pub(crate) sprite_lit_pipeline_additive: Option<crate::resources::DualPipeline>,
50 pub(crate) sprite_lit_pipeline_premultiplied: Option<crate::resources::DualPipeline>,
51 pub(crate) sprite_lit_bgl: Option<wgpu::BindGroupLayout>,
53 pub(crate) sprite_lit_fallback_bg: Option<wgpu::BindGroup>,
55 pub(crate) mesh_draw_bgl: Option<wgpu::BindGroupLayout>,
57 pub(crate) mesh_pipeline_alpha: Option<crate::resources::DualPipeline>,
59 pub(crate) mesh_pipeline_additive: Option<crate::resources::DualPipeline>,
60 pub(crate) mesh_pipeline_premultiplied: Option<crate::resources::DualPipeline>,
61}
62
63crate::resources::handle::registry_handle! {
64 pub struct GpuParticleSystemId;
70}
71
72#[derive(Debug, Clone)]
75#[non_exhaustive]
76pub struct GpuParticleSystemConfig {
77 pub capacity: u32,
81 pub render: ParticleRender,
83}
84
85impl Default for GpuParticleSystemConfig {
86 fn default() -> Self {
87 Self {
88 capacity: 10_000,
89 render: ParticleRender::default(),
90 }
91 }
92}
93
94#[derive(Debug, Clone)]
96#[non_exhaustive]
97pub enum ParticleRender {
98 Sprite {
100 texture_id: Option<crate::resources::TextureId>,
103 blend: SpriteBlend,
105 size_mode: SpriteSizeMode,
107 depth_write: bool,
109 lit: bool,
113 lit_params: SpriteLitParams,
115 normal_texture_id: Option<crate::resources::TextureId>,
117 },
118 Mesh {
123 mesh_id: crate::resources::mesh::mesh_store::MeshId,
125 texture_id: Option<crate::resources::TextureId>,
127 blend: SpriteBlend,
129 align: ParticleMeshAlign,
131 },
132}
133
134impl Default for ParticleRender {
135 fn default() -> Self {
136 ParticleRender::Sprite {
137 texture_id: None,
138 blend: SpriteBlend::AlphaBlend,
139 size_mode: SpriteSizeMode::ScreenSpace,
140 depth_write: false,
141 lit: false,
142 lit_params: SpriteLitParams {
143 roughness: 0.9,
144 normal_mode: crate::renderer::SpriteNormalMode::Spherical,
145 receive_shadows: false,
146 ambient_scale: 1.0,
147 },
148 normal_texture_id: None,
149 }
150 }
151}
152
153#[repr(C)]
157#[derive(Copy, Clone, Pod, Zeroable)]
158pub(crate) struct GpuParticle {
159 pub position: [f32; 3],
160 pub lifetime: f32, pub velocity: [f32; 3],
162 pub max_lifetime: f32, pub colour: [f32; 4],
164 pub size: f32,
165 pub spawn_seed: f32,
168 pub _pad: [f32; 2],
169}
170
171#[repr(C)]
173#[derive(Copy, Clone, Pod, Zeroable)]
174pub(crate) struct EmitParamsGpu {
175 pub spawn_min: [f32; 3],
176 pub spawn_kind: u32, pub spawn_max: [f32; 3],
178 pub spawn_radius: f32, pub vel_min: [f32; 3],
180 pub vel_kind: u32, pub vel_max: [f32; 3],
182 pub cone_half_angle: f32,
183 pub vel_axis: [f32; 3],
184 pub cone_min_speed: f32,
185 pub colour: [f32; 4],
186 pub spawn_count: u32,
187 pub capacity: u32,
188 pub rng_seed: u32,
189 pub size: f32,
190 pub lifetime_min: f32,
191 pub lifetime_max: f32,
192 pub cone_max_speed: f32,
193 pub _pad: f32,
194}
195
196pub(crate) const MAX_FORCES: usize = 8;
200
201#[repr(C)]
203#[derive(Copy, Clone, Pod, Zeroable)]
204pub(crate) struct GpuForce {
205 pub kind: u32, pub _pad: [u32; 3],
207 pub v0: [f32; 4], pub v1: [f32; 4], }
210
211#[repr(C)]
214#[derive(Copy, Clone, Pod, Zeroable)]
215pub(crate) struct SimParamsGpu {
216 pub dt: f32,
217 pub capacity: u32,
218 pub force_count: u32,
219 pub _pad: u32,
220 pub forces: [GpuForce; MAX_FORCES],
221}
222
223pub(crate) struct ParticleSystem {
225 pub capacity: u32,
226 pub render: ParticleRender,
227 pub particle_buf: wgpu::Buffer,
230 pub emit_counter_buf: wgpu::Buffer,
234 pub sim_bg: wgpu::BindGroup,
236 pub draw_bg: Option<wgpu::BindGroup>,
239 pub draw_bg_mesh: Option<wgpu::BindGroup>,
242 pub draw_lit_normal_bg: Option<wgpu::BindGroup>,
245 pub _draw_uniform_buf: Option<wgpu::Buffer>,
247 pub alive: bool,
249 pub frame_counter: u32,
253 pub spawn_accumulator: f32,
257}
258
259#[derive(Copy, Clone, Debug)]
261pub(crate) enum ParticleDrawRoute {
262 Sprite {
263 lit: bool,
264 },
265 Mesh {
266 mesh_id: crate::resources::mesh::mesh_store::MeshId,
267 },
268}
269
270pub(crate) struct ParticleFrameData {
273 pub system_idx: usize,
275 pub blend: SpriteBlend,
277 pub hidden: bool,
279 pub route: ParticleDrawRoute,
281}
282
283impl crate::resources::DeviceResources {
284 pub fn create_gpu_particle_system(
290 &mut self,
291 device: &wgpu::Device,
292 queue: &wgpu::Queue,
293 config: &GpuParticleSystemConfig,
294 ) -> GpuParticleSystemId {
295 self.ensure_particle_pipelines(device);
296
297 let capacity = config.capacity.max(1);
298
299 let particle_bytes_len = (capacity as usize) * std::mem::size_of::<GpuParticle>();
301 let zero_particles = vec![0u8; particle_bytes_len];
302 let particle_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
303 label: Some("gpu_particle_buf"),
304 contents: &zero_particles,
305 usage: wgpu::BufferUsages::STORAGE
306 | wgpu::BufferUsages::VERTEX
307 | wgpu::BufferUsages::COPY_DST,
308 });
309
310 let emit_counter_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
312 label: Some("gpu_particle_emit_counter"),
313 contents: bytemuck::bytes_of(&0u32),
314 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
315 });
316
317 enum DrawState {
321 Sprite {
322 texture_id: Option<crate::resources::TextureId>,
323 size_mode: SpriteSizeMode,
324 lit: bool,
325 lit_params: SpriteLitParams,
326 normal_texture_id: Option<crate::resources::TextureId>,
327 },
328 Mesh {
329 texture_id: Option<crate::resources::TextureId>,
330 align: ParticleMeshAlign,
331 },
332 }
333 let draw_state = match &config.render {
334 ParticleRender::Sprite {
335 texture_id,
336 blend: _,
337 size_mode,
338 depth_write: _,
339 lit,
340 lit_params,
341 normal_texture_id,
342 } => DrawState::Sprite {
343 texture_id: *texture_id,
344 size_mode: *size_mode,
345 lit: *lit,
346 lit_params: *lit_params,
347 normal_texture_id: *normal_texture_id,
348 },
349 ParticleRender::Mesh {
350 texture_id,
351 blend: _,
352 align,
353 mesh_id: _,
354 } => DrawState::Mesh {
355 texture_id: *texture_id,
356 align: *align,
357 },
358 };
359
360 let _ = queue; let sim_bgl = self
363 .particle
364 .sim_bgl
365 .as_ref()
366 .expect("ensure_particle_pipelines failed to create sim BGL");
367 let sim_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
368 label: Some("gpu_particle_sim_bg"),
369 layout: sim_bgl,
370 entries: &[
371 wgpu::BindGroupEntry {
372 binding: 0,
373 resource: particle_buf.as_entire_binding(),
374 },
375 wgpu::BindGroupEntry {
376 binding: 1,
377 resource: emit_counter_buf.as_entire_binding(),
378 },
379 ],
380 });
381
382 let mut sprite_draw_bg: Option<wgpu::BindGroup> = None;
385 let mut mesh_draw_bg: Option<wgpu::BindGroup> = None;
386 let mut sprite_lit_normal_bg: Option<wgpu::BindGroup> = None;
387 let draw_uniform_buf: Option<wgpu::Buffer>;
388
389 match draw_state {
390 DrawState::Sprite {
391 texture_id,
392 size_mode,
393 lit,
394 lit_params,
395 normal_texture_id,
396 } => {
397 #[repr(C)]
398 #[derive(Copy, Clone, Pod, Zeroable)]
399 struct SpriteDrawUniform {
400 model: [[f32; 4]; 4],
401 world_space: u32,
402 has_texture: u32,
403 normal_mode: u32,
404 has_normal_map: u32,
405 ambient_scale: f32,
406 roughness: f32,
407 _pad0: u32,
408 _pad1: u32,
409 }
410 let normal_mode_u32 = match lit_params.normal_mode {
411 crate::renderer::SpriteNormalMode::Spherical => 0u32,
412 crate::renderer::SpriteNormalMode::Flat => 1u32,
413 crate::renderer::SpriteNormalMode::NormalMap => 2u32,
414 };
415 let uniform = SpriteDrawUniform {
416 model: glam::Mat4::IDENTITY.to_cols_array_2d(),
417 world_space: matches!(size_mode, SpriteSizeMode::WorldSpace) as u32,
418 has_texture: texture_id.is_some() as u32,
419 normal_mode: normal_mode_u32,
420 has_normal_map: normal_texture_id.is_some() as u32,
421 ambient_scale: lit_params.ambient_scale,
422 roughness: lit_params.roughness,
423 _pad0: 0,
424 _pad1: 0,
425 };
426 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
427 label: Some("gpu_particle_sprite_draw_uniform"),
428 contents: bytemuck::bytes_of(&uniform),
429 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
430 });
431 let texture_view = match texture_id {
432 Some(id) if self.content.textures.get(id).is_some() => {
433 &self.content.textures.get(id).unwrap().view
434 }
435 _ => &self.content.fallback_lut_view,
436 };
437 let draw_bgl = self
438 .particle
439 .draw_bgl
440 .as_ref()
441 .expect("ensure_particle_pipelines failed to create draw BGL");
442 sprite_draw_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
443 label: Some("gpu_particle_draw_bg"),
444 layout: draw_bgl,
445 entries: &[
446 wgpu::BindGroupEntry {
447 binding: 0,
448 resource: uniform_buf.as_entire_binding(),
449 },
450 wgpu::BindGroupEntry {
451 binding: 1,
452 resource: wgpu::BindingResource::TextureView(texture_view),
453 },
454 wgpu::BindGroupEntry {
455 binding: 2,
456 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
457 },
458 wgpu::BindGroupEntry {
459 binding: 3,
460 resource: particle_buf.as_entire_binding(),
461 },
462 ],
463 }));
464 if lit {
465 let lit_bgl = self
466 .particle
467 .sprite_lit_bgl
468 .as_ref()
469 .expect("ensure_particle_pipelines failed to create lit BGL");
470 let normal_view = match normal_texture_id {
471 Some(id) if self.content.textures.get(id).is_some() => {
472 &self.content.textures.get(id).unwrap().view
473 }
474 _ => &self.fallback_normal_map_view,
475 };
476 sprite_lit_normal_bg =
477 Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
478 label: Some("gpu_particle_lit_normal_bg"),
479 layout: lit_bgl,
480 entries: &[
481 wgpu::BindGroupEntry {
482 binding: 0,
483 resource: wgpu::BindingResource::TextureView(normal_view),
484 },
485 wgpu::BindGroupEntry {
486 binding: 1,
487 resource: wgpu::BindingResource::Sampler(
488 &self.material_sampler,
489 ),
490 },
491 ],
492 }));
493 }
494 draw_uniform_buf = Some(uniform_buf);
495 }
496 DrawState::Mesh { texture_id, align } => {
497 #[repr(C)]
498 #[derive(Copy, Clone, Pod, Zeroable)]
499 struct MeshDrawUniform {
500 align: u32,
501 has_texture: u32,
502 _pad0: u32,
503 _pad1: u32,
504 }
505 let align_u32 = match align {
506 ParticleMeshAlign::Identity => 0u32,
507 ParticleMeshAlign::Velocity => 1u32,
508 ParticleMeshAlign::Random => 2u32,
509 };
510 let uniform = MeshDrawUniform {
511 align: align_u32,
512 has_texture: texture_id.is_some() as u32,
513 _pad0: 0,
514 _pad1: 0,
515 };
516 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
517 label: Some("gpu_particle_mesh_draw_uniform"),
518 contents: bytemuck::bytes_of(&uniform),
519 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
520 });
521 let texture_view = match texture_id {
522 Some(id) if self.content.textures.get(id).is_some() => {
523 &self.content.textures.get(id).unwrap().view
524 }
525 _ => &self.fallback_texture.view,
526 };
527 let mesh_bgl = self
528 .particle
529 .mesh_draw_bgl
530 .as_ref()
531 .expect("ensure_particle_pipelines failed to create mesh draw BGL");
532 mesh_draw_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
533 label: Some("gpu_particle_mesh_draw_bg"),
534 layout: mesh_bgl,
535 entries: &[
536 wgpu::BindGroupEntry {
537 binding: 0,
538 resource: uniform_buf.as_entire_binding(),
539 },
540 wgpu::BindGroupEntry {
541 binding: 1,
542 resource: wgpu::BindingResource::TextureView(texture_view),
543 },
544 wgpu::BindGroupEntry {
545 binding: 2,
546 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
547 },
548 wgpu::BindGroupEntry {
549 binding: 3,
550 resource: particle_buf.as_entire_binding(),
551 },
552 ],
553 }));
554 draw_uniform_buf = Some(uniform_buf);
555 }
556 }
557
558 let system = ParticleSystem {
559 capacity,
560 render: config.render.clone(),
561 particle_buf,
562 emit_counter_buf,
563 sim_bg,
564 draw_bg: sprite_draw_bg,
565 draw_bg_mesh: mesh_draw_bg,
566 draw_lit_normal_bg: sprite_lit_normal_bg,
567 _draw_uniform_buf: draw_uniform_buf,
568 alive: true,
569 frame_counter: 0,
570 spawn_accumulator: 0.0,
571 };
572
573 if let Some(idx) = self
574 .particle
575 .systems
576 .iter()
577 .position(|slot: &Option<ParticleSystem>| slot.as_ref().is_none_or(|s| !s.alive))
578 {
579 self.particle.systems[idx] = Some(system);
580 GpuParticleSystemId(idx)
581 } else {
582 self.particle.systems.push(Some(system));
583 GpuParticleSystemId(self.particle.systems.len() - 1)
584 }
585 }
586
587 pub fn drop_gpu_particle_system(&mut self, id: GpuParticleSystemId) {
590 if let Some(Some(s)) = self.particle.systems.get_mut(id.0) {
591 s.alive = false;
592 }
593 }
594
595 #[allow(dead_code)]
596 pub(crate) fn particle_system(&self, id: GpuParticleSystemId) -> Option<&ParticleSystem> {
597 self.particle
598 .systems
599 .get(id.0)?
600 .as_ref()
601 .filter(|s| s.alive)
602 }
603
604 #[allow(dead_code)]
605 pub(crate) fn particle_system_mut(
606 &mut self,
607 id: GpuParticleSystemId,
608 ) -> Option<&mut ParticleSystem> {
609 self.particle
610 .systems
611 .get_mut(id.0)?
612 .as_mut()
613 .filter(|s| s.alive)
614 }
615
616 pub(crate) fn ensure_particle_pipelines(&mut self, device: &wgpu::Device) {
619 if self.particle.sim_bgl.is_some() {
620 return;
621 }
622
623 let params_bgl = crate::resources::builders::uniform_bgl(
625 device,
626 "gpu_particle_params_bgl",
627 wgpu::ShaderStages::COMPUTE,
628 );
629
630 let sim_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
632 label: Some("gpu_particle_sim_bgl"),
633 entries: &[
634 wgpu::BindGroupLayoutEntry {
635 binding: 0,
636 visibility: wgpu::ShaderStages::COMPUTE,
637 ty: wgpu::BindingType::Buffer {
638 ty: wgpu::BufferBindingType::Storage { read_only: false },
639 has_dynamic_offset: false,
640 min_binding_size: None,
641 },
642 count: None,
643 },
644 wgpu::BindGroupLayoutEntry {
645 binding: 1,
646 visibility: wgpu::ShaderStages::COMPUTE,
647 ty: wgpu::BindingType::Buffer {
648 ty: wgpu::BufferBindingType::Storage { read_only: false },
649 has_dynamic_offset: false,
650 min_binding_size: None,
651 },
652 count: None,
653 },
654 ],
655 });
656
657 let draw_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
659 label: Some("gpu_particle_draw_bgl"),
660 entries: &[
661 wgpu::BindGroupLayoutEntry {
662 binding: 0,
663 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
664 ty: wgpu::BindingType::Buffer {
665 ty: wgpu::BufferBindingType::Uniform,
666 has_dynamic_offset: false,
667 min_binding_size: None,
668 },
669 count: None,
670 },
671 wgpu::BindGroupLayoutEntry {
672 binding: 1,
673 visibility: wgpu::ShaderStages::FRAGMENT,
674 ty: wgpu::BindingType::Texture {
675 sample_type: wgpu::TextureSampleType::Float { filterable: true },
676 view_dimension: wgpu::TextureViewDimension::D2,
677 multisampled: false,
678 },
679 count: None,
680 },
681 wgpu::BindGroupLayoutEntry {
682 binding: 2,
683 visibility: wgpu::ShaderStages::FRAGMENT,
684 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
685 count: None,
686 },
687 wgpu::BindGroupLayoutEntry {
688 binding: 3,
689 visibility: wgpu::ShaderStages::VERTEX,
690 ty: wgpu::BindingType::Buffer {
691 ty: wgpu::BufferBindingType::Storage { read_only: true },
692 has_dynamic_offset: false,
693 min_binding_size: None,
694 },
695 count: None,
696 },
697 ],
698 });
699
700 let emit_shader = crate::resources::builders::wgsl_module(
702 device,
703 "particle_emit_shader",
704 crate::resources::builders::wgsl_source!("particle_emit"),
705 );
706 let sim_shader = crate::resources::builders::wgsl_module(
707 device,
708 "particle_sim_shader",
709 crate::resources::builders::wgsl_source!("particle_sim"),
710 );
711
712 let compute_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
713 label: Some("particle_compute_layout"),
714 bind_group_layouts: &[¶ms_bgl, &sim_bgl],
715 push_constant_ranges: &[],
716 });
717
718 let emit_pipeline = crate::resources::builders::compute_pipeline(
719 device,
720 "particle_emit_pipeline",
721 &compute_layout,
722 &emit_shader,
723 "emit_main",
724 );
725
726 let sim_pipeline = crate::resources::builders::compute_pipeline(
727 device,
728 "particle_sim_pipeline",
729 &compute_layout,
730 &sim_shader,
731 "sim_main",
732 );
733
734 let sprite_shader = crate::resources::builders::wgsl_module(
736 device,
737 "particle_sprite_shader",
738 crate::resources::builders::wgsl_source!("particle_sprite"),
739 );
740
741 let draw_layout = crate::resources::builders::standard_scene_layout(
742 device,
743 "particle_draw_layout",
744 &self.camera_bind_group_layout,
745 &draw_bgl,
746 );
747
748 let sample_count = self.sample_count;
749 let ldr_format = self.target_format;
750 let alpha = wgpu::BlendState::ALPHA_BLENDING;
751 let additive = crate::resources::builders::ADDITIVE_BLEND;
752 let premul = crate::resources::builders::PREMULTIPLIED_BLEND;
753
754 let make_draw = |blend: wgpu::BlendState, label: &str| {
757 crate::resources::builders::build_dual_pipeline(
758 device,
759 &crate::resources::builders::DualPipelineDesc {
760 label,
761 layout: &draw_layout,
762 shader: &sprite_shader,
763 vertex_entry: "vs_main",
764 fragment_entry: "fs_main",
765 vertex_buffers: &[],
766 blend: Some(blend),
767 topology: wgpu::PrimitiveTopology::TriangleList,
768 cull_mode: None,
769 depth_write: false,
770 depth_compare: wgpu::CompareFunction::Less,
771 sample_count,
772 ldr_format,
773 },
774 )
775 };
776
777 self.particle.sprite_pipeline_alpha = Some(make_draw(alpha, "particle_sprite_alpha"));
778 self.particle.sprite_pipeline_additive =
779 Some(make_draw(additive, "particle_sprite_additive"));
780 self.particle.sprite_pipeline_premultiplied =
781 Some(make_draw(premul, "particle_sprite_premultiplied"));
782
783 let lit_bgl = crate::resources::builders::texture_sampler_bgl(
787 device,
788 "gpu_particle_lit_bgl",
789 wgpu::ShaderStages::FRAGMENT,
790 );
791
792 let lit_shader = crate::resources::builders::wgsl_module(
793 device,
794 "particle_sprite_lit_shader",
795 crate::resources::builders::wgsl_source!("particle_sprite_lit"),
796 );
797
798 let lit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
799 label: Some("particle_draw_lit_layout"),
800 bind_group_layouts: &[&self.camera_bind_group_layout, &draw_bgl, &lit_bgl],
801 push_constant_ranges: &[],
802 });
803
804 let make_lit_draw = |blend: wgpu::BlendState, label: &str| {
805 crate::resources::builders::build_dual_pipeline(
806 device,
807 &crate::resources::builders::DualPipelineDesc {
808 label,
809 layout: &lit_layout,
810 shader: &lit_shader,
811 vertex_entry: "vs_main",
812 fragment_entry: "fs_main",
813 vertex_buffers: &[],
814 blend: Some(blend),
815 topology: wgpu::PrimitiveTopology::TriangleList,
816 cull_mode: None,
817 depth_write: false,
818 depth_compare: wgpu::CompareFunction::Less,
819 sample_count,
820 ldr_format,
821 },
822 )
823 };
824
825 self.particle.sprite_lit_pipeline_alpha =
826 Some(make_lit_draw(alpha, "particle_sprite_lit_alpha"));
827 self.particle.sprite_lit_pipeline_additive =
828 Some(make_lit_draw(additive, "particle_sprite_lit_additive"));
829 self.particle.sprite_lit_pipeline_premultiplied =
830 Some(make_lit_draw(premul, "particle_sprite_lit_premultiplied"));
831
832 let lit_fallback_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
833 label: Some("gpu_particle_lit_fallback_bg"),
834 layout: &lit_bgl,
835 entries: &[
836 wgpu::BindGroupEntry {
837 binding: 0,
838 resource: wgpu::BindingResource::TextureView(&self.fallback_normal_map_view),
839 },
840 wgpu::BindGroupEntry {
841 binding: 1,
842 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
843 },
844 ],
845 });
846
847 self.particle.sprite_lit_bgl = Some(lit_bgl);
848 self.particle.sprite_lit_fallback_bg = Some(lit_fallback_bg);
849
850 let mesh_draw_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
855 label: Some("gpu_particle_mesh_draw_bgl"),
856 entries: &[
857 wgpu::BindGroupLayoutEntry {
858 binding: 0,
859 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
860 ty: wgpu::BindingType::Buffer {
861 ty: wgpu::BufferBindingType::Uniform,
862 has_dynamic_offset: false,
863 min_binding_size: None,
864 },
865 count: None,
866 },
867 wgpu::BindGroupLayoutEntry {
868 binding: 1,
869 visibility: wgpu::ShaderStages::FRAGMENT,
870 ty: wgpu::BindingType::Texture {
871 sample_type: wgpu::TextureSampleType::Float { filterable: true },
872 view_dimension: wgpu::TextureViewDimension::D2,
873 multisampled: false,
874 },
875 count: None,
876 },
877 wgpu::BindGroupLayoutEntry {
878 binding: 2,
879 visibility: wgpu::ShaderStages::FRAGMENT,
880 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
881 count: None,
882 },
883 wgpu::BindGroupLayoutEntry {
884 binding: 3,
885 visibility: wgpu::ShaderStages::VERTEX,
886 ty: wgpu::BindingType::Buffer {
887 ty: wgpu::BufferBindingType::Storage { read_only: true },
888 has_dynamic_offset: false,
889 min_binding_size: None,
890 },
891 count: None,
892 },
893 ],
894 });
895
896 let mesh_shader = crate::resources::builders::wgsl_module(
897 device,
898 "particle_mesh_shader",
899 crate::resources::builders::wgsl_source!("particle_mesh"),
900 );
901
902 let mesh_layout = crate::resources::builders::standard_scene_layout(
903 device,
904 "particle_mesh_draw_layout",
905 &self.camera_bind_group_layout,
906 &mesh_draw_bgl,
907 );
908
909 let make_mesh_draw = |blend: wgpu::BlendState, label: &str| {
912 crate::resources::builders::build_dual_pipeline(
913 device,
914 &crate::resources::builders::DualPipelineDesc {
915 label,
916 layout: &mesh_layout,
917 shader: &mesh_shader,
918 vertex_entry: "vs_main",
919 fragment_entry: "fs_main",
920 vertex_buffers: &[crate::resources::types::Vertex::buffer_layout()],
921 blend: Some(blend),
922 topology: wgpu::PrimitiveTopology::TriangleList,
923 cull_mode: Some(wgpu::Face::Back),
924 depth_write: false,
925 depth_compare: wgpu::CompareFunction::Less,
926 sample_count,
927 ldr_format,
928 },
929 )
930 };
931
932 self.particle.mesh_pipeline_alpha = Some(make_mesh_draw(alpha, "particle_mesh_alpha"));
933 self.particle.mesh_pipeline_additive =
934 Some(make_mesh_draw(additive, "particle_mesh_additive"));
935 self.particle.mesh_pipeline_premultiplied =
936 Some(make_mesh_draw(premul, "particle_mesh_premultiplied"));
937 self.particle.mesh_draw_bgl = Some(mesh_draw_bgl);
938
939 self.particle.params_bgl = Some(params_bgl);
940 self.particle.sim_bgl = Some(sim_bgl);
941 self.particle.draw_bgl = Some(draw_bgl);
942 self.particle.emit_pipeline = Some(emit_pipeline);
943 self.particle.sim_pipeline = Some(sim_pipeline);
944 }
945
946 pub(crate) fn run_particle_jobs(
949 &mut self,
950 device: &wgpu::Device,
951 queue: &wgpu::Queue,
952 items: &[crate::renderer::GpuParticleSystemItem],
953 ) -> Vec<ParticleFrameData> {
954 if items.is_empty() {
955 return Vec::new();
956 }
957 self.ensure_particle_pipelines(device);
958
959 let emit_pipeline = self
960 .particle
961 .emit_pipeline
962 .as_ref()
963 .expect("particle pipelines should exist after ensure")
964 .clone();
965 let sim_pipeline = self
966 .particle
967 .sim_pipeline
968 .as_ref()
969 .expect("particle pipelines should exist after ensure")
970 .clone();
971 let params_bgl = self
972 .particle
973 .params_bgl
974 .as_ref()
975 .expect("particle params BGL")
976 .clone();
977
978 let mut frame_data: Vec<ParticleFrameData> = Vec::with_capacity(items.len());
979 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
980 label: Some("particle_compute_encoder"),
981 });
982
983 for item in items {
984 let idx = item.system_id.0;
985 let (blend, route) = match self.particle.systems.get(idx).and_then(|s| s.as_ref()) {
986 Some(s) if s.alive => match &s.render {
987 ParticleRender::Sprite { blend, lit, .. } => {
988 (*blend, ParticleDrawRoute::Sprite { lit: *lit })
989 }
990 ParticleRender::Mesh { blend, mesh_id, .. } => {
991 (*blend, ParticleDrawRoute::Mesh { mesh_id: *mesh_id })
992 }
993 },
994 _ => continue,
995 };
996
997 let hidden = item.settings.hidden;
998
999 let (
1001 capacity,
1002 particle_buf_binding,
1003 emit_counter_binding,
1004 sim_bg,
1005 spawn_count,
1006 frame_counter,
1007 ) = {
1008 let system = self.particle.systems[idx].as_mut().unwrap();
1009 let dt = item.time_step.max(0.0);
1010 system.spawn_accumulator += item.emitter.rate * dt;
1011 let spawn_count = system.spawn_accumulator.floor() as u32;
1012 system.spawn_accumulator -= spawn_count as f32;
1013 system.frame_counter = system.frame_counter.wrapping_add(1);
1014 let frame_counter = system.frame_counter;
1015 (
1016 system.capacity,
1017 system.particle_buf.as_entire_binding(),
1018 system.emit_counter_buf.as_entire_binding(),
1019 system.sim_bg.clone(),
1021 spawn_count,
1022 frame_counter,
1023 )
1024 };
1025 let _ = (particle_buf_binding, emit_counter_binding); let workgroups = capacity.div_ceil(64);
1028
1029 if spawn_count > 0 {
1031 queue.write_buffer(
1032 &self.particle.systems[idx]
1033 .as_ref()
1034 .unwrap()
1035 .emit_counter_buf,
1036 0,
1037 bytemuck::bytes_of(&spawn_count),
1038 );
1039
1040 let emit_params =
1041 build_emit_params(&item.emitter, capacity, spawn_count, frame_counter);
1042 let emit_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1043 label: Some("particle_emit_params"),
1044 contents: bytemuck::bytes_of(&emit_params),
1045 usage: wgpu::BufferUsages::UNIFORM,
1046 });
1047 let emit_params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1048 label: Some("particle_emit_params_bg"),
1049 layout: ¶ms_bgl,
1050 entries: &[wgpu::BindGroupEntry {
1051 binding: 0,
1052 resource: emit_uniform.as_entire_binding(),
1053 }],
1054 });
1055
1056 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1057 label: Some("particle_emit_pass"),
1058 timestamp_writes: None,
1059 });
1060 pass.set_pipeline(&emit_pipeline);
1061 pass.set_bind_group(0, &emit_params_bg, &[]);
1062 pass.set_bind_group(1, &sim_bg, &[]);
1063 pass.dispatch_workgroups(workgroups, 1, 1);
1064 }
1065
1066 let sim_params = build_sim_params(item.time_step, capacity, &item.forces);
1068 let sim_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1069 label: Some("particle_sim_params"),
1070 contents: bytemuck::bytes_of(&sim_params),
1071 usage: wgpu::BufferUsages::UNIFORM,
1072 });
1073 let sim_params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1074 label: Some("particle_sim_params_bg"),
1075 layout: ¶ms_bgl,
1076 entries: &[wgpu::BindGroupEntry {
1077 binding: 0,
1078 resource: sim_uniform.as_entire_binding(),
1079 }],
1080 });
1081
1082 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1083 label: Some("particle_sim_pass"),
1084 timestamp_writes: None,
1085 });
1086 pass.set_pipeline(&sim_pipeline);
1087 pass.set_bind_group(0, &sim_params_bg, &[]);
1088 pass.set_bind_group(1, &sim_bg, &[]);
1089 pass.dispatch_workgroups(workgroups, 1, 1);
1090 drop(pass);
1091
1092 frame_data.push(ParticleFrameData {
1093 system_idx: idx,
1094 blend,
1095 hidden,
1096 route,
1097 });
1098 }
1099
1100 queue.submit(std::iter::once(encoder.finish()));
1101 frame_data
1102 }
1103}
1104
1105fn build_emit_params(
1106 e: &crate::renderer::EmitterConfig,
1107 capacity: u32,
1108 spawn_count: u32,
1109 frame_counter: u32,
1110) -> EmitParamsGpu {
1111 use crate::renderer::{SpawnShape, VelocityDist};
1112
1113 let mut out = EmitParamsGpu {
1114 spawn_min: [0.0; 3],
1115 spawn_kind: 0,
1116 spawn_max: [0.0; 3],
1117 spawn_radius: 0.0,
1118 vel_min: [0.0; 3],
1119 vel_kind: 0,
1120 vel_max: [0.0; 3],
1121 cone_half_angle: 0.0,
1122 vel_axis: [0.0; 3],
1123 cone_min_speed: 0.0,
1124 colour: e.colour,
1125 spawn_count,
1126 capacity,
1127 rng_seed: frame_counter.wrapping_mul(0x9E3779B1),
1128 size: e.size,
1129 lifetime_min: e.lifetime.0,
1130 lifetime_max: e.lifetime.1,
1131 cone_max_speed: 0.0,
1132 _pad: 0.0,
1133 };
1134
1135 match e.spawn_shape {
1136 SpawnShape::Point(p) => {
1137 out.spawn_kind = 0;
1138 out.spawn_min = p;
1139 }
1140 SpawnShape::Box { min, max } => {
1141 out.spawn_kind = 1;
1142 out.spawn_min = min;
1143 out.spawn_max = max;
1144 }
1145 SpawnShape::Sphere { center, radius } => {
1146 out.spawn_kind = 2;
1147 out.spawn_min = center;
1148 out.spawn_radius = radius;
1149 }
1150 }
1151
1152 match e.initial_velocity {
1153 VelocityDist::Fixed(v) => {
1154 out.vel_kind = 0;
1155 out.vel_min = v;
1156 }
1157 VelocityDist::UniformBox { min, max } => {
1158 out.vel_kind = 1;
1159 out.vel_min = min;
1160 out.vel_max = max;
1161 }
1162 VelocityDist::UniformCone {
1163 axis,
1164 half_angle,
1165 min_speed,
1166 max_speed,
1167 } => {
1168 out.vel_kind = 2;
1169 out.vel_axis = axis;
1170 out.cone_half_angle = half_angle;
1171 out.cone_min_speed = min_speed;
1172 out.cone_max_speed = max_speed;
1173 }
1174 }
1175
1176 out
1177}
1178
1179fn build_sim_params(
1180 dt: f32,
1181 capacity: u32,
1182 forces: &[crate::renderer::ForceField],
1183) -> SimParamsGpu {
1184 use crate::renderer::ForceField;
1185
1186 let mut gpu_forces = [GpuForce {
1187 kind: 0,
1188 _pad: [0; 3],
1189 v0: [0.0; 4],
1190 v1: [0.0; 4],
1191 }; MAX_FORCES];
1192
1193 let n = forces.len().min(MAX_FORCES);
1194 for (i, f) in forces.iter().take(n).enumerate() {
1195 match *f {
1196 ForceField::Gravity(a) => {
1197 gpu_forces[i].kind = 0;
1198 gpu_forces[i].v0 = [a[0], a[1], a[2], 0.0];
1199 }
1200 ForceField::Drag(k) => {
1201 gpu_forces[i].kind = 1;
1202 gpu_forces[i].v0 = [k, 0.0, 0.0, 0.0];
1203 }
1204 ForceField::PointAttractor {
1205 position,
1206 strength,
1207 falloff,
1208 } => {
1209 gpu_forces[i].kind = 2;
1210 gpu_forces[i].v0 = [position[0], position[1], position[2], strength];
1211 gpu_forces[i].v1 = [falloff, 0.0, 0.0, 0.0];
1212 }
1213 }
1214 }
1215
1216 SimParamsGpu {
1217 dt,
1218 capacity,
1219 force_count: n as u32,
1220 _pad: 0,
1221 forces: gpu_forces,
1222 }
1223}