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(
449 &self.material_sampler,
450 ),
451 },
452 ],
453 }));
454 }
455 draw_uniform_buf = Some(uniform_buf);
456 }
457 DrawState::Mesh { texture_id, align } => {
458 #[repr(C)]
459 #[derive(Copy, Clone, Pod, Zeroable)]
460 struct MeshDrawUniform {
461 align: u32,
462 has_texture: u32,
463 _pad0: u32,
464 _pad1: u32,
465 }
466 let align_u32 = match align {
467 ParticleMeshAlign::Identity => 0u32,
468 ParticleMeshAlign::Velocity => 1u32,
469 ParticleMeshAlign::Random => 2u32,
470 };
471 let uniform = MeshDrawUniform {
472 align: align_u32,
473 has_texture: texture_id.is_some() as u32,
474 _pad0: 0,
475 _pad1: 0,
476 };
477 let uniform_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
478 label: Some("gpu_particle_mesh_draw_uniform"),
479 contents: bytemuck::bytes_of(&uniform),
480 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
481 });
482 let texture_view = match texture_id {
483 Some(id) if (id as usize) < self.textures.len() => {
484 &self.textures[id as usize].view
485 }
486 _ => &self.fallback_texture.view,
487 };
488 let mesh_bgl = self
489 .particle_mesh_draw_bgl
490 .as_ref()
491 .expect("ensure_particle_pipelines failed to create mesh draw BGL");
492 mesh_draw_bg = Some(device.create_bind_group(&wgpu::BindGroupDescriptor {
493 label: Some("gpu_particle_mesh_draw_bg"),
494 layout: mesh_bgl,
495 entries: &[
496 wgpu::BindGroupEntry {
497 binding: 0,
498 resource: uniform_buf.as_entire_binding(),
499 },
500 wgpu::BindGroupEntry {
501 binding: 1,
502 resource: wgpu::BindingResource::TextureView(texture_view),
503 },
504 wgpu::BindGroupEntry {
505 binding: 2,
506 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
507 },
508 wgpu::BindGroupEntry {
509 binding: 3,
510 resource: particle_buf.as_entire_binding(),
511 },
512 ],
513 }));
514 draw_uniform_buf = Some(uniform_buf);
515 }
516 }
517
518 let system = ParticleSystem {
519 capacity,
520 render: config.render.clone(),
521 particle_buf,
522 emit_counter_buf,
523 sim_bg,
524 draw_bg: sprite_draw_bg,
525 draw_bg_mesh: mesh_draw_bg,
526 draw_lit_normal_bg: sprite_lit_normal_bg,
527 _draw_uniform_buf: draw_uniform_buf,
528 alive: true,
529 frame_counter: 0,
530 spawn_accumulator: 0.0,
531 };
532
533 if let Some(idx) = self
534 .particle_systems
535 .iter()
536 .position(|slot: &Option<ParticleSystem>| slot.as_ref().is_none_or(|s| !s.alive))
537 {
538 self.particle_systems[idx] = Some(system);
539 GpuParticleSystemId(idx)
540 } else {
541 self.particle_systems.push(Some(system));
542 GpuParticleSystemId(self.particle_systems.len() - 1)
543 }
544 }
545
546 pub fn drop_gpu_particle_system(&mut self, id: GpuParticleSystemId) {
549 if let Some(Some(s)) = self.particle_systems.get_mut(id.0) {
550 s.alive = false;
551 }
552 }
553
554 #[allow(dead_code)]
555 pub(crate) fn particle_system(&self, id: GpuParticleSystemId) -> Option<&ParticleSystem> {
556 self.particle_systems
557 .get(id.0)?
558 .as_ref()
559 .filter(|s| s.alive)
560 }
561
562 #[allow(dead_code)]
563 pub(crate) fn particle_system_mut(
564 &mut self,
565 id: GpuParticleSystemId,
566 ) -> Option<&mut ParticleSystem> {
567 self.particle_systems
568 .get_mut(id.0)?
569 .as_mut()
570 .filter(|s| s.alive)
571 }
572
573 pub(crate) fn ensure_particle_pipelines(&mut self, device: &wgpu::Device) {
576 if self.particle_sim_bgl.is_some() {
577 return;
578 }
579
580 let params_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
582 label: Some("gpu_particle_params_bgl"),
583 entries: &[wgpu::BindGroupLayoutEntry {
584 binding: 0,
585 visibility: wgpu::ShaderStages::COMPUTE,
586 ty: wgpu::BindingType::Buffer {
587 ty: wgpu::BufferBindingType::Uniform,
588 has_dynamic_offset: false,
589 min_binding_size: None,
590 },
591 count: None,
592 }],
593 });
594
595 let sim_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
597 label: Some("gpu_particle_sim_bgl"),
598 entries: &[
599 wgpu::BindGroupLayoutEntry {
600 binding: 0,
601 visibility: wgpu::ShaderStages::COMPUTE,
602 ty: wgpu::BindingType::Buffer {
603 ty: wgpu::BufferBindingType::Storage { read_only: false },
604 has_dynamic_offset: false,
605 min_binding_size: None,
606 },
607 count: None,
608 },
609 wgpu::BindGroupLayoutEntry {
610 binding: 1,
611 visibility: wgpu::ShaderStages::COMPUTE,
612 ty: wgpu::BindingType::Buffer {
613 ty: wgpu::BufferBindingType::Storage { read_only: false },
614 has_dynamic_offset: false,
615 min_binding_size: None,
616 },
617 count: None,
618 },
619 ],
620 });
621
622 let draw_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
624 label: Some("gpu_particle_draw_bgl"),
625 entries: &[
626 wgpu::BindGroupLayoutEntry {
627 binding: 0,
628 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
629 ty: wgpu::BindingType::Buffer {
630 ty: wgpu::BufferBindingType::Uniform,
631 has_dynamic_offset: false,
632 min_binding_size: None,
633 },
634 count: None,
635 },
636 wgpu::BindGroupLayoutEntry {
637 binding: 1,
638 visibility: wgpu::ShaderStages::FRAGMENT,
639 ty: wgpu::BindingType::Texture {
640 sample_type: wgpu::TextureSampleType::Float { filterable: true },
641 view_dimension: wgpu::TextureViewDimension::D2,
642 multisampled: false,
643 },
644 count: None,
645 },
646 wgpu::BindGroupLayoutEntry {
647 binding: 2,
648 visibility: wgpu::ShaderStages::FRAGMENT,
649 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
650 count: None,
651 },
652 wgpu::BindGroupLayoutEntry {
653 binding: 3,
654 visibility: wgpu::ShaderStages::VERTEX,
655 ty: wgpu::BindingType::Buffer {
656 ty: wgpu::BufferBindingType::Storage { read_only: true },
657 has_dynamic_offset: false,
658 min_binding_size: None,
659 },
660 count: None,
661 },
662 ],
663 });
664
665 let emit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
667 label: Some("particle_emit_shader"),
668 source: wgpu::ShaderSource::Wgsl(
669 include_str!(concat!(env!("OUT_DIR"), "/particle_emit.wgsl")).into(),
670 ),
671 });
672 let sim_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
673 label: Some("particle_sim_shader"),
674 source: wgpu::ShaderSource::Wgsl(
675 include_str!(concat!(env!("OUT_DIR"), "/particle_sim.wgsl")).into(),
676 ),
677 });
678
679 let compute_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
680 label: Some("particle_compute_layout"),
681 bind_group_layouts: &[¶ms_bgl, &sim_bgl],
682 push_constant_ranges: &[],
683 });
684
685 let emit_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
686 label: Some("particle_emit_pipeline"),
687 layout: Some(&compute_layout),
688 module: &emit_shader,
689 entry_point: Some("emit_main"),
690 compilation_options: wgpu::PipelineCompilationOptions::default(),
691 cache: None,
692 });
693
694 let sim_pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
695 label: Some("particle_sim_pipeline"),
696 layout: Some(&compute_layout),
697 module: &sim_shader,
698 entry_point: Some("sim_main"),
699 compilation_options: wgpu::PipelineCompilationOptions::default(),
700 cache: None,
701 });
702
703 let sprite_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
705 label: Some("particle_sprite_shader"),
706 source: wgpu::ShaderSource::Wgsl(
707 include_str!(concat!(env!("OUT_DIR"), "/particle_sprite.wgsl")).into(),
708 ),
709 });
710
711 let draw_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
712 label: Some("particle_draw_layout"),
713 bind_group_layouts: &[&self.camera_bind_group_layout, &draw_bgl],
714 push_constant_ranges: &[],
715 });
716
717 let sample_count = self.sample_count;
718 let additive = wgpu::BlendState {
719 color: wgpu::BlendComponent {
720 src_factor: wgpu::BlendFactor::One,
721 dst_factor: wgpu::BlendFactor::One,
722 operation: wgpu::BlendOperation::Add,
723 },
724 alpha: wgpu::BlendComponent {
725 src_factor: wgpu::BlendFactor::One,
726 dst_factor: wgpu::BlendFactor::One,
727 operation: wgpu::BlendOperation::Add,
728 },
729 };
730 let premul = wgpu::BlendState {
731 color: wgpu::BlendComponent {
732 src_factor: wgpu::BlendFactor::One,
733 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
734 operation: wgpu::BlendOperation::Add,
735 },
736 alpha: wgpu::BlendComponent {
737 src_factor: wgpu::BlendFactor::One,
738 dst_factor: wgpu::BlendFactor::OneMinusSrcAlpha,
739 operation: wgpu::BlendOperation::Add,
740 },
741 };
742
743 let make_draw = |fmt: wgpu::TextureFormat, blend: wgpu::BlendState, label: &str| {
744 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
745 label: Some(label),
746 layout: Some(&draw_layout),
747 vertex: wgpu::VertexState {
748 module: &sprite_shader,
749 entry_point: Some("vs_main"),
750 buffers: &[],
751 compilation_options: wgpu::PipelineCompilationOptions::default(),
752 },
753 fragment: Some(wgpu::FragmentState {
754 module: &sprite_shader,
755 entry_point: Some("fs_main"),
756 targets: &[Some(wgpu::ColorTargetState {
757 format: fmt,
758 blend: Some(blend),
759 write_mask: wgpu::ColorWrites::ALL,
760 })],
761 compilation_options: wgpu::PipelineCompilationOptions::default(),
762 }),
763 primitive: wgpu::PrimitiveState {
764 topology: wgpu::PrimitiveTopology::TriangleList,
765 cull_mode: None,
766 ..Default::default()
767 },
768 depth_stencil: Some(wgpu::DepthStencilState {
769 format: wgpu::TextureFormat::Depth24PlusStencil8,
770 depth_write_enabled: false,
771 depth_compare: wgpu::CompareFunction::Less,
772 stencil: wgpu::StencilState::default(),
773 bias: wgpu::DepthBiasState::default(),
774 }),
775 multisample: wgpu::MultisampleState {
776 count: sample_count,
777 ..Default::default()
778 },
779 multiview: None,
780 cache: None,
781 })
782 };
783
784 let ldr = self.target_format;
785 let hdr = wgpu::TextureFormat::Rgba16Float;
786 let alpha = wgpu::BlendState::ALPHA_BLENDING;
787 self.particle_sprite_pipeline_alpha = Some(crate::resources::DualPipeline {
788 ldr: make_draw(ldr, alpha, "particle_sprite_alpha"),
789 hdr: make_draw(hdr, alpha, "particle_sprite_alpha"),
790 });
791 self.particle_sprite_pipeline_additive = Some(crate::resources::DualPipeline {
792 ldr: make_draw(ldr, additive, "particle_sprite_additive"),
793 hdr: make_draw(hdr, additive, "particle_sprite_additive"),
794 });
795 self.particle_sprite_pipeline_premultiplied = Some(crate::resources::DualPipeline {
796 ldr: make_draw(ldr, premul, "particle_sprite_premultiplied"),
797 hdr: make_draw(hdr, premul, "particle_sprite_premultiplied"),
798 });
799
800 let lit_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
804 label: Some("gpu_particle_lit_bgl"),
805 entries: &[
806 wgpu::BindGroupLayoutEntry {
807 binding: 0,
808 visibility: wgpu::ShaderStages::FRAGMENT,
809 ty: wgpu::BindingType::Texture {
810 sample_type: wgpu::TextureSampleType::Float { filterable: true },
811 view_dimension: wgpu::TextureViewDimension::D2,
812 multisampled: false,
813 },
814 count: None,
815 },
816 wgpu::BindGroupLayoutEntry {
817 binding: 1,
818 visibility: wgpu::ShaderStages::FRAGMENT,
819 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
820 count: None,
821 },
822 ],
823 });
824
825 let lit_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
826 label: Some("particle_sprite_lit_shader"),
827 source: wgpu::ShaderSource::Wgsl(
828 include_str!(concat!(env!("OUT_DIR"), "/particle_sprite_lit.wgsl")).into(),
829 ),
830 });
831
832 let lit_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
833 label: Some("particle_draw_lit_layout"),
834 bind_group_layouts: &[&self.camera_bind_group_layout, &draw_bgl, &lit_bgl],
835 push_constant_ranges: &[],
836 });
837
838 let make_lit_draw = |fmt: wgpu::TextureFormat, blend: wgpu::BlendState, label: &str| {
839 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
840 label: Some(label),
841 layout: Some(&lit_layout),
842 vertex: wgpu::VertexState {
843 module: &lit_shader,
844 entry_point: Some("vs_main"),
845 buffers: &[],
846 compilation_options: wgpu::PipelineCompilationOptions::default(),
847 },
848 fragment: Some(wgpu::FragmentState {
849 module: &lit_shader,
850 entry_point: Some("fs_main"),
851 targets: &[Some(wgpu::ColorTargetState {
852 format: fmt,
853 blend: Some(blend),
854 write_mask: wgpu::ColorWrites::ALL,
855 })],
856 compilation_options: wgpu::PipelineCompilationOptions::default(),
857 }),
858 primitive: wgpu::PrimitiveState {
859 topology: wgpu::PrimitiveTopology::TriangleList,
860 cull_mode: None,
861 ..Default::default()
862 },
863 depth_stencil: Some(wgpu::DepthStencilState {
864 format: wgpu::TextureFormat::Depth24PlusStencil8,
865 depth_write_enabled: false,
866 depth_compare: wgpu::CompareFunction::Less,
867 stencil: wgpu::StencilState::default(),
868 bias: wgpu::DepthBiasState::default(),
869 }),
870 multisample: wgpu::MultisampleState {
871 count: sample_count,
872 ..Default::default()
873 },
874 multiview: None,
875 cache: None,
876 })
877 };
878
879 self.particle_sprite_lit_pipeline_alpha = Some(crate::resources::DualPipeline {
880 ldr: make_lit_draw(ldr, alpha, "particle_sprite_lit_alpha"),
881 hdr: make_lit_draw(hdr, alpha, "particle_sprite_lit_alpha"),
882 });
883 self.particle_sprite_lit_pipeline_additive = Some(crate::resources::DualPipeline {
884 ldr: make_lit_draw(ldr, additive, "particle_sprite_lit_additive"),
885 hdr: make_lit_draw(hdr, additive, "particle_sprite_lit_additive"),
886 });
887 self.particle_sprite_lit_pipeline_premultiplied = Some(crate::resources::DualPipeline {
888 ldr: make_lit_draw(ldr, premul, "particle_sprite_lit_premultiplied"),
889 hdr: make_lit_draw(hdr, premul, "particle_sprite_lit_premultiplied"),
890 });
891
892 let lit_fallback_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
893 label: Some("gpu_particle_lit_fallback_bg"),
894 layout: &lit_bgl,
895 entries: &[
896 wgpu::BindGroupEntry {
897 binding: 0,
898 resource: wgpu::BindingResource::TextureView(&self.fallback_normal_map_view),
899 },
900 wgpu::BindGroupEntry {
901 binding: 1,
902 resource: wgpu::BindingResource::Sampler(&self.material_sampler),
903 },
904 ],
905 });
906
907 self.particle_sprite_lit_bgl = Some(lit_bgl);
908 self.particle_sprite_lit_fallback_bg = Some(lit_fallback_bg);
909
910 let mesh_draw_bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
915 label: Some("gpu_particle_mesh_draw_bgl"),
916 entries: &[
917 wgpu::BindGroupLayoutEntry {
918 binding: 0,
919 visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
920 ty: wgpu::BindingType::Buffer {
921 ty: wgpu::BufferBindingType::Uniform,
922 has_dynamic_offset: false,
923 min_binding_size: None,
924 },
925 count: None,
926 },
927 wgpu::BindGroupLayoutEntry {
928 binding: 1,
929 visibility: wgpu::ShaderStages::FRAGMENT,
930 ty: wgpu::BindingType::Texture {
931 sample_type: wgpu::TextureSampleType::Float { filterable: true },
932 view_dimension: wgpu::TextureViewDimension::D2,
933 multisampled: false,
934 },
935 count: None,
936 },
937 wgpu::BindGroupLayoutEntry {
938 binding: 2,
939 visibility: wgpu::ShaderStages::FRAGMENT,
940 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
941 count: None,
942 },
943 wgpu::BindGroupLayoutEntry {
944 binding: 3,
945 visibility: wgpu::ShaderStages::VERTEX,
946 ty: wgpu::BindingType::Buffer {
947 ty: wgpu::BufferBindingType::Storage { read_only: true },
948 has_dynamic_offset: false,
949 min_binding_size: None,
950 },
951 count: None,
952 },
953 ],
954 });
955
956 let mesh_shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
957 label: Some("particle_mesh_shader"),
958 source: wgpu::ShaderSource::Wgsl(
959 include_str!(concat!(env!("OUT_DIR"), "/particle_mesh.wgsl")).into(),
960 ),
961 });
962
963 let mesh_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
964 label: Some("particle_mesh_draw_layout"),
965 bind_group_layouts: &[&self.camera_bind_group_layout, &mesh_draw_bgl],
966 push_constant_ranges: &[],
967 });
968
969 let make_mesh_draw = |fmt: wgpu::TextureFormat, blend: wgpu::BlendState, label: &str| {
970 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
971 label: Some(label),
972 layout: Some(&mesh_layout),
973 vertex: wgpu::VertexState {
974 module: &mesh_shader,
975 entry_point: Some("vs_main"),
976 buffers: &[crate::resources::types::Vertex::buffer_layout()],
977 compilation_options: wgpu::PipelineCompilationOptions::default(),
978 },
979 fragment: Some(wgpu::FragmentState {
980 module: &mesh_shader,
981 entry_point: Some("fs_main"),
982 targets: &[Some(wgpu::ColorTargetState {
983 format: fmt,
984 blend: Some(blend),
985 write_mask: wgpu::ColorWrites::ALL,
986 })],
987 compilation_options: wgpu::PipelineCompilationOptions::default(),
988 }),
989 primitive: wgpu::PrimitiveState {
990 topology: wgpu::PrimitiveTopology::TriangleList,
991 cull_mode: Some(wgpu::Face::Back),
992 ..Default::default()
993 },
994 depth_stencil: Some(wgpu::DepthStencilState {
995 format: wgpu::TextureFormat::Depth24PlusStencil8,
996 depth_write_enabled: false,
997 depth_compare: wgpu::CompareFunction::Less,
998 stencil: wgpu::StencilState::default(),
999 bias: wgpu::DepthBiasState::default(),
1000 }),
1001 multisample: wgpu::MultisampleState {
1002 count: sample_count,
1003 ..Default::default()
1004 },
1005 multiview: None,
1006 cache: None,
1007 })
1008 };
1009
1010 self.particle_mesh_pipeline_alpha = Some(crate::resources::DualPipeline {
1011 ldr: make_mesh_draw(ldr, alpha, "particle_mesh_alpha"),
1012 hdr: make_mesh_draw(hdr, alpha, "particle_mesh_alpha"),
1013 });
1014 self.particle_mesh_pipeline_additive = Some(crate::resources::DualPipeline {
1015 ldr: make_mesh_draw(ldr, additive, "particle_mesh_additive"),
1016 hdr: make_mesh_draw(hdr, additive, "particle_mesh_additive"),
1017 });
1018 self.particle_mesh_pipeline_premultiplied = Some(crate::resources::DualPipeline {
1019 ldr: make_mesh_draw(ldr, premul, "particle_mesh_premultiplied"),
1020 hdr: make_mesh_draw(hdr, premul, "particle_mesh_premultiplied"),
1021 });
1022 self.particle_mesh_draw_bgl = Some(mesh_draw_bgl);
1023
1024 self.particle_params_bgl = Some(params_bgl);
1025 self.particle_sim_bgl = Some(sim_bgl);
1026 self.particle_draw_bgl = Some(draw_bgl);
1027 self.particle_emit_pipeline = Some(emit_pipeline);
1028 self.particle_sim_pipeline = Some(sim_pipeline);
1029 }
1030
1031 pub(crate) fn run_particle_jobs(
1034 &mut self,
1035 device: &wgpu::Device,
1036 queue: &wgpu::Queue,
1037 items: &[crate::renderer::GpuParticleSystemItem],
1038 ) -> Vec<ParticleFrameData> {
1039 if items.is_empty() {
1040 return Vec::new();
1041 }
1042 self.ensure_particle_pipelines(device);
1043
1044 let emit_pipeline = self
1045 .particle_emit_pipeline
1046 .as_ref()
1047 .expect("particle pipelines should exist after ensure")
1048 .clone();
1049 let sim_pipeline = self
1050 .particle_sim_pipeline
1051 .as_ref()
1052 .expect("particle pipelines should exist after ensure")
1053 .clone();
1054 let params_bgl = self
1055 .particle_params_bgl
1056 .as_ref()
1057 .expect("particle params BGL")
1058 .clone();
1059
1060 let mut frame_data: Vec<ParticleFrameData> = Vec::with_capacity(items.len());
1061 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
1062 label: Some("particle_compute_encoder"),
1063 });
1064
1065 for item in items {
1066 let idx = item.system_id.0;
1067 let (blend, route) = match self.particle_systems.get(idx).and_then(|s| s.as_ref()) {
1068 Some(s) if s.alive => match &s.render {
1069 ParticleRender::Sprite { blend, lit, .. } => {
1070 (*blend, ParticleDrawRoute::Sprite { lit: *lit })
1071 }
1072 ParticleRender::Mesh { blend, mesh_id, .. } => {
1073 (*blend, ParticleDrawRoute::Mesh { mesh_id: *mesh_id })
1074 }
1075 },
1076 _ => continue,
1077 };
1078
1079 let hidden = item.settings.hidden;
1080
1081 let (
1083 capacity,
1084 particle_buf_binding,
1085 emit_counter_binding,
1086 sim_bg,
1087 spawn_count,
1088 frame_counter,
1089 ) = {
1090 let system = self.particle_systems[idx].as_mut().unwrap();
1091 let dt = item.time_step.max(0.0);
1092 system.spawn_accumulator += item.emitter.rate * dt;
1093 let spawn_count = system.spawn_accumulator.floor() as u32;
1094 system.spawn_accumulator -= spawn_count as f32;
1095 system.frame_counter = system.frame_counter.wrapping_add(1);
1096 let frame_counter = system.frame_counter;
1097 (
1098 system.capacity,
1099 system.particle_buf.as_entire_binding(),
1100 system.emit_counter_buf.as_entire_binding(),
1101 system.sim_bg.clone(),
1103 spawn_count,
1104 frame_counter,
1105 )
1106 };
1107 let _ = (particle_buf_binding, emit_counter_binding); let workgroups = capacity.div_ceil(64);
1110
1111 if spawn_count > 0 {
1113 queue.write_buffer(
1114 &self.particle_systems[idx]
1115 .as_ref()
1116 .unwrap()
1117 .emit_counter_buf,
1118 0,
1119 bytemuck::bytes_of(&spawn_count),
1120 );
1121
1122 let emit_params =
1123 build_emit_params(&item.emitter, capacity, spawn_count, frame_counter);
1124 let emit_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1125 label: Some("particle_emit_params"),
1126 contents: bytemuck::bytes_of(&emit_params),
1127 usage: wgpu::BufferUsages::UNIFORM,
1128 });
1129 let emit_params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1130 label: Some("particle_emit_params_bg"),
1131 layout: ¶ms_bgl,
1132 entries: &[wgpu::BindGroupEntry {
1133 binding: 0,
1134 resource: emit_uniform.as_entire_binding(),
1135 }],
1136 });
1137
1138 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1139 label: Some("particle_emit_pass"),
1140 timestamp_writes: None,
1141 });
1142 pass.set_pipeline(&emit_pipeline);
1143 pass.set_bind_group(0, &emit_params_bg, &[]);
1144 pass.set_bind_group(1, &sim_bg, &[]);
1145 pass.dispatch_workgroups(workgroups, 1, 1);
1146 }
1147
1148 let sim_params = build_sim_params(item.time_step, capacity, &item.forces);
1150 let sim_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1151 label: Some("particle_sim_params"),
1152 contents: bytemuck::bytes_of(&sim_params),
1153 usage: wgpu::BufferUsages::UNIFORM,
1154 });
1155 let sim_params_bg = device.create_bind_group(&wgpu::BindGroupDescriptor {
1156 label: Some("particle_sim_params_bg"),
1157 layout: ¶ms_bgl,
1158 entries: &[wgpu::BindGroupEntry {
1159 binding: 0,
1160 resource: sim_uniform.as_entire_binding(),
1161 }],
1162 });
1163
1164 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
1165 label: Some("particle_sim_pass"),
1166 timestamp_writes: None,
1167 });
1168 pass.set_pipeline(&sim_pipeline);
1169 pass.set_bind_group(0, &sim_params_bg, &[]);
1170 pass.set_bind_group(1, &sim_bg, &[]);
1171 pass.dispatch_workgroups(workgroups, 1, 1);
1172 drop(pass);
1173
1174 frame_data.push(ParticleFrameData {
1175 system_idx: idx,
1176 blend,
1177 hidden,
1178 route,
1179 });
1180 }
1181
1182 queue.submit(std::iter::once(encoder.finish()));
1183 frame_data
1184 }
1185}
1186
1187fn build_emit_params(
1188 e: &crate::renderer::EmitterConfig,
1189 capacity: u32,
1190 spawn_count: u32,
1191 frame_counter: u32,
1192) -> EmitParamsGpu {
1193 use crate::renderer::{SpawnShape, VelocityDist};
1194
1195 let mut out = EmitParamsGpu {
1196 spawn_min: [0.0; 3],
1197 spawn_kind: 0,
1198 spawn_max: [0.0; 3],
1199 spawn_radius: 0.0,
1200 vel_min: [0.0; 3],
1201 vel_kind: 0,
1202 vel_max: [0.0; 3],
1203 cone_half_angle: 0.0,
1204 vel_axis: [0.0; 3],
1205 cone_min_speed: 0.0,
1206 colour: e.colour,
1207 spawn_count,
1208 capacity,
1209 rng_seed: frame_counter.wrapping_mul(0x9E3779B1),
1210 size: e.size,
1211 lifetime_min: e.lifetime.0,
1212 lifetime_max: e.lifetime.1,
1213 cone_max_speed: 0.0,
1214 _pad: 0.0,
1215 };
1216
1217 match e.spawn_shape {
1218 SpawnShape::Point(p) => {
1219 out.spawn_kind = 0;
1220 out.spawn_min = p;
1221 }
1222 SpawnShape::Box { min, max } => {
1223 out.spawn_kind = 1;
1224 out.spawn_min = min;
1225 out.spawn_max = max;
1226 }
1227 SpawnShape::Sphere { center, radius } => {
1228 out.spawn_kind = 2;
1229 out.spawn_min = center;
1230 out.spawn_radius = radius;
1231 }
1232 }
1233
1234 match e.initial_velocity {
1235 VelocityDist::Fixed(v) => {
1236 out.vel_kind = 0;
1237 out.vel_min = v;
1238 }
1239 VelocityDist::UniformBox { min, max } => {
1240 out.vel_kind = 1;
1241 out.vel_min = min;
1242 out.vel_max = max;
1243 }
1244 VelocityDist::UniformCone {
1245 axis,
1246 half_angle,
1247 min_speed,
1248 max_speed,
1249 } => {
1250 out.vel_kind = 2;
1251 out.vel_axis = axis;
1252 out.cone_half_angle = half_angle;
1253 out.cone_min_speed = min_speed;
1254 out.cone_max_speed = max_speed;
1255 }
1256 }
1257
1258 out
1259}
1260
1261fn build_sim_params(
1262 dt: f32,
1263 capacity: u32,
1264 forces: &[crate::renderer::ForceField],
1265) -> SimParamsGpu {
1266 use crate::renderer::ForceField;
1267
1268 let mut gpu_forces = [GpuForce {
1269 kind: 0,
1270 _pad: [0; 3],
1271 v0: [0.0; 4],
1272 v1: [0.0; 4],
1273 }; MAX_FORCES];
1274
1275 let n = forces.len().min(MAX_FORCES);
1276 for (i, f) in forces.iter().take(n).enumerate() {
1277 match *f {
1278 ForceField::Gravity(a) => {
1279 gpu_forces[i].kind = 0;
1280 gpu_forces[i].v0 = [a[0], a[1], a[2], 0.0];
1281 }
1282 ForceField::Drag(k) => {
1283 gpu_forces[i].kind = 1;
1284 gpu_forces[i].v0 = [k, 0.0, 0.0, 0.0];
1285 }
1286 ForceField::PointAttractor {
1287 position,
1288 strength,
1289 falloff,
1290 } => {
1291 gpu_forces[i].kind = 2;
1292 gpu_forces[i].v0 = [position[0], position[1], position[2], strength];
1293 gpu_forces[i].v1 = [falloff, 0.0, 0.0, 0.0];
1294 }
1295 }
1296 }
1297
1298 SimParamsGpu {
1299 dt,
1300 capacity,
1301 force_count: n as u32,
1302 _pad: 0,
1303 forces: gpu_forces,
1304 }
1305}