Merge branch 'scott-c/particles' into 'master'

Particles

See merge request veloren/veloren!1156
This commit is contained in:
Imbris 2020-08-09 17:09:43 +00:00
commit 893da3622a
42 changed files with 1210 additions and 108 deletions

View File

@ -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

View File

@ -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,
),
}
)

BIN
assets/voxygen/audio/sfx/explosion.wav (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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",

View File

@ -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;
}

View File

@ -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) {

View File

@ -0,0 +1,38 @@
#version 330 core
#include <globals.glsl>
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 <sky.glsl>
#include <light.glsl>
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));
}

View File

@ -0,0 +1,131 @@
#version 330 core
#include <globals.glsl>
#include <srgb.glsl>
#include <random.glsl>
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);
}

BIN
assets/voxygen/voxel/particle.vox (Stored with Git LFS) Normal file

Binary file not shown.

View File

@ -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))
},
}
}
}

View File

@ -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",

View File

@ -261,6 +261,7 @@ impl Tool {
col: (0.85, 0.5, 0.11).into(),
..Default::default()
}),
projectile_gravity: None,
},
BasicRanged {

View File

@ -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};

View File

@ -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<f32> { &*self.0 }
}
impl Component for Ori {
type Storage = IdvStorage<Self>;
}

View File

@ -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;

View File

@ -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<Outcome>),
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]

30
common/src/outcome.rs Normal file
View File

@ -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<f32>,
power: f32,
},
ProjectileShot {
pos: Vec3<f32>,
body: comp::Body,
vel: Vec3<f32>,
},
}
impl Outcome {
pub fn get_pos(&self) -> Option<Vec3<f32>> {
match self {
Outcome::Explosion { pos, .. } => Some(*pos),
Outcome::ProjectileShot { pos, .. } => Some(*pos),
}
}
}

View File

@ -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::<comp::Pos>(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,

View File

@ -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::<Vec<Outcome>>()
.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)
}

View File

@ -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::<Vec<Outcome>>()
.push(Outcome::Explosion { pos, power });
let owner_entity = owner.and_then(|uid| {
ecs.read_resource::<UidAllocator>()
.retrieve_entity_internal(uid.into())

View File

@ -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::<Outcome>::new());
// System timers for performance monitoring
state.ecs_mut().insert(sys::EntitySyncTimer::default());

View File

@ -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<Pos>>,
WriteStorage<'a, Last<Vel>>,
WriteStorage<'a, Last<Ori>>,
@ -40,6 +45,7 @@ impl<'a> System<'a> for Sys {
WriteStorage<'a, ForceUpdate>,
WriteStorage<'a, InventoryUpdate>,
Write<'a, DeletedEntities>,
Write<'a, Vec<Outcome>>,
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<f32>| {
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::<Vec<_>>();
if outcomes.len() > 0 {
client.notify(ServerMsg::Outcomes(outcomes));
}
}
outcomes.clear();
// Remove all force flags.
force_updates.clear();
inventory_updates.clear();

View File

@ -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<f32>) { 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());
}
}

View File

@ -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<f32>,
ori: Vec3<f32>,
ear_left_rpos: Vec3<f32>,
ear_right_rpos: Vec3<f32>,
}
/// 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<f32>,
listener_ori: Vec3<f32>,
listener_ear_left: Vec3<f32>,
listener_ear_right: Vec3<f32>,
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<f32>, vol: Option<f32>) {
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<f32>, ori: &Vec3<f32>) {
self.listener_pos = *pos;
self.listener_ori = ori.normalized();
pub fn set_listener_pos(&mut self, pos: Vec3<f32>, ori: Vec3<f32>) {
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);
}
}
}

View File

@ -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::<EventBus<SfxEventItem>>();
let mut sfx_emitter = sfx_event_bus.emitter();
let player_position = ecs
.read_storage::<Pos>()
.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::<CharacterState>().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();

View File

@ -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);
}
}
}

View File

@ -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::<EventBus<SfxEventItem>>();
let mut sfx_emitter = sfx_event_bus.emitter();
let player_position = ecs
.read_storage::<Pos>()
.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::<CharacterState>().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();

View File

@ -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::<Stats>().get(player_entity).map_or(

View File

@ -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::<Pos>()
.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::<Ori>()
.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::<EventBus<SfxEventItem>>().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) {

View File

@ -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);
},

View File

@ -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);

View File

