macroquad_particles/
lib.rs

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    /// Key points for building a curve
19    pub points: Vec<(f32, f32)>,
20    /// The way middle points is interpolated during building a curve
21    /// Only Linear is implemented now
22    pub interpolation: Interpolation,
23    /// Interpolation steps used to build the curve from the key points
24    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    /// If false - particles spawns at position supplied to .draw(), but afterwards lives in current camera coordinate system.
161    /// If false particles use coordinate system originated to the emitter draw position
162    pub local_coords: bool,
163    /// Particles will be emitted inside that region.
164    pub emission_shape: EmissionShape,
165    /// If true only one emission cycle occurs. May be re-emitted by .emit() call.
166    pub one_shot: bool,
167    /// Lifespan of individual particle.
168    pub lifetime: f32,
169    /// Particle lifetime randomness ratio.
170    /// Each particle will spawned with "lifetime = lifetime - lifetime * rand::gen_range(0.0, lifetime_randomness)".
171    pub lifetime_randomness: f32,
172    /// 0..1 value, how rapidly particles in emission cycle are emitted.
173    /// With 0 particles will be emitted with equal gap.
174    /// With 1 all the particles will be emitted at the beginning of the cycle.
175    pub explosiveness: f32,
176    /// Amount of particles emitted in one emission cycle.
177    pub amount: u32,
178    /// Shape of each individual particle mesh.
179    pub shape: ParticleShape,
180    /// Particles are emitting when "emitting" is true.
181    /// If its a "one-shot" emitter, emitting will switch to false after active emission cycle.
182    pub emitting: bool,
183    /// Unit vector specifying emission direction.
184    #[cfg_attr(feature = "nanoserde", nserde(proxy = "Vec2Serializable"))]
185    pub initial_direction: Vec2,
186    /// Angle from 0 to "2 * Pi" for random fluctuation for direction vector.
187    pub initial_direction_spread: f32,
188    /// Initial speed for each emitted particle.
189    /// Direction of the initial speed vector is affected by "direction" and "spread"
190    pub initial_velocity: f32,
191    /// Initial velocity randomness ratio.
192    /// Each particle will spawned with "initial_velocity = initial_velocity - initial_velocity * rand::gen_range(0.0, initial_velocity_randomness)".
193    pub initial_velocity_randomness: f32,
194    /// Velocity acceleration applied to each particle in the direction of motion.
195    pub linear_accel: f32,
196
197    // Initial rotation for each emitted particle.
198    #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
199    pub initial_rotation: f32,
200    /// Initial rotation randomness.
201    /// Each particle will spawned with "initial_rotation = initial_rotation - initial_rotation * rand::gen_range(0.0, initial_rotation_randomness)"
202    #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
203    pub initial_rotation_randomness: f32,
204    // Initial rotational speed
205    #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
206    pub initial_angular_velocity: f32,
207    /// Initial angular velocity randomness.
208    /// Each particle will spawned with "initial_angular_velocity = initial_angular_velocity - initial_angular_velocity * rand::gen_range(0.0, initial_angular_velocity_randomness)".
209    #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
210    pub initial_angular_velocity_randomness: f32,
211    /// Angular velocity acceleration applied to each particle .'
212    #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
213    pub angular_accel: f32,
214    /// Angluar velocity damping
215    /// Each frame angular velocity will be transformed "angular_velocity *= (1.0 - angular_damping)".
216    #[cfg_attr(feature = "nanoserde", nserde(default = "0.0"))]
217    pub angular_damping: f32,
218    /// Each particle is a "size x size" square.
219    pub size: f32,
220    /// Each particle will spawned with "size = size - size * rand::gen_range(0.0, size_randomness)".
221    pub size_randomness: f32,
222    /// If curve is present in each moment of particle lifetime size would be multiplied by the value from the curve
223    pub size_curve: Option<Curve>,
224
225    /// Particles rendering mode.
226    pub blend_mode: BlendMode,
227
228    /// How particles should change base color along the lifetime.
229    pub colors_curve: ColorCurve,
230
231    /// Gravity applied to each individual particle.
232    #[cfg_attr(feature = "nanoserde", nserde(proxy = "Vec2Serializable"))]
233    pub gravity: Vec2,
234
235    /// Particle texture. If none particles going to be white squares.
236    #[cfg_attr(feature = "nanoserde", nserde(skip))]
237    pub texture: Option<Texture2D>,
238
239    /// For animated texture specify spritesheet layout.
240    /// If none the whole texture will be used.
241    pub atlas: Option<AtlasConfig>,
242
243    /// Custom material used to shade each particle.
244    pub material: Option<ParticleMaterial>,
245
246    /// If none particles will be rendered directly to the screen.
247    /// If not none all the particles will be rendered to a rectangle and than this rectangle
248    /// will be rendered to the screen.
249    /// This will allows some effects affecting particles as a whole.
250    /// NOTE: this is not really implemented and now Some will just make hardcoded downscaling
251    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                    // positions          uv          colors
303                    -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    /// Colors of overlapped particles will be blended by alpha channel.
403    Alpha,
404    /// Colors of overlapped particles will be added to each other.
405    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        // empty, dynamic instance-data vertex buffer
544        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                // positions   uv
656                -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                // to prevent division by 0 problems
803                self.config.amount as usize
804            } else {
805                // how many particles fits into this delta time
806                ((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            // TODO: this is not quite the way to apply acceleration, this is not
831            // fps independent and just wrong
832            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 = f32::min(cpu.lived + dt, cpu.lifetime);
861            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            // second if clause is just for the case when lifetime was changed in the editor
888            // normally particle lifetime is always less or equal config lifetime
889            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    /// Immediately emit N particles, ignoring "emitting" and "amount" params of EmitterConfig
907    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            // TODO: recreate pipeline
937            //self.pipeline
938            //    .set_blend(ctx, Some(self.config.blend_mode.blend_state()));
939            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
1002/// Multiple emitters drawn simultaneously.
1003/// Will reuse as much GPU resources as possible, so should be more efficient than
1004/// just Vec<Emitter>
1005pub 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        // prepopulate cache
1018        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}