1use glam::{Vec2, Vec3, Vec4, Quat, Mat4};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
10pub struct ParticleTag(pub u32);
11
12impl ParticleTag {
13 pub const NONE: ParticleTag = ParticleTag(0);
14 pub const FIRE: ParticleTag = ParticleTag(1 << 0);
15 pub const SMOKE: ParticleTag = ParticleTag(1 << 1);
16 pub const SPARK: ParticleTag = ParticleTag(1 << 2);
17 pub const MAGIC: ParticleTag = ParticleTag(1 << 3);
18 pub const WATER: ParticleTag = ParticleTag(1 << 4);
19 pub const DUST: ParticleTag = ParticleTag(1 << 5);
20 pub const BLOOD: ParticleTag = ParticleTag(1 << 6);
21 pub const DEBRIS: ParticleTag = ParticleTag(1 << 7);
22 pub const ENERGY: ParticleTag = ParticleTag(1 << 8);
23 pub const ALL: ParticleTag = ParticleTag(u32::MAX);
24
25 pub fn contains(self, other: ParticleTag) -> bool {
26 self.0 & other.0 == other.0
27 }
28 pub fn union(self, other: ParticleTag) -> ParticleTag {
29 ParticleTag(self.0 | other.0)
30 }
31}
32
33#[derive(Debug, Clone)]
37pub enum EmitterShape {
38 Point,
40
41 Line {
43 start: Vec3,
44 end: Vec3,
45 endpoints_only: bool,
47 },
48
49 Box {
51 half_extents: Vec3,
52 surface_only: bool,
54 },
55
56 Sphere {
58 radius: f32,
59 inner_radius: f32, hemisphere: bool, },
62
63 Disc {
65 radius: f32,
66 inner_radius: f32,
67 arc_degrees: f32, },
69
70 Cone {
72 angle_degrees: f32,
73 height: f32,
74 base_radius: f32,
75 },
76
77 Torus {
79 major_radius: f32,
80 minor_radius: f32,
81 },
82
83 MeshSurface {
85 vertices: Vec<Vec3>,
87 normals: Vec<Vec3>,
89 area_weights: Vec<f32>,
91 volume_fill: bool,
93 },
94}
95
96impl EmitterShape {
97 pub fn sample(&self, rng: &mut u64) -> (Vec3, Vec3) {
99 match self {
100 EmitterShape::Point => (Vec3::ZERO, Vec3::Y),
101
102 EmitterShape::Line { start, end, endpoints_only } => {
103 let t = if *endpoints_only {
104 if lcg_f32(rng) > 0.5 { 0.0 } else { 1.0 }
105 } else {
106 lcg_f32(rng)
107 };
108 let pos = *start + (*end - *start) * t;
109 let normal = (*end - *start).normalize_or_zero().cross(Vec3::Y).normalize_or_zero();
110 (pos, normal)
111 }
112
113 EmitterShape::Box { half_extents, surface_only } => {
114 if *surface_only {
115 let face = (lcg_f32(rng) * 6.0) as usize % 6;
117 let axis = face / 2;
118 let sign = if face % 2 == 0 { 1.0_f32 } else { -1.0 };
119 let u = lcg_f32(rng) * 2.0 - 1.0;
120 let v = lcg_f32(rng) * 2.0 - 1.0;
121 let mut pos = Vec3::ZERO;
122 let mut normal = Vec3::ZERO;
123 match axis {
124 0 => { pos = Vec3::new(sign * half_extents.x, u * half_extents.y, v * half_extents.z); normal = Vec3::new(sign, 0.0, 0.0); }
125 1 => { pos = Vec3::new(u * half_extents.x, sign * half_extents.y, v * half_extents.z); normal = Vec3::new(0.0, sign, 0.0); }
126 _ => { pos = Vec3::new(u * half_extents.x, v * half_extents.y, sign * half_extents.z); normal = Vec3::new(0.0, 0.0, sign); }
127 }
128 (pos, normal)
129 } else {
130 let pos = Vec3::new(
131 (lcg_f32(rng) * 2.0 - 1.0) * half_extents.x,
132 (lcg_f32(rng) * 2.0 - 1.0) * half_extents.y,
133 (lcg_f32(rng) * 2.0 - 1.0) * half_extents.z,
134 );
135 (pos, Vec3::Y)
136 }
137 }
138
139 EmitterShape::Sphere { radius, inner_radius, hemisphere } => {
140 let theta = lcg_f32(rng) * std::f32::consts::TAU;
141 let phi = if *hemisphere {
142 lcg_f32(rng) * std::f32::consts::FRAC_PI_2
143 } else {
144 (lcg_f32(rng) * 2.0 - 1.0).acos()
145 };
146 let r = inner_radius + (radius - inner_radius) * lcg_f32(rng);
147 let normal = Vec3::new(phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin());
148 (normal * r, normal)
149 }
150
151 EmitterShape::Disc { radius, inner_radius, arc_degrees } => {
152 let arc = arc_degrees.to_radians();
153 let angle = lcg_f32(rng) * arc;
154 let r = (inner_radius + (radius - inner_radius) * lcg_f32(rng).sqrt()).max(0.0);
155 let pos = Vec3::new(angle.cos() * r, 0.0, angle.sin() * r);
156 (pos, Vec3::Y)
157 }
158
159 EmitterShape::Cone { angle_degrees, height, base_radius } => {
160 let h = lcg_f32(rng) * height;
161 let max_r_at_h = base_radius * (h / height.max(0.001));
162 let angle = lcg_f32(rng) * std::f32::consts::TAU;
163 let r = max_r_at_h * lcg_f32(rng).sqrt();
164 let half_angle = angle_degrees.to_radians() * 0.5;
165 let normal = Vec3::new(
166 half_angle.sin() * angle.cos(),
167 half_angle.cos(),
168 half_angle.sin() * angle.sin(),
169 ).normalize_or_zero();
170 let pos = Vec3::new(angle.cos() * r, h, angle.sin() * r);
171 (pos, normal)
172 }
173
174 EmitterShape::Torus { major_radius, minor_radius } => {
175 let theta = lcg_f32(rng) * std::f32::consts::TAU;
176 let phi = lcg_f32(rng) * std::f32::consts::TAU;
177 let center = Vec3::new(theta.cos() * major_radius, 0.0, theta.sin() * major_radius);
178 let normal = Vec3::new(theta.cos() * phi.cos(), phi.sin(), theta.sin() * phi.cos());
179 let pos = center + normal * *minor_radius;
180 (pos, normal)
181 }
182
183 EmitterShape::MeshSurface { vertices, normals, area_weights, volume_fill } => {
184 if vertices.len() < 3 || area_weights.is_empty() {
185 return (Vec3::ZERO, Vec3::Y);
186 }
187 let target = lcg_f32(rng) * area_weights.last().copied().unwrap_or(1.0);
188 let tri_idx = area_weights.partition_point(|&w| w < target).min(area_weights.len() - 1);
189 let base = tri_idx * 3;
190 if base + 2 >= vertices.len() {
191 return (Vec3::ZERO, Vec3::Y);
192 }
193 let a = vertices[base];
194 let b = vertices[base + 1];
195 let c = vertices[base + 2];
196 let na = normals.get(base).copied().unwrap_or(Vec3::Y);
197 let nb = normals.get(base + 1).copied().unwrap_or(Vec3::Y);
198 let nc = normals.get(base + 2).copied().unwrap_or(Vec3::Y);
199 let u = lcg_f32(rng);
200 let v = lcg_f32(rng) * (1.0 - u);
201 let w = 1.0 - u - v;
202 let pos = a * u + b * v + c * w;
203 let normal = (na * u + nb * v + nc * w).normalize_or_zero();
204 if *volume_fill {
205 let centroid = (a + b + c) / 3.0;
206 let offset = (pos - centroid) * lcg_f32(rng);
207 (centroid + offset, normal)
208 } else {
209 (pos, normal)
210 }
211 }
212 }
213 }
214}
215
216#[inline]
219pub fn lcg_next(state: &mut u64) -> u64 {
220 *state = state.wrapping_mul(6364136223846793005).wrapping_add(1442695040888963407);
221 *state
222}
223
224#[inline]
225pub fn lcg_f32(state: &mut u64) -> f32 {
226 (lcg_next(state) >> 33) as f32 / (1u64 << 31) as f32
227}
228
229#[inline]
230pub fn lcg_range(state: &mut u64, min: f32, max: f32) -> f32 {
231 min + lcg_f32(state) * (max - min)
232}
233
234#[derive(Debug, Clone)]
238pub enum SpawnCurve {
239 Constant(f32),
241 Linear { start: f32, end: f32 },
243 SmoothStep { start: f32, peak: f32, end: f32 },
245 Keyframes(Vec<(f32, f32)>),
247 PeriodBurst { period: f32, burst_count: u32, timer: f32 },
249}
250
251impl SpawnCurve {
252 pub fn rate_at(&self, t: f32) -> f32 {
254 match self {
255 SpawnCurve::Constant(r) => *r,
256 SpawnCurve::Linear { start, end } => start + t * (end - start),
257 SpawnCurve::SmoothStep { start, peak, end } => {
258 if t < 0.5 {
259 let s = t * 2.0;
260 start + s * s * (3.0 - 2.0 * s) * (peak - start)
261 } else {
262 let s = (t - 0.5) * 2.0;
263 peak + s * s * (3.0 - 2.0 * s) * (end - peak)
264 }
265 }
266 SpawnCurve::Keyframes(kf) => {
267 if kf.is_empty() { return 0.0; }
268 if kf.len() == 1 { return kf[0].1; }
269 let i = kf.partition_point(|(kt, _)| *kt <= t);
270 if i == 0 { return kf[0].1; }
271 if i >= kf.len() { return kf.last().unwrap().1; }
272 let (t0, r0) = kf[i - 1];
273 let (t1, r1) = kf[i];
274 let frac = (t - t0) / (t1 - t0).max(1e-6);
275 r0 + frac * (r1 - r0)
276 }
277 SpawnCurve::PeriodBurst { period: _, burst_count, timer: _ } => *burst_count as f32,
278 }
279 }
280}
281
282#[derive(Debug, Clone, PartialEq)]
286pub enum SpawnMode {
287 Continuous,
289 Burst { count: u32 },
291 BurstOverTime { count: u32, duration: f32, emitted: u32 },
293}
294
295#[derive(Debug, Clone)]
299pub struct LodLevel {
300 pub distance: f32,
302 pub count_scale: f32,
304 pub rate_scale: f32,
306 pub size_scale: f32,
308}
309
310impl LodLevel {
311 pub fn new(distance: f32, count_scale: f32) -> Self {
312 Self { distance, count_scale, rate_scale: count_scale, size_scale: 1.0 }
313 }
314 pub fn with_size_scale(mut self, s: f32) -> Self { self.size_scale = s; self }
315}
316
317#[derive(Debug, Clone)]
319pub struct LodController {
320 pub levels: Vec<LodLevel>,
322 pub current_distance: f32,
323 pub enabled: bool,
324}
325
326impl LodController {
327 pub fn new() -> Self {
328 Self {
329 levels: vec![
330 LodLevel::new(0.0, 1.0),
331 LodLevel::new(20.0, 0.7),
332 LodLevel::new(50.0, 0.4),
333 LodLevel::new(100.0, 0.15),
334 LodLevel::new(200.0, 0.0),
335 ],
336 current_distance: 0.0,
337 enabled: true,
338 }
339 }
340
341 pub fn with_levels(mut self, levels: Vec<LodLevel>) -> Self {
342 self.levels = levels;
343 self.levels.sort_by(|a, b| a.distance.partial_cmp(&b.distance).unwrap());
344 self
345 }
346
347 pub fn update_distance(&mut self, emitter_pos: Vec3, camera_pos: Vec3) {
348 self.current_distance = (emitter_pos - camera_pos).length();
349 }
350
351 fn active_level(&self) -> &LodLevel {
352 if !self.enabled || self.levels.is_empty() {
353 return &LodLevel { distance: 0.0, count_scale: 1.0, rate_scale: 1.0, size_scale: 1.0 };
354 }
355 let mut best = &self.levels[0];
357 for lv in &self.levels {
358 if self.current_distance >= lv.distance {
359 best = lv;
360 }
361 }
362 best
363 }
364
365 pub fn count_scale(&self) -> f32 { self.active_level().count_scale }
366 pub fn rate_scale(&self) -> f32 { self.active_level().rate_scale }
367 pub fn size_scale(&self) -> f32 { self.active_level().size_scale }
368 pub fn is_culled(&self) -> bool { self.active_level().count_scale <= 0.0 }
369}
370
371impl Default for LodController {
372 fn default() -> Self { Self::new() }
373}
374
375#[derive(Debug, Clone)]
379pub struct TransformKeyframe {
380 pub time: f32,
381 pub position: Vec3,
382 pub rotation: Quat,
383 pub scale: Vec3,
384}
385
386#[derive(Debug, Clone)]
388pub struct EmitterTransformAnim {
389 pub keyframes: Vec<TransformKeyframe>,
390 pub looping: bool,
391 pub time: f32,
392 pub duration: f32,
393 pub playing: bool,
394}
395
396impl EmitterTransformAnim {
397 pub fn new(duration: f32) -> Self {
398 Self { keyframes: Vec::new(), looping: false, time: 0.0, duration, playing: true }
399 }
400
401 pub fn add_keyframe(&mut self, kf: TransformKeyframe) {
402 self.keyframes.push(kf);
403 self.keyframes.sort_by(|a, b| a.time.partial_cmp(&b.time).unwrap());
404 }
405
406 pub fn tick(&mut self, dt: f32) {
407 if !self.playing { return; }
408 self.time += dt;
409 if self.looping {
410 self.time %= self.duration.max(0.001);
411 } else {
412 self.time = self.time.min(self.duration);
413 }
414 }
415
416 pub fn sample(&self) -> Mat4 {
418 if self.keyframes.is_empty() {
419 return Mat4::IDENTITY;
420 }
421 if self.keyframes.len() == 1 {
422 let kf = &self.keyframes[0];
423 return Mat4::from_scale_rotation_translation(kf.scale, kf.rotation, kf.position);
424 }
425
426 let t = self.time;
427 let i = self.keyframes.partition_point(|kf| kf.time <= t);
428
429 if i == 0 {
430 let kf = &self.keyframes[0];
431 return Mat4::from_scale_rotation_translation(kf.scale, kf.rotation, kf.position);
432 }
433 if i >= self.keyframes.len() {
434 let kf = self.keyframes.last().unwrap();
435 return Mat4::from_scale_rotation_translation(kf.scale, kf.rotation, kf.position);
436 }
437
438 let a = &self.keyframes[i - 1];
439 let b = &self.keyframes[i];
440 let span = (b.time - a.time).max(1e-6);
441 let f = (t - a.time) / span;
442
443 let pos = a.position.lerp(b.position, f);
444 let rot = a.rotation.slerp(b.rotation, f);
445 let scale = a.scale.lerp(b.scale, f);
446 Mat4::from_scale_rotation_translation(scale, rot, pos)
447 }
448
449 pub fn is_done(&self) -> bool { !self.looping && self.time >= self.duration }
450}
451
452#[derive(Debug, Clone)]
456pub enum VelocityMode {
457 Radial {
459 speed_min: f32,
460 speed_max: f32,
461 },
462 Directional {
464 direction: Vec3,
465 speed_min: f32,
466 speed_max: f32,
467 spread_radians: f32,
468 },
469 Normal {
471 speed_min: f32,
472 speed_max: f32,
473 inward: bool,
474 },
475 Random {
477 speed_min: f32,
478 speed_max: f32,
479 },
480 Orbital {
482 tangent_speed: f32,
483 upward_speed: f32,
484 },
485 Fixed(Vec3),
487}
488
489impl VelocityMode {
490 pub fn sample(&self, spawn_pos: Vec3, spawn_normal: Vec3, rng: &mut u64) -> Vec3 {
491 match self {
492 VelocityMode::Radial { speed_min, speed_max } => {
493 let dir = spawn_pos.normalize_or_zero();
494 let dir = if dir.length_squared() < 0.001 {
495 random_unit_sphere(rng)
496 } else {
497 dir
498 };
499 dir * lcg_range(rng, *speed_min, *speed_max)
500 }
501 VelocityMode::Directional { direction, speed_min, speed_max, spread_radians } => {
502 let base = direction.normalize_or_zero();
503 let perp = cone_spread(base, *spread_radians, rng);
504 perp * lcg_range(rng, *speed_min, *speed_max)
505 }
506 VelocityMode::Normal { speed_min, speed_max, inward } => {
507 let n = if *inward { -spawn_normal } else { spawn_normal };
508 n * lcg_range(rng, *speed_min, *speed_max)
509 }
510 VelocityMode::Random { speed_min, speed_max } => {
511 random_unit_sphere(rng) * lcg_range(rng, *speed_min, *speed_max)
512 }
513 VelocityMode::Orbital { tangent_speed, upward_speed } => {
514 let radial = Vec3::new(spawn_pos.x, 0.0, spawn_pos.z).normalize_or_zero();
515 let tangent = Vec3::Y.cross(radial).normalize_or_zero();
516 tangent * *tangent_speed + Vec3::Y * *upward_speed
517 }
518 VelocityMode::Fixed(v) => *v,
519 }
520 }
521}
522
523fn random_unit_sphere(rng: &mut u64) -> Vec3 {
524 loop {
525 let x = lcg_f32(rng) * 2.0 - 1.0;
526 let y = lcg_f32(rng) * 2.0 - 1.0;
527 let z = lcg_f32(rng) * 2.0 - 1.0;
528 let v = Vec3::new(x, y, z);
529 if v.length_squared() <= 1.0 && v.length_squared() > 1e-8 {
530 return v.normalize();
531 }
532 }
533}
534
535fn cone_spread(dir: Vec3, half_angle: f32, rng: &mut u64) -> Vec3 {
536 if half_angle <= 0.0 { return dir; }
537 let theta = lcg_f32(rng) * std::f32::consts::TAU;
538 let phi = lcg_f32(rng) * half_angle;
539 let up = if dir.dot(Vec3::Y).abs() < 0.99 { Vec3::Y } else { Vec3::Z };
540 let right = dir.cross(up).normalize_or_zero();
541 let up2 = dir.cross(right).normalize_or_zero();
542 (dir * phi.cos() + (right * theta.cos() + up2 * theta.sin()) * phi.sin()).normalize_or_zero()
543}
544
545#[derive(Debug, Clone)]
549pub struct ColorOverLifetime {
550 pub stops: Vec<(f32, Vec4)>,
552}
553
554impl ColorOverLifetime {
555 pub fn constant(color: Vec4) -> Self {
556 Self { stops: vec![(0.0, color), (1.0, color)] }
557 }
558
559 pub fn two_stop(start: Vec4, end: Vec4) -> Self {
560 Self { stops: vec![(0.0, start), (1.0, end)] }
561 }
562
563 pub fn fire() -> Self {
564 Self { stops: vec![
565 (0.0, Vec4::new(1.0, 0.9, 0.2, 1.0)),
566 (0.4, Vec4::new(1.0, 0.4, 0.05, 1.0)),
567 (0.7, Vec4::new(0.5, 0.1, 0.0, 0.6)),
568 (1.0, Vec4::new(0.2, 0.1, 0.05, 0.0)),
569 ]}
570 }
571
572 pub fn smoke() -> Self {
573 Self { stops: vec![
574 (0.0, Vec4::new(0.6, 0.6, 0.6, 0.0)),
575 (0.1, Vec4::new(0.5, 0.5, 0.5, 0.7)),
576 (0.6, Vec4::new(0.3, 0.3, 0.3, 0.5)),
577 (1.0, Vec4::new(0.1, 0.1, 0.1, 0.0)),
578 ]}
579 }
580
581 pub fn sample(&self, t: f32) -> Vec4 {
582 if self.stops.is_empty() { return Vec4::ONE; }
583 if self.stops.len() == 1 { return self.stops[0].1; }
584 let i = self.stops.partition_point(|(st, _)| *st <= t);
585 if i == 0 { return self.stops[0].1; }
586 if i >= self.stops.len() { return self.stops.last().unwrap().1; }
587 let (t0, c0) = self.stops[i - 1];
588 let (t1, c1) = self.stops[i];
589 let f = (t - t0) / (t1 - t0).max(1e-6);
590 c0.lerp(c1, f)
591 }
592}
593
594#[derive(Debug, Clone)]
597pub struct SizeOverLifetime {
598 pub stops: Vec<(f32, f32)>,
599}
600
601impl SizeOverLifetime {
602 pub fn constant(size: f32) -> Self { Self { stops: vec![(0.0, size), (1.0, size)] } }
603 pub fn shrink(start: f32) -> Self { Self { stops: vec![(0.0, start), (1.0, 0.0)] } }
604 pub fn grow_shrink(peak: f32) -> Self {
605 Self { stops: vec![(0.0, 0.0), (0.3, peak), (1.0, 0.0)] }
606 }
607
608 pub fn sample(&self, t: f32) -> f32 {
609 if self.stops.is_empty() { return 1.0; }
610 if self.stops.len() == 1 { return self.stops[0].1; }
611 let i = self.stops.partition_point(|(st, _)| *st <= t);
612 if i == 0 { return self.stops[0].1; }
613 if i >= self.stops.len() { return self.stops.last().unwrap().1; }
614 let (t0, s0) = self.stops[i - 1];
615 let (t1, s1) = self.stops[i];
616 let f = (t - t0) / (t1 - t0).max(1e-6);
617 s0 + f * (s1 - s0)
618 }
619}
620
621#[derive(Debug, Clone)]
625pub struct Particle {
626 pub id: u64,
627 pub position: Vec3,
628 pub velocity: Vec3,
629 pub acceleration: Vec3,
630 pub color: Vec4,
631 pub size: f32,
632 pub rotation: f32, pub angular_vel: f32,
634 pub age: f32,
635 pub lifetime: f32,
636 pub mass: f32,
637 pub tag: ParticleTag,
638 pub emitter_id: u32,
639 pub custom: [f32; 4], }
641
642impl Particle {
643 pub fn normalized_age(&self) -> f32 {
644 (self.age / self.lifetime.max(1e-6)).min(1.0)
645 }
646
647 pub fn is_dead(&self) -> bool { self.age >= self.lifetime }
648
649 pub fn tick(&mut self, dt: f32) {
650 self.velocity += self.acceleration * dt;
651 self.position += self.velocity * dt;
652 self.rotation += self.angular_vel * dt;
653 self.age += dt;
654 }
655}
656
657#[derive(Debug, Clone)]
661pub struct EmitterConfig {
662 pub shape: EmitterShape,
663 pub spawn_mode: SpawnMode,
664 pub spawn_curve: SpawnCurve,
665 pub velocity_mode: VelocityMode,
666 pub color_over_life: ColorOverLifetime,
667 pub size_over_life: SizeOverLifetime,
668 pub lifetime_min: f32,
669 pub lifetime_max: f32,
670 pub size_min: f32,
671 pub size_max: f32,
672 pub mass_min: f32,
673 pub mass_max: f32,
674 pub angular_vel_min: f32,
675 pub angular_vel_max: f32,
676 pub max_particles: usize,
677 pub tag: ParticleTag,
678 pub inherit_velocity: f32, pub world_space: bool, pub simulation_speed: f32,
681}
682
683impl Default for EmitterConfig {
684 fn default() -> Self {
685 Self {
686 shape: EmitterShape::Point,
687 spawn_mode: SpawnMode::Continuous,
688 spawn_curve: SpawnCurve::Constant(10.0),
689 velocity_mode: VelocityMode::Radial { speed_min: 1.0, speed_max: 3.0 },
690 color_over_life: ColorOverLifetime::two_stop(Vec4::ONE, Vec4::new(1.0, 1.0, 1.0, 0.0)),
691 size_over_life: SizeOverLifetime::shrink(0.1),
692 lifetime_min: 1.0,
693 lifetime_max: 2.0,
694 size_min: 0.05,
695 size_max: 0.1,
696 mass_min: 1.0,
697 mass_max: 1.0,
698 angular_vel_min: -1.0,
699 angular_vel_max: 1.0,
700 max_particles: 256,
701 tag: ParticleTag::NONE,
702 inherit_velocity: 0.0,
703 world_space: true,
704 simulation_speed: 1.0,
705 }
706 }
707}
708
709pub struct Emitter {
713 pub id: u32,
714 pub config: EmitterConfig,
715 pub position: Vec3,
716 pub rotation: Quat,
717 pub scale: Vec3,
718 pub velocity: Vec3, pub particles: Vec<Particle>,
720 pub active: bool,
721 pub age: f32,
722 pub duration: f32, pub lod: LodController,
724 pub transform_anim: Option<EmitterTransformAnim>,
725 spawn_accumulator: f32,
726 next_particle_id: u64,
727 rng: u64,
728}
729
730impl Emitter {
731 pub fn new(id: u32, config: EmitterConfig) -> Self {
732 Self {
733 id,
734 position: Vec3::ZERO,
735 rotation: Quat::IDENTITY,
736 scale: Vec3::ONE,
737 velocity: Vec3::ZERO,
738 particles: Vec::with_capacity(config.max_particles.min(1024)),
739 active: true,
740 age: 0.0,
741 duration: -1.0,
742 lod: LodController::new(),
743 transform_anim: None,
744 spawn_accumulator: 0.0,
745 next_particle_id: 1,
746 rng: id as u64 ^ 0xDEAD_BEEF_1234_5678,
747 config,
748 }
749 }
750
751 pub fn at(mut self, pos: Vec3) -> Self { self.position = pos; self }
752 pub fn with_duration(mut self, secs: f32) -> Self { self.duration = secs; self }
753 pub fn with_lod(mut self, lod: LodController) -> Self { self.lod = lod; self }
754
755 pub fn transform(&self) -> Mat4 {
757 Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.position)
758 }
759
760 fn alloc_id(&mut self) -> u64 {
761 let id = self.next_particle_id;
762 self.next_particle_id += 1;
763 id
764 }
765
766 fn spawn_one(&mut self, spawn_normal_hint: Vec3) {
767 if self.particles.len() >= self.config.max_particles { return; }
768
769 let particle_id = self.alloc_id();
771 let emitter_id = self.id;
772 let position = self.position;
773 let rotation = self.rotation;
774 let scale = self.scale;
775 let velocity = self.velocity;
776 let world_space = self.config.world_space;
777 let inherit_vel = self.config.inherit_velocity;
778 let lod_size = self.lod.size_scale();
779
780 let rng = &mut self.rng;
781 let (local_pos, normal) = self.config.shape.sample(rng);
782 let effective_normal = if normal.length_squared() > 0.5 { normal } else { spawn_normal_hint };
783 let world_pos = if world_space {
784 position + rotation * (local_pos * scale)
785 } else {
786 local_pos
787 };
788 let vel = self.config.velocity_mode.sample(local_pos, effective_normal, rng);
789 let world_vel = if world_space {
790 rotation * vel + velocity * inherit_vel
791 } else {
792 vel
793 };
794
795 let lifetime = lcg_range(rng, self.config.lifetime_min, self.config.lifetime_max);
796 let size = lcg_range(rng, self.config.size_min, self.config.size_max) * lod_size;
797 let mass = lcg_range(rng, self.config.mass_min, self.config.mass_max);
798 let ang_vel = lcg_range(rng, self.config.angular_vel_min, self.config.angular_vel_max);
799 let spin = lcg_f32(rng) * std::f32::consts::TAU;
800 let color0 = self.config.color_over_life.sample(0.0);
801 let tag = self.config.tag;
802
803 self.particles.push(Particle {
804 id: particle_id,
805 position: world_pos,
806 velocity: world_vel,
807 acceleration: Vec3::ZERO,
808 color: color0,
809 size,
810 rotation: spin,
811 angular_vel: ang_vel,
812 age: 0.0,
813 lifetime,
814 mass,
815 tag,
816 emitter_id,
817 custom: [0.0; 4],
818 });
819 }
820
821 pub fn tick(&mut self, dt: f32, camera_pos: Vec3) {
822 if !self.active { return; }
823
824 let eff_dt = dt * self.config.simulation_speed;
825 self.age += eff_dt;
826
827 self.lod.update_distance(self.position, camera_pos);
829 if self.lod.is_culled() {
830 return;
832 }
833
834 if let Some(ref mut anim) = self.transform_anim {
836 anim.tick(eff_dt);
837 let mat = anim.sample();
838 let (scale, rot, trans) = mat.to_scale_rotation_translation();
839 self.position = trans;
840 self.rotation = rot;
841 self.scale = scale;
842 }
843
844 for p in &mut self.particles {
846 p.tick(eff_dt);
847 let t = p.normalized_age();
848 p.color = self.config.color_over_life.sample(t);
849 p.size = self.config.size_over_life.sample(t) * self.lod.size_scale();
850 }
851 self.particles.retain(|p| !p.is_dead());
852
853 if self.duration > 0.0 && self.age >= self.duration {
855 self.active = false;
856 return;
857 }
858
859 let t_norm = if self.duration > 0.0 { self.age / self.duration } else { 0.5 };
861
862 match &mut self.config.spawn_mode {
863 SpawnMode::Burst { count } => {
864 let n = *count;
865 for _ in 0..n { self.spawn_one(Vec3::Y); }
866 self.active = false;
867 }
868 SpawnMode::BurstOverTime { count, duration, emitted } => {
869 let total = *count;
870 let dur = *duration;
871 let progress = (self.age / dur.max(1e-6)).min(1.0);
872 let target = (progress * total as f32) as u32;
873 let to_spawn = target.saturating_sub(*emitted);
874 let em = emitted as *mut u32;
875 for _ in 0..to_spawn.min(64) {
876 self.spawn_one(Vec3::Y);
877 }
878 unsafe { *em += to_spawn.min(64); }
879 if self.age >= dur { self.active = false; }
880 }
881 SpawnMode::Continuous => {
882 let rate = self.config.spawn_curve.rate_at(t_norm) * self.lod.rate_scale();
883 self.spawn_accumulator += rate * eff_dt;
884 let to_spawn = self.spawn_accumulator as u32;
885 self.spawn_accumulator -= to_spawn as f32;
886 for _ in 0..to_spawn { self.spawn_one(Vec3::Y); }
887 }
888 }
889 }
890
891 pub fn particle_count(&self) -> usize { self.particles.len() }
892 pub fn is_dead(&self) -> bool { !self.active && self.particles.is_empty() }
893}
894
895pub struct EmitterPool {
899 emitters: HashMap<u32, Emitter>,
900 next_id: u32,
901 camera_pos: Vec3,
902}
903
904impl EmitterPool {
905 pub fn new() -> Self {
906 Self { emitters: HashMap::new(), next_id: 1, camera_pos: Vec3::ZERO }
907 }
908
909 pub fn set_camera(&mut self, pos: Vec3) { self.camera_pos = pos; }
910
911 pub fn spawn(&mut self, config: EmitterConfig) -> u32 {
912 let id = self.next_id; self.next_id += 1;
913 self.emitters.insert(id, Emitter::new(id, config));
914 id
915 }
916
917 pub fn spawn_at(&mut self, config: EmitterConfig, pos: Vec3) -> u32 {
918 let id = self.spawn(config);
919 if let Some(e) = self.emitters.get_mut(&id) { e.position = pos; }
920 id
921 }
922
923 pub fn get(&self, id: u32) -> Option<&Emitter> { self.emitters.get(&id) }
924 pub fn get_mut(&mut self, id: u32) -> Option<&mut Emitter> { self.emitters.get_mut(&id) }
925 pub fn remove(&mut self, id: u32) { self.emitters.remove(&id); }
926
927 pub fn tick(&mut self, dt: f32) {
928 let cam = self.camera_pos;
929 for e in self.emitters.values_mut() { e.tick(dt, cam); }
930 self.emitters.retain(|_, e| !e.is_dead());
931 }
932
933 pub fn all_emitters(&self) -> impl Iterator<Item = &Emitter> {
934 self.emitters.values()
935 }
936
937 pub fn all_particles(&self) -> impl Iterator<Item = &Particle> {
938 self.emitters.values().flat_map(|e| e.particles.iter())
939 }
940
941 pub fn total_particles(&self) -> usize {
942 self.emitters.values().map(|e| e.particle_count()).sum()
943 }
944
945 pub fn emitter_count(&self) -> usize { self.emitters.len() }
946}
947
948impl Default for EmitterPool {
949 fn default() -> Self { Self::new() }
950}
951
952pub struct EmitterBuilder {
956 cfg: EmitterConfig,
957}
958
959impl EmitterBuilder {
960 pub fn new() -> Self { Self { cfg: EmitterConfig::default() } }
961
962 pub fn shape(mut self, s: EmitterShape) -> Self { self.cfg.shape = s; self }
963 pub fn mode(mut self, m: SpawnMode) -> Self { self.cfg.spawn_mode = m; self }
964 pub fn curve(mut self, c: SpawnCurve) -> Self { self.cfg.spawn_curve = c; self }
965 pub fn velocity(mut self, v: VelocityMode) -> Self { self.cfg.velocity_mode = v; self }
966 pub fn color(mut self, c: ColorOverLifetime) -> Self { self.cfg.color_over_life = c; self }
967 pub fn size_curve(mut self, s: SizeOverLifetime) -> Self { self.cfg.size_over_life = s; self }
968 pub fn lifetime(mut self, min: f32, max: f32) -> Self { self.cfg.lifetime_min = min; self.cfg.lifetime_max = max; self }
969 pub fn size(mut self, min: f32, max: f32) -> Self { self.cfg.size_min = min; self.cfg.size_max = max; self }
970 pub fn max_particles(mut self, n: usize) -> Self { self.cfg.max_particles = n; self }
971 pub fn tag(mut self, t: ParticleTag) -> Self { self.cfg.tag = t; self }
972 pub fn world_space(mut self, ws: bool) -> Self { self.cfg.world_space = ws; self }
973 pub fn sim_speed(mut self, s: f32) -> Self { self.cfg.simulation_speed = s; self }
974
975 pub fn build(self) -> EmitterConfig { self.cfg }
976}
977
978impl Default for EmitterBuilder {
979 fn default() -> Self { Self::new() }
980}