1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
27use nalgebra::{Point3, Vector3};
28
29use crate::billboard::Billboard;
30
31#[derive(Clone, Copy)]
33pub struct Particle {
34 pub position: Point3<f32>,
36 pub velocity: Vector3<f32>,
38 pub acceleration: Vector3<f32>,
40 pub color_start: Rgb565,
42 pub color_end: Rgb565,
44 pub size_start: f32,
46 pub size_end: f32,
48 pub age: f32,
50 pub lifetime: f32,
52 pub(crate) active: bool,
53}
54
55impl Particle {
56 #[inline]
58 pub fn t(&self) -> f32 {
59 if self.lifetime > 0.0 {
60 (self.age / self.lifetime).clamp(0.0, 1.0)
61 } else {
62 1.0
63 }
64 }
65
66 #[inline]
68 pub fn color(&self) -> Rgb565 {
69 let t = self.t();
70 let r = lerp_ch(self.color_start.r(), self.color_end.r(), t, 31);
71 let g = lerp_ch(self.color_start.g(), self.color_end.g(), t, 63);
72 let b = lerp_ch(self.color_start.b(), self.color_end.b(), t, 31);
73 Rgb565::new(r, g, b)
74 }
75
76 #[inline]
78 pub fn size(&self) -> f32 {
79 self.size_start + self.t() * (self.size_end - self.size_start)
80 }
81}
82
83#[inline]
84fn lerp_ch(start: u8, end: u8, t: f32, max: u8) -> u8 {
85 (start as f32 + t * (end as f32 - start as f32)).clamp(0.0, max as f32) as u8
86}
87
88#[derive(Clone, Copy)]
90pub struct ParticleSpawn {
91 pub position: Point3<f32>,
93 pub velocity: Vector3<f32>,
95 pub acceleration: Vector3<f32>,
97 pub color_start: Rgb565,
99 pub color_end: Rgb565,
101 pub size_start: f32,
103 pub size_end: f32,
105 pub lifetime: f32,
107}
108
109pub struct ParticleSystem<const N: usize> {
114 slots: [Particle; N],
115 active: usize,
116}
117
118impl<const N: usize> ParticleSystem<N> {
119 pub fn new() -> Self {
121 Self {
122 slots: core::array::from_fn(|_| Particle {
123 position: Point3::new(0.0, 0.0, 0.0),
124 velocity: Vector3::new(0.0, 0.0, 0.0),
125 acceleration: Vector3::new(0.0, 0.0, 0.0),
126 color_start: Rgb565::new(0, 0, 0),
127 color_end: Rgb565::new(0, 0, 0),
128 size_start: 0.0,
129 size_end: 0.0,
130 age: 0.0,
131 lifetime: 0.0,
132 active: false,
133 }),
134 active: 0,
135 }
136 }
137
138 pub fn spawn(&mut self, s: ParticleSpawn) -> bool {
141 for slot in &mut self.slots {
142 if !slot.active {
143 *slot = Particle {
144 position: s.position,
145 velocity: s.velocity,
146 acceleration: s.acceleration,
147 color_start: s.color_start,
148 color_end: s.color_end,
149 size_start: s.size_start,
150 size_end: s.size_end,
151 age: 0.0,
152 lifetime: s.lifetime,
153 active: true,
154 };
155 self.active += 1;
156 return true;
157 }
158 }
159 false
160 }
161
162 pub fn update(&mut self, dt: f32, gravity: Vector3<f32>) {
167 let mut live = 0usize;
168 for slot in &mut self.slots {
169 if !slot.active {
170 continue;
171 }
172 slot.velocity += (slot.acceleration + gravity) * dt;
173 slot.position += slot.velocity * dt;
174 slot.age += dt;
175 if slot.age >= slot.lifetime {
176 slot.active = false;
177 } else {
178 live += 1;
179 }
180 }
181 self.active = live;
182 }
183
184 #[inline]
186 pub fn active_count(&self) -> usize {
187 self.active
188 }
189
190 #[inline]
192 pub fn is_empty(&self) -> bool {
193 self.active == 0
194 }
195
196 pub fn iter_active(&self) -> impl Iterator<Item = &Particle> {
198 self.slots.iter().filter(|p| p.active)
199 }
200
201 pub fn record<const MAX: usize>(
213 &self,
214 engine: &crate::K3dengine,
215 commands: &mut crate::command_buffer::CommandBuffer<MAX>,
216 ) -> usize {
217 use crate::command_buffer::RenderCommand;
218
219 if self.active == 0 {
220 return 0;
221 }
222
223 let camera_up = Vector3::new(0.0, 1.0, 0.0);
224 let vp = engine.camera.vp_matrix;
225 let mut emitted = 0usize;
226
227 for particle in self.iter_active() {
228 let size = particle.size();
229 if size <= 0.0 {
230 continue;
231 }
232 let color = particle.color();
233
234 let billboard = Billboard::new(particle.position, size, color);
235 let quad = billboard.generate_quad(engine.camera.position, camera_up);
236
237 let Some((pts1, _)) = engine.transform_points_with_w(&[0usize, 1, 2], &quad, vp) else {
239 continue;
240 };
241
242 let Some((pts2, _)) = engine.transform_points_with_w(&[0usize, 2, 3], &quad, vp) else {
244 continue;
245 };
246
247 let _ = commands.push(RenderCommand::Draw(
248 crate::DrawPrimitive::ColoredTriangleWithDepth {
249 points: [pts1[0].xy(), pts1[1].xy(), pts1[2].xy()],
250 depths: [pts1[0].z as f32, pts1[1].z as f32, pts1[2].z as f32],
251 color,
252 },
253 ));
254 let _ = commands.push(RenderCommand::Draw(
255 crate::DrawPrimitive::ColoredTriangleWithDepth {
256 points: [pts2[0].xy(), pts2[1].xy(), pts2[2].xy()],
257 depths: [pts2[0].z as f32, pts2[1].z as f32, pts2[2].z as f32],
258 color,
259 },
260 ));
261 emitted += 1;
262 }
263
264 emitted
265 }
266}
267
268impl<const N: usize> Default for ParticleSystem<N> {
269 fn default() -> Self {
270 Self::new()
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 extern crate std;
277 use super::*;
278 use embedded_graphics_core::pixelcolor::WebColors;
279
280 fn default_spawn(lifetime: f32) -> ParticleSpawn {
281 ParticleSpawn {
282 position: Point3::new(0.0, 0.0, 0.0),
283 velocity: Vector3::new(0.0, 1.0, 0.0),
284 acceleration: Vector3::zeros(),
285 color_start: Rgb565::CSS_RED,
286 color_end: Rgb565::CSS_BLUE,
287 size_start: 1.0,
288 size_end: 0.0,
289 lifetime,
290 }
291 }
292
293 #[test]
294 fn test_spawn_increments_count() {
295 let mut sys: ParticleSystem<4> = ParticleSystem::new();
296 assert_eq!(sys.active_count(), 0);
297 assert!(sys.spawn(default_spawn(2.0)));
298 assert_eq!(sys.active_count(), 1);
299 }
300
301 #[test]
302 fn test_pool_full_returns_false() {
303 let mut sys: ParticleSystem<2> = ParticleSystem::new();
304 assert!(sys.spawn(default_spawn(2.0)));
305 assert!(sys.spawn(default_spawn(2.0)));
306 assert!(!sys.spawn(default_spawn(2.0)));
307 }
308
309 #[test]
310 fn test_update_kills_expired_particles() {
311 let mut sys: ParticleSystem<4> = ParticleSystem::new();
312 sys.spawn(default_spawn(0.1));
313 sys.update(0.2, Vector3::zeros());
314 assert_eq!(sys.active_count(), 0);
315 }
316
317 #[test]
318 fn test_update_moves_particle_with_gravity() {
319 let mut sys: ParticleSystem<4> = ParticleSystem::new();
320 sys.spawn(default_spawn(5.0));
321 let y0 = sys.iter_active().next().unwrap().position.y;
322 sys.update(0.1, Vector3::new(0.0, -9.81, 0.0));
323 let y1 = sys.iter_active().next().unwrap().position.y;
324 assert!(y1 != y0);
326 }
327
328 #[test]
329 fn test_slot_reused_after_expiry() {
330 let mut sys: ParticleSystem<1> = ParticleSystem::new();
331 assert!(sys.spawn(default_spawn(0.05)));
332 sys.update(0.1, Vector3::zeros());
333 assert_eq!(sys.active_count(), 0);
334 assert!(sys.spawn(default_spawn(2.0)));
335 assert_eq!(sys.active_count(), 1);
336 }
337
338 #[test]
339 fn test_color_at_birth_equals_start() {
340 let mut sys: ParticleSystem<4> = ParticleSystem::new();
341 sys.spawn(default_spawn(2.0));
342 let p = sys.iter_active().next().unwrap();
343 assert_eq!(p.t(), 0.0);
344 assert_eq!(p.color(), Rgb565::CSS_RED);
345 }
346
347 #[test]
348 fn test_size_shrinks_over_time() {
349 let mut sys: ParticleSystem<4> = ParticleSystem::new();
350 sys.spawn(default_spawn(2.0));
351 let s0 = sys.iter_active().next().unwrap().size();
352 sys.update(1.0, Vector3::zeros());
353 let s1 = sys.iter_active().next().unwrap().size();
354 assert!(s1 < s0);
355 }
356}