ferravex_play/
particles.rs1#[derive(Clone, Copy, Debug, PartialEq)]
3pub struct ParticleDesc {
4 pub position: [f32; 3],
5 pub velocity: [f32; 3],
6 pub acceleration: [f32; 3],
7 pub lifetime: f32,
8 pub start_color: [f32; 4],
9 pub end_color: [f32; 4],
10 pub start_size: f32,
11 pub end_size: f32,
12}
13impl Default for ParticleDesc {
14 fn default() -> Self {
15 Self {
16 position: [0.0; 3],
17 velocity: [0.0; 3],
18 acceleration: [0.0; 3],
19 lifetime: 1.0,
20 start_color: [1.0; 4],
21 end_color: [1.0, 1.0, 1.0, 0.0],
22 start_size: 1.0,
23 end_size: 0.0,
24 }
25 }
26}
27#[derive(Clone, Copy, Debug, PartialEq, Eq)]
28pub enum ParticleError {
29 Full,
30 InvalidParticle,
31 InvalidDelta,
32}
33#[derive(Clone, Copy, Debug, PartialEq)]
34pub struct ParticleView {
35 pub position: [f32; 3],
36 pub color: [f32; 4],
37 pub size: f32,
38 pub normalized_age: f32,
39}
40struct Particle {
41 desc: ParticleDesc,
42 age: f32,
43}
44pub struct ParticlePool {
45 particles: Vec<Particle>,
46 limit: usize,
47 dropped: u64,
48}
49impl ParticlePool {
50 pub fn new(capacity: usize) -> Self {
51 Self {
52 particles: Vec::with_capacity(capacity),
53 limit: capacity,
54 dropped: 0,
55 }
56 }
57 pub fn len(&self) -> usize {
58 self.particles.len()
59 }
60 pub fn is_empty(&self) -> bool {
61 self.particles.is_empty()
62 }
63 pub fn capacity(&self) -> usize {
64 self.limit
65 }
66 pub fn dropped(&self) -> u64 {
67 self.dropped
68 }
69 pub fn clear(&mut self) {
70 self.particles.clear();
71 }
72 pub fn spawn(&mut self, desc: ParticleDesc) -> Result<(), ParticleError> {
73 if !desc
74 .position
75 .into_iter()
76 .chain(desc.velocity)
77 .chain(desc.acceleration)
78 .chain(desc.start_color)
79 .chain(desc.end_color)
80 .chain([desc.lifetime, desc.start_size, desc.end_size])
81 .all(f32::is_finite)
82 || desc.lifetime <= 0.0
83 || desc.start_size < 0.0
84 || desc.end_size < 0.0
85 {
86 return Err(ParticleError::InvalidParticle);
87 }
88 if self.particles.len() == self.limit {
89 self.dropped = self.dropped.saturating_add(1);
90 return Err(ParticleError::Full);
91 }
92 self.particles.push(Particle { desc, age: 0.0 });
93 Ok(())
94 }
95 pub fn update(&mut self, dt: f32) -> Result<(), ParticleError> {
96 if !dt.is_finite() || dt < 0.0 {
97 return Err(ParticleError::InvalidDelta);
98 }
99 let mut i = 0;
100 while i < self.particles.len() {
101 let particle = &mut self.particles[i];
102 particle.age += dt;
103 if particle.age >= particle.desc.lifetime {
104 self.particles.swap_remove(i);
105 continue;
106 }
107 for axis in 0..3 {
108 particle.desc.position[axis] += particle.desc.velocity[axis] * dt
110 + particle.desc.acceleration[axis] * (0.5 * dt * dt);
111 particle.desc.velocity[axis] += particle.desc.acceleration[axis] * dt;
112 }
113 if !particle
114 .desc
115 .position
116 .into_iter()
117 .chain(particle.desc.velocity)
118 .all(f32::is_finite)
119 {
120 self.particles.swap_remove(i);
121 continue;
122 }
123 i += 1;
124 }
125 Ok(())
126 }
127 pub fn iter(&self) -> impl Iterator<Item = ParticleView> + '_ {
128 self.particles.iter().map(|p| {
129 let age = (p.age / p.desc.lifetime).clamp(0.0, 1.0);
130 ParticleView {
131 position: p.desc.position,
132 color: std::array::from_fn(|i| {
133 p.desc.start_color[i] * (1.0 - age) + p.desc.end_color[i] * age
134 }),
135 size: p.desc.start_size * (1.0 - age) + p.desc.end_size * age,
136 normalized_age: age,
137 }
138 })
139 }
140}
141#[cfg(test)]
142mod tests {
143 use super::*;
144 #[test]
145 fn capacity_is_bounded_and_expired_slots_are_reusable() {
146 let mut pool = ParticlePool::new(2);
147 pool.spawn(ParticleDesc::default()).unwrap();
148 pool.spawn(ParticleDesc::default()).unwrap();
149 assert_eq!(
150 pool.spawn(ParticleDesc::default()),
151 Err(ParticleError::Full)
152 );
153 assert_eq!(pool.dropped(), 1);
154 pool.update(1.0).unwrap();
155 assert!(pool.is_empty());
156 pool.spawn(ParticleDesc::default()).unwrap();
157 assert_eq!(pool.len(), 1);
158 assert_eq!(pool.capacity(), 2);
159 }
160 #[test]
161 fn constant_acceleration_matches_frame_partition_and_color_fades() {
162 let desc = ParticleDesc {
163 velocity: [2.0, 0.0, 0.0],
164 acceleration: [0.0, -4.0, 0.0],
165 lifetime: 2.0,
166 ..ParticleDesc::default()
167 };
168 let mut a = ParticlePool::new(1);
169 let mut b = ParticlePool::new(1);
170 a.spawn(desc).unwrap();
171 b.spawn(desc).unwrap();
172 a.update(1.0).unwrap();
173 for _ in 0..10 {
174 b.update(0.1).unwrap();
175 }
176 let av = a.iter().next().unwrap();
177 let bv = b.iter().next().unwrap();
178 assert_eq!(av.position, [2.0, -2.0, 0.0]);
179 for i in 0..3 {
180 assert!((av.position[i] - bv.position[i]).abs() < 1e-5);
181 }
182 assert_eq!(av.color[3], 0.5);
183 }
184 #[test]
185 fn invalid_values_do_not_poison_pool() {
186 let mut pool = ParticlePool::new(1);
187 assert_eq!(
188 pool.spawn(ParticleDesc {
189 lifetime: f32::NAN,
190 ..ParticleDesc::default()
191 }),
192 Err(ParticleError::InvalidParticle)
193 );
194 pool.spawn(ParticleDesc::default()).unwrap();
195 assert_eq!(pool.update(-1.0), Err(ParticleError::InvalidDelta));
196 assert_eq!(pool.iter().next().unwrap().normalized_age, 0.0);
197 }
198}