ff_particles/
lib.rs

1#[cfg(feature = "serde")]
2mod serde_serializable;
3
4use macroquad::prelude::*;
5use macroquad::window::miniquad::*;
6
7#[cfg(feature = "nanoserde")]
8use nanoserde::{DeJson, SerJson};
9
10#[cfg(feature = "serde")]
11use serde::{Deserialize, Serialize};
12
13#[cfg(feature = "serde")]
14use crate::serde_serializable::{post_processing_opt, vec2_def, ColorDef};
15
16#[derive(Debug, Clone, Copy, PartialEq)]
17#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
18#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
19pub enum Interpolation {
20    Linear,
21    Bezier,
22}
23
24#[derive(Debug, Clone)]
25#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
26#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
27pub struct Curve {
28    /// Key points for building a curve
29    pub points: Vec<(f32, f32)>,
30    /// The way middle points is interpolated during building a curve
31    /// Only Linear is implemented now
32    pub interpolation: Interpolation,
33    /// Interpolation steps used to build the curve from the key points
34    pub resolution: usize,
35}
36
37impl Curve {
38    fn batch(&self) -> BatchedCurve {
39        if self.interpolation == Interpolation::Bezier {
40            unimplemented!()
41        }
42
43        let step_f32 = 1.0 / self.resolution as f32;
44        let mut x = 0.0;
45        let mut points = Vec::with_capacity(self.resolution);
46
47        for curve_part in self.points.windows(2) {
48            let start = curve_part[0];
49            let end = curve_part[1];
50
51            while x <= end.0 {
52                let t = (x - start.0) / (end.0 - start.0);
53                let point = start.1 + (end.1 - start.1) * t;
54                points.push(point);
55                x += step_f32;
56            }
57        }
58
59        BatchedCurve { points }
60    }
61}
62
63#[derive(Debug, Clone)]
64pub struct BatchedCurve {
65    pub points: Vec<f32>,
66}
67
68impl BatchedCurve {
69    fn get(&self, t: f32) -> f32 {
70        let t_scaled = t * self.points.len() as f32;
71        let previous_ix = (t_scaled as usize).min(self.points.len() - 1);
72        let next_ix = (previous_ix + 1).min(self.points.len() - 1);
73        let previous = self.points[previous_ix];
74        let next = self.points[next_ix];
75
76        previous + (next - previous) * (t_scaled - previous_ix as f32)
77    }
78}
79impl Default for Curve {
80    fn default() -> Curve {
81        Curve {
82            points: vec![],
83            interpolation: Interpolation::Linear,
84            resolution: 20,
85        }
86    }
87}
88
89#[derive(Copy, Clone, PartialEq, Debug)]
90#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
91#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
92pub enum EmissionShape {
93    Point,
94    Rect { width: f32, height: f32 },
95    Sphere { radius: f32 },
96}
97
98#[derive(Copy, Clone, PartialEq, Debug)]
99#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
100#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
101pub struct ColorSerializable {
102    pub r: f32,
103    pub g: f32,
104    pub b: f32,
105    pub a: f32,
106}
107
108impl From<&Color> for ColorSerializable {
109    fn from(color: &Color) -> ColorSerializable {
110        ColorSerializable {
111            r: color.r,
112            g: color.g,
113            b: color.b,
114            a: color.a,
115        }
116    }
117}
118
119impl From<&ColorSerializable> for Color {
120    fn from(color: &ColorSerializable) -> Color {
121        Color {
122            r: color.r,
123            g: color.g,
124            b: color.b,
125            a: color.a,
126        }
127    }
128}
129
130#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
131#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
132pub struct Vec2Serializable {
133    x: f32,
134    y: f32,
135}
136
137impl From<&Vec2> for Vec2Serializable {
138    fn from(vec: &Vec2) -> Vec2Serializable {
139        Vec2Serializable { x: vec.x, y: vec.y }
140    }
141}
142
143impl From<&Vec2Serializable> for Vec2 {
144    fn from(vec: &Vec2Serializable) -> Vec2 {
145        vec2(vec.x, vec.y)
146    }
147}
148
149#[derive(Copy, Clone, PartialEq, Debug)]
150#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
151#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
152pub struct ColorCurve {
153    #[cfg_attr(feature = "nanoserde", nserde(proxy = "ColorSerializable"))]
154    #[cfg_attr(feature = "serde", serde(with = "ColorDef"))]
155    pub start: Color,
156    #[cfg_attr(feature = "nanoserde", nserde(proxy = "ColorSerializable"))]
157    #[cfg_attr(feature = "serde", serde(with = "ColorDef"))]
158    pub mid: Color,
159    #[cfg_attr(feature = "nanoserde", nserde(proxy = "ColorSerializable"))]
160    #[cfg_attr(feature = "serde", serde(with = "ColorDef"))]
161    pub end: Color,
162}
163
164impl Default for ColorCurve {
165    fn default() -> ColorCurve {
166        ColorCurve {
167            start: WHITE,
168            mid: WHITE,
169            end: WHITE,
170        }
171    }
172}
173
174#[derive(Debug, Clone)]
175#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
176#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
177pub struct EmitterConfig {
178    /// If false - particles spawns at position supplied to .draw(), but afterwards lives in current camera coordinate system.
179    /// If false particles use coordinate system originated to the emitter draw position
180    pub local_coords: bool,
181    /// Particles will be emitted inside that region.
182    pub emission_shape: EmissionShape,
183    /// If true only one emission cycle occurs. May be re-emitted by .emit() call.
184    pub one_shot: bool,
185    /// Lifespan of individual particle.
186    pub lifetime: f32,
187    /// Particle lifetime randomness ratio.
188    /// Each particle will spawned with "lifetime = lifetime - lifetime * rand::gen_range(0.0, lifetime_randomness)".
189    pub lifetime_randomness: f32,
190    /// 0..1 value, how rapidly particles in emission cycle are emitted.
191    /// With 0 particles will be emitted with equal gap.
192    /// With 1 all the particles will be emitted at the beginning of the cycle.
193    pub explosiveness: f32,
194    /// Amount of particles emitted in one emission cycle.
195    pub amount: u32,
196    /// Shape of each individual particle mesh.
197    pub shape: ParticleShape,
198    /// Particles are emitting when "emitting" is true.
199    /// If its a "one-shot" emitter, emitting will switch to false after active emission cycle.
200    pub emitting: bool,
201    /// Unit vector specifying emission direction.
202    #[cfg_attr(feature = "nanoserde", nserde(proxy = "Vec2Serializable"))]
203    #[cfg_attr(feature = "serde", serde(with = "vec2_def"))]
204    pub initial_direction: Vec2,
205    /// Angle from 0 to "2 * Pi" for random fluctuation for direction vector.
206    pub initial_direction_spread: f32,
207    /// Initial speed for each emitted particle.
208    /// Direction of the initial speed vector is affected by "direction" and "spread"
209    pub initial_velocity: f32,
210    /// Initial velocity randomness ratio.
211    /// Each particle will spawned with "initial_velocity = initial_velocity - initial_velocity * rand::gen_range(0.0, initial_velocity_randomness)".
212    pub initial_velocity_randomness: f32,
213    /// Velocity acceleration applied to each particle in the direction of motion.
214    pub linear_accel: f32,
215
216    /// Each particle is a "size x size" square.
217    pub size: f32,
218    /// Each particle will spawned with "size = size - size * rand::gen_range(0.0, size_randomness)".
219    pub size_randomness: f32,
220    /// If curve is present in each moment of particle lifetime size would be multiplied by the value from the curve
221    #[cfg_attr(
222        feature = "serde",
223        serde(default, skip_serializing_if = "Option::is_none")
224    )]
225    pub size_curve: Option<Curve>,
226
227    /// Particles rendering mode.
228    pub blend_mode: BlendMode,
229
230    /// How particles should change base color along the lifetime.
231    pub colors_curve: ColorCurve,
232
233    /// Gravity applied to each individual particle.
234    #[cfg_attr(feature = "nanoserde", nserde(proxy = "Vec2Serializable"))]
235    #[cfg_attr(feature = "serde", serde(with = "vec2_def"))]
236    pub gravity: Vec2,
237
238    /// Particle texture. If none particles going to be white squares.
239    #[cfg_attr(feature = "nanoserde", nserde(skip))]
240    #[cfg_attr(feature = "serde", serde(skip, default))]
241    pub texture: Option<Texture2D>,
242
243    /// For animated texture specify spritesheet layout.
244    /// If none the whole texture will be used.
245    #[cfg_attr(
246        feature = "serde",
247        serde(default, skip_serializing_if = "Option::is_none")
248    )]
249    pub atlas: Option<AtlasConfig>,
250
251    /// Custom material used to shade each particle.
252    #[cfg_attr(
253        feature = "serde",
254        serde(default, skip_serializing_if = "Option::is_none")
255    )]
256    pub material: Option<ParticleMaterial>,
257
258    /// If none particles will be rendered directly to the screen.
259    /// If not none all the particles will be rendered to a rectangle and than this rectangle
260    /// will be rendered to the screen.
261    /// This will allows some effects affecting particles as a whole.
262    /// NOTE: this is not really implemented and now Some will just make hardcoded downscaling
263    #[cfg_attr(
264        feature = "serde",
265        serde(
266            default,
267            with = "post_processing_opt",
268            skip_serializing_if = "Option::is_none"
269        )
270    )]
271    pub post_processing: Option<PostProcessing>,
272}
273
274impl EmissionShape {
275    fn gen_random_point(&self) -> Vec2 {
276        match self {
277            EmissionShape::Point => vec2(0., 0.),
278            EmissionShape::Rect { width, height } => vec2(
279                rand::gen_range(-width / 2., width / 2.0),
280                rand::gen_range(-height / 2., height / 2.0),
281            ),
282            EmissionShape::Sphere { radius } => {
283                let ro = rand::gen_range(0., radius * radius).sqrt();
284                let phi = rand::gen_range(0., std::f32::consts::PI * 2.);
285
286                macroquad::math::polar_to_cartesian(ro, phi)
287            }
288        }
289    }
290}
291
292#[derive(Copy, Clone, PartialEq, Debug)]
293#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
294#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
295pub struct PostProcessing;
296
297#[derive(Clone, PartialEq, Debug)]
298#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
299#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
300pub enum ParticleShape {
301    Rectangle,
302    Circle {
303        subdivisions: u32,
304    },
305    CustomMesh {
306        vertices: Vec<f32>,
307        indices: Vec<u16>,
308    },
309}
310
311impl ParticleShape {
312    fn build_bindings(
313        &self,
314        ctx: &mut miniquad::Context,
315        positions_vertex_buffer: Buffer,
316        texture: Option<Texture2D>,
317    ) -> Bindings {
318        let (geometry_vertex_buffer, index_buffer) = match self {
319            ParticleShape::Rectangle => {
320                #[rustfmt::skip]
321                let vertices: &[f32] = &[
322                    // positions       uv         colors
323                    -1.0, -1.0, 0.0,   0.0, 0.0,  1.0, 1.0, 1.0, 1.0,
324                     1.0, -1.0, 0.0,   1.0, 0.0,  1.0, 1.0, 1.0, 1.0,
325                     1.0,  1.0, 0.0,   1.0, 1.0,  1.0, 1.0, 1.0, 1.0,
326                    -1.0,  1.0, 0.0,   0.0, 1.0,  1.0, 1.0, 1.0, 1.0,
327                ];
328
329                let vertex_buffer = Buffer::immutable(ctx, BufferType::VertexBuffer, &vertices);
330
331                #[rustfmt::skip]
332                let indices: &[u16] = &[
333                    0, 1, 2, 0, 2, 3
334                ];
335                let index_buffer = Buffer::immutable(ctx, BufferType::IndexBuffer, &indices);
336
337                (vertex_buffer, index_buffer)
338            }
339            ParticleShape::Circle { subdivisions } => {
340                let mut vertices = Vec::<f32>::new();
341                let mut indices = Vec::<u16>::new();
342
343                let rot = 0.0;
344                vertices.extend_from_slice(&[0., 0., 0., 0., 0., 1.0, 1.0, 1.0, 1.0]);
345                for i in 0..subdivisions + 1 {
346                    let rx =
347                        (i as f32 / *subdivisions as f32 * std::f32::consts::PI * 2. + rot).cos();
348                    let ry =
349                        (i as f32 / *subdivisions as f32 * std::f32::consts::PI * 2. + rot).sin();
350                    vertices.extend_from_slice(&[rx, ry, 0., rx, ry, 1., 1., 1., 1.]);
351
352                    if i != *subdivisions {
353                        indices.extend_from_slice(&[0, i as u16 + 1, i as u16 + 2]);
354                    }
355                }
356
357                let vertex_buffer = Buffer::immutable(ctx, BufferType::VertexBuffer, &vertices);
358                let index_buffer = Buffer::immutable(ctx, BufferType::IndexBuffer, &indices);
359                (vertex_buffer, index_buffer)
360            }
361            ParticleShape::CustomMesh { vertices, indices } => {
362                let vertex_buffer = Buffer::immutable(ctx, BufferType::VertexBuffer, &vertices);
363                let index_buffer = Buffer::immutable(ctx, BufferType::IndexBuffer, &indices);
364                (vertex_buffer, index_buffer)
365            }
366        };
367
368        Bindings {
369            vertex_buffers: vec![geometry_vertex_buffer, positions_vertex_buffer],
370            index_buffer,
371            images: vec![texture.map_or_else(
372                || Texture::from_rgba8(ctx, 1, 1, &[255, 255, 255, 255]),
373                |texture| texture.raw_miniquad_texture_handle(),
374            )],
375        }
376    }
377}
378
379#[derive(Debug, Clone)]
380#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
381#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
382pub struct ParticleMaterial {
383    vertex: String,
384    fragment: String,
385}
386
387impl ParticleMaterial {
388    pub fn new(vertex: &str, fragment: &str) -> ParticleMaterial {
389        ParticleMaterial {
390            vertex: vertex.to_owned(),
391            fragment: fragment.to_owned(),
392        }
393    }
394}
395
396#[derive(Debug, Clone, Copy, PartialEq)]
397#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
398#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
399pub enum BlendMode {
400    /// Colors of overlapped particles will be blended by alpha channel.
401    Alpha,
402    /// Colors of overlapped particles will be added to each other.
403    Additive,
404}
405
406impl BlendMode {
407    fn blend_state(&self) -> BlendState {
408        match self {
409            BlendMode::Alpha => BlendState::new(
410                Equation::Add,
411                BlendFactor::Value(BlendValue::SourceAlpha),
412                BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
413            ),
414            BlendMode::Additive => BlendState::new(
415                Equation::Add,
416                BlendFactor::Value(BlendValue::SourceAlpha),
417                BlendFactor::One,
418            ),
419        }
420    }
421}
422
423#[derive(Debug, Clone)]
424#[cfg_attr(feature = "nanoserde", derive(DeJson, SerJson))]
425#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
426pub struct AtlasConfig {
427    n: u16,
428    m: u16,
429    start_index: u16,
430    end_index: u16,
431}
432
433impl AtlasConfig {
434    pub fn new<T: std::ops::RangeBounds<u16>>(n: u16, m: u16, range: T) -> AtlasConfig {
435        let start_index = match range.start_bound() {
436            std::ops::Bound::Unbounded => 0,
437            std::ops::Bound::Included(i) => *i,
438            std::ops::Bound::Excluded(i) => i + 1,
439        };
440        let end_index = match range.end_bound() {
441            std::ops::Bound::Unbounded => n * m,
442            std::ops::Bound::Included(i) => i - 1,
443            std::ops::Bound::Excluded(i) => *i,
444        };
445
446        AtlasConfig {
447            n,
448            m,
449            start_index,
450            end_index,
451        }
452    }
453}
454
455impl Default for EmitterConfig {
456    fn default() -> EmitterConfig {
457        EmitterConfig {
458            local_coords: false,
459            emission_shape: EmissionShape::Point,
460            one_shot: false,
461            lifetime: 1.0,
462            lifetime_randomness: 0.0,
463            amount: 8,
464            shape: ParticleShape::Rectangle,
465            explosiveness: 0.0,
466            emitting: true,
467            initial_direction: vec2(0., -1.),
468            initial_direction_spread: 0.,
469            initial_velocity: 50.0,
470            initial_velocity_randomness: 0.0,
471            linear_accel: 0.0,
472            size: 10.0,
473            size_randomness: 0.0,
474            size_curve: None,
475            blend_mode: BlendMode::Alpha,
476            colors_curve: ColorCurve::default(),
477            gravity: vec2(0.0, 0.0),
478            texture: None,
479            atlas: None,
480            material: None,
481            post_processing: None,
482        }
483    }
484}
485
486#[repr(C)]
487struct GpuParticle {
488    pos: Vec4,
489    uv: Vec4,
490    data: Vec4,
491    color: Vec4,
492}
493
494struct CpuParticle {
495    velocity: Vec2,
496    lived: f32,
497    lifetime: f32,
498    frame: u16,
499    initial_size: f32,
500}
501
502pub struct Emitter {
503    pipeline: Pipeline,
504    bindings: Bindings,
505    post_processing_pass: RenderPass,
506    post_processing_pipeline: Pipeline,
507    post_processing_bindings: Bindings,
508
509    gpu_particles: Vec<GpuParticle>,
510    cpu_counterpart: Vec<CpuParticle>,
511
512    last_emit_time: f32,
513    time_passed: f32,
514
515    particles_spawned: u64,
516    position: Vec2,
517
518    batched_size_curve: Option<BatchedCurve>,
519
520    blend_mode: BlendMode,
521    mesh_dirty: bool,
522
523    pub config: EmitterConfig,
524}
525
526impl Emitter {
527    const MAX_PARTICLES: usize = 10000;
528
529    pub fn new(config: EmitterConfig) -> Emitter {
530        let InternalGlContext {
531            quad_context: ctx, ..
532        } = unsafe { get_internal_gl() };
533
534        // empty, dynamic instance-data vertex buffer
535        let positions_vertex_buffer = Buffer::stream(
536            ctx,
537            BufferType::VertexBuffer,
538            Self::MAX_PARTICLES * std::mem::size_of::<Vec3>(),
539        );
540
541        let bindings = config
542            .shape
543            .build_bindings(ctx, positions_vertex_buffer, config.texture);
544
545        let (vertex, fragment) = config.material.as_ref().map_or_else(
546            || (shader::VERTEX, shader::FRAGMENT),
547            |material| (&material.vertex, &material.fragment),
548        );
549
550        let shader = {
551            use macroquad::material::shaders::{preprocess_shader, PreprocessorConfig};
552
553            let config = PreprocessorConfig {
554                includes: vec![(
555                    "particles.glsl".to_string(),
556                    include_str!("particles.glsl").to_owned(),
557                )],
558                ..Default::default()
559            };
560
561            let vertex = preprocess_shader(&vertex, &config);
562            let fragment = preprocess_shader(&fragment, &config);
563
564            Shader::new(ctx, &vertex, &fragment, shader::meta()).unwrap()
565        };
566
567        let blend_mode = config.blend_mode.blend_state();
568        let pipeline = Pipeline::with_params(
569            ctx,
570            &[
571                BufferLayout::default(),
572                BufferLayout {
573                    step_func: VertexStep::PerInstance,
574                    ..Default::default()
575                },
576            ],
577            &[
578                VertexAttribute::with_buffer("in_attr_pos", VertexFormat::Float3, 0),
579                VertexAttribute::with_buffer("in_attr_uv", VertexFormat::Float2, 0),
580                VertexAttribute::with_buffer("in_attr_color", VertexFormat::Float4, 0),
581                VertexAttribute::with_buffer("in_attr_inst_pos", VertexFormat::Float4, 1),
582                VertexAttribute::with_buffer("in_attr_inst_uv", VertexFormat::Float4, 1),
583                VertexAttribute::with_buffer("in_attr_inst_data", VertexFormat::Float4, 1),
584                VertexAttribute::with_buffer("in_attr_inst_color", VertexFormat::Float4, 1),
585            ],
586            shader,
587            PipelineParams {
588                color_blend: Some(blend_mode),
589                ..Default::default()
590            },
591        );
592
593        let post_processing_shader = Shader::new(
594            ctx,
595            &post_processing_shader::VERTEX,
596            &post_processing_shader::FRAGMENT,
597            post_processing_shader::meta(),
598        )
599        .unwrap();
600
601        let post_processing_pipeline = Pipeline::with_params(
602            ctx,
603            &[BufferLayout::default(), BufferLayout::default()],
604            &[
605                VertexAttribute::with_buffer("pos", VertexFormat::Float2, 0),
606                VertexAttribute::with_buffer("uv", VertexFormat::Float2, 0),
607            ],
608            post_processing_shader,
609            PipelineParams {
610                color_blend: Some(BlendState::new(
611                    Equation::Add,
612                    BlendFactor::Value(BlendValue::SourceAlpha),
613                    BlendFactor::OneMinusValue(BlendValue::SourceAlpha),
614                )),
615                ..Default::default()
616            },
617        );
618        let post_processing_pass = {
619            let color_img = Texture::new_render_texture(
620                ctx,
621                TextureParams {
622                    width: 320,
623                    height: 200,
624                    format: TextureFormat::RGBA8,
625                    ..Default::default()
626                },
627            );
628            color_img.set_filter(ctx, FilterMode::Nearest);
629
630            RenderPass::new(ctx, color_img, None)
631        };
632
633        let post_processing_bindings = {
634            #[rustfmt::skip]
635            let vertices: &[f32] = &[
636                // positions   uv
637                -1.0, -1.0,    0.0, 0.0,
638                 1.0, -1.0,    1.0, 0.0,
639                 1.0,  1.0,    1.0, 1.0,
640                -1.0,  1.0,    0.0, 1.0,
641            ];
642
643            let vertex_buffer = Buffer::immutable(ctx, BufferType::VertexBuffer, &vertices);
644
645            #[rustfmt::skip]
646            let indices: &[u16] = &[
647                0, 1, 2, 0, 2, 3
648            ];
649            let index_buffer = Buffer::immutable(ctx, BufferType::IndexBuffer, &indices);
650            Bindings {
651                vertex_buffers: vec![vertex_buffer],
652                index_buffer,
653                images: vec![post_processing_pass.texture(ctx)],
654            }
655        };
656
657        Emitter {
658            blend_mode: config.blend_mode.clone(),
659            batched_size_curve: config.size_curve.as_ref().map(|curve| curve.batch()),
660            post_processing_pass,
661            post_processing_pipeline,
662            post_processing_bindings,
663            config,
664            pipeline,
665            bindings,
666            position: vec2(0.0, 0.0),
667            gpu_particles: Vec::with_capacity(Self::MAX_PARTICLES),
668            cpu_counterpart: Vec::with_capacity(Self::MAX_PARTICLES),
669            particles_spawned: 0,
670            last_emit_time: 0.0,
671            time_passed: 0.0,
672            mesh_dirty: false,
673        }
674    }
675
676    fn reset(&mut self) {
677        self.gpu_particles.clear();
678        self.cpu_counterpart.clear();
679        self.last_emit_time = 0.0;
680        self.time_passed = 0.0;
681        self.particles_spawned = 0;
682    }
683    pub fn rebuild_size_curve(&mut self) {
684        self.batched_size_curve = self.config.size_curve.as_ref().map(|curve| curve.batch());
685    }
686
687    pub fn update_particle_mesh(&mut self) {
688        self.mesh_dirty = true;
689    }
690
691    fn emit_particle(&mut self, offset: Vec2) {
692        let offset = offset + self.config.emission_shape.gen_random_point();
693
694        fn random_initial_vector(dir: Vec2, spread: f32, velocity: f32) -> Vec2 {
695            let angle = rand::gen_range(-spread / 2.0, spread / 2.0);
696
697            let quat = glam::Quat::from_rotation_z(angle);
698            let dir = quat * vec3(dir.x, dir.y, 0.0);
699            let res = dir * velocity;
700
701            vec2(res.x, res.y)
702        }
703
704        let r =
705            self.config.size - self.config.size * rand::gen_range(0.0, self.config.size_randomness);
706
707        let particle = if self.config.local_coords {
708            GpuParticle {
709                pos: vec4(offset.x, offset.y, 0.0, r),
710                uv: vec4(1.0, 1.0, 0.0, 0.0),
711                data: vec4(self.particles_spawned as f32, 0.0, 0.0, 0.0),
712                color: self.config.colors_curve.start.to_vec(),
713            }
714        } else {
715            GpuParticle {
716                pos: vec4(
717                    self.position.x + offset.x,
718                    self.position.y + offset.y,
719                    0.0,
720                    r,
721                ),
722                uv: vec4(1.0, 1.0, 0.0, 0.0),
723                data: vec4(self.particles_spawned as f32, 0.0, 0.0, 0.0),
724                color: self.config.colors_curve.start.to_vec(),
725            }
726        };
727
728        self.particles_spawned += 1;
729        self.gpu_particles.push(particle);
730        self.cpu_counterpart.push(CpuParticle {
731            velocity: random_initial_vector(
732                vec2(
733                    self.config.initial_direction.x,
734                    self.config.initial_direction.y,
735                ),
736                self.config.initial_direction_spread,
737                self.config.initial_velocity
738                    - self.config.initial_velocity
739                        * rand::gen_range(0.0, self.config.initial_velocity_randomness),
740            ),
741            lived: 0.0,
742            lifetime: self.config.lifetime
743                - self.config.lifetime * rand::gen_range(0.0, self.config.lifetime_randomness),
744            frame: 0,
745            initial_size: r,
746        });
747    }
748
749    fn update(&mut self, ctx: &mut Context, dt: f32) {
750        if self.mesh_dirty {
751            self.bindings = self.config.shape.build_bindings(
752                ctx,
753                self.bindings.vertex_buffers[1],
754                self.config.texture,
755            );
756            self.mesh_dirty = false;
757        }
758        if self.config.emitting {
759            self.time_passed += dt;
760
761            let gap = (self.config.lifetime / self.config.amount as f32)
762                * (1.0 - self.config.explosiveness);
763
764            let spawn_amount = if gap < 0.001 {
765                // to prevent division by 0 problems
766                self.config.amount as usize
767            } else {
768                // how many particles fits into this delta time
769                ((self.time_passed - self.last_emit_time) / gap) as usize
770            };
771
772            for _ in 0..spawn_amount {
773                self.last_emit_time = self.time_passed;
774
775                if self.particles_spawned < self.config.amount as u64 {
776                    self.emit_particle(vec2(0.0, 0.0));
777                }
778
779                if self.gpu_particles.len() >= self.config.amount as usize {
780                    break;
781                }
782            }
783        }
784
785        if self.config.one_shot && self.time_passed > self.config.lifetime {
786            self.time_passed = 0.0;
787            self.last_emit_time = 0.0;
788            self.config.emitting = false;
789        }
790
791        for (gpu, cpu) in self.gpu_particles.iter_mut().zip(&mut self.cpu_counterpart) {
792            // TODO: this is not quite the way to apply acceleration, this is not
793            // fps independent and just wrong
794            cpu.velocity += cpu.velocity * self.config.linear_accel * dt;
795
796            gpu.color = {
797                let t = cpu.lived / cpu.lifetime;
798                if t < 0.5 {
799                    let t = t * 2.;
800                    self.config.colors_curve.start.to_vec() * (1.0 - t)
801                        + self.config.colors_curve.mid.to_vec() * t
802                } else {
803                    let t = (t - 0.5) * 2.;
804                    self.config.colors_curve.mid.to_vec() * (1.0 - t)
805                        + self.config.colors_curve.end.to_vec() * t
806                }
807            };
808            gpu.pos += vec4(cpu.velocity.x, cpu.velocity.y, 0.0, 0.0) * dt;
809
810            gpu.pos.w = cpu.initial_size
811                * self
812                    .batched_size_curve
813                    .as_ref()
814                    .map_or(1.0, |curve| curve.get(cpu.lived / cpu.lifetime));
815
816            if cpu.lifetime != 0.0 {
817                gpu.data.y = cpu.lived / cpu.lifetime;
818            }
819
820            cpu.lived += dt;
821
822            cpu.velocity += self.config.gravity * dt;
823
824            if let Some(atlas) = &self.config.atlas {
825                if cpu.lifetime != 0.0 {
826                    cpu.frame = (cpu.lived / cpu.lifetime
827                        * (atlas.end_index - atlas.start_index) as f32)
828                        as u16
829                        + atlas.start_index;
830                }
831
832                let x = cpu.frame % atlas.n;
833                let y = cpu.frame / atlas.m;
834
835                gpu.uv = vec4(
836                    x as f32 / atlas.n as f32,
837                    y as f32 / atlas.m as f32,
838                    1.0 / atlas.n as f32,
839                    1.0 / atlas.m as f32,
840                );
841            } else {
842                gpu.uv = vec4(0.0, 0.0, 1.0, 1.0);
843            }
844        }
845
846        for i in (0..self.gpu_particles.len()).rev() {
847            // second if clause is just for the case when lifetime was changed in the editor
848            // normally particle lifetime is always less or equal config lifetime
849            if self.cpu_counterpart[i].lived > self.cpu_counterpart[i].lifetime
850                || self.cpu_counterpart[i].lived > self.config.lifetime
851            {
852                self.gpu_particles.remove(i);
853                self.cpu_counterpart.remove(i);
854                self.particles_spawned -= 1;
855            }
856        }
857
858        self.bindings.vertex_buffers[1].update(ctx, &self.gpu_particles[..]);
859    }
860
861    /// Immediately emit N particles, ignoring "emitting" and "amount" params of EmitterConfig
862    pub fn emit(&mut self, pos: Vec2, n: usize) {
863        for _ in 0..n {
864            self.emit_particle(pos);
865            self.particles_spawned += 1;
866        }
867    }
868
869    fn perform_render_pass(&mut self, quad_gl: &QuadGl, ctx: &mut Context) {
870        ctx.apply_bindings(&self.bindings);
871        ctx.apply_uniforms(&shader::Uniforms {
872            mvp: quad_gl.get_projection_matrix(),
873            emitter_position: vec3(self.position.x, self.position.y, 0.0),
874            local_coords: if self.config.local_coords { 1.0 } else { 0.0 },
875        });
876
877        ctx.draw(
878            0,
879            self.bindings.index_buffer.size() as i32 / std::mem::size_of::<u16>() as i32,
880            self.gpu_particles.len() as i32,
881        );
882    }
883
884    pub fn setup_render_pass(&mut self, quad_gl: &QuadGl, ctx: &mut Context) {
885        if self.config.blend_mode != self.blend_mode {
886            self.pipeline
887                .set_blend(ctx, Some(self.config.blend_mode.blend_state()));
888            self.blend_mode = self.config.blend_mode.clone();
889        }
890
891        if self.config.post_processing.is_none() {
892            let pass = quad_gl.get_active_render_pass();
893            if let Some(pass) = pass {
894                ctx.begin_pass(pass, PassAction::Nothing);
895            } else {
896                ctx.begin_default_pass(PassAction::Nothing);
897            }
898        } else {
899            ctx.begin_pass(
900                self.post_processing_pass,
901                PassAction::clear_color(0.0, 0.0, 0.0, 0.0),
902            );
903        };
904
905        ctx.apply_pipeline(&self.pipeline);
906    }
907
908    pub fn end_render_pass(&mut self, quad_gl: &QuadGl, ctx: &mut Context) {
909        ctx.end_render_pass();
910
911        if self.config.post_processing.is_some() {
912            let pass = quad_gl.get_active_render_pass();
913            if let Some(pass) = pass {
914                ctx.begin_pass(pass, PassAction::Nothing);
915            } else {
916                ctx.begin_default_pass(PassAction::Nothing);
917            }
918
919            ctx.apply_pipeline(&self.post_processing_pipeline);
920            let (x, y, w, h) = quad_gl.get_viewport();
921            ctx.apply_viewport(x, y, w, h);
922
923            ctx.apply_bindings(&self.post_processing_bindings);
924
925            ctx.draw(0, 6, 1);
926
927            ctx.end_render_pass();
928        }
929    }
930
931    pub fn draw(&mut self, pos: Vec2) {
932        let mut gl = unsafe { get_internal_gl() };
933
934        gl.flush();
935
936        let InternalGlContext {
937            quad_context: ctx,
938            quad_gl,
939        } = gl;
940
941        self.position = pos;
942
943        self.update(ctx, get_frame_time());
944
945        self.setup_render_pass(quad_gl, ctx);
946        self.perform_render_pass(quad_gl, ctx);
947        self.end_render_pass(quad_gl, ctx);
948    }
949}
950
951/// Multiple emitters drawn simultaneously.
952/// Will reuse as much GPU resources as possible, so should be more efficient than
953/// just Vec<Emitter>
954pub struct EmittersCache {
955    emitter: Emitter,
956    emitters_cache: Vec<Emitter>,
957    active_emitters: Vec<Option<(Emitter, Vec2)>>,
958    config: EmitterConfig,
959}
960
961impl EmittersCache {
962    const CACHE_DEFAULT_SIZE: usize = 10;
963
964    pub fn new(config: EmitterConfig) -> EmittersCache {
965        let mut emitters_cache = vec![];
966        // prepopulate cache
967        for _ in 0..Self::CACHE_DEFAULT_SIZE {
968            emitters_cache.push(Emitter::new(EmitterConfig {
969                emitting: false,
970                ..config.clone()
971            }));
972        }
973        EmittersCache {
974            emitter: Emitter::new(config.clone()),
975            emitters_cache,
976            active_emitters: vec![],
977            config,
978        }
979    }
980
981    pub fn spawn(&mut self, pos: Vec2) {
982        let mut emitter = if let Some(emitter) = self.emitters_cache.pop() {
983            emitter
984        } else {
985            Emitter::new(self.config.clone())
986        };
987
988        emitter.mesh_dirty = true;
989        emitter.config.emitting = true;
990        emitter.reset();
991
992        self.active_emitters.push(Some((emitter, pos)));
993    }
994
995    pub fn draw(&mut self) {
996        let mut gl = unsafe { get_internal_gl() };
997
998        gl.flush();
999
1000        let InternalGlContext {
1001            quad_context: ctx,
1002            quad_gl,
1003        } = gl;
1004
1005        if self.active_emitters.len() > 0 {
1006            self.emitter.setup_render_pass(quad_gl, ctx);
1007        }
1008        for i in &mut self.active_emitters {
1009            if let Some((emitter, pos)) = i {
1010                emitter.position = *pos;
1011
1012                emitter.update(ctx, get_frame_time());
1013
1014                emitter.perform_render_pass(quad_gl, ctx);
1015
1016                if emitter.config.emitting == false {
1017                    self.emitters_cache.push(i.take().unwrap().0);
1018                }
1019            }
1020        }
1021        if self.active_emitters.len() > 0 {
1022            self.emitter.end_render_pass(quad_gl, ctx);
1023        }
1024
1025        self.active_emitters.retain(|emitter| emitter.is_some())
1026    }
1027}
1028
1029mod shader {
1030    use super::*;
1031
1032    pub const VERTEX: &str = r#"#version 100
1033    #define DEF_VERTEX_ATTRIBUTES
1034    #include "particles.glsl"
1035
1036    varying lowp vec2 texcoord;
1037    varying lowp vec4 color;
1038
1039    void main() {
1040        gl_Position = particle_transform_vertex();
1041        color = in_attr_inst_color;
1042        texcoord = particle_transform_uv();
1043    }
1044    "#;
1045
1046    pub const FRAGMENT: &str = r#"#version 100
1047    varying lowp vec2 texcoord;
1048    varying lowp vec4 color;
1049
1050    uniform sampler2D texture;
1051
1052    void main() {
1053        gl_FragColor = texture2D(texture, texcoord) * color;
1054    }
1055    "#;
1056
1057    pub fn meta() -> ShaderMeta {
1058        ShaderMeta {
1059            images: vec!["texture".to_string()],
1060            uniforms: UniformBlockLayout {
1061                uniforms: vec![
1062                    UniformDesc::new("_mvp", UniformType::Mat4),
1063                    UniformDesc::new("_local_coords", UniformType::Float1),
1064                    UniformDesc::new("_emitter_position", UniformType::Float3),
1065                ],
1066            },
1067        }
1068    }
1069
1070    #[repr(C)]
1071    pub struct Uniforms {
1072        pub mvp: Mat4,
1073        pub local_coords: f32,
1074        pub emitter_position: Vec3,
1075    }
1076}
1077
1078mod post_processing_shader {
1079    use super::*;
1080
1081    pub const VERTEX: &str = r#"#version 100
1082    attribute vec2 pos;
1083    attribute vec2 uv;
1084
1085    varying lowp vec2 texcoord;
1086
1087    void main() {
1088        gl_Position = vec4(pos, 0, 1);
1089        texcoord = uv;
1090    }
1091    "#;
1092
1093    pub const FRAGMENT: &str = r#"#version 100
1094    precision lowp float;
1095
1096    varying vec2 texcoord;
1097    uniform sampler2D tex;
1098
1099    void main() {
1100        gl_FragColor = texture2D(tex, texcoord);
1101    }
1102    "#;
1103
1104    pub fn meta() -> ShaderMeta {
1105        ShaderMeta {
1106            images: vec!["tex".to_string()],
1107            uniforms: UniformBlockLayout { uniforms: vec![] },
1108        }
1109    }
1110}