use crate::renderer::components::ParticleEmitter;
use crate::renderer::gpu_particles::{GpuParticle, GpuParticleSystem};
use gizmo_core::World;
use gizmo_math::Vec3;
use gizmo_physics_core::components::Transform;
const MAX_SPAWNS_PER_FRAME: u32 = 100;
pub fn spawn_from_emitters(
world: &mut World,
particles: &GpuParticleSystem,
queue: &wgpu::Queue,
dt: f32,
) {
let emitter_entities: Vec<u32> = world.borrow::<ParticleEmitter>().entities().collect();
if emitter_entities.is_empty() {
return;
}
let mut emitters = unsafe { world.borrow_mut_unchecked::<ParticleEmitter>() };
let transforms = world.borrow::<Transform>();
let frame = world
.get_resource::<gizmo_core::time::Time>()
.map_or(0, |t| t.frame());
let mut spawned = Vec::new();
for id in emitter_entities {
let Some(mut emitter) = emitters.get_mut(id) else {
continue;
};
if !emitter.is_active || emitter.spawn_rate <= 0.0 {
continue;
}
let origin = match transforms.get(id) {
Some(t) => t.position + t.rotation.mul_vec3(emitter.local_offset),
None => emitter.local_offset,
};
emitter.add_time(dt);
let interval = 1.0 / emitter.spawn_rate;
let mut this_frame = 0;
let mut rng = Xorshift::seeded(frame, id);
while emitter.get_accumulator() >= interval && this_frame < MAX_SPAWNS_PER_FRAME {
emitter.consume_time(interval);
this_frame += 1;
let jitter = Vec3::new(
rng.signed() * emitter.velocity_randomness,
rng.signed() * emitter.velocity_randomness,
rng.signed() * emitter.velocity_randomness,
);
let velocity = emitter.initial_velocity + jitter;
let life = (emitter.lifespan + rng.signed() * emitter.lifespan_randomness).max(0.1);
spawned.push(GpuParticle {
position: [origin.x, origin.y, origin.z],
life: 0.0,
velocity: [velocity.x, velocity.y, velocity.z],
max_life: life,
color: emitter.color_start.into(),
size_start: emitter.size_start,
size_end: emitter.size_end,
_padding: [0.0; 2],
});
}
}
if !spawned.is_empty() {
particles.spawn_particles(queue, &spawned);
}
}
struct Xorshift(u32);
impl Xorshift {
fn seeded(frame: u64, entity: u32) -> Self {
let mixed = (frame as u32)
.wrapping_mul(0x9E37_79B9)
.wrapping_add(entity.wrapping_mul(0x85EB_CA6B));
Self(mixed | 1)
}
fn next_u32(&mut self) -> u32 {
self.0 ^= self.0 << 13;
self.0 ^= self.0 >> 17;
self.0 ^= self.0 << 5;
self.0
}
fn signed(&mut self) -> f32 {
(self.next_u32() as f32 / u32::MAX as f32) * 2.0 - 1.0
}
}
#[cfg(test)]
mod tests {
use super::Xorshift;
#[test]
fn the_jitter_stays_in_range_and_moves() {
let mut rng = Xorshift::seeded(7, 3);
let mut seen = [0.0f32; 64];
for slot in &mut seen {
*slot = rng.signed();
assert!((-1.0..=1.0).contains(slot), "out of range: {slot}");
}
assert!(
seen.windows(2).any(|w| (w[0] - w[1]).abs() > 1e-6),
"the sequence never changed"
);
}
#[test]
fn two_emitters_in_one_frame_do_not_emit_in_lockstep() {
let (mut a, mut b) = (Xorshift::seeded(7, 3), Xorshift::seeded(7, 4));
assert_ne!(a.signed(), b.signed());
}
#[test]
fn the_same_frame_and_entity_replays_identically() {
let (mut a, mut b) = (Xorshift::seeded(12, 5), Xorshift::seeded(12, 5));
for _ in 0..8 {
assert_eq!(a.signed(), b.signed());
}
}
}