Files
MetaCrate/crates/libremetaverse/src/particle_simulator.rs
Chili Palmer c9a1170a27
Some checks failed
API and SemVer surface / api-surface (push) Failing after 1m34s
Native code generation / deterministic (push) Has been cancelled
Concurrency and resource soak audit / soak (push) Has been cancelled
Documentation / documentation (push) Has been cancelled
performance evidence / audit (push) Has been cancelled
First release candidate / non-fuzz-release-gate (push) Has been cancelled
Release platform and feature matrix / audit (push) Has been cancelled
Release platform and feature matrix / matrix (false, linux-stable-minimal, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, macos-stable-portable, x86_64-apple-darwin, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (false, windows-stable-portable, x86_64-pc-windows-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-msrv-portable, x86_64-unknown-linux-gnu, 1.96.0) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-default, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-features, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Release platform and feature matrix / matrix (true, linux-stable-release-surface, x86_64-unknown-linux-gnu, stable) (push) Has been cancelled
Dependency and supply-chain audit / audit (push) Has been cancelled
Native release artifact audit / audit (push) Has been cancelled
Imaging and meshing gate / native (push) Failing after 53s
JPEG 2000 feature / linux (push) Successful in 2m49s
Native Rust workspace compile / compile (push) Failing after 59s
Skia feature / linux (push) Has been cancelled
Complete first release candidate audit (#107)
2026-08-12 14:44:28 +00:00

357 lines
12 KiB
Rust

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<LiveParticle>,
random: u64,
}
pub struct ParticleSimulator {
system: PrimitiveParticleSystem,
state: Mutex<State>,
}
impl ParticleSimulator {
pub fn new(system: PrimitiveParticleSystem, seed: Option<i32>) -> Result<Self, Error> {
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<Vec<LiveParticle>, 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);
}
}