1#[allow(dead_code)]
4#[derive(Clone, Copy, PartialEq, Debug)]
5pub enum ParticleBlend {
6 Additive,
7 AlphaBlend,
8 Multiply,
9}
10
11#[allow(dead_code)]
12#[derive(Clone)]
13pub struct RenderParticle {
14 pub position: [f32; 3],
15 pub size: f32,
16 pub color: [f32; 4],
17 pub rotation: f32,
18 pub tex_frame: u32,
19 pub age: f32,
20 pub lifetime: f32,
21}
22
23#[allow(dead_code)]
24pub struct ParticleSystem {
25 pub particles: Vec<RenderParticle>,
26 pub blend_mode: ParticleBlend,
27 pub texture_id: u32,
28 pub sort_by_depth: bool,
29 pub max_particles: usize,
30 pub camera_position: [f32; 3],
31}
32
33#[allow(dead_code)]
34pub fn new_particle_system(max: usize, blend: ParticleBlend) -> ParticleSystem {
35 ParticleSystem {
36 particles: Vec::with_capacity(max),
37 blend_mode: blend,
38 texture_id: 0,
39 sort_by_depth: false,
40 max_particles: max,
41 camera_position: [0.0, 0.0, 0.0],
42 }
43}
44
45#[allow(dead_code)]
47pub fn emit_particle(sys: &mut ParticleSystem, p: RenderParticle) -> bool {
48 if sys.particles.len() >= sys.max_particles {
49 return false;
50 }
51 sys.particles.push(p);
52 true
53}
54
55#[allow(dead_code)]
57pub fn update_particles(sys: &mut ParticleSystem, dt: f32) {
58 for p in sys.particles.iter_mut() {
59 p.age += dt;
60 }
61 sys.particles.retain(|p| p.age < p.lifetime);
62}
63
64#[allow(dead_code)]
66pub fn sort_particles_by_depth(sys: &mut ParticleSystem) {
67 let cam = sys.camera_position;
68 sys.particles.sort_by(|a, b| {
69 let da = dist_sq(a.position, cam);
70 let db = dist_sq(b.position, cam);
71 db.partial_cmp(&da).unwrap_or(std::cmp::Ordering::Equal)
73 });
74}
75
76fn dist_sq(a: [f32; 3], b: [f32; 3]) -> f32 {
77 let dx = a[0] - b[0];
78 let dy = a[1] - b[1];
79 let dz = a[2] - b[2];
80 dx * dx + dy * dy + dz * dz
81}
82
83#[allow(dead_code)]
84pub fn particle_count(sys: &ParticleSystem) -> usize {
85 sys.particles.len()
86}
87
88#[allow(dead_code)]
89pub fn alive_particle_count(sys: &ParticleSystem) -> usize {
90 sys.particles
91 .iter()
92 .filter(|p| is_particle_alive(p))
93 .count()
94}
95
96#[allow(dead_code)]
98pub fn particles_as_quads(sys: &ParticleSystem) -> Vec<[[f32; 3]; 4]> {
99 let right = [1.0_f32, 0.0, 0.0];
101 let up = [0.0_f32, 1.0, 0.0];
102 sys.particles
103 .iter()
104 .filter(|p| is_particle_alive(p))
105 .map(|p| billboard_corners(p.position, p.size, right, up))
106 .collect()
107}
108
109#[allow(dead_code)]
111pub fn billboard_corners(pos: [f32; 3], size: f32, right: [f32; 3], up: [f32; 3]) -> [[f32; 3]; 4] {
112 let half = size * 0.5;
113 let r = [right[0] * half, right[1] * half, right[2] * half];
114 let u = [up[0] * half, up[1] * half, up[2] * half];
115 [
116 [
117 pos[0] - r[0] - u[0],
118 pos[1] - r[1] - u[1],
119 pos[2] - r[2] - u[2],
120 ], [
122 pos[0] + r[0] - u[0],
123 pos[1] + r[1] - u[1],
124 pos[2] + r[2] - u[2],
125 ], [
127 pos[0] + r[0] + u[0],
128 pos[1] + r[1] + u[1],
129 pos[2] + r[2] + u[2],
130 ], [
132 pos[0] - r[0] + u[0],
133 pos[1] - r[1] + u[1],
134 pos[2] - r[2] + u[2],
135 ], ]
137}
138
139#[allow(dead_code)]
141pub fn particle_uv_frame(frame: u32, atlas_cols: u32, atlas_rows: u32) -> [f32; 4] {
142 let cols = atlas_cols.max(1);
143 let rows = atlas_rows.max(1);
144 let col = frame % cols;
145 let row = frame / cols % rows;
146 let inv_cols = 1.0 / cols as f32;
147 let inv_rows = 1.0 / rows as f32;
148 let u0 = col as f32 * inv_cols;
149 let v0 = row as f32 * inv_rows;
150 let u1 = u0 + inv_cols;
151 let v1 = v0 + inv_rows;
152 [u0, v0, u1, v1]
153}
154
155#[allow(dead_code)]
156pub fn clear_particles(sys: &mut ParticleSystem) {
157 sys.particles.clear();
158}
159
160#[allow(dead_code)]
161pub fn is_particle_alive(p: &RenderParticle) -> bool {
162 p.age < p.lifetime
163}
164
165#[allow(dead_code)]
167pub fn particle_normalized_age(p: &RenderParticle) -> f32 {
168 if p.lifetime <= 0.0 {
169 return 1.0;
170 }
171 (p.age / p.lifetime).clamp(0.0, 1.0)
172}
173
174#[allow(dead_code)]
175pub fn average_particle_age(sys: &ParticleSystem) -> f32 {
176 if sys.particles.is_empty() {
177 return 0.0;
178 }
179 let total: f32 = sys.particles.iter().map(|p| p.age).sum();
180 total / sys.particles.len() as f32
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186
187 fn make_particle(age: f32, lifetime: f32) -> RenderParticle {
188 RenderParticle {
189 position: [0.0, 0.0, 0.0],
190 size: 1.0,
191 color: [1.0, 1.0, 1.0, 1.0],
192 rotation: 0.0,
193 tex_frame: 0,
194 age,
195 lifetime,
196 }
197 }
198
199 #[test]
200 fn test_new_particle_system() {
201 let sys = new_particle_system(100, ParticleBlend::Additive);
202 assert_eq!(sys.max_particles, 100);
203 assert_eq!(sys.blend_mode, ParticleBlend::Additive);
204 assert!(sys.particles.is_empty());
205 }
206
207 #[test]
208 fn test_emit_particle() {
209 let mut sys = new_particle_system(2, ParticleBlend::AlphaBlend);
210 let p = make_particle(0.0, 1.0);
211 assert!(emit_particle(&mut sys, p.clone()));
212 assert!(emit_particle(&mut sys, p.clone()));
213 assert!(!emit_particle(&mut sys, p));
215 }
216
217 #[test]
218 fn test_update_particles_ages() {
219 let mut sys = new_particle_system(10, ParticleBlend::Additive);
220 emit_particle(&mut sys, make_particle(0.0, 2.0));
221 update_particles(&mut sys, 0.5);
222 assert!((sys.particles[0].age - 0.5).abs() < f32::EPSILON);
223 }
224
225 #[test]
226 fn test_update_particles_removes_dead() {
227 let mut sys = new_particle_system(10, ParticleBlend::Additive);
228 emit_particle(&mut sys, make_particle(0.0, 0.1));
229 emit_particle(&mut sys, make_particle(0.0, 5.0));
230 update_particles(&mut sys, 0.5);
231 assert_eq!(particle_count(&sys), 1);
232 }
233
234 #[test]
235 fn test_particle_count() {
236 let mut sys = new_particle_system(10, ParticleBlend::Additive);
237 assert_eq!(particle_count(&sys), 0);
238 emit_particle(&mut sys, make_particle(0.0, 1.0));
239 assert_eq!(particle_count(&sys), 1);
240 }
241
242 #[test]
243 fn test_alive_particle_count() {
244 let mut sys = new_particle_system(10, ParticleBlend::Additive);
245 emit_particle(&mut sys, make_particle(0.5, 1.0)); emit_particle(&mut sys, make_particle(2.0, 1.0)); assert_eq!(alive_particle_count(&sys), 1);
248 }
249
250 #[test]
251 fn test_billboard_corners_produces_4_points() {
252 let corners = billboard_corners([0.0, 0.0, 0.0], 1.0, [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]);
253 assert_eq!(corners.len(), 4);
254 assert_ne!(corners[0], corners[1]);
256 assert_ne!(corners[0], corners[2]);
257 }
258
259 #[test]
260 fn test_particle_uv_frame() {
261 let uv = particle_uv_frame(0, 4, 4);
263 assert!((uv[0] - 0.0).abs() < f32::EPSILON);
264 assert!((uv[1] - 0.0).abs() < f32::EPSILON);
265 assert!((uv[2] - 0.25).abs() < f32::EPSILON);
266 assert!((uv[3] - 0.25).abs() < f32::EPSILON);
267 }
268
269 #[test]
270 fn test_particle_uv_frame_second() {
271 let uv = particle_uv_frame(1, 4, 4);
273 assert!((uv[0] - 0.25).abs() < f32::EPSILON);
274 assert!((uv[2] - 0.5).abs() < f32::EPSILON);
275 }
276
277 #[test]
278 fn test_clear_particles() {
279 let mut sys = new_particle_system(10, ParticleBlend::Additive);
280 emit_particle(&mut sys, make_particle(0.0, 1.0));
281 clear_particles(&mut sys);
282 assert_eq!(particle_count(&sys), 0);
283 }
284
285 #[test]
286 fn test_is_particle_alive() {
287 let alive = make_particle(0.5, 1.0);
288 let dead = make_particle(1.5, 1.0);
289 assert!(is_particle_alive(&alive));
290 assert!(!is_particle_alive(&dead));
291 }
292
293 #[test]
294 fn test_particle_normalized_age() {
295 let p = make_particle(0.5, 2.0);
296 let n = particle_normalized_age(&p);
297 assert!((n - 0.25).abs() < f32::EPSILON);
298 }
299
300 #[test]
301 fn test_particle_normalized_age_clamped() {
302 let p = make_particle(5.0, 1.0);
303 assert!((particle_normalized_age(&p) - 1.0).abs() < f32::EPSILON);
304 }
305
306 #[test]
307 fn test_average_particle_age() {
308 let mut sys = new_particle_system(10, ParticleBlend::Additive);
309 emit_particle(&mut sys, make_particle(1.0, 5.0));
310 emit_particle(&mut sys, make_particle(3.0, 5.0));
311 let avg = average_particle_age(&sys);
312 assert!((avg - 2.0).abs() < f32::EPSILON);
313 }
314
315 #[test]
316 fn test_average_particle_age_empty() {
317 let sys = new_particle_system(10, ParticleBlend::Additive);
318 assert!((average_particle_age(&sys)).abs() < f32::EPSILON);
319 }
320}