spitfire-draw 0.36.14

Drawing helper module for Spitfire toolset
Documentation
use crate::{
    context::DrawContext,
    sprite::SpriteTexture,
    utils::{Drawable, ShaderRef, Vertex, transform_to_matrix},
};
use smallvec::SmallVec;
use spitfire_glow::{
    graphics::{GraphicsBatch, GraphicsTarget},
    renderer::{GlowBlending, GlowUniformValue},
};
use std::{borrow::Cow, cell::RefCell, collections::HashMap, marker::PhantomData};
use vek::{Quaternion, Rect, Rgba, Transform, Vec2, Vec3};

#[derive(Debug, Default, Clone)]
pub struct ParticleEmitter {
    pub shader: Option<ShaderRef>,
    pub textures: SmallVec<[SpriteTexture; 4]>,
    pub uniforms: HashMap<Cow<'static, str>, GlowUniformValue>,
    pub blending: Option<GlowBlending>,
    pub screen_space: bool,
}

impl ParticleEmitter {
    pub fn single(texture: SpriteTexture) -> Self {
        Self {
            textures: vec![texture].into(),
            ..Default::default()
        }
    }

    pub fn shader(mut self, value: ShaderRef) -> Self {
        self.shader = Some(value);
        self
    }

    pub fn texture(mut self, value: SpriteTexture) -> Self {
        self.textures.push(value);
        self
    }

    pub fn uniform(mut self, key: Cow<'static, str>, value: GlowUniformValue) -> Self {
        self.uniforms.insert(key, value);
        self
    }

    pub fn blending(mut self, value: GlowBlending) -> Self {
        self.blending = Some(value);
        self
    }

    pub fn screen_space(mut self, value: bool) -> Self {
        self.screen_space = value;
        self
    }

    pub fn emit<I: IntoIterator<Item = ParticleInstance>>(
        &'_ self,
        instances: I,
    ) -> ParticleDraw<'_, I> {
        ParticleDraw {
            emitter: self,
            instances: RefCell::new(Some(instances)),
        }
    }
}

#[derive(Debug, Clone)]
pub struct ParticleInstance {
    pub region: Rect<f32, f32>,
    pub page: f32,
    pub tint: Rgba<f32>,
    pub transform: Transform<f32, f32, f32>,
    pub size: Vec2<f32>,
    pub pivot: Vec2<f32>,
}

impl Default for ParticleInstance {
    fn default() -> Self {
        Self {
            region: Rect::new(0.0, 0.0, 1.0, 1.0),
            page: Default::default(),
            tint: Rgba::white(),
            transform: Default::default(),
            size: Default::default(),
            pivot: Default::default(),
        }
    }
}

impl ParticleInstance {
    pub fn region_page(mut self, region: Rect<f32, f32>, page: f32) -> Self {
        self.region = region;
        self.page = page;
        self
    }

    pub fn tint(mut self, value: Rgba<f32>) -> Self {
        self.tint = value;
        self
    }

    pub fn transform(mut self, value: Transform<f32, f32, f32>) -> Self {
        self.transform = value;
        self
    }

    pub fn position(mut self, value: Vec2<f32>) -> Self {
        self.transform.position = value.into();
        self
    }

    pub fn orientation(mut self, value: Quaternion<f32>) -> Self {
        self.transform.orientation = value;
        self
    }

    pub fn rotation(mut self, angle_radians: f32) -> Self {
        self.transform.orientation = Quaternion::rotation_z(angle_radians);
        self
    }

    pub fn scale(mut self, value: Vec2<f32>) -> Self {
        self.transform.scale = Vec3::new(value.x, value.y, 1.0);
        self
    }

    pub fn size(mut self, value: Vec2<f32>) -> Self {
        self.size = value;
        self
    }

    pub fn pivot(mut self, value: Vec2<f32>) -> Self {
        self.pivot = value;
        self
    }
}

pub struct ParticleDraw<'a, I: IntoIterator<Item = ParticleInstance>> {
    emitter: &'a ParticleEmitter,
    instances: RefCell<Option<I>>,
}

