use std::f32::consts::PI; use std::sync::Mutex; use std::time::{SystemTime, UNIX_EPOCH}; use libremetaverse_types::{Color4, Quaternion, Vector3}; use crate::{ Error, LiveParticle, PrimitiveParticleSystem, PrimitiveParticleSystemParticleDataFlags as DataFlags, PrimitiveParticleSystemParticleFlags as ParticleFlags, PrimitiveParticleSystemSourcePattern as SourcePattern, }; const MAX_PARTICLES: usize = 4096; struct State { source_position: Vector3, source_rotation: Quaternion, target_position: Vector3, wind: Vector3, system_age: f32, burst_timer: f32, active: bool, particles: Vec, random: u64, } pub struct ParticleSimulator { system: PrimitiveParticleSystem, state: Mutex, } impl ParticleSimulator { pub fn new(system: PrimitiveParticleSystem, seed: Option) -> Result { let random = match seed.unwrap_or_default() { 0 => { let elapsed = SystemTime::now() .duration_since(UNIX_EPOCH) .map_err(|_| Error::InvalidOperation)?; elapsed.as_secs() ^ u64::from(elapsed.subsec_nanos()) } seed => seed as i64 as u64, } .max(1); Ok(Self { system, state: Mutex::new(State { source_position: Vector3::zero(), source_rotation: Quaternion::identity(), target_position: Vector3::zero(), wind: Vector3::zero(), system_age: 0.0, burst_timer: 0.0, active: true, particles: Vec::with_capacity(256), random, }), }) } pub fn get_particles(&self) -> Result, Error> { Ok(self.lock().particles.clone()) } pub fn tick(&self, dt: f32) -> Result<(), Error> { if !dt.is_finite() { return Err(Error::Argument); } if dt <= 0.0 { return Ok(()); } let mut state = self.lock(); state.system_age += dt; if self.system.max_age > 0.0 && state.system_age > self.system.max_age { state.active = false; } let flags = self.system.part_data_flags.0; let wind_enabled = flags & DataFlags::WIND.0 != 0; let bounce = flags & DataFlags::BOUNCE.0 != 0; let target_pos = flags & DataFlags::TARGET_POS.0 != 0; let target_linear = flags & DataFlags::TARGET_LINEAR.0 != 0; let interp_color = flags & DataFlags::INTERP_COLOR.0 != 0; let interp_scale = flags & DataFlags::INTERP_SCALE.0 != 0; let wind = state.wind; let source = state.source_position; let target = state.target_position; let mut index = state.particles.len(); while index > 0 { index -= 1; let particle = &mut state.particles[index]; particle.age += dt; if particle.age >= self.system.part_max_age { state.particles.remove(index); continue; } particle.velocity.x += self.system.part_acceleration.x * dt; particle.velocity.y += self.system.part_acceleration.y * dt; particle.velocity.z += self.system.part_acceleration.z * dt; if wind_enabled { particle.velocity.x += (wind.x - particle.velocity.x) * dt * 2.0; particle.velocity.y += (wind.y - particle.velocity.y) * dt * 2.0; particle.velocity.z += (wind.z - particle.velocity.z) * dt * 2.0; } particle.position.x += particle.velocity.x * dt; particle.position.y += particle.velocity.y * dt; particle.position.z += particle.velocity.z * dt; if bounce && particle.position.z < 0.0 { particle.position.z = 0.0; particle.velocity.z *= -0.5; } if target_pos || target_linear { let direction = normalize(Vector3 { x: target.x - source.x - particle.position.x, y: target.y - source.y - particle.position.y, z: target.z - source.z - particle.position.z, }); if target_linear { particle.velocity = scale( direction, (self.system.burst_speed_min + self.system.burst_speed_max) * 0.5, ); } else { particle.velocity.x += direction.x * dt * 2.0; particle.velocity.y += direction.y * dt * 2.0; particle.velocity.z += direction.z * dt * 2.0; } } let age = if self.system.part_max_age > 0.0 { (particle.age / self.system.part_max_age).clamp(0.0, 1.0) } else { 0.0 }; particle.color = if interp_color { Color4::lerp( self.system.part_start_color, self.system.part_end_color, age, )? } else { self.system.part_start_color }; particle.scale_x = if interp_scale { lerp( self.system.part_start_scale_x, self.system.part_end_scale_x, age, ) } else { self.system.part_start_scale_x }; particle.scale_y = if interp_scale { lerp( self.system.part_start_scale_y, self.system.part_end_scale_y, age, ) } else { self.system.part_start_scale_y }; particle.glow = lerp(self.system.part_start_glow, self.system.part_end_glow, age); particle.normalized_age = age; } if state.active && state.system_age >= self.system.start_age { state.burst_timer += dt; let rate = self.system.burst_rate.max(0.01); while state.burst_timer >= rate { state.burst_timer -= rate; self.emit_burst(&mut state); } } Ok(()) } pub fn is_active(&self) -> bool { self.lock().active } pub fn source_position(&self) -> Vector3 { self.lock().source_position } pub fn set_source_position(&mut self, value: Vector3) { self.lock().source_position = value; } pub fn source_rotation(&self) -> Quaternion { self.lock().source_rotation } pub fn set_source_rotation(&mut self, value: Quaternion) { self.lock().source_rotation = value; } pub fn system_age(&self) -> f32 { self.lock().system_age } pub fn target_position(&self) -> Vector3 { self.lock().target_position } pub fn set_target_position(&mut self, value: Vector3) { self.lock().target_position = value; } pub fn wind(&self) -> Vector3 { self.lock().wind } pub fn set_wind(&mut self, value: Vector3) { self.lock().wind = value; } fn emit_burst(&self, state: &mut State) { for _ in 0..self.system.burst_part_count { if state.particles.len() >= MAX_PARTICLES { break; } let position = self.initial_position(state); let velocity = self.initial_velocity(state); state.particles.push(LiveParticle { age: 0.0, color: self.system.part_start_color, glow: self.system.part_start_glow, normalized_age: 0.0, position, scale_x: self.system.part_start_scale_x, scale_y: self.system.part_start_scale_y, velocity, }); } } fn initial_position(&self, state: &mut State) -> Vector3 { if self.system.burst_radius <= 0.0 { return Vector3::zero(); } let radius = random_f32(state) * self.system.burst_radius; let theta = random_f32(state) * 2.0 * PI; let phi = (random_f32(state) - 0.5) * PI; Vector3 { x: radius * phi.cos() * theta.cos(), y: radius * phi.cos() * theta.sin(), z: radius * phi.sin(), } } fn initial_velocity(&self, state: &mut State) -> Vector3 { let speed = lerp( self.system.burst_speed_min, self.system.burst_speed_max, random_f32(state), ); let mut direction = if self.system.pattern == SourcePattern::DROP { Vector3::zero() } else if self.system.pattern == SourcePattern::EXPLODE { random_unit_sphere(state) } else if self.system.pattern == SourcePattern::ANGLE || self.system.pattern == SourcePattern::ANGLE_CONE { cone_direction(state, self.system.inner_angle, self.system.outer_angle) } else if self.system.pattern == SourcePattern::ANGLE_CONE_EMPTY { cone_direction(state, self.system.outer_angle, PI) } else { Vector3::unit_z() }; if self.system.part_flags & ParticleFlags::OBJECT_RELATIVE.0 != 0 && direction != Vector3::zero() { direction = Vector3::mul_with_vector3_quaternion(direction, state.source_rotation); } scale(direction, speed) } fn lock(&self) -> std::sync::MutexGuard<'_, State> { self.state .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) } } fn random_f32(state: &mut State) -> f32 { let mut value = state.random; value ^= value << 13; value ^= value >> 7; value ^= value << 17; state.random = value; (value >> 40) as f32 / (1_u32 << 24) as f32 } fn random_unit_sphere(state: &mut State) -> Vector3 { let theta = random_f32(state) * 2.0 * PI; let phi = (2.0 * random_f32(state) - 1.0).acos(); Vector3 { x: phi.sin() * theta.cos(), y: phi.sin() * theta.sin(), z: phi.cos(), } } fn cone_direction(state: &mut State, inner: f32, outer: f32) -> Vector3 { let angle = lerp(inner, outer, random_f32(state)); let rotation = random_f32(state) * 2.0 * PI; Vector3 { x: angle.sin() * rotation.cos(), y: angle.sin() * rotation.sin(), z: angle.cos(), } } fn normalize(value: Vector3) -> Vector3 { let length = (value.x * value.x + value.y * value.y + value.z * value.z).sqrt(); if length > 0.0 && length.is_finite() { scale(value, length.recip()) } else { Vector3::zero() } } const fn scale(value: Vector3, amount: f32) -> Vector3 { Vector3 { x: value.x * amount, y: value.y * amount, z: value.z * amount, } } fn lerp(start: f32, end: f32, amount: f32) -> f32 { start + (end - start) * amount } #[cfg(test)] mod tests { use super::*; #[test] fn emits_ages_and_expires_particles_deterministically() { let mut system = PrimitiveParticleSystem::default(); system.pattern = SourcePattern::DROP; system.burst_part_count = 2; system.burst_rate = 0.1; system.part_max_age = 0.25; system.part_start_scale_x = 1.0; system.part_start_scale_y = 2.0; let simulator = ParticleSimulator::new(system, Some(7)).unwrap(); simulator.tick(0.1).unwrap(); assert_eq!(simulator.get_particles().unwrap().len(), 2); simulator.tick(0.1).unwrap(); assert_eq!(simulator.get_particles().unwrap().len(), 4); simulator.tick(0.2).unwrap(); assert_eq!(simulator.get_particles().unwrap().len(), 6); } }