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>, pub spread: f32, pub rate: f32, pub time_accum: f32,
21
22 pub color: [u8; 4], pub color_variation: u8, pub lifetime_range: (f32, f32), pub radius_range: (f32, f32), pub speed_range: (f32, f32), pub particles: Vec<Particle>, }
31
32impl ParticleEmitter {
33 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, 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 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 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
109fn random_unit_vector_in_cone(dir: Vec3<f32>, spread: f32) -> Vec3<f32> {
111 let mut rng = rand::rng();
112
113 let theta = rng.random_range(0.0..std::f32::consts::TAU);
115 let phi = rng.random_range(0.0..spread);
116
117 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 align_vector(local, dir)
125}
126
127fn align_vector(v: Vec3<f32>, target: Vec3<f32>) -> Vec3<f32> {
129 let from = Vec3::unit_z(); let to = target.normalized();
131
132 let cos_theta = from.dot(to);
133 if cos_theta > 0.9999 {
134 return v; } else if cos_theta < -0.9999 {
136 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
149fn 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}