Skip to main content

rusterix/map/
particle.rs

1use rand::Rng;
2use theframework::prelude::*;
3use vek::{Mat3, Vec3};
4
5#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
6pub struct Particle {
7    pub pos: Vec3<f32>,
8    pub vel: Vec3<f32>,
9    pub lifetime: f32,
10    pub radius: f32,
11    pub color: [u8; 4],
12}
13
14#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
15pub struct ParticleEmitter {
16    pub origin: Vec3<f32>,
17    pub direction: Vec3<f32>, // Preferred direction (normalized)
18    pub spread: f32,          // Angle in radians (0 = tight beam, PI = full sphere)
19    pub rate: f32,            // Particles per second
20    pub time_accum: f32,
21
22    pub color: [u8; 4],      // Base color
23    pub color_variation: u8, // +/- variation for flicker
24
25    pub lifetime_range: (f32, f32), // Seconds
26    pub radius_range: (f32, f32),   // Radius size range
27    pub speed_range: (f32, f32),    // Velocity magnitude range
28
29    pub particles: Vec<Particle>, // Active particles
30}
31
32impl ParticleEmitter {
33    /// Creates a new ParticleEmitter with default parameters.
34    pub fn new(origin: Vec3<f32>, direction: Vec3<f32>) -> Self {
35        Self {
36            origin,
37            direction: direction.normalized(),
38            spread: std::f32::consts::FRAC_PI_4, // 45° cone by default
39            rate: 30.0,
40            time_accum: 0.0,
41
42            color: [255, 160, 0, 255],
43            color_variation: 30,
44
45            lifetime_range: (0.5, 1.5),
46            radius_range: (0.05, 0.15),
47            speed_range: (0.5, 1.5),
48
49            particles: vec![],
50        }
51    }
52
53    /// Updates the emitter and its particles over time.
54    pub fn update(&mut self, dt: f32) {
55        self.time_accum += dt;
56
57        let emit_count = (self.rate * self.time_accum).floor() as usize;
58        if emit_count > 0 {
59            self.time_accum -= emit_count as f32 / self.rate;
60            for _ in 0..emit_count {
61                self.emit_particle();
62            }
63        }
64
65        self.particles.retain_mut(|p| {
66            p.lifetime -= dt;
67            if p.lifetime > 0.0 {
68                p.pos += p.vel * dt;
69                p.radius *= 0.98;
70                true
71            } else {
72                false
73            }
74        });
75    }
76
77    /// Emits a single new particle with randomized properties.
78    fn emit_particle(&mut self) {
79        let mut rng = rand::rng();
80
81        let angle_offset = random_unit_vector_in_cone(self.direction, self.spread);
82        let speed = rng.random_range(self.speed_range.0..=self.speed_range.1);
83        let velocity = angle_offset * speed;
84
85        let lifetime = rng.random_range(self.lifetime_range.0..=self.lifetime_range.1);
86        let radius = rng.random_range(self.radius_range.0..=self.radius_range.1);
87
88        let mut color = self.color;
89        for i in 0..3 {
90            let v = rng.random_range(
91                (color[i] as i16 - self.color_variation as i16).max(0)
92                    ..=(color[i] as i16 + self.color_variation as i16).min(255),
93            );
94            color[i] = v as u8;
95        }
96
97        let p = Particle {
98            pos: self.origin,
99            vel: velocity,
100            lifetime,
101            radius,
102            color,
103        };
104
105        self.particles.push(p);
106    }
107}
108
109/// Generates a random unit vector within a cone defined by direction and spread.
110fn random_unit_vector_in_cone(dir: Vec3<f32>, spread: f32) -> Vec3<f32> {
111    let mut rng = rand::rng();
112
113    // Generate a random direction in spherical coordinates
114    let theta = rng.random_range(0.0..std::f32::consts::TAU);
115    let phi = rng.random_range(0.0..spread);
116
117    // Local vector
118    let x = phi.sin() * theta.cos();
119    let y = phi.sin() * theta.sin();
120    let z = phi.cos();
121    let local = Vec3::new(x, y, z);
122
123    // Rotate local cone direction to align with `dir`
124    align_vector(local, dir)
125}
126
127/// Rotates vector `v` to align with the target direction.
128fn align_vector(v: Vec3<f32>, target: Vec3<f32>) -> Vec3<f32> {
129    let from = Vec3::unit_z(); // Local cone direction
130    let to = target.normalized();
131
132    let cos_theta = from.dot(to);
133    if cos_theta > 0.9999 {
134        return v; // Already aligned
135    } else if cos_theta < -0.9999 {
136        // 180 degree flip — any perpendicular axis works
137        let up = Vec3::unit_y();
138        let axis = from.cross(up).normalized();
139        let rot = rotation_matrix(axis, std::f32::consts::PI);
140        return rot * v;
141    }
142
143    let axis = from.cross(to).normalized();
144    let angle = cos_theta.acos();
145    let rot = rotation_matrix(axis, angle);
146    rot * v
147}
148
149/// Constructs a rotation matrix from an axis and angle (Rodrigues' formula).
150fn rotation_matrix(axis: Vec3<f32>, angle: f32) -> Mat3<f32> {
151    let (sin, cos) = angle.sin_cos();
152    let one_minus_cos = 1.0 - cos;
153
154    let x = axis.x;
155    let y = axis.y;
156    let z = axis.z;
157
158    Mat3::new(
159        cos + x * x * one_minus_cos,
160        y * x * one_minus_cos + z * sin,
161        z * x * one_minus_cos - y * sin,
162        x * y * one_minus_cos - z * sin,
163        cos + y * y * one_minus_cos,
164        z * y * one_minus_cos + x * sin,
165        x * z * one_minus_cos + y * sin,
166        y * z * one_minus_cos - x * sin,
167        cos + z * z * one_minus_cos,
168    )
169}