@ -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 = <FigurePipeline as render::Pipeline>::Vertex;
type SpriteVertex = <SpritePipeline as render::Pipeline>::Vertex;
type ParticleVertex = <ParticlePipeline as render::Pipeline>::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<Vox = Cell> + ReadVol + SizedVol,
/* TODO: Use VolIterator instead of manually iterating
* &'a V: IntoVolIterator<'a> + IntoFullVolIterator<'a>,
* &'a V: BaseVol<Vox=Cell>, */
{
type Pipeline = ParticlePipeline;
type Supplement = (Vec3<f32>, Vec3<f32>);
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<Self::Pipeline>, Mesh<Self::TranslucentPipeline>) {
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<V: ReadVol>(

View File

@ -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,
},

View File

@ -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<f32> { Vec3::new(self.pos[0], self.pos[1], self.pos[2]) }
pub fn with_strength(mut self, strength: f32) -> Self {
self.col = (Vec4::<f32>::from(self.col) * strength).into_array();
self
}
}
impl Default for Light {

View File

@ -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<Vertex> = (),
ibuf: gfx::InstanceBuffer<Instance> = (),
globals: gfx::ConstantBuffer<Globals> = "u_globals",
lights: gfx::ConstantBuffer<Light> = "u_lights",
shadows: gfx::ConstantBuffer<Shadow> = "u_shadows",
noise: gfx::TextureSampler<f32> = "t_noise",
tgt_color: gfx::BlendTarget<TgtColorFmt> = ("tgt_color", ColorMask::all(), gfx::preset::blend::ALPHA),
tgt_depth_stencil: gfx::DepthStencilTarget<TgtDepthStencilFmt> = (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<f32>, norm: Vec3<f32>, col: Rgb<f32>, 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<f32>) -> 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;
}

View File

@ -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<terrain::pipe::Init<'static>>,
fluid_pipeline: GfxPipeline<fluid::pipe::Init<'static>>,
sprite_pipeline: GfxPipeline<sprite::pipe::Init<'static>>,
particle_pipeline: GfxPipeline<particle::pipe::Init<'static>>,
ui_pipeline: GfxPipeline<ui::pipe::Init<'static>>,
postprocess_pipeline: GfxPipeline<postprocess::pipe::Init<'static>>,
player_shadow_pipeline: GfxPipeline<figure::pipe::Init<'static>>,
@ -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<particle::ParticlePipeline>,
globals: &Consts<Globals>,
instances: &Instances<particle::Instance>,
lights: &Consts<Light>,
shadows: &Consts<Shadow>,
) {
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<terrain::pipe::Init<'static>>,
GfxPipeline<fluid::pipe::Init<'static>>,
GfxPipeline<sprite::pipe::Init<'static>>,
GfxPipeline<particle::pipe::Init<'static>>,
GfxPipeline<ui::pipe::Init<'static>>,
GfxPipeline<postprocess::pipe::Init<'static>>,
GfxPipeline<figure::pipe::Init<'static>>,
@ -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::<String>("voxygen.shaders.particle-vert", shader_reload_indicator)
.unwrap(),
&assets::load_watched::<String>("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,

View File

@ -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<f32>,
pub proj_mat: Mat4<f32>,
pub cam_pos: Vec3<f32>,
pub cam_dir: Vec3<f32>,
}
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<f32> {
@ -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.

View File

@ -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<SkyboxPipeline>,
locals: Consts<SkyboxLocals>,
@ -55,6 +64,7 @@ pub struct Scene {
shadows: Consts<Shadow>,
camera: Camera,
camera_input_state: Vec2<f32>,
event_lights: Vec<EventLight>,
skybox: Skybox,
postprocess: PostProcess,
@ -62,6 +72,7 @@ pub struct Scene {
loaded_distance: f32,
select_pos: Option<Vec3<i32>>,
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<TerrainChunk> { &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::<Vec<_>>();
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::<DeltaTime>().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::<comp::Pos>(),
@ -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);

View File

@ -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<Particle>,
/// keep track of timings
scheduler: HeartbeatScheduler,
/// GPU Instance Buffer
instances: Instances<ParticleInstance>,
/// GPU Vertex Buffers
model_cache: HashMap<&'static str, Model<ParticlePipeline>>,
}
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::<f32>::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::<Body>(),
&ecs.read_storage::<Pos>(),
)
.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::<Pos>(),
&ecs.read_storage::<CharacterState>(),
)
.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::<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;
}
pub fn render(
&self,
renderer: &mut Renderer,
scene_data: &SceneData,
globals: &Consts<Globals>,
lights: &Consts<Light>,
shadows: &Consts<Shadow>,
) {
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<ParticleInstance> {
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<ParticlePipeline>> {
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::<DotVoxData>(DEFAULT_MODEL_KEY);
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
}
/// 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<Duration, (Instant, u8)>,
}
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<f32>) -> Self {
Particle {
alive_until: Instant::now() + lifespan,
instance: ParticleInstance::new(time, mode, pos),
}
}
}

View File

@ -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

View File

@ -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<TickAction, Error> {
fn tick(
&mut self,
dt: Duration,
global_state: &mut GlobalState,
outcomes: &mut Vec<Outcome>,
) -> Result<TickAction, Error> {
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<f32> = 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(

View File

@ -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,