diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs index e8e222e224..eae013242d 100644 --- a/voxygen/src/render/pipelines/particle.rs +++ b/voxygen/src/render/pipelines/particle.rs @@ -94,15 +94,11 @@ impl ParticleMode { } impl Instance { - pub fn new( - inst_time: f64, - inst_entropy: f32, - inst_mode: ParticleMode, - inst_pos: Vec3, - ) -> Self { + pub fn new(inst_time: f64, inst_mode: ParticleMode, inst_pos: Vec3) -> Self { + use rand::Rng; Self { inst_time: inst_time as f32, - inst_entropy, + inst_entropy: rand::thread_rng().gen(), inst_mode: inst_mode as i32, inst_pos: inst_pos.into_array(), } @@ -110,7 +106,7 @@ impl Instance { } impl Default for Instance { - fn default() -> Self { Self::new(0.0, 0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } + fn default() -> Self { Self::new(0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } } pub struct ParticlePipeline; diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs index 6784390b5a..11e3b74a1d 100644 --- a/voxygen/src/scene/particle.rs +++ b/voxygen/src/scene/particle.rs @@ -19,61 +19,51 @@ use specs::{Join, WorldExt}; use std::time::{Duration, Instant}; use vek::*; -struct Particles { - alive_until: Instant, // created_at + lifespan - instance: ParticleInstance, -} - pub struct ParticleMgr { - // keep track of lifespans - particles: Vec, + /// keep track of lifespans + particles: Vec, + + /// keep track of timings + scheduler: HeartbeatScheduler, + + /// GPU Instance Buffer instances: Instances, + + /// GPU Vertex Buffers model_cache: HashMap<&'static str, Model>, } -const MODEL_KEY: &str = "voxygen.voxel.particle"; - impl ParticleMgr { pub fn new(renderer: &mut Renderer) -> Self { Self { particles: Vec::new(), + scheduler: HeartbeatScheduler::new(), instances: default_instances(renderer), model_cache: default_cache(renderer), } } - pub fn particle_count(&self) -> usize { self.instances.count() } - - pub fn particle_count_visible(&self) -> usize { self.instances.count() } - pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) { let time = scene_data.state.get_time(); - let now = Instant::now(); let mut rng = rand::thread_rng(); match outcome { Outcome::Explosion { pos, power } => { for _ in 0..150 { - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new( - time, - rng.gen(), - ParticleMode::Shrapnel, - *pos, - ), - }); + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::Shrapnel, + *pos, + )); } for _ in 0..200 { - self.particles.push(Particles { - alive_until: now + Duration::from_secs(4), - instance: ParticleInstance::new( - time, - rng.gen(), - ParticleMode::CampfireSmoke, - *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power), - ), - }); + self.particles.push(Particle::new( + Duration::from_secs(4), + time, + ParticleMode::CampfireSmoke, + *pos + Vec2::::zero().map(|_| rng.gen_range(-1.0, 1.0) * power), + )); } }, Outcome::ProjectileShot { .. } => {}, @@ -84,35 +74,26 @@ impl ParticleMgr { if scene_data.particles_enabled { let now = Instant::now(); - // remove dead particles + // remove dead Particle self.particles.retain(|p| p.alive_until > now); - // add new particles + // add new Particle self.maintain_body_particles(scene_data); self.maintain_boost_particles(scene_data); + + // update timings + self.scheduler.maintain(); } else { - // remove all particles + // remove all particle lifespans self.particles.clear(); + + // remove all timings + self.scheduler.clear(); } self.upload_particles(renderer); } - fn upload_particles(&mut self, renderer: &mut Renderer) { - let all_cpu_instances = self - .particles - .iter() - .map(|p| p.instance) - .collect::>(); - - // TODO: optimise buffer writes - let gpu_instances = renderer - .create_instances(&all_cpu_instances) - .expect("Failed to upload particle instances to the GPU!"); - - self.instances = gpu_instances; - } - fn maintain_body_particles(&mut self, scene_data: &SceneData) { let ecs = scene_data.state.ecs(); for (_i, (_entity, body, pos)) in ( @@ -141,106 +122,93 @@ impl ParticleMgr { fn maintain_campfirelit_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::CampfireFire, + pos.0, + )); - self.particles.push(Particles { - alive_until: now + Duration::from_secs(10), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + self.particles.push(Particle::new( + Duration::from_secs(10), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_boltfire_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); - - self.particles.push(Particles { - alive_until: now + Duration::from_secs(1), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::CampfireFire, + pos.0, + )); + self.particles.push(Particle::new( + Duration::from_secs(1), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_boltfirebig_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); // fire - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(250), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireFire, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(3)) { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::CampfireFire, + pos.0, + )); + } // smoke - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) { + self.particles.push(Particle::new( + Duration::from_secs(2), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_bomb_particles(&mut self, scene_data: &SceneData, pos: &Pos) { let time = scene_data.state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); - // sparks - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); - self.particles.push(Particles { - alive_until: now + Duration::from_millis(1500), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::GunPowderSpark, pos.0), - }); + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + // sparks + self.particles.push(Particle::new( + Duration::from_millis(1500), + time, + ParticleMode::GunPowderSpark, + pos.0, + )); - // smoke - self.particles.push(Particles { - alive_until: now + Duration::from_secs(2), - instance: ParticleInstance::new(time, rng.gen(), ParticleMode::CampfireSmoke, pos.0), - }); + // smoke + self.particles.push(Particle::new( + Duration::from_secs(2), + time, + ParticleMode::CampfireSmoke, + pos.0, + )); + } } fn maintain_boost_particles(&mut self, scene_data: &SceneData) { let state = scene_data.state; let ecs = state.ecs(); let time = state.get_time(); - let now = Instant::now(); - let mut rng = rand::thread_rng(); for (_i, (_entity, pos, character_state)) in ( &ecs.entities(), @@ -251,19 +219,33 @@ impl ParticleMgr { .enumerate() { if let CharacterState::Boost(_) = character_state { - self.particles.push(Particles { - alive_until: now + Duration::from_secs(15), - instance: ParticleInstance::new( + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_secs(15), time, - rng.gen(), ParticleMode::CampfireSmoke, pos.0, - ), - }); + )); + } } } } + fn upload_particles(&mut self, renderer: &mut Renderer) { + let all_cpu_instances = self + .particles + .iter() + .map(|p| p.instance) + .collect::>(); + + // TODO: optimise buffer writes + let gpu_instances = renderer + .create_instances(&all_cpu_instances) + .expect("Failed to upload particle instances to the GPU!"); + + self.instances = gpu_instances; + } + pub fn render( &self, renderer: &mut Renderer, @@ -275,12 +257,16 @@ impl ParticleMgr { if scene_data.particles_enabled { let model = &self .model_cache - .get(MODEL_KEY) + .get(DEFAULT_MODEL_KEY) .expect("Expected particle model in cache"); renderer.render_particles(model, globals, &self.instances, lights, shadows); } } + + pub fn particle_count(&self) -> usize { self.instances.count() } + + pub fn particle_count_visible(&self) -> usize { self.instances.count() } } fn default_instances(renderer: &mut Renderer) -> Instances { @@ -291,14 +277,16 @@ fn default_instances(renderer: &mut Renderer) -> Instances { .expect("Failed to upload particle instances to the GPU!") } +const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle"; + fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model> { let mut model_cache = HashMap::new(); - model_cache.entry(MODEL_KEY).or_insert_with(|| { + model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| { let offset = Vec3::zero(); let lod_scale = Vec3::one(); - let vox = assets::load_expect::(MODEL_KEY); + let vox = assets::load_expect::(DEFAULT_MODEL_KEY); let mesh = &Meshable::::generate_mesh( &Segment::from(vox.as_ref()), @@ -313,3 +301,69 @@ fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model, +} + +impl HeartbeatScheduler { + pub fn new() -> Self { + HeartbeatScheduler { + timers: HashMap::new(), + } + } + + /// updates the last elapsed times and elasped counts + /// this should be called once, and only once per tick. + pub fn maintain(&mut self) { + for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() { + // the number of iterations since last update + *heartbeats = + // TODO: use nightly api once stable; https://github.com/rust-lang/rust/issues/63139 + (last_update.elapsed().as_secs_f32() / frequency.as_secs_f32()).floor() as u8; + + // Instant::now() minus the heart beat count precision, + // or alternatively as expressed below. + *last_update += frequency.mul_f32(*heartbeats as f32); + // Note: we want to preserve incomplete heartbeats, and include them + // in the next update. + } + } + + /// returns the number of times this duration has elasped since the last + /// tick: + /// - if it's more frequent then tick rate, it could be 1 or more. + /// - if it's less frequent then tick rate, it could be 1 or 0. + /// - if it's equal to the tick rate, it could be between 2 and 0, due to + /// delta time variance. + pub fn heartbeats(&mut self, frequency: Duration) -> u8 { + self.timers + .entry(frequency) + .or_insert_with(|| (Instant::now(), 0)) + .1 + } + + pub fn clear(&mut self) { self.timers.clear() } +} + +struct Particle { + alive_until: Instant, // created_at + lifespan + instance: ParticleInstance, +} + +impl Particle { + fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3) -> Self { + Particle { + alive_until: Instant::now() + lifespan, + instance: ParticleInstance::new(time, mode, pos), + } + } +}