mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'scott-c/particles' into 'master'
Particles See merge request veloren/veloren!1156
This commit is contained in:
commit
893da3622a
@ -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
|
||||
|
||||
|
@ -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
BIN
assets/voxygen/audio/sfx/explosion.wav
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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",
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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) {
|
||||
|
38
assets/voxygen/shaders/particle-frag.glsl
Normal file
38
assets/voxygen/shaders/particle-frag.glsl
Normal 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));
|
||||
}
|
131
assets/voxygen/shaders/particle-vert.glsl
Normal file
131
assets/voxygen/shaders/particle-vert.glsl
Normal 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
BIN
assets/voxygen/voxel/particle.vox
(Stored with Git LFS)
Normal file
Binary file not shown.
@ -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))
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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",
|
||||
|
@ -261,6 +261,7 @@ impl Tool {
|
||||
col: (0.85, 0.5, 0.11).into(),
|
||||
..Default::default()
|
||||
}),
|
||||
|
||||
projectile_gravity: None,
|
||||
},
|
||||
BasicRanged {
|
||||
|
@ -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};
|
||||
|
@ -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>;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
30
common/src/outcome.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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());
|
||||
|
@ -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();
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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(
|
||||
|
@ -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) {
|
||||
|
@ -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);
|
||||
},
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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>(
|
||||
|
@ -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,
|
||||
},
|
||||
|
@ -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 {
|
||||
|
116
voxygen/src/render/pipelines/particle.rs
Normal file
116
voxygen/src/render/pipelines/particle.rs
Normal 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;
|
||||
}
|
@ -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,
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
369
voxygen/src/scene/particle.rs
Normal file
369
voxygen/src/scene/particle.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user