2020-07-05 12:10:58 +00:00
|
|
|
use super::SceneData;
|
|
|
|
use crate::{
|
|
|
|
mesh::Meshable,
|
|
|
|
render::{
|
2020-07-15 15:45:47 +00:00
|
|
|
pipelines::particle::ParticleMode, Consts, Globals, Instances, Light, Model,
|
|
|
|
ParticleInstance, ParticlePipeline, Renderer, Shadow,
|
2020-07-05 12:10:58 +00:00
|
|
|
},
|
|
|
|
};
|
|
|
|
use common::{
|
|
|
|
assets,
|
2020-08-11 14:05:34 +00:00
|
|
|
comp::{item::Reagent, object, Body, CharacterState, Pos},
|
2020-07-05 12:10:58 +00:00
|
|
|
figure::Segment,
|
2020-07-31 22:25:23 +00:00
|
|
|
outcome::Outcome,
|
2020-07-05 12:10:58 +00:00
|
|
|
};
|
|
|
|
use dot_vox::DotVoxData;
|
|
|
|
use hashbrown::HashMap;
|
|
|
|
use rand::Rng;
|
2020-07-15 15:45:47 +00:00
|
|
|
use specs::{Join, WorldExt};
|
2020-08-10 11:04:30 +00:00
|
|
|
use std::time::Duration;
|
2020-07-31 22:25:23 +00:00
|
|
|
use vek::*;
|
2020-07-11 10:37:19 +00:00
|
|
|
|
2020-07-05 12:10:58 +00:00
|
|
|
pub struct ParticleMgr {
|
2020-08-07 14:04:52 +00:00
|
|
|
/// keep track of lifespans
|
|
|
|
particles: Vec<Particle>,
|
|
|
|
|
|
|
|
/// keep track of timings
|
|
|
|
scheduler: HeartbeatScheduler,
|
|
|
|
|
|
|
|
/// GPU Instance Buffer
|
2020-07-21 15:48:20 +00:00
|
|
|
instances: Instances<ParticleInstance>,
|
2020-08-07 14:04:52 +00:00
|
|
|
|
|
|
|
/// GPU Vertex Buffers
|
2020-07-11 10:37:19 +00:00
|
|
|
model_cache: HashMap<&'static str, Model<ParticlePipeline>>,
|
2020-07-05 12:10:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl ParticleMgr {
|
|
|
|
pub fn new(renderer: &mut Renderer) -> Self {
|
|
|
|
Self {
|
2020-07-11 10:37:19 +00:00
|
|
|
particles: Vec::new(),
|
2020-08-07 14:04:52 +00:00
|
|
|
scheduler: HeartbeatScheduler::new(),
|
2020-07-25 15:56:50 +00:00
|
|
|
instances: default_instances(renderer),
|
|
|
|
model_cache: default_cache(renderer),
|
2020-07-05 12:10:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-31 22:25:23 +00:00
|
|
|
pub fn handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) {
|
|
|
|
let time = scene_data.state.get_time();
|
|
|
|
let mut rng = rand::thread_rng();
|
|
|
|
|
|
|
|
match outcome {
|
2020-08-11 14:05:34 +00:00
|
|
|
Outcome::Explosion {
|
|
|
|
pos,
|
|
|
|
power,
|
|
|
|
reagent,
|
|
|
|
} => {
|
2020-08-03 11:56:12 +00:00
|
|
|
for _ in 0..150 {
|
2020-08-07 14:04:52 +00:00
|
|
|
self.particles.push(Particle::new(
|
2020-08-11 14:05:34 +00:00
|
|
|
Duration::from_millis(if reagent.is_some() { 1000 } else { 250 }),
|
2020-08-07 14:04:52 +00:00
|
|
|
time,
|
2020-08-11 14:05:34 +00:00
|
|
|
match reagent {
|
|
|
|
Some(Reagent::Blue) => ParticleMode::FireworkBlue,
|
|
|
|
Some(Reagent::Green) => ParticleMode::FireworkGreen,
|
|
|
|
Some(Reagent::Purple) => ParticleMode::FireworkPurple,
|
|
|
|
Some(Reagent::Red) => ParticleMode::FireworkRed,
|
|
|
|
Some(Reagent::Yellow) => ParticleMode::FireworkYellow,
|
|
|
|
None => ParticleMode::Shrapnel,
|
|
|
|
},
|
2020-08-07 14:04:52 +00:00
|
|
|
*pos,
|
|
|
|
));
|
2020-08-03 11:56:12 +00:00
|
|
|
}
|
2020-08-11 14:05:34 +00:00
|
|
|
|
2020-08-03 11:56:12 +00:00
|
|
|
for _ in 0..200 {
|
2020-08-07 14:04:52 +00:00
|
|
|
self.particles.push(Particle::new(
|
|
|
|
Duration::from_secs(4),
|
|
|
|
time,
|
|
|
|
ParticleMode::CampfireSmoke,
|
|
|
|
*pos + Vec2::<f32>::zero().map(|_| rng.gen_range(-1.0, 1.0) * power),
|
|
|
|
));
|
2020-07-31 22:25:23 +00:00
|
|
|
}
|
|
|
|
},
|
2020-08-06 16:41:43 +00:00
|
|
|
Outcome::ProjectileShot { .. } => {},
|
2020-07-31 22:25:23 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-15 15:45:47 +00:00
|
|
|
pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) {
|
2020-07-25 15:46:45 +00:00
|
|
|
if scene_data.particles_enabled {
|
2020-08-10 11:04:30 +00:00
|
|
|
// update timings
|
|
|
|
self.scheduler.maintain(scene_data.state.get_time());
|
2020-07-05 12:10:58 +00:00
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
// remove dead Particle
|
2020-08-10 11:04:30 +00:00
|
|
|
self.particles
|
|
|
|
.retain(|p| p.alive_until > scene_data.state.get_time());
|
2020-07-21 15:48:20 +00:00
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
// add new Particle
|
2020-07-25 15:46:45 +00:00
|
|
|
self.maintain_body_particles(scene_data);
|
|
|
|
self.maintain_boost_particles(scene_data);
|
|
|
|
} else {
|
2020-08-07 14:04:52 +00:00
|
|
|
// remove all particle lifespans
|
2020-07-25 15:46:45 +00:00
|
|
|
self.particles.clear();
|
2020-08-07 14:04:52 +00:00
|
|
|
|
|
|
|
// remove all timings
|
|
|
|
self.scheduler.clear();
|
2020-07-25 15:46:45 +00:00
|
|
|
}
|
2020-08-04 10:27:55 +00:00
|
|
|
|
|
|
|
self.upload_particles(renderer);
|
2020-07-25 15:46:45 +00:00
|
|
|
}
|
2020-07-21 15:48:20 +00:00
|
|
|
|
2020-07-25 15:46:45 +00:00
|
|
|
fn maintain_body_particles(&mut self, scene_data: &SceneData) {
|
2020-07-19 07:16:06 +00:00
|
|
|
let ecs = scene_data.state.ecs();
|
|
|
|
for (_i, (_entity, body, pos)) in (
|
2020-07-05 12:10:58 +00:00
|
|
|
&ecs.entities(),
|
2020-07-15 15:45:47 +00:00
|
|
|
&ecs.read_storage::<Body>(),
|
2020-07-19 07:16:06 +00:00
|
|
|
&ecs.read_storage::<Pos>(),
|
2020-07-05 12:10:58 +00:00
|
|
|
)
|
|
|
|
.join()
|
|
|
|
.enumerate()
|
|
|
|
{
|
2020-07-15 15:45:47 +00:00
|
|
|
match body {
|
|
|
|
Body::Object(object::Body::CampfireLit) => {
|
2020-07-25 15:46:45 +00:00
|
|
|
self.maintain_campfirelit_particles(scene_data, pos)
|
2020-07-19 07:16:06 +00:00
|
|
|
},
|
|
|
|
Body::Object(object::Body::BoltFire) => {
|
2020-07-25 15:46:45 +00:00
|
|
|
self.maintain_boltfire_particles(scene_data, pos)
|
2020-07-15 15:45:47 +00:00
|
|
|
},
|
2020-07-19 07:16:06 +00:00
|
|
|
Body::Object(object::Body::BoltFireBig) => {
|
2020-07-25 15:46:45 +00:00
|
|
|
self.maintain_boltfirebig_particles(scene_data, pos)
|
2020-07-19 07:16:06 +00:00
|
|
|
},
|
2020-08-11 14:05:34 +00:00
|
|
|
Body::Object(
|
|
|
|
object::Body::Bomb
|
|
|
|
| object::Body::FireworkBlue
|
|
|
|
| object::Body::FireworkGreen
|
|
|
|
| object::Body::FireworkPurple
|
|
|
|
| object::Body::FireworkRed
|
|
|
|
| object::Body::FireworkYellow,
|
|
|
|
) => self.maintain_bomb_particles(scene_data, pos),
|
2020-07-15 15:45:47 +00:00
|
|
|
_ => {},
|
2020-07-13 13:44:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-07-25 15:46:45 +00:00
|
|
|
fn maintain_campfirelit_particles(&mut self, scene_data: &SceneData, pos: &Pos) {
|
2020-07-19 07:16:06 +00:00
|
|
|
let time = scene_data.state.get_time();
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
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(10),
|
|
|
|
time,
|
|
|
|
ParticleMode::CampfireSmoke,
|
|
|
|
pos.0,
|
|
|
|
));
|
|
|
|
}
|
2020-07-19 07:16:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 15:46:45 +00:00
|
|
|
fn maintain_boltfire_particles(&mut self, scene_data: &SceneData, pos: &Pos) {
|
2020-07-19 07:16:06 +00:00
|
|
|
let time = scene_data.state.get_time();
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
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,
|
|
|
|
));
|
|
|
|
}
|
2020-07-19 07:16:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 15:46:45 +00:00
|
|
|
fn maintain_boltfirebig_particles(&mut self, scene_data: &SceneData, pos: &Pos) {
|
2020-07-19 07:16:06 +00:00
|
|
|
let time = scene_data.state.get_time();
|
|
|
|
|
2020-07-21 15:48:20 +00:00
|
|
|
// fire
|
2020-08-07 14:04:52 +00:00
|
|
|
for _ in 0..self.scheduler.heartbeats(Duration::from_millis(3)) {
|
|
|
|
self.particles.push(Particle::new(
|
|
|
|
Duration::from_millis(250),
|
|
|
|
time,
|
|
|
|
ParticleMode::CampfireFire,
|
|
|
|
pos.0,
|
|
|
|
));
|
|
|
|
}
|
2020-07-19 07:16:06 +00:00
|
|
|
|
2020-07-21 15:48:20 +00:00
|
|
|
// smoke
|
2020-08-07 14:04:52 +00:00
|
|
|
for _ in 0..self.scheduler.heartbeats(Duration::from_millis(5)) {
|
|
|
|
self.particles.push(Particle::new(
|
|
|
|
Duration::from_secs(2),
|
|
|
|
time,
|
|
|
|
ParticleMode::CampfireSmoke,
|
|
|
|
pos.0,
|
|
|
|
));
|
|
|
|
}
|
2020-07-19 07:16:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 15:46:45 +00:00
|
|
|
fn maintain_bomb_particles(&mut self, scene_data: &SceneData, pos: &Pos) {
|
2020-07-19 07:16:06 +00:00
|
|
|
let time = scene_data.state.get_time();
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
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(Particle::new(
|
|
|
|
Duration::from_secs(2),
|
|
|
|
time,
|
|
|
|
ParticleMode::CampfireSmoke,
|
|
|
|
pos.0,
|
|
|
|
));
|
|
|
|
}
|
2020-07-19 07:16:06 +00:00
|
|
|
}
|
|
|
|
|
2020-07-25 15:46:45 +00:00
|
|
|
fn maintain_boost_particles(&mut self, scene_data: &SceneData) {
|
2020-07-13 13:44:28 +00:00
|
|
|
let state = scene_data.state;
|
|
|
|
let ecs = state.ecs();
|
|
|
|
let time = state.get_time();
|
|
|
|
|
2020-07-15 15:45:47 +00:00
|
|
|
for (_i, (_entity, pos, character_state)) in (
|
2020-07-13 13:44:28 +00:00
|
|
|
&ecs.entities(),
|
|
|
|
&ecs.read_storage::<Pos>(),
|
|
|
|
&ecs.read_storage::<CharacterState>(),
|
|
|
|
)
|
|
|
|
.join()
|
|
|
|
.enumerate()
|
|
|
|
{
|
2020-07-15 15:45:47 +00:00
|
|
|
if let CharacterState::Boost(_) = character_state {
|
2020-08-07 14:04:52 +00:00
|
|
|
for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) {
|
|
|
|
self.particles.push(Particle::new(
|
|
|
|
Duration::from_secs(15),
|
2020-07-21 15:48:20 +00:00
|
|
|
time,
|
|
|
|
ParticleMode::CampfireSmoke,
|
|
|
|
pos.0,
|
2020-08-07 14:04:52 +00:00
|
|
|
));
|
|
|
|
}
|
2020-07-11 10:37:19 +00:00
|
|
|
}
|
2020-07-05 12:10:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
fn upload_particles(&mut self, renderer: &mut Renderer) {
|
|
|
|
let all_cpu_instances = self
|
|
|
|
.particles
|
|
|
|
.iter()
|
|
|
|
.map(|p| p.instance)
|
|
|
|
.collect::<Vec<ParticleInstance>>();
|
|
|
|
|
|
|
|
// 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;
|
|
|
|
}
|
|
|
|
|
2020-07-05 12:10:58 +00:00
|
|
|
pub fn render(
|
|
|
|
&self,
|
|
|
|
renderer: &mut Renderer,
|
2020-07-25 15:46:45 +00:00
|
|
|
scene_data: &SceneData,
|
2020-07-05 12:10:58 +00:00
|
|
|
globals: &Consts<Globals>,
|
|
|
|
lights: &Consts<Light>,
|
|
|
|
shadows: &Consts<Shadow>,
|
|
|
|
) {
|
2020-07-25 15:46:45 +00:00
|
|
|
if scene_data.particles_enabled {
|
|
|
|
let model = &self
|
|
|
|
.model_cache
|
2020-08-07 14:04:52 +00:00
|
|
|
.get(DEFAULT_MODEL_KEY)
|
2020-07-25 15:46:45 +00:00
|
|
|
.expect("Expected particle model in cache");
|
2020-07-19 07:16:06 +00:00
|
|
|
|
2020-07-25 15:46:45 +00:00
|
|
|
renderer.render_particles(model, globals, &self.instances, lights, shadows);
|
|
|
|
}
|
2020-07-05 12:10:58 +00:00
|
|
|
}
|
2020-08-07 14:04:52 +00:00
|
|
|
|
|
|
|
pub fn particle_count(&self) -> usize { self.instances.count() }
|
|
|
|
|
|
|
|
pub fn particle_count_visible(&self) -> usize { self.instances.count() }
|
2020-07-05 12:10:58 +00:00
|
|
|
}
|
2020-07-25 15:56:50 +00:00
|
|
|
|
|
|
|
fn default_instances(renderer: &mut Renderer) -> Instances<ParticleInstance> {
|
|
|
|
let empty_vec = Vec::new();
|
|
|
|
|
|
|
|
renderer
|
|
|
|
.create_instances(&empty_vec)
|
|
|
|
.expect("Failed to upload particle instances to the GPU!")
|
|
|
|
}
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
const DEFAULT_MODEL_KEY: &str = "voxygen.voxel.particle";
|
|
|
|
|
2020-07-25 15:56:50 +00:00
|
|
|
fn default_cache(renderer: &mut Renderer) -> HashMap<&'static str, Model<ParticlePipeline>> {
|
|
|
|
let mut model_cache = HashMap::new();
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
model_cache.entry(DEFAULT_MODEL_KEY).or_insert_with(|| {
|
2020-07-25 15:56:50 +00:00
|
|
|
let offset = Vec3::zero();
|
|
|
|
let lod_scale = Vec3::one();
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
let vox = assets::load_expect::<DotVoxData>(DEFAULT_MODEL_KEY);
|
2020-07-25 15:56:50 +00:00
|
|
|
|
|
|
|
let mesh = &Meshable::<ParticlePipeline, ParticlePipeline>::generate_mesh(
|
|
|
|
&Segment::from(vox.as_ref()),
|
|
|
|
(offset * lod_scale, Vec3::one() / lod_scale),
|
|
|
|
)
|
|
|
|
.0;
|
|
|
|
|
|
|
|
renderer
|
|
|
|
.create_model(mesh)
|
|
|
|
.expect("Failed to create particle model")
|
|
|
|
});
|
|
|
|
|
|
|
|
model_cache
|
|
|
|
}
|
2020-08-07 14:04:52 +00:00
|
|
|
|
|
|
|
/// Accumulates heartbeats to be consumed on the next tick.
|
|
|
|
struct HeartbeatScheduler {
|
|
|
|
/// Duration = Heartbeat Frequency/Intervals
|
2020-08-10 11:04:30 +00:00
|
|
|
/// f64 = Last update time
|
2020-08-07 14:04:52 +00:00
|
|
|
/// u8 = number of heartbeats since last update
|
|
|
|
/// - 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 etc.
|
2020-08-10 11:04:30 +00:00
|
|
|
timers: HashMap<Duration, (f64, u8)>,
|
|
|
|
|
|
|
|
last_known_time: f64,
|
2020-08-07 14:04:52 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
impl HeartbeatScheduler {
|
|
|
|
pub fn new() -> Self {
|
|
|
|
HeartbeatScheduler {
|
|
|
|
timers: HashMap::new(),
|
2020-08-10 11:04:30 +00:00
|
|
|
last_known_time: 0.0,
|
2020-08-07 14:04:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// updates the last elapsed times and elasped counts
|
|
|
|
/// this should be called once, and only once per tick.
|
2020-08-10 11:04:30 +00:00
|
|
|
pub fn maintain(&mut self, now: f64) {
|
|
|
|
self.last_known_time = now;
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
for (frequency, (last_update, heartbeats)) in self.timers.iter_mut() {
|
2020-08-10 11:04:30 +00:00
|
|
|
// the number of frequency cycles that have occurred.
|
|
|
|
let total_heartbeats = (now - *last_update) / frequency.as_secs_f64();
|
|
|
|
|
|
|
|
// exclude partial frequency cycles
|
|
|
|
let full_heartbeats = total_heartbeats.floor();
|
|
|
|
|
|
|
|
*heartbeats = full_heartbeats as u8;
|
|
|
|
|
|
|
|
// the remaining partial freqency cycle, as a decimal.
|
|
|
|
let partial_heartbeat = total_heartbeats - full_heartbeats;
|
|
|
|
|
|
|
|
// the remaining partial freqency cycle, as a unit of time(f64).
|
|
|
|
let partial_heartbeat_as_time = frequency.mul_f64(partial_heartbeat).as_secs_f64();
|
|
|
|
|
|
|
|
// now minus the left over heart beat count precision as seconds,
|
|
|
|
// Note: we want to preserve incomplete heartbeats, and roll them
|
|
|
|
// over into the next update.
|
|
|
|
*last_update = now - partial_heartbeat_as_time;
|
2020-08-07 14:04:52 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/// 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 {
|
2020-08-10 11:04:30 +00:00
|
|
|
let last_known_time = self.last_known_time;
|
|
|
|
|
2020-08-07 14:04:52 +00:00
|
|
|
self.timers
|
|
|
|
.entry(frequency)
|
2020-08-10 11:04:30 +00:00
|
|
|
.or_insert_with(|| (last_known_time, 0))
|
2020-08-07 14:04:52 +00:00
|
|
|
.1
|
|
|
|
}
|
|
|
|
|
|
|
|
pub fn clear(&mut self) { self.timers.clear() }
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Particle {
|
2020-08-10 11:04:30 +00:00
|
|
|
alive_until: f64, // created_at + lifespan
|
2020-08-07 14:04:52 +00:00
|
|
|
instance: ParticleInstance,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Particle {
|
|
|
|
fn new(lifespan: Duration, time: f64, mode: ParticleMode, pos: Vec3<f32>) -> Self {
|
|
|
|
Particle {
|
2020-08-10 11:04:30 +00:00
|
|
|
alive_until: time + lifespan.as_secs_f64(),
|
2020-08-07 14:04:52 +00:00
|
|
|
instance: ParticleInstance::new(time, mode, pos),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|