1use macroquad::prelude::*;
2use macroquad::window::miniquad::*;
3use miniquad::graphics::RenderPass;
4
5#[cfg(feature = "nanoserde")]
6use nanoserde::{DeJson, SerJson};
7
8#[derive(Debug, Clone, Copy, PartialEq)]
9#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
10pub enum Interpolation {
11 Linear,
12 Bezier,
13}
14
15#[derive(Debug, Clone)]
16#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
17pub struct Curve {
18 pub points: Vec<(f32, f32)>,
20 pub interpolation: Interpolation,
23 pub resolution: usize,
25}
26
27impl Curve {
28 fn batch(&self) -> BatchedCurve {
29 if self.interpolation == Interpolation::Bezier {
30 unimplemented!()
31 }
32
33 let step_f32 = 1.0 / self.resolution as f32;
34 let mut x = 0.0;
35 let mut points = Vec::with_capacity(self.resolution);
36
37 for curve_part in self.points.windows(2) {
38 let start = curve_part[0];
39 let end = curve_part[1];
40
41 while x <= end.0 {
42 let t = (x - start.0) / (end.0 - start.0);
43 let point = start.1 + (end.1 - start.1) * t;
44 points.push(point);
45 x += step_f32;
46 }
47 }
48
49 BatchedCurve { points }
50 }
51}
52
53#[derive(Debug, Clone)]
54pub struct BatchedCurve {
55 pub points: Vec<f32>,
56}
57
58impl BatchedCurve {
59 fn get(&self, t: f32) -> f32 {
60 let t_scaled = t * self.points.len() as f32;
61 let previous_ix = (t_scaled as usize).min(self.points.len() - 1);
62 let next_ix = (previous_ix + 1).min(self.points.len() - 1);
63 let previous = self.points[previous_ix];
64 let next = self.points[next_ix];
65
66 previous + (next - previous) * (t_scaled - previous_ix as f32)
67 }
68}
69impl Default for Curve {
70 fn default() -> Curve {
71 Curve {
72 points: vec![],
73 interpolation: Interpolation::Linear,
74 resolution: 20,
75 }
76 }
77}
78
79#[derive(Copy, Clone, PartialEq, Debug)]
80#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
81pub enum EmissionShape {
82 Point,
83 Rect { width: f32, height: f32 },
84 Sphere { radius: f32 },
85}
86
87#[derive(Copy, Clone, PartialEq, Debug)]
88#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
89pub struct ColorSerializable {
90 pub r: f32,
91 pub g: f32,
92 pub b: f32,
93 pub a: f32,
94}
95
96impl From<&Color> for ColorSerializable {
97 fn from(color: &Color) -> ColorSerializable {
98 ColorSerializable {
99 r: color.r,
100 g: color.g,
101 b: color.b,
102 a: color.a,
103 }
104 }
105}
106
107impl From<&ColorSerializable> for Color {
108 fn from(color: &ColorSerializable) -> Color {
109 Color {
110 r: color.r,
111 g: color.g,
112 b: color.b,
113 a: color.a,
114 }
115 }
116}
117
118#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
119pub struct Vec2Serializable {
120 x: f32,
121 y: f32,
122}
123
124impl From<&Vec2> for Vec2Serializable {
125 fn from(vec: &Vec2) -> Vec2Serializable {
126 Vec2Serializable { x: vec.x, y: vec.y }
127 }
128}
129
130impl From<&Vec2Serializable> for Vec2 {
131 fn from(vec: &Vec2Serializable) -> Vec2 {
132 vec2(vec.x, vec.y)
133 }
134}
135
136#[derive(Copy, Clone, PartialEq, Debug)]
137#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
138pub struct ColorCurve {
139 #[cfg_attr(feature = "nanoserde", nserde(proxy = "ColorSerializable"))]
140 pub start: Color,
141 #[cfg_attr(feature = "nanoserde", nserde(proxy = "ColorSerializable"))]
142 pub mid: Color,
143 #[cfg_attr(feature = "nanoserde", nserde(proxy = "ColorSerializable"))]
144 pub end: Color,
145}
146
147impl Default for ColorCurve {
148 fn default() -> ColorCurve {
149 ColorCurve {
150 start: WHITE,
151 mid: WHITE,
152 end: WHITE,
153 }
154 }
155}
156
157#[derive(Debug, Clone)]
158#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
159pub struct EmitterConfig {
160 pub local_coords: bool,
163 pub emission_shape: EmissionShape,
165 pub one_shot: bool,
167 pub lifetime: f32,
169 pub lifetime_randomness: f32,
172 pub explosiveness: f32,
176 pub amount: u32,
178 pub shape: ParticleShape,
180 pub emitting: bool,
183 #[cfg_attr(feature = "nanoserde", nserde(proxy = "Vec2Serializable"))]
185 pub initial_direction: Vec2,
186 pub initial_direction_spread: f32,
188 pub initial_velocity: f32,
191 pub initial_velocity_randomness: f32,
194 pub linear_accel: f32,
196
197 #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
199 pub initial_rotation: f32,
200 #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
203 pub initial_rotation_randomness: f32,
204 #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
206 pub initial_angular_velocity: f32,
207 #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
210 pub initial_angular_velocity_randomness: f32,
211 #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
213 pub angular_accel: f32,
214 #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
217 pub angular_damping: f32,
218 pub size: f32,
220 pub size_randomness: f32,
222 pub size_curve: Option<Curve>,
224
225 pub blend_mode: BlendMode,
227
228 pub colors_curve: ColorCurve,
230
231 #[cfg_attr(feature = "nanoserde", nserde(proxy = "Vec2Serializable"))]
233 pub gravity: Vec2,
234
235 #[cfg_attr(feature = "nanoserde", nserde(skip))]
237 pub texture: Option<Texture2D>,
238
239 pub atlas: Option<AtlasConfig>,
242
243 pub material: Option<ParticleMaterial>,
245
246 pub post_processing: Option<PostProcessing>,
252}
253
254impl EmissionShape {
255 fn gen_random_point(&self) -> Vec2 {
256 match self {
257 EmissionShape::Point => vec2(0., 0.),
258 EmissionShape::Rect { width, height } => vec2(
259 rand::gen_range(-width / 2., width / 2.0),
260 rand::gen_range(-height / 2., height / 2.0),
261 ),
262 EmissionShape::Sphere { radius } => {
263 let ro = rand::gen_range(0., radius * radius).sqrt();
264 let phi = rand::gen_range(0., std::f32::consts::PI * 2.);
265
266 macroquad::math::polar_to_cartesian(ro, phi)
267 }
268 }
269 }
270}
271
272#[derive(Copy, Clone, PartialEq, Debug)]
273#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
274pub struct PostProcessing;
275
276#[derive(Clone, PartialEq, Debug)]
277#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
278pub enum ParticleShape {
279 Rectangle {
280 aspect_ratio: f32,
281 },
282 Circle {
283 subdivisions: u32,
284 },
285 CustomMesh {
286 vertices: Vec<f32>,
287 indices: Vec<u16>,
288 },
289}
290
291impl ParticleShape {
292 fn build_bindings(
293 &self,
294 ctx: &mut dyn miniquad::RenderingBackend,
295 positions_vertex_buffer: BufferId,
296 texture: Option<Texture2D>,
297 ) -> Bindings {
298 let (geometry_vertex_buffer, index_buffer) = match self {
299 ParticleShape::Rectangle { aspect_ratio } => {
300 #[rustfmt::skip]
301 let vertices: &[f32] = &[
302 -1.0 * aspect_ratio, -1.0, 0.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0,
304 1.0 * aspect_ratio, -1.0, 0.0, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0,
305 1.0 * aspect_ratio, 1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0,
306 -1.0 * aspect_ratio, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 1.0, 1.0,
307 ];
308
309 let vertex_buffer = ctx.new_buffer(
310 BufferType::VertexBuffer,
311 BufferUsage::Immutable,
312 BufferSource::slice(&vertices),
313 );
314
315 #[rustfmt::skip]
316 let indices: &[u16] = &[
317 0, 1, 2, 0, 2, 3
318 ];
319 let index_buffer = ctx.new_buffer(
320 BufferType::IndexBuffer,
321 BufferUsage::Immutable,
322 BufferSource::slice(&indices),
323 );
324
325 (vertex_buffer, index_buffer)
326 }
327 ParticleShape::Circle { subdivisions } => {
328 let mut vertices = Vec::<f32>::new();
329 let mut indices = Vec::<u16>::new();
330
331 let rot = 0.0;
332 vertices.extend_from_slice(&[0., 0., 0., 0., 0., 1.0, 1.0, 1.0, 1.0]);
333 for i in 0..subdivisions + 1 {
334 let rx =
335 (i as f32 / *subdivisions as f32 * std::f32::consts::PI * 2. + rot).cos();
336 let ry =
337 (i as f32 / *subdivisions as f32 * std::f32::consts::PI * 2. + rot).sin();
338 vertices.extend_from_slice(&[rx, ry, 0., rx, ry, 1., 1., 1., 1.]);
339
340 if i != *subdivisions {
341 indices.extend_from_slice(&[0, i as u16 + 1, i as u16 + 2]);
342 }
343 }
344
345 let vertex_buffer = ctx.new_buffer(
346 BufferType::VertexBuffer,
347 BufferUsage::Immutable,
348 BufferSource::slice(&vertices),
349 );
350 let index_buffer = ctx.new_buffer(
351 BufferType::IndexBuffer,
352 BufferUsage::Immutable,
353 BufferSource::slice(&indices),
354 );
355 (vertex_buffer, index_buffer)
356 }
357 ParticleShape::CustomMesh { vertices, indices } => {
358 let vertex_buffer = ctx.new_buffer(
359 BufferType::VertexBuffer,
360 BufferUsage::Immutable,
361 BufferSource::slice(&vertices),
362 );
363 let index_buffer = ctx.new_buffer(
364 BufferType::IndexBuffer,
365 BufferUsage::Immutable,
366 BufferSource::slice(&indices),
367 );
368 (vertex_buffer, index_buffer)
369 }
370 };
371
372 Bindings {
373 vertex_buffers: vec![geometry_vertex_buffer, positions_vertex_buffer],
374 index_buffer,
375 images: vec![texture.map_or_else(
376 || ctx.new_texture_from_rgba8(1, 1, &[255, 255, 255, 255]),
377 |texture| texture.raw_miniquad_id(),
378 )],
379 }
380 }
381}
382
383#[derive(Debug, Clone)]
384#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
385pub struct ParticleMaterial {
386 vertex: String,
387 fragment: String,
388}
389
390impl ParticleMaterial {
391 pub fn new(vertex: &str, fragment: &str) -> ParticleMaterial {
392 ParticleMaterial {
393 vertex: vertex.to_owned(),
394 fragment: fragment.to_owned(),
395 }
396 }
397}
398
399#[derive(Debug, Clone, Copy, PartialEq)]
400#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
401pub enum BlendMode {
402 Alpha,
404 Additive,
406}
407
408impl BlendMode {
409 fn blend_state(&self) -> BlendState {
410 match self {
411 BlendMode::Alpha => BlendState::new(
412 Equation::Add,
413 BlendFactor::Value(BlendValue::SourceAlpha),
414 BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
415 ),
416 BlendMode::Additive => BlendState::new(
417 Equation::Add,
418 BlendFactor::Value(BlendValue::SourceAlpha),
419 BlendFactor::One,
420 ),
421 }
422 }
423}
424
425#[derive(Debug, Clone)]
426#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
427pub struct AtlasConfig {
428 n: u16,
429 m: u16,
430 start_index: u16,
431 end_index: u16,
432}
433
434impl AtlasConfig {
435 pub fn new<T: std::ops::RangeBounds<u16>>(n: u16, m: u16, range: T) -> AtlasConfig {
436 let start_index = match range.start_bound() {
437 std::ops::Bound::Unbounded => 0,
438 std::ops::Bound::Included(i) => *i,
439 std::ops::Bound::Excluded(i) => i + 1,
440 };
441 let end_index = match range.end_bound() {
442 std::ops::Bound::Unbounded => n * m,
443 std::ops::Bound::Included(i) => i - 1,
444 std::ops::Bound::Excluded(i) => *i,
445 };
446
447 AtlasConfig {
448 n,
449 m,
450 start_index,
451 end_index,
452 }
453 }
454}
455
456impl Default for EmitterConfig {
457 fn default() -> EmitterConfig {
458 EmitterConfig {
459 local_coords: false,
460 emission_shape: EmissionShape::Point,
461 one_shot: false,
462 lifetime: 1.0,
463 lifetime_randomness: 0.0,
464 amount: 8,
465 shape: ParticleShape::Rectangle { aspect_ratio: 1.0 },
466 explosiveness: 0.0,
467 emitting: true,
468 initial_direction: vec2(0., -1.),
469 initial_direction_spread: 0.,
470 initial_velocity: 50.0,
471 initial_velocity_randomness: 0.0,
472 linear_accel: 0.0,
473 initial_rotation: 0.0,
474 initial_rotation_randomness: 0.0,
475 initial_angular_velocity: 0.0,
476 initial_angular_velocity_randomness: 0.0,
477 angular_accel: 0.0,
478 angular_damping: 0.0,
479 size: 10.0,
480 size_randomness: 0.0,
481 size_curve: None,
482 blend_mode: BlendMode::Alpha,
483 colors_curve: ColorCurve::default(),
484 gravity: vec2(0.0, 0.0),
485 texture: None,
486 atlas: None,
487 material: None,
488 post_processing: None,
489 }
490 }
491}
492
493#[repr(C)]
494struct GpuParticle {
495 pos: Vec4,
496 uv: Vec4,
497 data: Vec4,
498 color: Vec4,
499}
500
501struct CpuParticle {
502 velocity: Vec2,
503 angular_velocity: f32,
504 lived: f32,
505 lifetime: f32,
506 frame: u16,
507 initial_size: f32,
508}
509
510pub struct Emitter {
511 pipeline: Pipeline,
512 bindings: Bindings,
513 post_processing_pass: RenderPass,
514 post_processing_pipeline: Pipeline,
515 post_processing_bindings: Bindings,
516
517 gpu_particles: Vec<GpuParticle>,
518 cpu_counterpart: Vec<CpuParticle>,
519
520 last_emit_time: f32,
521 time_passed: f32,
522 particles_current_cycle: u32,
523
524 particles_spawned: u64,
525 position: Vec2,
526
527 batched_size_curve: Option<BatchedCurve>,
528
529 blend_mode: BlendMode,
530 mesh_dirty: bool,
531
532 pub config: EmitterConfig,
533}
534
535impl Emitter {
536 const MAX_PARTICLES: usize = 10000;
537
538 pub fn new(config: EmitterConfig) -> Emitter {
539 let InternalGlContext {
540 quad_context: ctx, ..
541 } = unsafe { get_internal_gl() };
542
543 let positions_vertex_buffer = ctx.new_buffer(
545 BufferType::VertexBuffer,
546 BufferUsage::Stream,
547 BufferSource::empty::<GpuParticle>(Self::MAX_PARTICLES),
548 );
549
550 let bindings =
551 config
552 .shape
553 .build_bindings(ctx, positions_vertex_buffer, config.texture.clone());
554
555 let (vertex, fragment) = config.material.as_ref().map_or_else(
556 || (shader::VERTEX, shader::FRAGMENT),
557 |material| (&material.vertex, &material.fragment),
558 );
559
560 let shader = {
561 use macroquad::material::shaders::{preprocess_shader, PreprocessorConfig};
562
563 let config = PreprocessorConfig {
564 includes: vec![(
565 "particles.glsl".to_string(),
566 include_str!("particles.glsl").to_owned(),
567 )],
568 ..Default::default()
569 };
570
571 let vertex = preprocess_shader(&vertex, &config);
572 let fragment = preprocess_shader(&fragment, &config);
573
574 ctx.new_shader(
575 ShaderSource::Glsl {
576 vertex: &vertex,
577 fragment: &fragment,
578 },
579 shader::meta(),
580 )
581 .unwrap()
582 };
583
584 let blend_mode = config.blend_mode.blend_state();
585 let pipeline = ctx.new_pipeline(
586 &[
587 BufferLayout::default(),
588 BufferLayout {
589 step_func: VertexStep::PerInstance,
590 ..Default::default()
591 },
592 ],
593 &[
594 VertexAttribute::with_buffer("in_attr_pos", VertexFormat::Float3, 0),
595 VertexAttribute::with_buffer("in_attr_uv", VertexFormat::Float2, 0),
596 VertexAttribute::with_buffer("in_attr_color", VertexFormat::Float4, 0),
597 VertexAttribute::with_buffer("in_attr_inst_pos", VertexFormat::Float4, 1),
598 VertexAttribute::with_buffer("in_attr_inst_uv", VertexFormat::Float4, 1),
599 VertexAttribute::with_buffer("in_attr_inst_data", VertexFormat::Float4, 1),
600 VertexAttribute::with_buffer("in_attr_inst_color", VertexFormat::Float4, 1),
601 ],
602 shader,
603 PipelineParams {
604 color_blend: Some(blend_mode),
605 alpha_blend: Some(BlendState::new(
606 Equation::Add,
607 BlendFactor::Zero,
608 BlendFactor::One,
609 )),
610 ..Default::default()
611 },
612 );
613
614 let post_processing_shader = ctx
615 .new_shader(
616 ShaderSource::Glsl {
617 vertex: &post_processing_shader::VERTEX,
618 fragment: &post_processing_shader::FRAGMENT,
619 },
620 post_processing_shader::meta(),
621 )
622 .unwrap();
623
624 let post_processing_pipeline = ctx.new_pipeline(
625 &[BufferLayout::default(), BufferLayout::default()],
626 &[
627 VertexAttribute::with_buffer("pos", VertexFormat::Float2, 0),
628 VertexAttribute::with_buffer("uv", VertexFormat::Float2, 0),
629 ],
630 post_processing_shader,
631 PipelineParams {
632 color_blend: Some(BlendState::new(
633 Equation::Add,
634 BlendFactor::Value(BlendValue::SourceAlpha),
635 BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
636 )),
637 ..Default::default()
638 },
639 );
640 let post_processing_pass = {
641 let color_img = ctx.new_render_texture(TextureParams {
642 width: 320,
643 height: 200,
644 format: TextureFormat::RGBA8,
645 ..Default::default()
646 });
647 ctx.texture_set_filter(color_img, FilterMode::Nearest, MipmapFilterMode::None);
648
649 ctx.new_render_pass(color_img, None)
650 };
651
652 let post_processing_bindings = {
653 #[rustfmt::skip]
654 let vertices: &[f32] = &[
655 -1.0, -1.0, 0.0, 0.0,
657 1.0, -1.0, 1.0, 0.0,
658 1.0, 1.0, 1.0, 1.0,
659 -1.0, 1.0, 0.0, 1.0,
660 ];
661
662 let vertex_buffer = ctx.new_buffer(
663 BufferType::VertexBuffer,
664 BufferUsage::Immutable,
665 BufferSource::slice(&vertices),
666 );
667
668 #[rustfmt::skip]
669 let indices: &[u16] = &[
670 0, 1, 2, 0, 2, 3
671 ];
672 let index_buffer = ctx.new_buffer(
673 BufferType::IndexBuffer,
674 BufferUsage::Immutable,
675 BufferSource::slice(&indices),
676 );
677 Bindings {
678 vertex_buffers: vec![vertex_buffer],
679 index_buffer,
680 images: vec![ctx.render_pass_texture(post_processing_pass)],
681 }
682 };
683
684 Emitter {
685 blend_mode: config.blend_mode.clone(),
686 batched_size_curve: config.size_curve.as_ref().map(|curve| curve.batch()),
687 post_processing_pass,
688 post_processing_pipeline,
689 post_processing_bindings,
690 config,
691 pipeline,
692 bindings,
693 position: vec2(0.0, 0.0),
694 gpu_particles: Vec::with_capacity(Self::MAX_PARTICLES),
695 cpu_counterpart: Vec::with_capacity(Self::MAX_PARTICLES),
696 particles_spawned: 0,
697 last_emit_time: 0.0,
698 time_passed: 0.0,
699 particles_current_cycle: 0,
700 mesh_dirty: false,
701 }
702 }
703
704 fn reset(&mut self) {
705 self.gpu_particles.clear();
706 self.cpu_counterpart.clear();
707 self.last_emit_time = 0.0;
708 self.time_passed = 0.0;
709 self.particles_spawned = 0;
710 self.particles_current_cycle = 0;
711 }
712 pub fn rebuild_size_curve(&mut self) {
713 self.batched_size_curve = self.config.size_curve.as_ref().map(|curve| curve.batch());
714 }
715
716 pub fn update_particle_mesh(&mut self) {
717 self.mesh_dirty = true;
718 }
719
720 fn emit_particle(&mut self, offset: Vec2) {
721 let offset = offset + self.config.emission_shape.gen_random_point();
722
723 fn random_initial_vector(dir: Vec2, spread: f32, velocity: f32) -> Vec2 {
724 let angle = rand::gen_range(-spread / 2.0, spread / 2.0);
725
726 let quat = glam::Quat::from_rotation_z(angle);
727 let dir = quat * vec3(dir.x, dir.y, 0.0);
728 let res = dir * velocity;
729
730 vec2(res.x, res.y)
731 }
732
733 let r =
734 self.config.size - self.config.size * rand::gen_range(0.0, self.config.size_randomness);
735
736 let rotation = self.config.initial_rotation
737 - self.config.initial_rotation
738 * rand::gen_range(0.0, self.config.initial_rotation_randomness);
739
740 let particle = if self.config.local_coords {
741 GpuParticle {
742 pos: vec4(offset.x, offset.y, rotation, r),
743 uv: vec4(1.0, 1.0, 0.0, 0.0),
744 data: vec4(self.particles_spawned as f32, 0.0, 0.0, 0.0),
745 color: self.config.colors_curve.start.to_vec(),
746 }
747 } else {
748 GpuParticle {
749 pos: vec4(
750 self.position.x + offset.x,
751 self.position.y + offset.y,
752 rotation,
753 r,
754 ),
755 uv: vec4(1.0, 1.0, 0.0, 0.0),
756 data: vec4(self.particles_spawned as f32, 0.0, 0.0, 0.0),
757 color: self.config.colors_curve.start.to_vec(),
758 }
759 };
760
761 self.particles_spawned += 1;
762 self.particles_current_cycle += 1;
763 self.gpu_particles.push(particle);
764 self.cpu_counterpart.push(CpuParticle {
765 velocity: random_initial_vector(
766 vec2(
767 self.config.initial_direction.x,
768 self.config.initial_direction.y,
769 ),
770 self.config.initial_direction_spread,
771 self.config.initial_velocity
772 - self.config.initial_velocity
773 * rand::gen_range(0.0, self.config.initial_velocity_randomness),
774 ),
775 angular_velocity: self.config.initial_angular_velocity
776 - self.config.initial_angular_velocity
777 * rand::gen_range(0.0, self.config.initial_angular_velocity_randomness),
778 lived: 0.0,
779 lifetime: self.config.lifetime
780 - self.config.lifetime * rand::gen_range(0.0, self.config.lifetime_randomness),
781 frame: 0,
782 initial_size: r,
783 });
784 }
785
786 fn update(&mut self, ctx: &mut dyn miniquad::RenderingBackend, dt: f32) {
787 if self.mesh_dirty {
788 self.bindings = self.config.shape.build_bindings(
789 ctx,
790 self.bindings.vertex_buffers[1],
791 self.config.texture.clone(),
792 );
793 self.mesh_dirty = false;
794 }
795 if self.config.emitting {
796 self.time_passed += dt;
797
798 let gap = (self.config.lifetime / self.config.amount as f32)
799 * (1.0 - self.config.explosiveness);
800
801 let spawn_amount = if gap < 0.001 {
802 self.config.amount as usize
804 } else {
805 ((self.time_passed - self.last_emit_time) / gap) as usize
807 };
808
809 for _ in 0..spawn_amount {
810 self.last_emit_time = self.time_passed;
811
812 if self.particles_spawned < self.config.amount as u64 {
813 self.emit_particle(vec2(0.0, 0.0));
814 }
815
816 if self.gpu_particles.len() >= self.config.amount as usize {
817 break;
818 }
819 }
820 }
821
822 if self.config.one_shot && self.particles_current_cycle >= self.config.amount {
823 self.time_passed = 0.0;
824 self.last_emit_time = 0.0;
825 self.particles_current_cycle = 0;
826 self.config.emitting = false;
827 }
828
829 for (gpu, cpu) in self.gpu_particles.iter_mut().zip(&mut self.cpu_counterpart) {
830 cpu.velocity += cpu.velocity * self.config.linear_accel * dt;
833 cpu.angular_velocity += cpu.angular_velocity * self.config.angular_accel * dt;
834 cpu.angular_velocity *= 1.0 - self.config.angular_damping;
835
836 gpu.color = {
837 let t = cpu.lived / cpu.lifetime;
838 if t < 0.5 {
839 let t = t * 2.;
840 self.config.colors_curve.start.to_vec() * (1.0 - t)
841 + self.config.colors_curve.mid.to_vec() * t
842 } else {
843 let t = (t - 0.5) * 2.;
844 self.config.colors_curve.mid.to_vec() * (1.0 - t)
845 + self.config.colors_curve.end.to_vec() * t
846 }
847 };
848 gpu.pos += vec4(cpu.velocity.x, cpu.velocity.y, cpu.angular_velocity, 0.0) * dt;
849
850 gpu.pos.w = cpu.initial_size
851 * self
852 .batched_size_curve
853 .as_ref()
854 .map_or(1.0, |curve| curve.get(cpu.lived / cpu.lifetime));
855
856 if cpu.lifetime != 0.0 {
857 gpu.data.y = cpu.lived / cpu.lifetime;
858 }
859
860 cpu.lived += dt;
862 cpu.velocity += self.config.gravity * dt;
863
864 if let Some(atlas) = &self.config.atlas {
865 if cpu.lifetime != 0.0 {
866 cpu.frame = (cpu.lived / cpu.lifetime
867 * (atlas.end_index - atlas.start_index) as f32)
868 as u16
869 + atlas.start_index;
870 }
871
872 let x = cpu.frame % atlas.n;
873 let y = cpu.frame / atlas.n;
874
875 gpu.uv = vec4(
876 x as f32 / atlas.n as f32,
877 y as f32 / atlas.m as f32,
878 1.0 / atlas.n as f32,
879 1.0 / atlas.m as f32,
880 );
881 } else {
882 gpu.uv = vec4(0.0, 0.0, 1.0, 1.0);
883 }
884 }
885
886 for i in (0..self.gpu_particles.len()).rev() {
887 if self.cpu_counterpart[i].lived >= self.cpu_counterpart[i].lifetime
890 || self.cpu_counterpart[i].lived > self.config.lifetime
891 {
892 if self.cpu_counterpart[i].lived != self.cpu_counterpart[i].lifetime {
893 self.particles_spawned -= 1;
894 }
895 self.gpu_particles.remove(i);
896 self.cpu_counterpart.remove(i);
897 }
898 }
899
900 ctx.buffer_update(
901 self.bindings.vertex_buffers[1],
902 BufferSource::slice(&self.gpu_particles[..]),
903 );
904 }
905
906 pub fn emit(&mut self, pos: Vec2, n: usize) {
908 for _ in 0..n {
909 self.emit_particle(pos);
910 self.particles_spawned += 1;
911 }
912 }
913
914 fn perform_render_pass(&mut self, quad_gl: &QuadGl, ctx: &mut dyn miniquad::RenderingBackend) {
915 ctx.apply_bindings(&self.bindings);
916 ctx.apply_uniforms(UniformsSource::table(&shader::Uniforms {
917 mvp: quad_gl.get_projection_matrix(),
918 emitter_position: vec3(self.position.x, self.position.y, 0.0),
919 local_coords: if self.config.local_coords { 1.0 } else { 0.0 },
920 }));
921
922 let index_buffer_size = ctx.buffer_size(self.bindings.index_buffer);
923 ctx.draw(
924 0,
925 (index_buffer_size / 2) as i32,
926 self.gpu_particles.len() as i32,
927 );
928 }
929
930 pub fn setup_render_pass(
931 &mut self,
932 quad_gl: &QuadGl,
933 ctx: &mut dyn miniquad::RenderingBackend,
934 ) {
935 if self.config.blend_mode != self.blend_mode {
936 self.blend_mode = self.config.blend_mode.clone();
940 }
941
942 if self.config.post_processing.is_none() {
943 let pass = quad_gl.get_active_render_pass();
944 if let Some(pass) = pass {
945 ctx.begin_pass(Some(pass), PassAction::Nothing);
946 } else {
947 ctx.begin_default_pass(PassAction::Nothing);
948 }
949 } else {
950 ctx.begin_pass(
951 Some(self.post_processing_pass),
952 PassAction::clear_color(0.0, 0.0, 0.0, 0.0),
953 );
954 };
955
956 ctx.apply_pipeline(&self.pipeline);
957 }
958
959 pub fn end_render_pass(&mut self, quad_gl: &QuadGl, ctx: &mut dyn miniquad::RenderingBackend) {
960 ctx.end_render_pass();
961
962 if self.config.post_processing.is_some() {
963 let pass = quad_gl.get_active_render_pass();
964 if let Some(pass) = pass {
965 ctx.begin_pass(Some(pass), PassAction::Nothing);
966 } else {
967 ctx.begin_default_pass(PassAction::Nothing);
968 }
969
970 ctx.apply_pipeline(&self.post_processing_pipeline);
971 let (x, y, w, h) = quad_gl.get_viewport();
972 ctx.apply_viewport(x, y, w, h);
973
974 ctx.apply_bindings(&self.post_processing_bindings);
975
976 ctx.draw(0, 6, 1);
977
978 ctx.end_render_pass();
979 }
980 }
981
982 pub fn draw(&mut self, pos: Vec2) {
983 let mut gl = unsafe { get_internal_gl() };
984
985 gl.flush();
986
987 let InternalGlContext {
988 quad_context: ctx,
989 quad_gl,
990 } = gl;
991
992 self.position = pos;
993
994 self.update(ctx, get_frame_time());
995
996 self.setup_render_pass(quad_gl, ctx);
997 self.perform_render_pass(quad_gl, ctx);
998 self.end_render_pass(quad_gl, ctx);
999 }
1000}
1001
1002pub struct EmittersCache {
1006 emitter: Emitter,
1007 emitters_cache: Vec<Emitter>,
1008 active_emitters: Vec<Option<(Emitter, Vec2)>>,
1009 config: EmitterConfig,
1010}
1011
1012impl EmittersCache {
1013 const CACHE_DEFAULT_SIZE: usize = 10;
1014
1015 pub fn new(config: EmitterConfig) -> EmittersCache {
1016 let mut emitters_cache = vec![];
1017 for _ in 0..Self::CACHE_DEFAULT_SIZE {
1019 emitters_cache.push(Emitter::new(EmitterConfig {
1020 emitting: false,
1021 ..config.clone()
1022 }));
1023 }
1024 EmittersCache {
1025 emitter: Emitter::new(config.clone()),
1026 emitters_cache,
1027 active_emitters: vec![],
1028 config,
1029 }
1030 }
1031
1032 pub fn spawn(&mut self, pos: Vec2) {
1033 let mut emitter = if let Some(emitter) = self.emitters_cache.pop() {
1034 emitter
1035 } else {
1036 Emitter::new(self.config.clone())
1037 };
1038
1039 emitter.mesh_dirty = true;
1040 emitter.config.emitting = true;
1041 emitter.reset();
1042
1043 self.active_emitters.push(Some((emitter, pos)));
1044 }
1045
1046 pub fn draw(&mut self) {
1047 let mut gl = unsafe { get_internal_gl() };
1048
1049 gl.flush();
1050
1051 let InternalGlContext {
1052 quad_context: ctx,
1053 quad_gl,
1054 } = gl;
1055
1056 if self.active_emitters.len() > 0 {
1057 self.emitter.setup_render_pass(quad_gl, ctx);
1058 }
1059 for i in &mut self.active_emitters {
1060 if let Some((emitter, pos)) = i {
1061 emitter.position = *pos;
1062
1063 emitter.update(ctx, get_frame_time());
1064
1065 emitter.perform_render_pass(quad_gl, ctx);
1066
1067 if emitter.config.emitting == false {
1068 self.emitters_cache.push(i.take().unwrap().0);
1069 }
1070 }
1071 }
1072 if self.active_emitters.len() > 0 {
1073 self.emitter.end_render_pass(quad_gl, ctx);
1074 }
1075
1076 self.active_emitters.retain(|emitter| emitter.is_some())
1077 }
1078}
1079
1080mod shader {
1081 use super::*;
1082
1083 pub const VERTEX: &str = r#"#version 100
1084 #define DEF_VERTEX_ATTRIBUTES
1085 #include "particles.glsl"
1086
1087 varying lowp vec2 texcoord;
1088 varying lowp vec4 color;
1089
1090 void main() {
1091 gl_Position = particle_transform_vertex();
1092 color = in_attr_inst_color;
1093 texcoord = particle_transform_uv();
1094 }
1095 "#;
1096
1097 pub const FRAGMENT: &str = r#"#version 100
1098 varying lowp vec2 texcoord;
1099 varying lowp vec4 color;
1100
1101 uniform sampler2D texture;
1102
1103 void main() {
1104 gl_FragColor = texture2D(texture, texcoord) * color;
1105 }
1106 "#;
1107
1108 pub fn meta() -> ShaderMeta {
1109 ShaderMeta {
1110 images: vec!["texture".to_string()],
1111 uniforms: UniformBlockLayout {
1112 uniforms: vec![
1113 UniformDesc::new("_mvp", UniformType::Mat4),
1114 UniformDesc::new("_local_coords", UniformType::Float1),
1115 UniformDesc::new("_emitter_position", UniformType::Float3),
1116 ],
1117 },
1118 }
1119 }
1120
1121 #[repr(C)]
1122 pub struct Uniforms {
1123 pub mvp: Mat4,
1124 pub local_coords: f32,
1125 pub emitter_position: Vec3,
1126 }
1127}
1128
1129mod post_processing_shader {
1130 use super::*;
1131
1132 pub const VERTEX: &str = r#"#version 100
1133 attribute vec2 pos;
1134 attribute vec2 uv;
1135
1136 varying lowp vec2 texcoord;
1137
1138 void main() {
1139 gl_Position = vec4(pos, 0, 1);
1140 texcoord = uv;
1141 }
1142 "#;
1143
1144 pub const FRAGMENT: &str = r#"#version 100
1145 precision lowp float;
1146
1147 varying vec2 texcoord;
1148 uniform sampler2D tex;
1149
1150 void main() {
1151 gl_FragColor = texture2D(tex, texcoord);
1152 }
1153 "#;
1154
1155 pub fn meta() -> ShaderMeta {
1156 ShaderMeta {
1157 images: vec!["tex".to_string()],
1158 uniforms: UniformBlockLayout { uniforms: vec![] },
1159 }
1160 }
1161}