impl<I: IntoIterator<Item = ParticleInstance>> Drawable for ParticleDraw<'_, I> {
    fn draw(&self, context: &mut DrawContext, graphics: &mut dyn GraphicsTarget<Vertex>) {
        let instances = match self.instances.borrow_mut().take() {
            Some(instances) => instances,
            None => return,
        };
        let batch = GraphicsBatch {
            shader: context.shader(self.emitter.shader.as_ref()),
            uniforms: self
                .emitter
                .uniforms
                .iter()
                .map(|(k, v)| (k.clone(), v.to_owned()))
                .chain(std::iter::once((
                    "u_projection_view".into(),
                    GlowUniformValue::M4(
                        if self.emitter.screen_space {
                            graphics.state().main_camera.screen_matrix()
                        } else {
                            graphics.state().main_camera.world_matrix()
                        }
                        .into_col_array(),
                    ),
                )))
                .chain(
                    self.emitter
                        .textures
                        .iter()
                        .enumerate()
                        .map(|(index, texture)| {
                            (texture.sampler.clone(), GlowUniformValue::I1(index as _))
                        }),
                )
                .collect(),
            textures: self
                .emitter
                .textures
                .iter()
                .filter_map(|texture| {
                    Some((context.texture(Some(&texture.texture))?, texture.filtering))
                })
                .collect(),
            blending: self
                .emitter
                .blending
                .unwrap_or_else(|| context.top_blending()),
            scissor: None,
            wireframe: context.wireframe,
        };
        graphics.state_mut().stream.batch_optimized(batch);
        let parent = context.top_transform();
        for instance in instances {
            let transform = parent * transform_to_matrix(instance.transform);
            let offset = instance.size * instance.pivot;
            let color = instance.tint.into_array();
            graphics.state_mut().stream.transformed(
                |stream| {
                    stream.quad([
                        Vertex {
                            position: [0.0, 0.0],
                            uv: [instance.region.x, instance.region.y, instance.page],
                            color,
                        },
                        Vertex {
                            position: [instance.size.x, 0.0],
                            uv: [
                                instance.region.x + instance.region.w,
                                instance.region.y,
                                instance.page,
                            ],
                            color,
                        },
                        Vertex {
                            position: [instance.size.x, instance.size.y],
                            uv: [
                                instance.region.x + instance.region.w,
                                instance.region.y + instance.region.h,
                                instance.page,
                            ],
                            color,
                        },
                        Vertex {
                            position: [0.0, instance.size.y],
                            uv: [
                                instance.region.x,
                                instance.region.y + instance.region.h,
                                instance.page,
                            ],
                            color,
                        },
                    ]);
                },
                |vertex| {
                    let point = transform.mul_point(Vec2::from(vertex.position) - offset);
                    vertex.position[0] = point.x;
                    vertex.position[1] = point.y;
                },
            );
        }
    }
}

pub trait ParticleSystemProcessor<D, C> {
    fn process(config: &C, data: D) -> Option<D>;
    fn emit(config: &C, data: &D) -> Option<ParticleInstance>;
}

pub struct ParticleSystem<P: ParticleSystemProcessor<D, C>, D, C> {
    pub config: C,
    source: Vec<D>,
    target: Vec<D>,
    _phantom: PhantomData<fn() -> P>,
}

impl<P: ParticleSystemProcessor<D, C>, D, C> ParticleSystem<P, D, C> {
    pub fn new(config: C, capacity: usize) -> Self {
        Self {
            config,
            source: Vec::with_capacity(capacity),
            target: Vec::with_capacity(capacity),
            _phantom: Default::default(),
        }
    }

    pub fn len(&self) -> usize {
        self.source.len()
    }

    pub fn is_empty(&self) -> bool {
        self.source.is_empty()
    }

    pub fn push(&mut self, data: D) {
        if self.source.len() < self.source.capacity() {
            self.source.push(data);
        }
    }

    pub fn extend(&mut self, iter: impl IntoIterator<Item = D>) {
        self.source.extend(iter);
    }

    pub fn clear(&mut self) {
        self.source.clear();
        self.target.clear();
    }

    pub fn process(&mut self) {
        self.target.clear();
        self.target.reserve(self.source.len());
        for item in self.source.drain(..) {
            if let Some(item) = P::process(&self.config, item) {
                self.target.push(item);
            }
        }
        std::mem::swap(&mut self.source, &mut self.target);
    }

    pub fn emit(&self) -> impl Iterator<Item = ParticleInstance> + '_ {
        self.source
            .iter()
            .filter_map(|item| P::emit(&self.config, item))
    }
}

impl<P: ParticleSystemProcessor<D, C>, D: std::fmt::Debug, C: std::fmt::Debug> std::fmt::Debug
    for ParticleSystem<P, D, C>
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ParticleSystem")
            .field("config", &self.config)
            .field("data", &self.source)
            .finish_non_exhaustive()
    }
}