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
357 lines
12 KiB
Rust
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);
|
|
}
|
|
}
|