diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c03261b87..078d5790e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -51,6 +51,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Power stat to weapons which affects weapon damage - Add detection of entities under the cursor - Functional group-system with exp-sharing and disabled damage to group members +- Some Campfire, fireball & bomb; particle, light & sound effects. ### Changed diff --git a/assets/voxygen/audio/sfx.ron b/assets/voxygen/audio/sfx.ron index c2725ba3cd..e81459a2df 100644 --- a/assets/voxygen/audio/sfx.ron +++ b/assets/voxygen/audio/sfx.ron @@ -106,6 +106,18 @@ "voxygen.audio.sfx.inventory.consumable.food", ], threshold: 0.3, - ) + ), + Explosion: ( + files: [ + "voxygen.audio.sfx.explosion", + ], + threshold: 0.2, + ), + ProjectileShot: ( + files: [ + "voxygen.audio.sfx.glider_open", + ], + threshold: 0.5, + ), } ) diff --git a/assets/voxygen/audio/sfx/explosion.wav b/assets/voxygen/audio/sfx/explosion.wav new file mode 100644 index 0000000000..f7269ac71c --- /dev/null +++ b/assets/voxygen/audio/sfx/explosion.wav @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8d7f0bb0a0865d45e98d107c1d24a448aaeeced9c37db9f9e472ab3e1bd2eb03 +size 604946 diff --git a/assets/voxygen/i18n/en.ron b/assets/voxygen/i18n/en.ron index e57424b780..b3a2004539 100644 --- a/assets/voxygen/i18n/en.ron +++ b/assets/voxygen/i18n/en.ron @@ -297,6 +297,7 @@ magically infused items?"#, "hud.settings.fluid_rendering_mode.cheap": "Cheap", "hud.settings.fluid_rendering_mode.shiny": "Shiny", "hud.settings.cloud_rendering_mode.regular": "Regular", + "hud.settings.particles": "Particles", "hud.settings.fullscreen": "Fullscreen", "hud.settings.save_window_size": "Save window size", diff --git a/assets/voxygen/shaders/include/random.glsl b/assets/voxygen/shaders/include/random.glsl index 240d17e50a..7d217d858e 100644 --- a/assets/voxygen/shaders/include/random.glsl +++ b/assets/voxygen/shaders/include/random.glsl @@ -1,5 +1,5 @@ float hash(vec4 p) { - p = fract(p * 0.3183099 + 0.1); + p = fract(p * 0.3183099 + 0.1) - fract(p + 23.22121); p *= 17.0; return (fract(p.x * p.y * p.z * p.w * (p.x + p.y + p.z + p.w)) - 0.5) * 2.0; } diff --git a/assets/voxygen/shaders/include/sky.glsl b/assets/voxygen/shaders/include/sky.glsl index 6fd726230b..fe12499086 100644 --- a/assets/voxygen/shaders/include/sky.glsl +++ b/assets/voxygen/shaders/include/sky.glsl @@ -94,10 +94,10 @@ float is_star_at(vec3 dir) { vec3 pos = (floor(dir * star_scale) - 0.5) / star_scale; // Noisy offsets - pos += (3.0 / star_scale) * rand_perm_3(pos); + pos += (3.0 / star_scale) * (1.0 + hash(pos.yxzz) * 0.85); // Find distance to fragment - float dist = length(normalize(pos) - dir); + float dist = length(pos - dir); // Star threshold if (dist < 0.0015) { diff --git a/assets/voxygen/shaders/particle-frag.glsl b/assets/voxygen/shaders/particle-frag.glsl new file mode 100644 index 0000000000..3d730f7a50 --- /dev/null +++ b/assets/voxygen/shaders/particle-frag.glsl @@ -0,0 +1,38 @@ +#version 330 core + +#include + +in vec3 f_pos; +flat in vec3 f_norm; +in vec3 f_col; +in float f_ao; +in float f_light; + +out vec4 tgt_color; + +#include +#include + +const float FADE_DIST = 32.0; + +void main() { + vec3 light, diffuse_light, ambient_light; + get_sun_diffuse(f_norm, time_of_day.x, light, diffuse_light, ambient_light, 1.0); + float point_shadow = shadow_at(f_pos, f_norm); + diffuse_light *= f_light * point_shadow; + ambient_light *= f_light, point_shadow; + vec3 point_light = light_at(f_pos, f_norm); + light += point_light; + diffuse_light += point_light; + float ao = pow(f_ao, 0.5) * 0.85 + 0.15; + ambient_light *= ao; + diffuse_light *= ao; + vec3 surf_color = illuminate(f_col, light, diffuse_light, ambient_light); + + float fog_level = fog(f_pos.xyz, focus_pos.xyz, medium.x); + vec4 clouds; + vec3 fog_color = get_sky_color(normalize(f_pos - cam_pos.xyz), time_of_day.x, cam_pos.xyz, f_pos, 0.5, true, clouds); + vec3 color = mix(mix(surf_color, fog_color, fog_level), clouds.rgb, clouds.a); + + tgt_color = vec4(color, 1.0 - clamp((distance(focus_pos.xy, f_pos.xy) - (1000.0 - FADE_DIST)) / FADE_DIST, 0, 1)); +} diff --git a/assets/voxygen/shaders/particle-vert.glsl b/assets/voxygen/shaders/particle-vert.glsl new file mode 100644 index 0000000000..2f2914f63d --- /dev/null +++ b/assets/voxygen/shaders/particle-vert.glsl @@ -0,0 +1,131 @@ +#version 330 core + +#include +#include +#include + +in vec3 v_pos; +in uint v_col; +in uint v_norm_ao; +in vec3 inst_pos; +in float inst_time; +in float inst_entropy; +in int inst_mode; + +out vec3 f_pos; +flat out vec3 f_norm; +out vec3 f_col; +out float f_ao; +out float f_light; + +const float SCALE = 1.0 / 11.0; + +// Modes +const int SMOKE = 0; +const int FIRE = 1; +const int GUN_POWDER_SPARK = 2; +const int SHRAPNEL = 3; + +// meters per second squared (acceleration) +const float earth_gravity = 9.807; + +struct Attr { + vec3 offs; + float scale; + vec3 col; +}; + +float lifetime = tick.x - inst_time; + +vec3 linear_motion(vec3 init_offs, vec3 vel) { + return init_offs + vel * lifetime; +} + +vec3 grav_vel(float grav) { + return vec3(0, 0, -grav * lifetime); +} + +float exp_scale(float factor) { + return 1 / (1 - lifetime * factor); +} + +void main() { + float rand0 = hash(vec4(inst_entropy + 0)); + float rand1 = hash(vec4(inst_entropy + 1)); + float rand2 = hash(vec4(inst_entropy + 2)); + float rand3 = hash(vec4(inst_entropy + 3)); + float rand4 = hash(vec4(inst_entropy + 4)); + float rand5 = hash(vec4(inst_entropy + 5)); + float rand6 = hash(vec4(inst_entropy + 6)); + float rand7 = hash(vec4(inst_entropy + 7)); + + Attr attr; + + if (inst_mode == SMOKE) { + attr = Attr( + linear_motion( + vec3(rand0 * 0.25, rand1 * 0.25, 1.7 + rand5), + vec3(rand2 * 0.2, rand3 * 0.2, 1.0 + rand4 * 0.5)// + vec3(sin(lifetime), sin(lifetime + 1.5), sin(lifetime * 4) * 0.25) + ), + exp_scale(-0.2), + vec3(1) + ); + } else if (inst_mode == FIRE) { + attr = Attr( + linear_motion( + vec3(rand0 * 0.25, rand1 * 0.25, 0.3), + vec3(rand2 * 0.1, rand3 * 0.1, 2.0 + rand4 * 1.0) + ), + 1.0, + vec3(2, rand5 + 2, 0) + ); + } else if (inst_mode == GUN_POWDER_SPARK) { + attr = Attr( + linear_motion( + vec3(rand0, rand1, rand3) * 0.3, + vec3(rand4, rand5, rand6) * 2.0 + grav_vel(earth_gravity) + ), + 1.0, + vec3(3.5, 3 + rand7, 0) + ); + } else if (inst_mode == SHRAPNEL) { + attr = Attr( + linear_motion( + vec3(0), + vec3(rand4, rand5, rand6) * 40.0 + grav_vel(earth_gravity) + ), + 3.0 + rand0, + vec3(0.6 + rand7 * 0.4) + ); + } else { + attr = Attr( + linear_motion( + vec3(rand0 * 0.25, rand1 * 0.25, 1.7 + rand5), + vec3(rand2 * 0.1, rand3 * 0.1, 1.0 + rand4 * 0.5) + ), + exp_scale(-0.2), + vec3(1) + ); + } + + f_pos = inst_pos + (v_pos * attr.scale * SCALE + attr.offs); + + // First 3 normals are negative, next 3 are positive + vec3 normals[6] = vec3[](vec3(-1,0,0), vec3(1,0,0), vec3(0,-1,0), vec3(0,1,0), vec3(0,0,-1), vec3(0,0,1)); + f_norm = + // inst_pos * + normals[(v_norm_ao >> 0) & 0x7u]; + + //vec3 col = vec3((uvec3(v_col) >> uvec3(0, 8, 16)) & uvec3(0xFFu)) / 255.0; + f_col = + //srgb_to_linear(col) * + srgb_to_linear(attr.col); + f_ao = float((v_norm_ao >> 3) & 0x3u) / 4.0; + + f_light = 1.0; + + gl_Position = + all_mat * + vec4(f_pos, 1); + gl_Position.z = -1000.0 / (gl_Position.z + 10000.0); +} diff --git a/assets/voxygen/voxel/particle.vox b/assets/voxygen/voxel/particle.vox new file mode 100644 index 0000000000..1f8fde6d26 --- /dev/null +++ b/assets/voxygen/voxel/particle.vox @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ba07287771bbfa8f369d0d634a6f847138b605962362517af16bec1e2a7c7951 +size 64 diff --git a/client/src/lib.rs b/client/src/lib.rs index f4a864b2de..0901a8fd27 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -25,6 +25,7 @@ use common::{ Notification, PlayerInfo, PlayerListUpdate, RegisterError, RequestStateError, ServerInfo, ServerMsg, MAX_BYTES_CHAT_MSG, }, + outcome::Outcome, recipe::RecipeBook, state::State, sync::{Uid, UidAllocator, WorldSyncExt}, @@ -66,6 +67,7 @@ pub enum Event { InventoryUpdated(InventoryUpdateEvent), Notification(Notification), SetViewDistance(u32), + Outcome(Outcome), } pub struct Client { @@ -1244,6 +1246,9 @@ impl Client { self.view_distance = Some(vd); frontend_events.push(Event::SetViewDistance(vd)); }, + ServerMsg::Outcomes(outcomes) => { + frontend_events.extend(outcomes.into_iter().map(Event::Outcome)) + }, } } } diff --git a/common/src/cmd.rs b/common/src/cmd.rs index 6d8ed3af46..e128901f46 100644 --- a/common/src/cmd.rs +++ b/common/src/cmd.rs @@ -38,6 +38,7 @@ pub enum ChatCommand { Adminify, Alias, Build, + Campfire, Debug, DebugColumn, Dummy, @@ -79,6 +80,7 @@ pub static CHAT_COMMANDS: &[ChatCommand] = &[ ChatCommand::Adminify, ChatCommand::Alias, ChatCommand::Build, + ChatCommand::Campfire, ChatCommand::Debug, ChatCommand::DebugColumn, ChatCommand::Dummy, @@ -185,6 +187,7 @@ impl ChatCommand { ), ChatCommand::Alias => cmd(vec![Any("name", Required)], "Change your alias", NoAdmin), ChatCommand::Build => cmd(vec![], "Toggles build mode on and off", Admin), + ChatCommand::Campfire => cmd(vec![], "Spawns a campfire", Admin), ChatCommand::Debug => cmd(vec![], "Place all debug items into your pack.", Admin), ChatCommand::DebugColumn => cmd( vec![Integer("x", 15000, Required), Integer("y", 15000, Required)], @@ -365,6 +368,7 @@ impl ChatCommand { ChatCommand::Adminify => "adminify", ChatCommand::Alias => "alias", ChatCommand::Build => "build", + ChatCommand::Campfire => "campfire", ChatCommand::Debug => "debug", ChatCommand::DebugColumn => "debug_column", ChatCommand::Dummy => "dummy", diff --git a/common/src/comp/inventory/item/tool.rs b/common/src/comp/inventory/item/tool.rs index 401f9925c7..b040f0517f 100644 --- a/common/src/comp/inventory/item/tool.rs +++ b/common/src/comp/inventory/item/tool.rs @@ -261,6 +261,7 @@ impl Tool { col: (0.85, 0.5, 0.11).into(), ..Default::default() }), + projectile_gravity: None, }, BasicRanged { diff --git a/common/src/comp/mod.rs b/common/src/comp/mod.rs index ccef80dc8b..963f3c95a6 100644 --- a/common/src/comp/mod.rs +++ b/common/src/comp/mod.rs @@ -18,7 +18,7 @@ mod player; pub mod projectile; pub mod skills; mod stats; -mod visual; +pub mod visual; // Reexports pub use ability::{CharacterAbility, CharacterAbilityType, ItemConfig, Loadout}; diff --git a/common/src/comp/phys.rs b/common/src/comp/phys.rs index e9496f047c..466734cb4c 100644 --- a/common/src/comp/phys.rs +++ b/common/src/comp/phys.rs @@ -24,6 +24,10 @@ impl Component for Vel { #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] pub struct Ori(pub Dir); +impl Ori { + pub fn vec(&self) -> &Vec3 { &*self.0 } +} + impl Component for Ori { type Storage = IdvStorage; } diff --git a/common/src/lib.rs b/common/src/lib.rs index fb04ff28b1..3bddacde79 100644 --- a/common/src/lib.rs +++ b/common/src/lib.rs @@ -24,6 +24,7 @@ pub mod generation; pub mod loadout_builder; pub mod msg; pub mod npc; +pub mod outcome; pub mod path; pub mod ray; pub mod recipe; diff --git a/common/src/msg/server.rs b/common/src/msg/server.rs index 156c18fd61..d6d55e46e8 100644 --- a/common/src/msg/server.rs +++ b/common/src/msg/server.rs @@ -2,6 +2,7 @@ use super::{ClientState, EcsCompPacket}; use crate::{ character::CharacterItem, comp, + outcome::Outcome, recipe::RecipeBook, state, sync, sync::Uid, @@ -120,6 +121,7 @@ pub enum ServerMsg { /// Send a popup notification such as "Waypoint Saved" Notification(Notification), SetViewDistance(u32), + Outcomes(Vec), } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)] diff --git a/common/src/outcome.rs b/common/src/outcome.rs new file mode 100644 index 0000000000..4f89f418b0 --- /dev/null +++ b/common/src/outcome.rs @@ -0,0 +1,30 @@ +use crate::comp; +use serde::{Deserialize, Serialize}; +use vek::*; + +/// An outcome represents the final result of an instantaneous event. It implies +/// that said event has already occurred. It is not a request for that event to +/// occur, nor is it something that may be cancelled or otherwise altered. Its +/// primary purpose is to act as something for frontends (both server and +/// client) to listen to in order to receive feedback about events in the world. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub enum Outcome { + Explosion { + pos: Vec3, + power: f32, + }, + ProjectileShot { + pos: Vec3, + body: comp::Body, + vel: Vec3, + }, +} + +impl Outcome { + pub fn get_pos(&self) -> Option> { + match self { + Outcome::Explosion { pos, .. } => Some(*pos), + Outcome::ProjectileShot { pos, .. } => Some(*pos), + } + } +} diff --git a/server/src/cmd.rs b/server/src/cmd.rs index b580cfbd98..5a282766fd 100644 --- a/server/src/cmd.rs +++ b/server/src/cmd.rs @@ -7,7 +7,7 @@ use chrono::{NaiveTime, Timelike}; use common::{ assets, cmd::{ChatCommand, CHAT_COMMANDS, CHAT_SHORTCUTS}, - comp::{self, ChatType, Item}, + comp::{self, ChatType, Item, LightEmitter, WaypointArea}, event::{EventBus, ServerEvent}, msg::{Notification, PlayerListUpdate, ServerMsg}, npc::{self, get_npc_name}, @@ -65,6 +65,7 @@ fn get_handler(cmd: &ChatCommand) -> CommandHandler { ChatCommand::Adminify => handle_adminify, ChatCommand::Alias => handle_alias, ChatCommand::Build => handle_build, + ChatCommand::Campfire => handle_spawn_campfire, ChatCommand::Debug => handle_debug, ChatCommand::DebugColumn => handle_debug_column, ChatCommand::Dummy => handle_spawn_training_dummy, @@ -664,6 +665,39 @@ fn handle_spawn_training_dummy( } } +fn handle_spawn_campfire( + server: &mut Server, + client: EcsEntity, + target: EcsEntity, + _args: String, + _action: &ChatCommand, +) { + match server.state.read_component_copied::(target) { + Some(pos) => { + server + .state + .create_object(pos, comp::object::Body::CampfireLit) + .with(LightEmitter { + col: Rgb::new(1.0, 0.65, 0.2), + strength: 2.0, + flicker: 1.0, + animated: true, + }) + .with(WaypointArea::default()) + .build(); + + server.notify_client( + client, + ChatType::CommandInfo.server_msg("Spawned a campfire"), + ); + }, + None => server.notify_client( + client, + ChatType::CommandError.server_msg("You have no position!"), + ), + } +} + fn handle_players( server: &mut Server, client: EcsEntity, diff --git a/server/src/events/entity_creation.rs b/server/src/events/entity_creation.rs index b2924d9d15..5e910452c9 100644 --- a/server/src/events/entity_creation.rs +++ b/server/src/events/entity_creation.rs @@ -1,11 +1,13 @@ use crate::{sys, Server, StateExt}; use common::{ comp::{ - self, group, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, + self, Agent, Alignment, Body, Gravity, Item, ItemDrop, LightEmitter, Loadout, Pos, Projectile, Scale, Stats, Vel, WaypointArea, }, + outcome::Outcome, util::Dir, }; +use comp::group; use specs::{Builder, Entity as EcsEntity, WorldExt}; use vek::{Rgb, Vec3}; @@ -89,10 +91,18 @@ pub fn handle_shoot( .expect("Failed to fetch entity") .0; + let vel = *dir * 100.0; + + // Add an outcome + state + .ecs() + .write_resource::>() + .push(Outcome::ProjectileShot { pos, body, vel }); + // TODO: Player height pos.z += 1.2; - let mut builder = state.create_projectile(Pos(pos), Vel(*dir * 100.0), body, projectile); + let mut builder = state.create_projectile(Pos(pos), Vel(vel), body, projectile); if let Some(light) = light { builder = builder.with(light) } diff --git a/server/src/events/entity_manipulation.rs b/server/src/events/entity_manipulation.rs index 0a73123a28..ff6f4ae2b6 100644 --- a/server/src/events/entity_manipulation.rs +++ b/server/src/events/entity_manipulation.rs @@ -6,6 +6,7 @@ use common::{ HealthChange, HealthSource, Player, Pos, Stats, }, msg::{PlayerListUpdate, ServerMsg}, + outcome::Outcome, state::BlockChange, sync::{Uid, UidAllocator, WorldSyncExt}, sys::combat::BLOCK_ANGLE, @@ -288,6 +289,10 @@ pub fn handle_explosion( let hit_range = 3.0 * power; let ecs = &server.state.ecs(); + // Add an outcome + ecs.write_resource::>() + .push(Outcome::Explosion { pos, power }); + let owner_entity = owner.and_then(|uid| { ecs.read_resource::() .retrieve_entity_internal(uid.into()) diff --git a/server/src/lib.rs b/server/src/lib.rs index 7d27b933ab..1447793c98 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -34,6 +34,7 @@ use common::{ comp::{self, ChatType}, event::{EventBus, ServerEvent}, msg::{ClientState, ServerInfo, ServerMsg}, + outcome::Outcome, recipe::default_recipe_book, state::{State, TimeOfDay}, sync::WorldSyncExt, @@ -118,6 +119,7 @@ impl Server { state .ecs_mut() .insert(comp::AdminList(settings.admins.clone())); + state.ecs_mut().insert(Vec::::new()); // System timers for performance monitoring state.ecs_mut().insert(sys::EntitySyncTimer::default()); diff --git a/server/src/sys/entity_sync.rs b/server/src/sys/entity_sync.rs index d4bbf62d20..2dc20648b4 100644 --- a/server/src/sys/entity_sync.rs +++ b/server/src/sys/entity_sync.rs @@ -7,15 +7,19 @@ use crate::{ Tick, }; use common::{ - comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Pos, Vel}, + comp::{ForceUpdate, Inventory, InventoryUpdate, Last, Ori, Player, Pos, Vel}, msg::ServerMsg, + outcome::Outcome, region::{Event as RegionEvent, RegionMap}, state::TimeOfDay, sync::{CompSyncPackage, Uid}, + terrain::TerrainChunkSize, + vol::RectVolSize, }; use specs::{ Entities, Entity as EcsEntity, Join, Read, ReadExpect, ReadStorage, System, Write, WriteStorage, }; +use vek::*; /// This system will send physics updates to the client pub struct Sys; @@ -33,6 +37,7 @@ impl<'a> System<'a> for Sys { ReadStorage<'a, Ori>, ReadStorage<'a, Inventory>, ReadStorage<'a, RegionSubscription>, + ReadStorage<'a, Player>, WriteStorage<'a, Last>, WriteStorage<'a, Last>, WriteStorage<'a, Last>, @@ -40,6 +45,7 @@ impl<'a> System<'a> for Sys { WriteStorage<'a, ForceUpdate>, WriteStorage<'a, InventoryUpdate>, Write<'a, DeletedEntities>, + Write<'a, Vec>, TrackedComps<'a>, ReadTrackers<'a>, ); @@ -58,6 +64,7 @@ impl<'a> System<'a> for Sys { orientations, inventories, subscriptions, + players, mut last_pos, mut last_vel, mut last_ori, @@ -65,6 +72,7 @@ impl<'a> System<'a> for Sys { mut force_updates, mut inventory_updates, mut deleted_entities, + mut outcomes, tracked_comps, trackers, ): Self::SystemData, @@ -316,6 +324,26 @@ impl<'a> System<'a> for Sys { )); } + // Sync outcomes + for (client, player, pos) in (&mut clients, &players, positions.maybe()).join() { + let is_near = |o_pos: Vec3| { + pos.zip_with(player.view_distance, |pos, vd| { + pos.0.xy().distance_squared(o_pos.xy()) + < (vd as f32 * TerrainChunkSize::RECT_SIZE.x as f32).powf(2.0) + }) + }; + + let outcomes = outcomes + .iter() + .filter(|o| o.get_pos().and_then(&is_near).unwrap_or(true)) + .cloned() + .collect::>(); + if outcomes.len() > 0 { + client.notify(ServerMsg::Outcomes(outcomes)); + } + } + outcomes.clear(); + // Remove all force flags. force_updates.clear(); inventory_updates.clear(); diff --git a/voxygen/src/audio/channel.rs b/voxygen/src/audio/channel.rs index 8a88923f95..5f19e42532 100644 --- a/voxygen/src/audio/channel.rs +++ b/voxygen/src/audio/channel.rs @@ -16,7 +16,10 @@ //! the channel capacity has been reached and all channels are occupied, a //! warning is logged, and no sound is played. -use crate::audio::fader::{FadeDirection, Fader}; +use crate::audio::{ + fader::{FadeDirection, Fader}, + Listener, +}; use rodio::{Device, Sample, Sink, Source, SpatialSink}; use vek::*; @@ -163,11 +166,16 @@ impl SfxChannel { pub fn is_done(&self) -> bool { self.sink.empty() } - pub fn set_emitter_position(&mut self, pos: [f32; 3]) { self.sink.set_emitter_position(pos); } + pub fn set_pos(&mut self, pos: Vec3) { self.pos = pos; } - pub fn set_left_ear_position(&mut self, pos: [f32; 3]) { self.sink.set_left_ear_position(pos); } + pub fn update(&mut self, listener: &Listener) { + const FALLOFF: f32 = 0.13; - pub fn set_right_ear_position(&mut self, pos: [f32; 3]) { - self.sink.set_right_ear_position(pos); + self.sink + .set_emitter_position(((self.pos - listener.pos) * FALLOFF).into_array()); + self.sink + .set_left_ear_position(listener.ear_left_rpos.into_array()); + self.sink + .set_right_ear_position(listener.ear_right_rpos.into_array()); } } diff --git a/voxygen/src/audio/mod.rs b/voxygen/src/audio/mod.rs index 9c7b022dc4..5af08f8dc6 100644 --- a/voxygen/src/audio/mod.rs +++ b/voxygen/src/audio/mod.rs @@ -16,7 +16,14 @@ use cpal::traits::DeviceTrait; use rodio::{source::Source, Decoder, Device}; use vek::*; -const FALLOFF: f32 = 0.13; +#[derive(Default, Clone)] +pub struct Listener { + pos: Vec3, + ori: Vec3, + + ear_left_rpos: Vec3, + ear_right_rpos: Vec3, +} /// Holds information about the system audio devices and internal channels used /// for sfx and music playback. An instance of `AudioFrontend` is used by @@ -34,11 +41,7 @@ pub struct AudioFrontend { sfx_volume: f32, music_volume: f32, - listener_pos: Vec3, - listener_ori: Vec3, - - listener_ear_left: Vec3, - listener_ear_right: Vec3, + listener: Listener, } impl AudioFrontend { @@ -63,10 +66,8 @@ impl AudioFrontend { sfx_channels, sfx_volume: 1.0, music_volume: 1.0, - listener_pos: Vec3::zero(), - listener_ori: Vec3::zero(), - listener_ear_left: Vec3::zero(), - listener_ear_right: Vec3::zero(), + + listener: Listener::default(), } } @@ -81,10 +82,7 @@ impl AudioFrontend { sfx_channels: Vec::new(), sfx_volume: 1.0, music_volume: 1.0, - listener_pos: Vec3::zero(), - listener_ori: Vec3::zero(), - listener_ear_left: Vec3::zero(), - listener_ear_right: Vec3::zero(), + listener: Listener::default(), } } @@ -146,20 +144,15 @@ impl AudioFrontend { /// Play (once) an sfx file by file path at the give position and volume pub fn play_sfx(&mut self, sound: &str, pos: Vec3, vol: Option) { if self.audio_device.is_some() { - let calc_pos = ((pos - self.listener_pos) * FALLOFF).into_array(); - let sound = self .sound_cache .load_sound(sound) .amplify(vol.unwrap_or(1.0)); - let left_ear = self.listener_ear_left.into_array(); - let right_ear = self.listener_ear_right.into_array(); - + let listener = self.listener.clone(); if let Some(channel) = self.get_sfx_channel() { - channel.set_emitter_position(calc_pos); - channel.set_left_ear_position(left_ear); - channel.set_right_ear_position(right_ear); + channel.set_pos(pos); + channel.update(&listener); channel.play(sound); } } @@ -174,27 +167,17 @@ impl AudioFrontend { } } - pub fn set_listener_pos(&mut self, pos: &Vec3, ori: &Vec3) { - self.listener_pos = *pos; - self.listener_ori = ori.normalized(); + pub fn set_listener_pos(&mut self, pos: Vec3, ori: Vec3) { + self.listener.pos = pos; + self.listener.ori = ori.normalized(); let up = Vec3::new(0.0, 0.0, 1.0); - - let pos_left = up.cross(self.listener_ori).normalized(); - let pos_right = self.listener_ori.cross(up).normalized(); - - self.listener_ear_left = pos_left; - self.listener_ear_right = pos_right; + self.listener.ear_left_rpos = up.cross(self.listener.ori).normalized(); + self.listener.ear_right_rpos = -up.cross(self.listener.ori).normalized(); for channel in self.sfx_channels.iter_mut() { if !channel.is_done() { - // TODO: Update this to correctly determine the updated relative position of - // the SFX emitter when the player (listener) moves - // channel.set_emitter_position( - // ((channel.pos - self.listener_pos) * FALLOFF).into_array(), - // ); - channel.set_left_ear_position(pos_left.into_array()); - channel.set_right_ear_position(pos_right.into_array()); + channel.update(&self.listener); } } } diff --git a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs index eeeafca25d..56aca955b0 100644 --- a/voxygen/src/audio/sfx/event_mapper/combat/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/combat/mod.rs @@ -1,6 +1,9 @@ /// EventMapper::Combat watches the combat states of surrounding entities' and /// emits sfx related to weapons and attacks/abilities -use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}; +use crate::{ + audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}, + scene::Camera, +}; use super::EventMapper; @@ -15,7 +18,6 @@ use common::{ use hashbrown::HashMap; use specs::{Entity as EcsEntity, Join, WorldExt}; use std::time::{Duration, Instant}; -use vek::*; #[derive(Clone)] struct PreviousEntityState { @@ -39,16 +41,19 @@ pub struct CombatEventMapper { } impl EventMapper for CombatEventMapper { - fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) { + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + camera: &Camera, + triggers: &SfxTriggers, + ) { let ecs = state.ecs(); let sfx_event_bus = ecs.read_resource::>(); let mut sfx_emitter = sfx_event_bus.emitter(); - let player_position = ecs - .read_storage::() - .get(player_entity) - .map_or(Vec3::zero(), |pos| pos.0); + let cam_pos = camera.dependents().cam_pos; for (entity, pos, loadout, character) in ( &ecs.entities(), @@ -57,9 +62,7 @@ impl EventMapper for CombatEventMapper { ecs.read_storage::().maybe(), ) .join() - .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR - }) + .filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR) { if let Some(character) = character { let state = self.event_history.entry(entity).or_default(); diff --git a/voxygen/src/audio/sfx/event_mapper/mod.rs b/voxygen/src/audio/sfx/event_mapper/mod.rs index 1628185479..d5a9d6f8cb 100644 --- a/voxygen/src/audio/sfx/event_mapper/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/mod.rs @@ -9,9 +9,16 @@ use movement::MovementEventMapper; use progression::ProgressionEventMapper; use super::SfxTriggers; +use crate::scene::Camera; trait EventMapper { - fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers); + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + camera: &Camera, + triggers: &SfxTriggers, + ); } pub struct SfxEventMapper { @@ -33,10 +40,11 @@ impl SfxEventMapper { &mut self, state: &State, player_entity: specs::Entity, + camera: &Camera, triggers: &SfxTriggers, ) { for mapper in &mut self.mappers { - mapper.maintain(state, player_entity, triggers); + mapper.maintain(state, player_entity, camera, triggers); } } } diff --git a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs index a884066274..55bff58c96 100644 --- a/voxygen/src/audio/sfx/event_mapper/movement/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/movement/mod.rs @@ -2,7 +2,10 @@ /// and triggers sfx related to running, climbing and gliding, at a volume /// proportionate to the extity's size use super::EventMapper; -use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}; +use crate::{ + audio::sfx::{SfxEvent, SfxEventItem, SfxTriggerItem, SfxTriggers, SFX_DIST_LIMIT_SQR}, + scene::Camera, +}; use common::{ comp::{Body, CharacterState, PhysicsState, Pos, Vel}, event::EventBus, @@ -35,16 +38,19 @@ pub struct MovementEventMapper { } impl EventMapper for MovementEventMapper { - fn maintain(&mut self, state: &State, player_entity: EcsEntity, triggers: &SfxTriggers) { + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + camera: &Camera, + triggers: &SfxTriggers, + ) { let ecs = state.ecs(); let sfx_event_bus = ecs.read_resource::>(); let mut sfx_emitter = sfx_event_bus.emitter(); - let player_position = ecs - .read_storage::() - .get(player_entity) - .map_or(Vec3::zero(), |pos| pos.0); + let cam_pos = camera.dependents().cam_pos; for (entity, pos, vel, body, physics, character) in ( &ecs.entities(), @@ -55,9 +61,7 @@ impl EventMapper for MovementEventMapper { ecs.read_storage::().maybe(), ) .join() - .filter(|(_, e_pos, ..)| { - (e_pos.0.distance_squared(player_position)) < SFX_DIST_LIMIT_SQR - }) + .filter(|(_, e_pos, ..)| (e_pos.0.distance_squared(cam_pos)) < SFX_DIST_LIMIT_SQR) { if let Some(character) = character { let state = self.event_history.entry(entity).or_default(); diff --git a/voxygen/src/audio/sfx/event_mapper/progression/mod.rs b/voxygen/src/audio/sfx/event_mapper/progression/mod.rs index 0b845e03a9..499e39e559 100644 --- a/voxygen/src/audio/sfx/event_mapper/progression/mod.rs +++ b/voxygen/src/audio/sfx/event_mapper/progression/mod.rs @@ -2,7 +2,10 @@ /// and triggers sfx for gaining experience and levelling up use super::EventMapper; -use crate::audio::sfx::{SfxEvent, SfxEventItem, SfxTriggers}; +use crate::{ + audio::sfx::{SfxEvent, SfxEventItem, SfxTriggers}, + scene::Camera, +}; use common::{comp::Stats, event::EventBus, state::State}; use specs::WorldExt; @@ -23,7 +26,13 @@ pub struct ProgressionEventMapper { impl EventMapper for ProgressionEventMapper { #[allow(clippy::op_ref)] // TODO: Pending review in #587 - fn maintain(&mut self, state: &State, player_entity: specs::Entity, triggers: &SfxTriggers) { + fn maintain( + &mut self, + state: &State, + player_entity: specs::Entity, + _camera: &Camera, + triggers: &SfxTriggers, + ) { let ecs = state.ecs(); let next_state = ecs.read_storage::().get(player_entity).map_or( diff --git a/voxygen/src/audio/sfx/mod.rs b/voxygen/src/audio/sfx/mod.rs index b76c287cdb..783056271b 100644 --- a/voxygen/src/audio/sfx/mod.rs +++ b/voxygen/src/audio/sfx/mod.rs @@ -83,15 +83,16 @@ mod event_mapper; -use crate::audio::AudioFrontend; +use crate::{audio::AudioFrontend, scene::Camera}; use common::{ assets, comp::{ item::{ItemKind, ToolCategory}, - CharacterAbilityType, InventoryUpdateEvent, Ori, Pos, + CharacterAbilityType, InventoryUpdateEvent, }, event::EventBus, + outcome::Outcome, state::State, }; use event_mapper::SfxEventMapper; @@ -146,6 +147,8 @@ pub enum SfxEvent { Wield(ToolCategory), Unwield(ToolCategory), Inventory(SfxInventoryEvent), + Explosion, + ProjectileShot, } #[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)] @@ -224,36 +227,27 @@ impl SfxMgr { audio: &mut AudioFrontend, state: &State, player_entity: specs::Entity, + camera: &Camera, ) { if !audio.sfx_enabled() { return; } - self.event_mapper - .maintain(state, player_entity, &self.triggers); - let ecs = state.ecs(); - let player_position = ecs - .read_storage::() - .get(player_entity) - .map_or(Vec3::zero(), |pos| pos.0); + audio.set_listener_pos(camera.dependents().cam_pos, camera.dependents().cam_dir); - let player_ori = *ecs - .read_storage::() - .get(player_entity) - .copied() - .unwrap_or_default() - .0; - - audio.set_listener_pos(&player_position, &player_ori); + // TODO: replace; deprecated in favor of outcomes + self.event_mapper + .maintain(state, player_entity, camera, &self.triggers); + // TODO: replace; deprecated in favor of outcomes let events = ecs.read_resource::>().recv_all(); for event in events { let position = match event.pos { Some(pos) => pos, - _ => player_position, + _ => camera.dependents().cam_pos, }; if let Some(item) = self.triggers.get_trigger(&event.sfx) { @@ -273,6 +267,31 @@ impl SfxMgr { } } + pub fn handle_outcome(&mut self, outcome: &Outcome, audio: &mut AudioFrontend) { + if !audio.sfx_enabled() { + return; + } + + match outcome { + Outcome::Explosion { pos, power } => { + audio.play_sfx( + // TODO: from sfx triggers config + "voxygen.audio.sfx.explosion", + *pos, + Some((*power / 2.5).min(1.5)), + ); + }, + Outcome::ProjectileShot { pos, .. } => { + audio.play_sfx( + // TODO: from sfx triggers config + "voxygen.audio.sfx.glider_open", + *pos, + None, + ); + }, + } + } + fn load_sfx_items() -> SfxTriggers { match assets::load_file("voxygen.audio.sfx", &["ron"]) { Ok(file) => match ron::de::from_reader(file) { diff --git a/voxygen/src/hud/mod.rs b/voxygen/src/hud/mod.rs index 33d400735e..20a48df35e 100644 --- a/voxygen/src/hud/mod.rs +++ b/voxygen/src/hud/mod.rs @@ -179,6 +179,7 @@ widget_ids! { entity_count, num_chunks, num_figures, + num_particles, // Game Version version, @@ -247,6 +248,8 @@ pub struct DebugInfo { pub num_visible_chunks: u32, pub num_figures: u32, pub num_figures_visible: u32, + pub num_particles: u32, + pub num_particles_visible: u32, } pub struct HudInfo { @@ -275,6 +278,7 @@ pub enum Event { ChangeGamma(f32), MapZoom(f64), AdjustWindowSize([u16; 2]), + ToggleParticlesEnabled(bool), ToggleFullscreen, ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), @@ -1495,6 +1499,17 @@ impl Hud { .font_size(self.fonts.cyri.scale(14)) .set(self.ids.num_figures, ui_widgets); + // Number of particles + Text::new(&format!( + "Particles: {} ({} visible)", + debug_info.num_particles, debug_info.num_particles_visible, + )) + .color(TEXT_COLOR) + .down_from(self.ids.num_figures, 5.0) + .font_id(self.fonts.cyri.conrod_id) + .font_size(self.fonts.cyri.scale(14)) + .set(self.ids.num_particles, ui_widgets); + // Help Window if let Some(help_key) = global_state.settings.controls.get_binding(GameInput::Help) { Text::new( @@ -1504,7 +1519,7 @@ impl Hud { .replace("{key}", help_key.to_string().as_str()), ) .color(TEXT_COLOR) - .down_from(self.ids.num_figures, 5.0) + .down_from(self.ids.num_particles, 5.0) .font_id(self.fonts.cyri.conrod_id) .font_size(self.fonts.cyri.scale(14)) .set(self.ids.help_info, ui_widgets); @@ -1896,6 +1911,9 @@ impl Hud { settings_window::Event::ChangeLanguage(language) => { events.push(Event::ChangeLanguage(language)); }, + settings_window::Event::ToggleParticlesEnabled(particles_enabled) => { + events.push(Event::ToggleParticlesEnabled(particles_enabled)); + }, settings_window::Event::ToggleFullscreen => { events.push(Event::ToggleFullscreen); }, diff --git a/voxygen/src/hud/settings_window.rs b/voxygen/src/hud/settings_window.rs index 142b4d0984..f9e6b7d17b 100644 --- a/voxygen/src/hud/settings_window.rs +++ b/voxygen/src/hud/settings_window.rs @@ -111,6 +111,8 @@ widget_ids! { cloud_mode_list, fluid_mode_text, fluid_mode_list, + particles_button, + particles_label, fullscreen_button, fullscreen_label, save_window_size_button, @@ -232,6 +234,7 @@ pub enum Event { AdjustFOV(u16), AdjustGamma(f32), AdjustWindowSize([u16; 2]), + ToggleParticlesEnabled(bool), ToggleFullscreen, ChangeAaMode(AaMode), ChangeCloudMode(CloudMode), @@ -2013,11 +2016,34 @@ impl<'a> Widget for SettingsWindow<'a> { events.push(Event::ChangeFluidMode(mode_list[clicked])); } + // Particles + Text::new(&self.localized_strings.get("hud.settings.particles")) + .font_size(self.fonts.cyri.scale(14)) + .font_id(self.fonts.cyri.conrod_id) + .down_from(state.ids.fluid_mode_list, 8.0) + .color(TEXT_COLOR) + .set(state.ids.particles_label, ui); + + let particles_enabled = ToggleButton::new( + self.global_state.settings.graphics.particles_enabled, + self.imgs.checkbox, + self.imgs.checkbox_checked, + ) + .w_h(18.0, 18.0) + .right_from(state.ids.particles_label, 10.0) + .hover_images(self.imgs.checkbox_mo, self.imgs.checkbox_checked_mo) + .press_images(self.imgs.checkbox_press, self.imgs.checkbox_checked) + .set(state.ids.particles_button, ui); + + if self.global_state.settings.graphics.particles_enabled != particles_enabled { + events.push(Event::ToggleParticlesEnabled(particles_enabled)); + } + // Fullscreen Text::new(&self.localized_strings.get("hud.settings.fullscreen")) .font_size(self.fonts.cyri.scale(14)) .font_id(self.fonts.cyri.conrod_id) - .down_from(state.ids.fluid_mode_list, 8.0) + .down_from(state.ids.particles_label, 8.0) .color(TEXT_COLOR) .set(state.ids.fullscreen_label, ui); diff --git a/voxygen/src/mesh/segment.rs b/voxygen/src/mesh/segment.rs index c69d348b45..2909c51f35 100644 --- a/voxygen/src/mesh/segment.rs +++ b/voxygen/src/mesh/segment.rs @@ -1,6 +1,6 @@ use crate::{ mesh::{vol, Meshable}, - render::{self, FigurePipeline, Mesh, SpritePipeline}, + render::{self, FigurePipeline, Mesh, ParticlePipeline, SpritePipeline}, }; use common::{ figure::Cell, @@ -11,6 +11,7 @@ use vek::*; type FigureVertex = ::Vertex; type SpriteVertex = ::Vertex; +type ParticleVertex = ::Vertex; impl<'a, V: 'a> Meshable<'a, FigurePipeline, FigurePipeline> for V where @@ -147,6 +148,73 @@ where } } +impl<'a, V: 'a> Meshable<'a, ParticlePipeline, ParticlePipeline> for V +where + V: BaseVol + ReadVol + SizedVol, + /* TODO: Use VolIterator instead of manually iterating + * &'a V: IntoVolIterator<'a> + IntoFullVolIterator<'a>, + * &'a V: BaseVol, */ +{ + type Pipeline = ParticlePipeline; + type Supplement = (Vec3, Vec3); + type TranslucentPipeline = ParticlePipeline; + + #[allow(clippy::needless_range_loop)] // TODO: Pending review in #587 + #[allow(clippy::or_fun_call)] // TODO: Pending review in #587 + fn generate_mesh( + &'a self, + (offs, scale): Self::Supplement, + ) -> (Mesh, Mesh) { + let mut mesh = Mesh::new(); + + let vol_iter = (self.lower_bound().x..self.upper_bound().x) + .map(|i| { + (self.lower_bound().y..self.upper_bound().y).map(move |j| { + (self.lower_bound().z..self.upper_bound().z).map(move |k| Vec3::new(i, j, k)) + }) + }) + .flatten() + .flatten() + .map(|pos| (pos, self.get(pos).map(|x| *x).unwrap_or(Vox::empty()))); + + for (pos, vox) in vol_iter { + if let Some(col) = vox.get_color() { + vol::push_vox_verts( + &mut mesh, + faces_to_make(self, pos, true, |vox| vox.is_empty()), + offs + pos.map(|e| e as f32), + &[[[Rgba::from_opaque(col); 3]; 3]; 3], + |origin, norm, col, light, ao| { + ParticleVertex::new( + origin * scale, + norm, + linear_to_srgb(srgb_to_linear(col) * light), + ao, + ) + }, + &{ + let mut ls = [[[None; 3]; 3]; 3]; + for x in 0..3 { + for y in 0..3 { + for z in 0..3 { + ls[z][y][x] = self + .get(pos + Vec3::new(x as i32, y as i32, z as i32) - 1) + .map(|v| v.is_empty()) + .unwrap_or(true) + .then_some(1.0); + } + } + } + ls + }, + ); + } + } + + (mesh, Mesh::new()) + } +} + /// Use the 6 voxels/blocks surrounding the one at the specified position /// to detemine which faces should be drawn fn faces_to_make( diff --git a/voxygen/src/render/mod.rs b/voxygen/src/render/mod.rs index 9bf74d5aa9..317ba97db6 100644 --- a/voxygen/src/render/mod.rs +++ b/voxygen/src/render/mod.rs @@ -18,6 +18,7 @@ pub use self::{ pipelines::{ figure::{BoneData as FigureBoneData, FigurePipeline, Locals as FigureLocals}, fluid::FluidPipeline, + particle::{Instance as ParticleInstance, ParticlePipeline}, postprocess::{ create_mesh as create_pp_mesh, Locals as PostProcessLocals, PostProcessPipeline, }, diff --git a/voxygen/src/render/pipelines/mod.rs b/voxygen/src/render/pipelines/mod.rs index 81ef76456f..442b31be32 100644 --- a/voxygen/src/render/pipelines/mod.rs +++ b/voxygen/src/render/pipelines/mod.rs @@ -1,5 +1,6 @@ pub mod figure; pub mod fluid; +pub mod particle; pub mod postprocess; pub mod skybox; pub mod sprite; @@ -116,6 +117,11 @@ impl Light { } pub fn get_pos(&self) -> Vec3 { Vec3::new(self.pos[0], self.pos[1], self.pos[2]) } + + pub fn with_strength(mut self, strength: f32) -> Self { + self.col = (Vec4::::from(self.col) * strength).into_array(); + self + } } impl Default for Light { diff --git a/voxygen/src/render/pipelines/particle.rs b/voxygen/src/render/pipelines/particle.rs new file mode 100644 index 0000000000..eae013242d --- /dev/null +++ b/voxygen/src/render/pipelines/particle.rs @@ -0,0 +1,116 @@ +use super::{ + super::{Pipeline, TgtColorFmt, TgtDepthStencilFmt}, + Globals, Light, Shadow, +}; +use gfx::{ + self, gfx_defines, gfx_impl_struct_meta, gfx_pipeline, gfx_pipeline_inner, + gfx_vertex_struct_meta, + state::{ColorMask, Comparison, Stencil, StencilOp}, +}; +use vek::*; + +gfx_defines! { + vertex Vertex { + pos: [f32; 3] = "v_pos", + // ____BBBBBBBBGGGGGGGGRRRRRRRR + col: u32 = "v_col", + // ...AANNN + // A = AO + // N = Normal + norm_ao: u32 = "v_norm_ao", + } + + vertex Instance { + // created_at time, so we can calculate time relativity, needed for relative animation. + // can save 32 bits per instance, for particles that are not relatively animated. + inst_time: f32 = "inst_time", + + // a seed value for randomness + // can save 32 bits per instance, for particles that don't need randomness/uniqueness. + inst_entropy: f32 = "inst_entropy", + + // modes should probably be seperate shaders, as a part of scaling and optimisation efforts. + // can save 32 bits per instance, and have cleaner tailor made code. + inst_mode: i32 = "inst_mode", + + // a triangle is: f32 x 3 x 3 x 1 = 288 bits + // a quad is: f32 x 3 x 3 x 2 = 576 bits + // a cube is: f32 x 3 x 3 x 12 = 3456 bits + // this vec is: f32 x 3 x 1 x 1 = 96 bits (per instance!) + // consider using a throw-away mesh and + // positioning the vertex verticies instead, + // if we have: + // - a triangle mesh, and 3 or more instances. + // - a quad mesh, and 6 or more instances. + // - a cube mesh, and 36 or more instances. + inst_pos: [f32; 3] = "inst_pos", + } + + pipeline pipe { + vbuf: gfx::VertexBuffer = (), + ibuf: gfx::InstanceBuffer = (), + + globals: gfx::ConstantBuffer = "u_globals", + lights: gfx::ConstantBuffer = "u_lights", + shadows: gfx::ConstantBuffer = "u_shadows", + + noise: gfx::TextureSampler = "t_noise", + + tgt_color: gfx::BlendTarget = ("tgt_color", ColorMask::all(), gfx::preset::blend::ALPHA), + tgt_depth_stencil: gfx::DepthStencilTarget = (gfx::preset::depth::LESS_EQUAL_WRITE,Stencil::new(Comparison::Always,0xff,(StencilOp::Keep,StencilOp::Keep,StencilOp::Keep))), + } +} + +impl Vertex { + #[allow(clippy::collapsible_if)] + pub fn new(pos: Vec3, norm: Vec3, col: Rgb, ao: f32) -> Self { + let norm_bits = if norm.x != 0.0 { + if norm.x < 0.0 { 0 } else { 1 } + } else if norm.y != 0.0 { + if norm.y < 0.0 { 2 } else { 3 } + } else { + if norm.z < 0.0 { 4 } else { 5 } + }; + + Self { + pos: pos.into_array(), + col: col + .map2(Rgb::new(0, 8, 16), |e, shift| ((e * 255.0) as u32) << shift) + .reduce_bitor(), + norm_ao: norm_bits | (((ao * 3.9999) as u32) << 3), + } + } +} + +pub enum ParticleMode { + CampfireSmoke = 0, + CampfireFire = 1, + GunPowderSpark = 2, + Shrapnel = 3, +} + +impl ParticleMode { + pub fn into_uint(self) -> u32 { self as u32 } +} + +impl Instance { + 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: rand::thread_rng().gen(), + inst_mode: inst_mode as i32, + inst_pos: inst_pos.into_array(), + } + } +} + +impl Default for Instance { + fn default() -> Self { Self::new(0.0, ParticleMode::CampfireSmoke, Vec3::zero()) } +} + +pub struct ParticlePipeline; + +impl Pipeline for ParticlePipeline { + type Vertex = Vertex; +} diff --git a/voxygen/src/render/renderer.rs b/voxygen/src/render/renderer.rs index a6e37ff192..1fa5cc6f2c 100644 --- a/voxygen/src/render/renderer.rs +++ b/voxygen/src/render/renderer.rs @@ -4,7 +4,9 @@ use super::{ instances::Instances, mesh::Mesh, model::{DynamicModel, Model}, - pipelines::{figure, fluid, postprocess, skybox, sprite, terrain, ui, Globals, Light, Shadow}, + pipelines::{ + figure, fluid, particle, postprocess, skybox, sprite, terrain, ui, Globals, Light, Shadow, + }, texture::Texture, AaMode, CloudMode, FluidMode, Pipeline, RenderError, }; @@ -70,6 +72,7 @@ pub struct Renderer { terrain_pipeline: GfxPipeline>, fluid_pipeline: GfxPipeline>, sprite_pipeline: GfxPipeline>, + particle_pipeline: GfxPipeline>, ui_pipeline: GfxPipeline>, postprocess_pipeline: GfxPipeline>, player_shadow_pipeline: GfxPipeline>, @@ -103,6 +106,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, @@ -146,6 +150,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, @@ -341,6 +346,7 @@ impl Renderer { terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, @@ -350,6 +356,7 @@ impl Renderer { self.terrain_pipeline = terrain_pipeline; self.fluid_pipeline = fluid_pipeline; self.sprite_pipeline = sprite_pipeline; + self.particle_pipeline = particle_pipeline; self.ui_pipeline = ui_pipeline; self.postprocess_pipeline = postprocess_pipeline; self.player_shadow_pipeline = player_shadow_pipeline; @@ -711,6 +718,37 @@ impl Renderer { ); } + /// Queue the rendering of the provided particle in the upcoming frame. + pub fn render_particles( + &mut self, + model: &Model, + globals: &Consts, + instances: &Instances, + lights: &Consts, + shadows: &Consts, + ) { + self.encoder.draw( + &gfx::Slice { + start: model.vertex_range().start, + end: model.vertex_range().end, + base_vertex: 0, + instances: Some((instances.count() as u32, 0)), + buffer: gfx::IndexBuffer::Auto, + }, + &self.particle_pipeline.pso, + &particle::pipe::Data { + vbuf: model.vbuf.clone(), + ibuf: instances.ibuf.clone(), + globals: globals.buf.clone(), + lights: lights.buf.clone(), + shadows: shadows.buf.clone(), + noise: (self.noise_tex.srv.clone(), self.noise_tex.sampler.clone()), + tgt_color: self.tgt_color_view.clone(), + tgt_depth_stencil: (self.tgt_depth_stencil_view.clone(), (1, 1)), + }, + ); + } + /// Queue the rendering of the provided UI element in the upcoming frame. pub fn render_ui_element( &mut self, @@ -793,6 +831,7 @@ fn create_pipelines( GfxPipeline>, GfxPipeline>, GfxPipeline>, + GfxPipeline>, GfxPipeline>, GfxPipeline>, GfxPipeline>, @@ -914,6 +953,18 @@ fn create_pipelines( gfx::state::CullFace::Back, )?; + // Construct a pipeline for rendering particles + let particle_pipeline = create_pipeline( + factory, + particle::pipe::new(), + &assets::load_watched::("voxygen.shaders.particle-vert", shader_reload_indicator) + .unwrap(), + &assets::load_watched::("voxygen.shaders.particle-frag", shader_reload_indicator) + .unwrap(), + &include_ctx, + gfx::state::CullFace::Back, + )?; + // Construct a pipeline for rendering UI elements let ui_pipeline = create_pipeline( factory, @@ -975,6 +1026,7 @@ fn create_pipelines( terrain_pipeline, fluid_pipeline, sprite_pipeline, + particle_pipeline, ui_pipeline, postprocess_pipeline, player_shadow_pipeline, diff --git a/voxygen/src/scene/camera.rs b/voxygen/src/scene/camera.rs index 265688dc82..deaadcc963 100644 --- a/voxygen/src/scene/camera.rs +++ b/voxygen/src/scene/camera.rs @@ -24,11 +24,12 @@ impl Default for CameraMode { fn default() -> Self { Self::ThirdPerson } } -#[derive(Clone)] +#[derive(Clone, Copy)] pub struct Dependents { pub view_mat: Mat4, pub proj_mat: Mat4, pub cam_pos: Vec3, + pub cam_dir: Vec3, } pub struct Camera { @@ -67,6 +68,7 @@ impl Camera { view_mat: Mat4::identity(), proj_mat: Mat4::identity(), cam_pos: Vec3::zero(), + cam_dir: Vec3::unit_y(), }, } } @@ -104,6 +106,8 @@ impl Camera { // TODO: Make this more efficient. self.dependents.cam_pos = Vec3::from(self.dependents.view_mat.inverted() * Vec4::unit_w()); + + self.dependents.cam_dir = Vec3::from(self.dependents.view_mat.inverted() * -Vec4::unit_z()); } pub fn frustum(&self) -> Frustum { @@ -112,7 +116,7 @@ impl Camera { ) } - pub fn dependents(&self) -> Dependents { self.dependents.clone() } + pub fn dependents(&self) -> Dependents { self.dependents } /// Rotate the camera about its focus by the given delta, limiting the input /// accordingly. diff --git a/voxygen/src/scene/mod.rs b/voxygen/src/scene/mod.rs index 4c18a92207..5eacf0870b 100644 --- a/voxygen/src/scene/mod.rs +++ b/voxygen/src/scene/mod.rs @@ -1,11 +1,13 @@ pub mod camera; pub mod figure; +pub mod particle; pub mod simple; pub mod terrain; -use self::{ +pub use self::{ camera::{Camera, CameraMode}, figure::FigureMgr, + particle::ParticleMgr, terrain::Terrain, }; use crate::{ @@ -19,7 +21,8 @@ use crate::{ use anim::character::SkeletonAttr; use common::{ comp, - state::State, + outcome::Outcome, + state::{DeltaTime, State}, terrain::{BlockKind, TerrainChunk}, vol::ReadVol, }; @@ -39,6 +42,12 @@ const SHADOW_MAX_DIST: f32 = 96.0; // The distance beyond which shadows may not /// Used for first person camera effects const RUNNING_THRESHOLD: f32 = 0.7; +struct EventLight { + light: Light, + timeout: f32, + fadeout: fn(f32) -> f32, +} + struct Skybox { model: Model, locals: Consts, @@ -55,6 +64,7 @@ pub struct Scene { shadows: Consts, camera: Camera, camera_input_state: Vec2, + event_lights: Vec, skybox: Skybox, postprocess: PostProcess, @@ -62,6 +72,7 @@ pub struct Scene { loaded_distance: f32, select_pos: Option>, + particle_mgr: ParticleMgr, figure_mgr: FigureMgr, sfx_mgr: SfxMgr, music_mgr: MusicMgr, @@ -78,6 +89,7 @@ pub struct SceneData<'a> { pub gamma: f32, pub mouse_smoothing: bool, pub sprite_render_distance: f32, + pub particles_enabled: bool, pub figure_lod_render_distance: f32, pub is_aiming: bool, } @@ -97,6 +109,7 @@ impl Scene { .unwrap(), camera: Camera::new(resolution.x / resolution.y, CameraMode::ThirdPerson), camera_input_state: Vec2::zero(), + event_lights: Vec::new(), skybox: Skybox { model: renderer.create_model(&create_skybox_mesh()).unwrap(), @@ -112,6 +125,7 @@ impl Scene { loaded_distance: 0.0, select_pos: None, + particle_mgr: ParticleMgr::new(renderer), figure_mgr: FigureMgr::new(), sfx_mgr: SfxMgr::new(), music_mgr: MusicMgr::new(), @@ -127,6 +141,9 @@ impl Scene { /// Get a reference to the scene's terrain. pub fn terrain(&self) -> &Terrain { &self.terrain } + /// Get a reference to the scene's particle manager. + pub fn particle_mgr(&self) -> &ParticleMgr { &self.particle_mgr } + /// Get a reference to the scene's figure manager. pub fn figure_mgr(&self) -> &FigureMgr { &self.figure_mgr } @@ -176,6 +193,25 @@ impl Scene { } } + pub fn handle_outcome( + &mut self, + outcome: &Outcome, + scene_data: &SceneData, + audio: &mut AudioFrontend, + ) { + self.particle_mgr.handle_outcome(&outcome, &scene_data); + self.sfx_mgr.handle_outcome(&outcome, audio); + + match outcome { + Outcome::Explosion { pos, power, .. } => self.event_lights.push(EventLight { + light: Light::new(*pos, Rgb::new(1.0, 0.5, 0.0), *power * 2.5), + timeout: 0.5, + fadeout: |timeout| timeout * 2.0, + }), + Outcome::ProjectileShot { .. } => {}, + } + } + /// Maintain data such as GPU constant buffers, models, etc. To be called /// once per tick. pub fn maintain( @@ -265,6 +301,7 @@ impl Scene { view_mat, proj_mat, cam_pos, + .. } = self.camera.dependents(); // Update chunk loaded distance smoothly for nice shader fog @@ -307,6 +344,11 @@ impl Scene { light_anim.strength, ) }) + .chain( + self.event_lights + .iter() + .map(|el| el.light.with_strength((el.fadeout)(el.timeout))), + ) .collect::>(); lights.sort_by_key(|light| light.get_pos().distance_squared(player_pos) as i32); lights.truncate(MAX_LIGHT_COUNT); @@ -314,6 +356,13 @@ impl Scene { .update_consts(&mut self.lights, &lights) .expect("Failed to update light constants"); + // Update event lights + let dt = ecs.fetch::().0; + self.event_lights.drain_filter(|el| { + el.timeout -= dt; + el.timeout <= 0.0 + }); + // Update shadow constants let mut shadows = ( &scene_data.state.ecs().read_storage::(), @@ -388,9 +437,16 @@ impl Scene { // Remove unused figures. self.figure_mgr.clean(scene_data.tick); + // Maintain the particles. + self.particle_mgr.maintain(renderer, &scene_data); + // Maintain audio - self.sfx_mgr - .maintain(audio, scene_data.state, scene_data.player_entity); + self.sfx_mgr.maintain( + audio, + scene_data.state, + scene_data.player_entity, + &self.camera, + ); self.music_mgr.maintain(audio, scene_data.state); } @@ -423,6 +479,14 @@ impl Scene { scene_data.figure_lod_render_distance, ); + self.particle_mgr.render( + renderer, + scene_data, + &self.globals, + &self.lights, + &self.shadows, + ); + // Render the skybox. renderer.render_skybox(&self.skybox.model, &self.globals, &self.skybox.locals); diff --git a/voxygen/src/scene/particle.rs b/voxygen/src/scene/particle.rs new file mode 100644 index 0000000000..11e3b74a1d --- /dev/null +++ b/voxygen/src/scene/particle.rs @@ -0,0 +1,369 @@ +use super::SceneData; +use crate::{ + mesh::Meshable, + render::{ + pipelines::particle::ParticleMode, Consts, Globals, Instances, Light, Model, + ParticleInstance, ParticlePipeline, Renderer, Shadow, + }, +}; +use common::{ + assets, + comp::{object, Body, CharacterState, Pos}, + figure::Segment, + outcome::Outcome, +}; +use dot_vox::DotVoxData; +use hashbrown::HashMap; +use rand::Rng; +use specs::{Join, WorldExt}; +use std::time::{Duration, Instant}; +use vek::*; + +pub struct ParticleMgr { + /// 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>, +} + +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 handle_outcome(&mut self, outcome: &Outcome, scene_data: &SceneData) { + let time = scene_data.state.get_time(); + let mut rng = rand::thread_rng(); + + match outcome { + Outcome::Explosion { pos, power } => { + for _ in 0..150 { + self.particles.push(Particle::new( + Duration::from_millis(250), + time, + ParticleMode::Shrapnel, + *pos, + )); + } + for _ in 0..200 { + 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 { .. } => {}, + } + } + + pub fn maintain(&mut self, renderer: &mut Renderer, scene_data: &SceneData) { + if scene_data.particles_enabled { + let now = Instant::now(); + + // remove dead Particle + self.particles.retain(|p| p.alive_until > now); + + // add new Particle + self.maintain_body_particles(scene_data); + self.maintain_boost_particles(scene_data); + + // update timings + self.scheduler.maintain(); + } else { + // remove all particle lifespans + self.particles.clear(); + + // remove all timings + self.scheduler.clear(); + } + + self.upload_particles(renderer); + } + + fn maintain_body_particles(&mut self, scene_data: &SceneData) { + let ecs = scene_data.state.ecs(); + for (_i, (_entity, body, pos)) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .enumerate() + { + match body { + Body::Object(object::Body::CampfireLit) => { + self.maintain_campfirelit_particles(scene_data, pos) + }, + Body::Object(object::Body::BoltFire) => { + self.maintain_boltfire_particles(scene_data, pos) + }, + Body::Object(object::Body::BoltFireBig) => { + self.maintain_boltfirebig_particles(scene_data, pos) + }, + Body::Object(object::Body::Bomb) => self.maintain_bomb_particles(scene_data, pos), + _ => {}, + } + } + } + + fn maintain_campfirelit_particles(&mut self, scene_data: &SceneData, pos: &Pos) { + let time = scene_data.state.get_time(); + + 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, + )); + } + } + + fn maintain_boltfire_particles(&mut self, scene_data: &SceneData, pos: &Pos) { + let time = scene_data.state.get_time(); + + 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(); + + // fire + 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 + 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(); + + 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, + )); + } + } + + fn maintain_boost_particles(&mut self, scene_data: &SceneData) { + let state = scene_data.state; + let ecs = state.ecs(); + let time = state.get_time(); + + for (_i, (_entity, pos, character_state)) in ( + &ecs.entities(), + &ecs.read_storage::(), + &ecs.read_storage::(), + ) + .join() + .enumerate() + { + if let CharacterState::Boost(_) = character_state { + for _ in 0..self.scheduler.heartbeats(Duration::from_millis(10)) { + self.particles.push(Particle::new( + Duration::from_secs(15), + time, + 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, + scene_data: &SceneData, + globals: &Consts, + lights: &Consts, + shadows: &Consts, + ) { + if scene_data.particles_enabled { + let model = &self + .model_cache + .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 { + let empty_vec = Vec::new(); + + renderer + .create_instances(&empty_vec) + .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(DEFAULT_MODEL_KEY).or_insert_with(|| { + let offset = Vec3::zero(); + let lod_scale = Vec3::one(); + + let vox = assets::load_expect::(DEFAULT_MODEL_KEY); + + let mesh = &Meshable::::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 +} + +/// Accumulates heartbeats to be consumed on the next tick. +struct HeartbeatScheduler { + /// Duration = Heartbeat Frequency/Intervals + /// Instant = Last update time + /// 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. + timers: HashMap, +} + +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), + } + } +} diff --git a/voxygen/src/scene/simple.rs b/voxygen/src/scene/simple.rs index c89ebc6511..259419fee6 100644 --- a/voxygen/src/scene/simple.rs +++ b/voxygen/src/scene/simple.rs @@ -171,6 +171,7 @@ impl Scene { view_mat, proj_mat, cam_pos, + .. } = self.camera.dependents(); const VD: f32 = 115.0; // View Distance const TIME: f64 = 43200.0; // 12 hours*3600 seconds diff --git a/voxygen/src/session.rs b/voxygen/src/session.rs index 2f5cf8df9e..0decee01f3 100644 --- a/voxygen/src/session.rs +++ b/voxygen/src/session.rs @@ -21,6 +21,7 @@ use common::{ }, event::EventBus, msg::ClientState, + outcome::Outcome, terrain::{Block, BlockKind}, util::Dir, vol::ReadVol, @@ -100,7 +101,12 @@ impl SessionState { } /// Tick the session (and the client attached to it). - fn tick(&mut self, dt: Duration, global_state: &mut GlobalState) -> Result { + fn tick( + &mut self, + dt: Duration, + global_state: &mut GlobalState, + outcomes: &mut Vec, + ) -> Result { self.inputs.tick(dt); let mut client = self.client.borrow_mut(); @@ -158,6 +164,7 @@ impl SessionState { global_state.settings.graphics.view_distance = vd; global_state.settings.save_to_file_warn(); }, + client::Event::Outcome(outcome) => outcomes.push(outcome), } } @@ -209,7 +216,7 @@ impl PlayState for SessionState { .camera_mut() .compute_dependents(&*self.client.borrow().state().terrain()); let camera::Dependents { - view_mat, cam_pos, .. + cam_pos, cam_dir, .. } = self.scene.camera().dependents(); let (is_aiming, aim_dir_offset) = { @@ -232,8 +239,6 @@ impl PlayState for SessionState { }; self.is_aiming = is_aiming; - let cam_dir: Vec3 = Vec3::from(view_mat.inverted() * -Vec4::unit_z()); - // Check to see whether we're aiming at anything let (build_pos, select_pos, target_entity) = under_cursor(&self.client.borrow(), cam_pos, cam_dir); @@ -645,10 +650,16 @@ impl PlayState for SessionState { self.inputs.climb = self.key_state.climb(); + let mut outcomes = Vec::new(); + // Runs if either in a multiplayer server or the singleplayer server is unpaused if !global_state.paused() { // Perform an in-game tick. - match self.tick(global_state.clock.get_avg_delta(), global_state) { + match self.tick( + global_state.clock.get_avg_delta(), + global_state, + &mut outcomes, + ) { Ok(TickAction::Continue) => {}, // Do nothing Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu Err(err) => { @@ -701,6 +712,9 @@ impl PlayState for SessionState { num_visible_chunks: self.scene.terrain().visible_chunk_count() as u32, num_figures: self.scene.figure_mgr().figure_count() as u32, num_figures_visible: self.scene.figure_mgr().figure_count_visible() as u32, + num_particles: self.scene.particle_mgr().particle_count() as u32, + num_particles_visible: self.scene.particle_mgr().particle_count_visible() + as u32, }, &self.scene.camera(), global_state.clock.get_last_delta(), @@ -946,6 +960,10 @@ impl PlayState for SessionState { self.voxygen_i18n.log_missing_entries(); self.hud.update_language(self.voxygen_i18n.clone()); }, + HudEvent::ToggleParticlesEnabled(particles_enabled) => { + global_state.settings.graphics.particles_enabled = particles_enabled; + global_state.settings.save_to_file_warn(); + }, HudEvent::ToggleFullscreen => { global_state .window @@ -1010,6 +1028,7 @@ impl PlayState for SessionState { mouse_smoothing: global_state.settings.gameplay.smooth_pan_enable, sprite_render_distance: global_state.settings.graphics.sprite_render_distance as f32, + particles_enabled: global_state.settings.graphics.particles_enabled, figure_lod_render_distance: global_state .settings .graphics @@ -1025,6 +1044,12 @@ impl PlayState for SessionState { &mut global_state.audio, &scene_data, ); + + // Process outcomes from client + for outcome in outcomes { + self.scene + .handle_outcome(&outcome, &scene_data, &mut global_state.audio); + } } } @@ -1065,6 +1090,7 @@ impl PlayState for SessionState { mouse_smoothing: settings.gameplay.smooth_pan_enable, sprite_render_distance: settings.graphics.sprite_render_distance as f32, figure_lod_render_distance: settings.graphics.figure_lod_render_distance as f32, + particles_enabled: settings.graphics.particles_enabled, is_aiming: self.is_aiming, }; self.scene.render( diff --git a/voxygen/src/settings.rs b/voxygen/src/settings.rs index e8ef4c38a3..43b0f2f19c 100644 --- a/voxygen/src/settings.rs +++ b/voxygen/src/settings.rs @@ -611,6 +611,7 @@ impl Default for Log { pub struct GraphicsSettings { pub view_distance: u32, pub sprite_render_distance: u32, + pub particles_enabled: bool, pub figure_lod_render_distance: u32, pub max_fps: u32, pub fov: u16, @@ -627,6 +628,7 @@ impl Default for GraphicsSettings { Self { view_distance: 10, sprite_render_distance: 150, + particles_enabled: true, figure_lod_render_distance: 250, max_fps: 60, fov: 50,