Create Dir type for better enforcement of non NaN, normalized representations of directions

This commit is contained in:
Imbris 2020-03-27 21:31:22 -04:00
parent df5a7ef0e3
commit ba3fa16c33
27 changed files with 296 additions and 179 deletions

3
Cargo.lock generated
View File

@ -4847,8 +4847,7 @@ checksum = "05c78687fb1a80548ae3250346c3db86a80a7cdd77bda190189f2d0a0987c81a"
[[package]] [[package]]
name = "vek" name = "vek"
version = "0.9.12" version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/Imberflur/vek?branch=is_normalized#7442254deb67ad6930bedc671057d14ea8e885eb"
checksum = "b833a133490ae98e9e3db1c77fc28e844f8e51b12eb35b4ab8a2082cb7cb441a"
dependencies = [ dependencies = [
"approx 0.1.1", "approx 0.1.1",
"num-integer", "num-integer",

View File

@ -68,3 +68,6 @@ debug = false
[profile.releasedebuginfo] [profile.releasedebuginfo]
inherits = 'release' inherits = 'release'
debug = 1 debug = 1
[patch.crates-io]
vek = {git = "https://github.com/Imberflur/vek", branch = "is_normalized"}

View File

@ -1,4 +1,4 @@
use crate::sync::Uid; use crate::{sync::Uid, util::Dir};
use specs::{Component, FlaggedStorage}; use specs::{Component, FlaggedStorage};
use specs_idvs::IDVStorage; use specs_idvs::IDVStorage;
use std::time::Duration; use std::time::Duration;
@ -146,7 +146,7 @@ pub struct ControllerInputs {
pub charge: Input, pub charge: Input,
pub climb: Option<Climb>, pub climb: Option<Climb>,
pub move_dir: Vec2<f32>, pub move_dir: Vec2<f32>,
pub look_dir: Vec3<f32>, pub look_dir: Dir,
} }
#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]

View File

@ -1,4 +1,4 @@
use crate::sync::Uid; use crate::{sync::Uid, util::Dir};
use specs::{Component, FlaggedStorage, NullStorage}; use specs::{Component, FlaggedStorage, NullStorage};
use specs_idvs::IDVStorage; use specs_idvs::IDVStorage;
use vek::*; use vek::*;
@ -21,7 +21,7 @@ impl Component for Vel {
// Orientation // Orientation
#[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)] #[derive(Copy, Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
pub struct Ori(pub Vec3<f32>); pub struct Ori(pub Dir);
impl Component for Ori { impl Component for Ori {
type Storage = IDVStorage<Self>; type Storage = IDVStorage<Self>;

View File

@ -1,4 +1,4 @@
use crate::{comp, sync::Uid}; use crate::{comp, sync::Uid, util::Dir};
use comp::{item::ToolKind, InventoryUpdateEvent}; use comp::{item::ToolKind, InventoryUpdateEvent};
use parking_lot::Mutex; use parking_lot::Mutex;
use serde::Deserialize; use serde::Deserialize;
@ -87,7 +87,7 @@ pub enum ServerEvent {
Respawn(EcsEntity), Respawn(EcsEntity),
Shoot { Shoot {
entity: EcsEntity, entity: EcsEntity,
dir: Vec3<f32>, dir: Dir,
body: comp::Body, body: comp::Body,
light: Option<comp::LightEmitter>, light: Option<comp::LightEmitter>,
projectile: comp::Projectile, projectile: comp::Projectile,

View File

@ -23,7 +23,7 @@ impl CharacterBehavior for Data {
if self.only_up { if self.only_up {
update.vel.0.z += 500.0 * data.dt.0; update.vel.0.z += 500.0 * data.dt.0;
} else { } else {
update.vel.0 += data.inputs.look_dir * 500.0 * data.dt.0; update.vel.0 += *data.inputs.look_dir * 500.0 * data.dt.0;
} }
update.character = CharacterState::Boost(Data { update.character = CharacterState::Boost(Data {
duration: self duration: self

View File

@ -5,7 +5,7 @@ use crate::{
character_behavior::{CharacterBehavior, JoinData}, character_behavior::{CharacterBehavior, JoinData},
phys::GRAVITY, phys::GRAVITY,
}, },
util::safe_slerp, util::Dir,
}; };
use vek::{ use vek::{
vec::{Vec2, Vec3}, vec::{Vec2, Vec3},
@ -23,7 +23,13 @@ impl CharacterBehavior for Data {
let mut update = StateUpdate::from(data); let mut update = StateUpdate::from(data);
// If no wall is in front of character or we stopped climbing; // If no wall is in front of character or we stopped climbing;
if data.physics.on_wall.is_none() || data.physics.on_ground || data.inputs.climb.is_none() { let (wall_dir, climb) = if let (Some(wall_dir), Some(climb), false) = (
data.physics.on_wall,
data.inputs.climb,
data.physics.on_ground,
) {
(wall_dir, climb)
} else {
if data.inputs.jump.is_pressed() { if data.inputs.jump.is_pressed() {
// They've climbed atop something, give them a boost // They've climbed atop something, give them a boost
update update
@ -32,7 +38,7 @@ impl CharacterBehavior for Data {
} }
update.character = CharacterState::Idle {}; update.character = CharacterState::Idle {};
return update; return update;
} };
// Move player // Move player
update.vel.0 += Vec2::broadcast(data.dt.0) update.vel.0 += Vec2::broadcast(data.dt.0)
@ -44,11 +50,9 @@ impl CharacterBehavior for Data {
}; };
// Expend energy if climbing // Expend energy if climbing
let energy_use = match data.inputs.climb { let energy_use = match climb {
Some(Climb::Up) | Some(Climb::Down) => 8, Climb::Up | Climb::Down => 8,
Some(Climb::Hold) => 1, Climb::Hold => 1,
// Note: this is currently unreachable
None => 0,
}; };
if let Err(_) = update if let Err(_) = update
.energy .energy
@ -58,25 +62,17 @@ impl CharacterBehavior for Data {
} }
// Set orientation direction based on wall direction // Set orientation direction based on wall direction
let ori_dir = if let Some(wall_dir) = data.physics.on_wall { let ori_dir = Vec2::from(wall_dir);
Vec2::from(wall_dir)
} else {
Vec2::from(update.vel.0)
};
// Smooth orientation // Smooth orientation
update.ori.0 = safe_slerp( update.ori.0 = Dir::slerp_to_vec3(
update.ori.0, update.ori.0,
ori_dir.into(), ori_dir.into(),
if data.physics.on_ground { 9.0 } else { 2.0 } * data.dt.0, if data.physics.on_ground { 9.0 } else { 2.0 } * data.dt.0,
); );
// Apply Vertical Climbing Movement // Apply Vertical Climbing Movement
if let (Some(climb), true, Some(_wall_dir)) = ( if update.vel.0.z <= CLIMB_SPEED {
data.inputs.climb,
update.vel.0.z <= CLIMB_SPEED,
data.physics.on_wall,
) {
match climb { match climb {
Climb::Down => { Climb::Down => {
update.vel.0 -= update.vel.0 -=

View File

@ -2,7 +2,7 @@ use crate::{
comp::{Attacking, CharacterState, EnergySource, StateUpdate}, comp::{Attacking, CharacterState, EnergySource, StateUpdate},
states::utils::*, states::utils::*,
sys::character_behavior::*, sys::character_behavior::*,
util::safe_slerp, util::Dir,
}; };
use std::time::Duration; use std::time::Duration;
use vek::Vec3; use vek::Vec3;
@ -27,9 +27,9 @@ impl CharacterBehavior for Data {
let mut update = StateUpdate::from(data); let mut update = StateUpdate::from(data);
if self.initialize { if self.initialize {
update.vel.0 = data.inputs.look_dir * 20.0; update.vel.0 = *data.inputs.look_dir * 20.0;
if let Some(dir) = Vec3::from(data.vel.0.xy()).try_normalized() { if let Some(dir) = Vec3::from(data.vel.0.xy()).try_normalized() {
update.ori.0 = dir; update.ori.0 = dir.into();
} }
} }
@ -98,7 +98,7 @@ impl CharacterBehavior for Data {
} }
} }
update.ori.0 = safe_slerp(update.ori.0, update.vel.0, 9.0 * data.dt.0); update.ori.0 = Dir::slerp_to_vec3(update.ori.0, update.vel.0, 9.0 * data.dt.0);
update update
} }

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
comp::{CharacterState, StateUpdate}, comp::{CharacterState, StateUpdate},
sys::character_behavior::{CharacterBehavior, JoinData}, sys::character_behavior::{CharacterBehavior, JoinData},
util::safe_slerp, util::Dir,
}; };
use vek::Vec2; use vek::Vec2;
@ -40,7 +40,7 @@ impl CharacterBehavior for Data {
// Determine orientation vector from movement direction vector // Determine orientation vector from movement direction vector
let ori_dir = Vec2::from(update.vel.0); let ori_dir = Vec2::from(update.vel.0);
update.ori.0 = safe_slerp(update.ori.0, ori_dir.into(), 0.1); update.ori.0 = Dir::slerp_to_vec3(update.ori.0, ori_dir.into(), 2.0 * data.dt.0);
// Apply Glide antigrav lift // Apply Glide antigrav lift
if Vec2::<f32>::from(update.vel.0).magnitude_squared() < GLIDE_SPEED.powf(2.0) if Vec2::<f32>::from(update.vel.0).magnitude_squared() < GLIDE_SPEED.powf(2.0)

View File

@ -1,7 +1,7 @@
use crate::{ use crate::{
comp::{CharacterState, StateUpdate}, comp::{CharacterState, StateUpdate},
sys::character_behavior::{CharacterBehavior, JoinData}, sys::character_behavior::{CharacterBehavior, JoinData},
util::safe_slerp, util::Dir,
}; };
use std::time::Duration; use std::time::Duration;
use vek::Vec3; use vek::Vec3;
@ -28,7 +28,7 @@ impl CharacterBehavior for Data {
* ROLL_SPEED; * ROLL_SPEED;
// Smooth orientation // Smooth orientation
update.ori.0 = safe_slerp(update.ori.0, update.vel.0.into(), 9.0 * data.dt.0); update.ori.0 = Dir::slerp_to_vec3(update.ori.0, update.vel.0, 9.0 * data.dt.0);
if self.remaining_duration == Duration::default() { if self.remaining_duration == Duration::default() {
// Roll duration has expired // Roll duration has expired

View File

@ -57,7 +57,7 @@ impl CharacterBehavior for Data {
if !self.initialized { if !self.initialized {
update.vel.0 = Vec3::zero(); update.vel.0 = Vec3::zero();
if let Some(dir) = data.inputs.look_dir.try_normalized() { if let Some(dir) = data.inputs.look_dir.try_normalized() {
update.ori.0 = dir; update.ori.0 = dir.into();
} }
} }
let initialized = true; let initialized = true;

View File

@ -3,7 +3,7 @@ use crate::{
event::LocalEvent, event::LocalEvent,
states::*, states::*,
sys::{character_behavior::JoinData, phys::GRAVITY}, sys::{character_behavior::JoinData, phys::GRAVITY},
util::safe_slerp, util::Dir,
}; };
use vek::vec::Vec2; use vek::vec::Vec2;
@ -68,13 +68,13 @@ fn basic_move(data: &JoinData, update: &mut StateUpdate, efficiency: f32) {
pub fn handle_orientation(data: &JoinData, update: &mut StateUpdate, strength: f32) { pub fn handle_orientation(data: &JoinData, update: &mut StateUpdate, strength: f32) {
// Set direction based on move direction // Set direction based on move direction
let ori_dir = if update.character.is_attack() || update.character.is_block() { let ori_dir = if update.character.is_attack() || update.character.is_block() {
Vec2::from(data.inputs.look_dir) Vec2::from(*data.inputs.look_dir)
} else { } else {
Vec2::from(data.inputs.move_dir) Vec2::from(data.inputs.move_dir)
}; };
// Smooth orientation // Smooth orientation
update.ori.0 = safe_slerp(update.ori.0, ori_dir.into(), strength * data.dt.0); update.ori.0 = Dir::slerp_to_vec3(update.ori.0, ori_dir.into(), strength * data.dt.0);
} }
/// Updates components to move player as if theyre swimming /// Updates components to move player as if theyre swimming

View File

@ -1,9 +1,10 @@
use crate::{ use crate::{
comp::{self, agent::Activity, Agent, Alignment, Controller, MountState, Pos, Stats}, comp::{self, agent::Activity, Agent, Alignment, Controller, MountState, Ori, Pos, Stats},
path::Chaser, path::Chaser,
state::Time, state::Time,
sync::UidAllocator, sync::UidAllocator,
terrain::TerrainGrid, terrain::TerrainGrid,
util::Dir,
vol::ReadVol, vol::ReadVol,
}; };
use rand::{thread_rng, Rng}; use rand::{thread_rng, Rng};
@ -21,6 +22,7 @@ impl<'a> System<'a> for Sys {
Read<'a, Time>, Read<'a, Time>,
Entities<'a>, Entities<'a>,
ReadStorage<'a, Pos>, ReadStorage<'a, Pos>,
ReadStorage<'a, Ori>,
ReadStorage<'a, Stats>, ReadStorage<'a, Stats>,
ReadExpect<'a, TerrainGrid>, ReadExpect<'a, TerrainGrid>,
ReadStorage<'a, Alignment>, ReadStorage<'a, Alignment>,
@ -36,6 +38,7 @@ impl<'a> System<'a> for Sys {
time, time,
entities, entities,
positions, positions,
orientations,
stats, stats,
terrain, terrain,
alignments, alignments,
@ -44,9 +47,10 @@ impl<'a> System<'a> for Sys {
mount_states, mount_states,
): Self::SystemData, ): Self::SystemData,
) { ) {
for (entity, pos, alignment, agent, controller, mount_state) in ( for (entity, pos, ori, alignment, agent, controller, mount_state) in (
&entities, &entities,
&positions, &positions,
&orientations,
alignments.maybe(), alignments.maybe(),
&mut agents, &mut agents,
&mut controllers, &mut controllers,
@ -70,9 +74,11 @@ impl<'a> System<'a> for Sys {
controller.reset(); controller.reset();
//TODO: Make npcs have valid `look_dir` during all activities
let mut inputs = &mut controller.inputs; let mut inputs = &mut controller.inputs;
// Default to looking in orientation direction
inputs.look_dir = ori.0;
const AVG_FOLLOW_DIST: f32 = 6.0; const AVG_FOLLOW_DIST: f32 = 6.0;
const MAX_FOLLOW_DIST: f32 = 12.0; const MAX_FOLLOW_DIST: f32 = 12.0;
const MAX_CHASE_DIST: f32 = 24.0; const MAX_CHASE_DIST: f32 = 24.0;
@ -162,7 +168,9 @@ impl<'a> System<'a> for Sys {
.copied() .copied()
.unwrap_or(Alignment::Owned(*target)), .unwrap_or(Alignment::Owned(*target)),
) { ) {
inputs.look_dir = tgt_pos.0 - pos.0; if let Some(dir) = Dir::from_unnormalized(tgt_pos.0 - pos.0) {
inputs.look_dir = dir;
}
// Don't attack entities we are passive towards // Don't attack entities we are passive towards
// TODO: This is here, it's a bit of a hack // TODO: This is here, it's a bit of a hack

View File

@ -4,6 +4,7 @@ use crate::{
}, },
event::{EventBus, LocalEvent, ServerEvent}, event::{EventBus, LocalEvent, ServerEvent},
sync::Uid, sync::Uid,
util::Dir,
}; };
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage}; use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
use vek::*; use vek::*;
@ -92,7 +93,7 @@ impl<'a> System<'a> for Sys {
// 2D versions // 2D versions
let pos2 = Vec2::from(pos.0); let pos2 = Vec2::from(pos.0);
let pos_b2 = Vec2::<f32>::from(pos_b.0); let pos_b2 = Vec2::<f32>::from(pos_b.0);
let ori2 = Vec2::from(ori.0); let ori2 = Vec2::from(*ori.0);
// Scales // Scales
let scale = scale_maybe.map_or(1.0, |s| s.0); let scale = scale_maybe.map_or(1.0, |s| s.0);
@ -140,7 +141,7 @@ impl<'a> System<'a> for Sys {
if attack.knockback != 0.0 { if attack.knockback != 0.0 {
local_emitter.emit(LocalEvent::ApplyForce { local_emitter.emit(LocalEvent::ApplyForce {
entity: b, entity: b,
dir: Vec3::slerp(ori.0, Vec3::new(0.0, 0.0, 1.0), 0.5), dir: *Dir::slerp(ori.0, Dir::new(Vec3::new(0.0, 0.0, 1.0)), 0.5),
force: attack.knockback, force: attack.knockback,
}); });
} }

View File

@ -63,12 +63,6 @@ impl<'a> System<'a> for Sys {
inputs.move_dir inputs.move_dir
}; };
// Update `inputs.look_dir`
inputs
.look_dir
.try_normalized()
.unwrap_or(inputs.move_dir.into());
// Process other controller events // Process other controller events
for event in controller.events.drain(..) { for event in controller.events.drain(..) {
match event { match event {

View File

@ -5,6 +5,7 @@ use crate::{
event::{EventBus, LocalEvent, ServerEvent}, event::{EventBus, LocalEvent, ServerEvent},
state::DeltaTime, state::DeltaTime,
sync::UidAllocator, sync::UidAllocator,
util::Dir,
}; };
use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage}; use specs::{saveload::MarkerAllocator, Entities, Join, Read, ReadStorage, System, WriteStorage};
use std::time::Duration; use std::time::Duration;
@ -107,7 +108,7 @@ impl<'a> System<'a> for Sys {
{ {
local_emitter.emit(LocalEvent::ApplyForce { local_emitter.emit(LocalEvent::ApplyForce {
entity, entity,
dir: Vec3::slerp(ori.0, Vec3::new(0.0, 0.0, 1.0), 0.5), dir: *Dir::slerp(ori.0, Dir::new(Vec3::unit_z()), 0.5),
force: knockback, force: knockback,
}); });
} }
@ -145,7 +146,7 @@ impl<'a> System<'a> for Sys {
.get(entity) .get(entity)
.and_then(|vel| vel.0.try_normalized()) .and_then(|vel| vel.0.try_normalized())
{ {
ori.0 = dir; ori.0 = dir.into();
} }
} }

186
common/src/util/dir.rs Normal file
View File

@ -0,0 +1,186 @@
use vek::*;
/// Type representing a direction using Vec3 that is normalized and NaN free
/// These properties are enforced actively via panics when `debug_assertions` is
/// enabled
#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(into = "SerdeDir")]
#[serde(from = "SerdeDir")]
pub struct Dir(Vec3<f32>);
impl Default for Dir {
fn default() -> Self { Self(Vec3::unit_y()) }
}
// Validate at Deserialization
#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
struct SerdeDir(Vec3<f32>);
impl From<SerdeDir> for Dir {
fn from(dir: SerdeDir) -> Self {
let dir = dir.0;
if dir.map(f32::is_nan).reduce_or() {
warn!("Deserialized dir containing NaNs, replacing with default");
Default::default()
} else if !dir.is_normalized() {
warn!("Deserialized unnormalized dir, replacing with default");
Default::default()
} else {
Self(dir)
}
}
}
impl Into<SerdeDir> for Dir {
fn into(self) -> SerdeDir { SerdeDir(*self) }
}
/*pub enum TryFromVec3Error {
ContainsNans,
NotNormalized,
}
impl TryFrom<Vec3<f32>> for Dir {
type Error = TryFromVec3Error;
fn try_from(v: Vec3) -> Result<Self, TryFromVec3Error> {
if v.map(f32::is_nan).reduce_or() {
Err(TryFromVec3Error::ContainsNans)
} else {
v.try_normalized()
.map(|n| Self(n))
.ok_or(TryFromVec3Error::NotNormalized)
}
}
}*/
impl Dir {
pub fn new(dir: Vec3<f32>) -> Self {
debug_assert!(!dir.map(f32::is_nan).reduce_or());
debug_assert!(dir.is_normalized());
Self(dir)
}
pub fn from_unnormalized(dirs: Vec3<f32>) -> Option<Self> {
dirs.try_normalized().map(|dir| {
#[cfg(debug_assertions)]
{
if dir.map(f32::is_nan).reduce_or() {
panic!("{} => {}", dirs, dir);
}
}
Self(dir)
})
}
pub fn slerp(from: Self, to: Self, factor: f32) -> Self {
Self(slerp_normalized(from.0, to.0, factor))
}
/// Note: this uses `from` if `to` is unormalizable
pub fn slerp_to_vec3(from: Self, to: Vec3<f32>, factor: f32) -> Self {
Self(slerp_to_unnormalized(from.0, to, factor).unwrap_or_else(|e| e))
}
pub fn is_valid(&self) -> bool { !self.0.map(f32::is_nan).reduce_or() && self.is_normalized() }
}
impl std::ops::Deref for Dir {
type Target = Vec3<f32>;
fn deref(&self) -> &Vec3<f32> { &self.0 }
}
impl From<Vec3<f32>> for Dir {
fn from(dir: Vec3<f32>) -> Self { Dir::new(dir.into()) }
}
/// Begone ye NaN's
/// Slerp two `Vec3`s skipping the slerp if their directions are very close
/// This avoids a case where `vek`s slerp produces NaN's
/// Additionally, it avoids unnecessary calculations if they are near identical
/// Assumes `from` and `to` are normalized and returns a normalized vector
#[inline(always)]
fn slerp_normalized(from: vek::Vec3<f32>, to: vek::Vec3<f32>, factor: f32) -> vek::Vec3<f32> {
debug_assert!(!to.map(f32::is_nan).reduce_or());
debug_assert!(!from.map(f32::is_nan).reduce_or());
// Ensure from is normalized
#[cfg(debug_assertions)]
{
if {
let len_sq = from.magnitude_squared();
len_sq < 0.999 || len_sq > 1.001
} {
panic!("Called slerp_normalized with unnormalized from: {:?}", from);
}
}
// Ensure to is normalized
#[cfg(debug_assertions)]
{
if {
let len_sq = from.magnitude_squared();
len_sq < 0.999 || len_sq > 1.001
} {
panic!("Called slerp_normalized with unnormalized to: {:?}", to);
}
}
let dot = from.dot(to);
if dot >= 1.0 - 1E-6 {
// Close together, just use to
return to;
}
let (from, to, factor) = if dot < -0.999 {
// Not linearly independent (slerp will fail since it doesn't check for this)
// Instead we will choose a midpoint and slerp from or to that depending on the
// factor
let mid_dir = if from.z.abs() > 0.999 {
// If vec's lie along the z-axis default to (1, 0, 0) as midpoint
Vec3::unit_x()
} else {
// Default to picking midpoint in the xy plane
Vec3::new(from.y, -from.x, 0.0).normalized()
};
if factor > 0.5 {
(mid_dir, to, factor * 2.0 - 1.0)
} else {
(from, mid_dir, factor * 2.0)
}
} else {
(from, to, factor)
};
let slerped = Vec3::slerp(from, to, factor);
let slerped_normalized = slerped.normalized();
// Ensure normalization worked
// This should not be possible but I will leave it here for now just in case
// something was missed
#[cfg(debug_assertions)]
{
if !slerped_normalized.is_normalized() || slerped_normalized.map(f32::is_nan).reduce_or() {
panic!(
"Failed to normalize {:?} produced from:\nslerp(\n {:?},\n {:?},\n \
{:?},\n)\nWith result: {:?})",
slerped, from, to, factor, slerped_normalized
);
}
}
slerped_normalized
}
/// Begone ye NaN's
/// Slerp two `Vec3`s skipping the slerp if their directions are very close
/// This avoids a case where `vek`s slerp produces NaN's
/// Additionally, it avoids unnecessary calculations if they are near identical
/// Assumes `from` is normalized and returns a normalized vector, but `to`
/// doesn't need to be normalized
/// Returns `Err(from)`` if `to` is unormalizable
// TODO: in some cases we might want to base the slerp rate on the magnitude of
// `to` for example when `to` is velocity and `from` is orientation
fn slerp_to_unnormalized(
from: Vec3<f32>,
to: Vec3<f32>,
factor: f32,
) -> Result<Vec3<f32>, Vec3<f32>> {
to.try_normalized()
.map(|to| slerp_normalized(from, to, factor))
.ok_or(from)
}

View File

@ -1,4 +1,5 @@
mod color; mod color;
mod dir;
pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash")); pub const GIT_VERSION: &str = include_str!(concat!(env!("OUT_DIR"), "/githash"));
@ -8,84 +9,4 @@ lazy_static::lazy_static! {
} }
pub use color::*; pub use color::*;
pub use dir::*;
/// Begone ye NaN's
/// Slerp two `Vec3`s skipping the slerp if their directions are very close
/// This avoids a case where `vek`s slerp produces NaN's
/// Additionally, it avoids unnecessary calculations if they are near identical
/// Assumes `from` is normalized and returns a normalized vector, but `to`
/// doesn't need to be normalized
// TODO: in some cases we might want to base the slerp rate on the magnitude of
// `to` for example when `to` is velocity and `from` is orientation
#[inline(always)]
pub fn safe_slerp(from: vek::Vec3<f32>, to: vek::Vec3<f32>, factor: f32) -> vek::Vec3<f32> {
use vek::Vec3;
debug_assert!(!to.map(f32::is_nan).reduce_or());
debug_assert!(!from.map(f32::is_nan).reduce_or());
// Ensure from is normalized
#[cfg(debug_assertions)]
{
if {
let len_sq = from.magnitude_squared();
len_sq < 0.999 || len_sq > 1.001
} {
panic!("Called safe_slerp with unnormalized from: {:?}", from);
}
}
let to = if to.magnitude_squared() > 0.001 {
to.normalized()
} else {
return from;
};
let dot = from.dot(to);
if dot >= 1.0 - 1E-6 {
// Close together, just use to
return to;
}
let (from, to, factor) = if dot < -0.999 {
// Not linearly independent (slerp will fail since it doesn't check for this)
// Instead we will choose a midpoint and slerp from or to that depending on the
// factor
let mid_dir = if from.z > 0.999 {
// If vec's lie along the z-axis default to (1, 0, 0) as midpoint
Vec3::unit_x()
} else {
// Default to picking midpoint in the xy plane
Vec3::new(from.y, -from.x, 0.0).normalized()
};
if factor > 0.5 {
(mid_dir, to, factor * 2.0 - 1.0)
} else {
(from, mid_dir, factor * 2.0)
}
} else {
(from, to, factor)
};
let slerped = Vec3::slerp(from, to, factor);
let slerped_normalized = slerped.normalized();
// Ensure normalization worked
// This should not be possible but I will leave it here for now just in case
// something was missed
#[cfg(debug_assertions)]
{
if {
let len_sq = slerped_normalized.magnitude_squared();
len_sq < 0.999 || len_sq > 1.001
} || slerped_normalized.map(f32::is_nan).reduce_or()
{
panic!(
"Failed to normalize {:?} produced from:\nslerp(\n {:?},\n {:?},\n \
{:?},\n)\nWith result: {:?})",
slerped, from, to, factor, slerped_normalized
);
}
}
slerped_normalized
}

View File

@ -12,6 +12,7 @@ use common::{
state::TimeOfDay, state::TimeOfDay,
sync::{Uid, WorldSyncExt}, sync::{Uid, WorldSyncExt},
terrain::TerrainChunkSize, terrain::TerrainChunkSize,
util::Dir,
vol::RectVolSize, vol::RectVolSize,
}; };
use rand::Rng; use rand::Rng;
@ -711,15 +712,14 @@ fn handle_object(server: &mut Server, entity: EcsEntity, args: String, _action:
.with(comp::Ori( .with(comp::Ori(
// converts player orientation into a 90° rotation for the object by using the axis // converts player orientation into a 90° rotation for the object by using the axis
// with the highest value // with the highest value
ori.0 Dir::from_unnormalized(ori.0.map(|e| {
.map(|e| { if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() {
if e.abs() == ori.0.map(|e| e.abs()).reduce_partial_max() { e
e } else {
} else { 0.0
0.0 }
} }))
}) .unwrap_or_default(),
.normalized(),
)) ))
.build(); .build();
server.notify_client( server.notify_client(

View File

@ -1,7 +1,10 @@
use crate::{sys, Server, StateExt}; use crate::{sys, Server, StateExt};
use common::comp::{ use common::{
self, Agent, Alignment, Body, Gravity, LightEmitter, Loadout, Pos, Projectile, Scale, Stats, comp::{
Vel, WaypointArea, self, Agent, Alignment, Body, Gravity, LightEmitter, Loadout, Pos, Projectile, Scale,
Stats, Vel, WaypointArea,
},
util::Dir,
}; };
use specs::{Builder, Entity as EcsEntity, WorldExt}; use specs::{Builder, Entity as EcsEntity, WorldExt};
use vek::{Rgb, Vec3}; use vek::{Rgb, Vec3};
@ -42,7 +45,7 @@ pub fn handle_create_npc(
pub fn handle_shoot( pub fn handle_shoot(
server: &mut Server, server: &mut Server,
entity: EcsEntity, entity: EcsEntity,
dir: Vec3<f32>, dir: Dir,
body: Body, body: Body,
light: Option<LightEmitter>, light: Option<LightEmitter>,
projectile: Projectile, projectile: Projectile,
@ -60,7 +63,7 @@ pub fn handle_shoot(
// TODO: Player height // TODO: Player height
pos.z += 1.2; 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(*dir * 100.0), body, projectile);
if let Some(light) = light { if let Some(light) = light {
builder = builder.with(light) builder = builder.with(light)
} }

View File

@ -256,7 +256,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
.read_storage::<comp::Ori>() .read_storage::<comp::Ori>()
.get(entity) .get(entity)
.copied() .copied()
.unwrap_or(comp::Ori(Vec3::unit_y())), .unwrap_or_default(),
item, item,
)); ));
} }
@ -269,7 +269,7 @@ pub fn handle_inventory(server: &mut Server, entity: EcsEntity, manip: comp::Inv
// Drop items // Drop items
for (pos, ori, item) in dropped_items { for (pos, ori, item) in dropped_items {
let vel = ori.0.normalized() * 5.0 let vel = *ori.0 * 5.0
+ Vec3::unit_z() * 10.0 + Vec3::unit_z() * 10.0
+ Vec3::<f32>::zero().map(|_| rand::thread_rng().gen::<f32>() - 0.5) * 4.0; + Vec3::<f32>::zero().map(|_| rand::thread_rng().gen::<f32>() - 0.5) * 4.0;

View File

@ -5,6 +5,7 @@ use common::{
msg::{ClientState, ServerMsg}, msg::{ClientState, ServerMsg},
state::State, state::State,
sync::{Uid, WorldSyncExt}, sync::{Uid, WorldSyncExt},
util::Dir,
}; };
use log::warn; use log::warn;
use specs::{Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder, Join, WorldExt}; use specs::{Builder, Entity as EcsEntity, EntityBuilder as EcsEntityBuilder, Join, WorldExt};
@ -89,7 +90,7 @@ impl StateExt for State {
.create_entity_synced() .create_entity_synced()
.with(pos) .with(pos)
.with(comp::Vel(Vec3::zero())) .with(comp::Vel(Vec3::zero()))
.with(comp::Ori(Vec3::unit_y())) .with(comp::Ori::default())
.with(comp::Controller::default()) .with(comp::Controller::default())
.with(body) .with(body)
.with(stats) .with(stats)
@ -106,7 +107,7 @@ impl StateExt for State {
.create_entity_synced() .create_entity_synced()
.with(pos) .with(pos)
.with(comp::Vel(Vec3::zero())) .with(comp::Vel(Vec3::zero()))
.with(comp::Ori(Vec3::unit_y())) .with(comp::Ori::default())
.with(comp::Body::Object(object)) .with(comp::Body::Object(object))
.with(comp::Mass(100.0)) .with(comp::Mass(100.0))
.with(comp::Gravity(1.0)) .with(comp::Gravity(1.0))
@ -125,7 +126,7 @@ impl StateExt for State {
.create_entity_synced() .create_entity_synced()
.with(pos) .with(pos)
.with(vel) .with(vel)
.with(comp::Ori(vel.0.normalized())) .with(comp::Ori(Dir::from_unnormalized(vel.0).unwrap_or_default()))
.with(comp::Mass(0.0)) .with(comp::Mass(0.0))
.with(body) .with(body)
.with(projectile) .with(projectile)
@ -151,7 +152,7 @@ impl StateExt for State {
self.write_component(entity, comp::Controller::default()); self.write_component(entity, comp::Controller::default());
self.write_component(entity, comp::Pos(spawn_point)); self.write_component(entity, comp::Pos(spawn_point));
self.write_component(entity, comp::Vel(Vec3::zero())); self.write_component(entity, comp::Vel(Vec3::zero()));
self.write_component(entity, comp::Ori(Vec3::unit_y())); self.write_component(entity, comp::Ori::default());
self.write_component(entity, comp::Gravity(1.0)); self.write_component(entity, comp::Gravity(1.0));
self.write_component(entity, comp::CharacterState::default()); self.write_component(entity, comp::CharacterState::default());
self.write_component(entity, comp::Alignment::Owned(entity)); self.write_component(entity, comp::Alignment::Owned(entity));

View File

@ -70,10 +70,12 @@ impl SfxMgr {
.get(player_entity) .get(player_entity)
.map_or(Vec3::zero(), |pos| pos.0); .map_or(Vec3::zero(), |pos| pos.0);
let player_ori = ecs let player_ori = *ecs
.read_storage::<Ori>() .read_storage::<Ori>()
.get(player_entity) .get(player_entity)
.map_or(Vec3::zero(), |pos| pos.0); .copied()
.unwrap_or_default()
.0;
audio.set_listener_pos(&player_position, &player_ori); audio.set_listener_pos(&player_position, &player_ori);

View File

@ -1,3 +1,4 @@
use common::util::Dir;
use specs::Component; use specs::Component;
use specs_idvs::IDVStorage; use specs_idvs::IDVStorage;
use vek::*; use vek::*;
@ -32,7 +33,7 @@ impl Component for HpFloaterList {
#[derive(Copy, Clone, Debug)] #[derive(Copy, Clone, Debug)]
pub struct Interpolated { pub struct Interpolated {
pub pos: Vec3<f32>, pub pos: Vec3<f32>,
pub ori: Vec3<f32>, pub ori: Dir,
} }
impl Component for Interpolated { impl Component for Interpolated {
type Storage = IDVStorage<Self>; type Storage = IDVStorage<Self>;

View File

@ -2,7 +2,7 @@ use crate::ecs::comp::Interpolated;
use common::{ use common::{
comp::{Ori, Pos, Vel}, comp::{Ori, Pos, Vel},
state::DeltaTime, state::DeltaTime,
util::safe_slerp, util::Dir,
}; };
use log::warn; use log::warn;
use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage}; use specs::{Entities, Join, Read, ReadStorage, System, WriteStorage};
@ -30,7 +30,7 @@ impl<'a> System<'a> for Sys {
// Update interpolation values // Update interpolation values
if i.pos.distance_squared(pos.0) < 64.0 * 64.0 { if i.pos.distance_squared(pos.0) < 64.0 * 64.0 {
i.pos = Lerp::lerp(i.pos, pos.0 + vel.0 * 0.03, 10.0 * dt.0); i.pos = Lerp::lerp(i.pos, pos.0 + vel.0 * 0.03, 10.0 * dt.0);
i.ori = safe_slerp(i.ori, ori.0, 5.0 * dt.0); i.ori = Dir::slerp(i.ori, ori.0, 5.0 * dt.0);
} else { } else {
i.pos = pos.0; i.pos = pos.0;
i.ori = ori.0; i.ori = ori.0;

View File

@ -142,8 +142,8 @@ impl FigureMgr {
.join() .join()
{ {
let (pos, ori) = interpolated let (pos, ori) = interpolated
.map(|i| (Pos(i.pos), Ori(i.ori))) .map(|i| (Pos(i.pos), *i.ori))
.unwrap_or((*pos, Ori(Vec3::unit_y()))); .unwrap_or((*pos, Vec3::unit_y()));
// Don't process figures outside the vd // Don't process figures outside the vd
let vd_frac = Vec2::from(pos.0 - player_pos) let vd_frac = Vec2::from(pos.0 - player_pos)
@ -439,7 +439,7 @@ impl FigureMgr {
// Running // Running
(true, true, _) => anim::character::RunAnimation::update_skeleton( (true, true, _) => anim::character::RunAnimation::update_skeleton(
&CharacterSkeleton::new(), &CharacterSkeleton::new(),
(active_tool_kind, vel.0, ori.0, state.last_ori, time), (active_tool_kind, vel.0, ori, state.last_ori, time),
state.state_time, state.state_time,
&mut state_animation_rate, &mut state_animation_rate,
skeleton_attr, skeleton_attr,
@ -455,7 +455,7 @@ impl FigureMgr {
// Swim // Swim
(false, _, true) => anim::character::SwimAnimation::update_skeleton( (false, _, true) => anim::character::SwimAnimation::update_skeleton(
&CharacterSkeleton::new(), &CharacterSkeleton::new(),
(active_tool_kind, vel.0, ori.0.magnitude(), time), (active_tool_kind, vel.0, ori.magnitude(), time),
state.state_time, state.state_time,
&mut state_animation_rate, &mut state_animation_rate,
skeleton_attr, skeleton_attr,
@ -465,7 +465,7 @@ impl FigureMgr {
CharacterState::Roll { .. } => { CharacterState::Roll { .. } => {
anim::character::RollAnimation::update_skeleton( anim::character::RollAnimation::update_skeleton(
&target_base, &target_base,
(active_tool_kind, ori.0, state.last_ori, time), (active_tool_kind, ori, state.last_ori, time),
state.state_time, state.state_time,
&mut state_animation_rate, &mut state_animation_rate,
skeleton_attr, skeleton_attr,
@ -592,7 +592,7 @@ impl FigureMgr {
CharacterState::Glide { .. } => { CharacterState::Glide { .. } => {
anim::character::GlidingAnimation::update_skeleton( anim::character::GlidingAnimation::update_skeleton(
&target_base, &target_base,
(active_tool_kind, vel.0, ori.0, state.last_ori, time), (active_tool_kind, vel.0, ori, state.last_ori, time),
state.state_time, state.state_time,
&mut state_animation_rate, &mut state_animation_rate,
skeleton_attr, skeleton_attr,
@ -601,7 +601,7 @@ impl FigureMgr {
CharacterState::Climb { .. } => { CharacterState::Climb { .. } => {
anim::character::ClimbAnimation::update_skeleton( anim::character::ClimbAnimation::update_skeleton(
&CharacterSkeleton::new(), &CharacterSkeleton::new(),
(active_tool_kind, vel.0, ori.0, time), (active_tool_kind, vel.0, ori, time),
state.state_time, state.state_time,
&mut state_animation_rate, &mut state_animation_rate,
skeleton_attr, skeleton_attr,
@ -623,7 +623,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -703,7 +703,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -785,7 +785,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -859,7 +859,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -933,7 +933,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -1007,7 +1007,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -1081,7 +1081,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -1155,7 +1155,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -1229,7 +1229,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -1303,7 +1303,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,
@ -1322,7 +1322,7 @@ impl FigureMgr {
state.update( state.update(
renderer, renderer,
pos.0, pos.0,
ori.0, ori,
scale, scale,
col, col,
dt, dt,

View File

@ -17,6 +17,7 @@ use common::{
comp::{Pos, Vel, MAX_PICKUP_RANGE_SQR}, comp::{Pos, Vel, MAX_PICKUP_RANGE_SQR},
msg::ClientState, msg::ClientState,
terrain::{Block, BlockKind}, terrain::{Block, BlockKind},
util::Dir,
vol::ReadVol, vol::ReadVol,
ChatType, ChatType,
}; };
@ -446,7 +447,7 @@ impl PlayState for SessionState {
if !free_look { if !free_look {
ori = self.scene.camera().get_orientation(); ori = self.scene.camera().get_orientation();
self.inputs.look_dir = cam_dir; self.inputs.look_dir = Dir::from_unnormalized(cam_dir).unwrap();
} }
// Calculate the movement input vector of the player from the current key // Calculate the movement input vector of the player from the current key
// presses and the camera direction. // presses and the camera direction.