mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-16 00:03:29 +00:00
Refactoring, reorganization and cleanup (#7)
### New `math` module * Traits for error-tolerant floating-point comparisons * Traits for interpolation and easing * Bevy plugin to manage 3D interpolations with ECS components ### `skybox` module refactor * ECS resource to enable external skybox selection/updates * System run conditions for more idiomatic/ECS-friendly control flow ### Camera controller refactor * Renamed from `orbit_cam` to `camera_controller` * Now delegates to the new interpolation plugin for WASD motion smoothing * Simple API for attaching the `CameraController` component to the camera from another module -- this resolves an awkward separation-of-concerns situation where we had to configure a bunch of render-quality settings in the camera controller module ### Scene module refactor * Renamed from `test_scene` to `scene` * This module is now responsible for configuring the skybox * This module is now responsible for spawning the camera and configuring the render settings
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
//! Helper traits for floating-point equality checks with configurable tolerance.
|
||||
|
||||
use bevy::prelude::Vec3;
|
||||
|
||||
use super::internal::Zero;
|
||||
|
||||
pub trait NearlyEq<Rhs, Tolerance = Rhs> {
|
||||
const DEFAULT_TOLERANCE: Tolerance;
|
||||
|
||||
#[inline]
|
||||
fn nearly_eq(self, rhs: Rhs) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.nearly_eq_within(rhs, Self::DEFAULT_TOLERANCE)
|
||||
}
|
||||
|
||||
fn nearly_eq_within(self, rhs: Rhs, tolerance: Tolerance) -> bool;
|
||||
}
|
||||
|
||||
impl NearlyEq<f32> for f32 {
|
||||
const DEFAULT_TOLERANCE: f32 = f32::EPSILON;
|
||||
|
||||
#[inline]
|
||||
fn nearly_eq_within(self, rhs: f32, tolerance: f32) -> bool {
|
||||
(self - rhs).abs() < tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl NearlyEq<f64> for f64 {
|
||||
const DEFAULT_TOLERANCE: f64 = f64::EPSILON;
|
||||
|
||||
#[inline]
|
||||
fn nearly_eq_within(self, rhs: f64, tolerance: f64) -> bool {
|
||||
(self - rhs).abs() < tolerance
|
||||
}
|
||||
}
|
||||
|
||||
impl NearlyEq<Vec3, f32> for Vec3 {
|
||||
const DEFAULT_TOLERANCE: f32 = <f32 as NearlyEq<f32>>::DEFAULT_TOLERANCE;
|
||||
|
||||
fn nearly_eq_within(self, rhs: Vec3, tolerance: f32) -> bool {
|
||||
self.x.nearly_eq_within(rhs.x, tolerance)
|
||||
&& self.y.nearly_eq_within(rhs.y, tolerance)
|
||||
&& self.z.nearly_eq_within(rhs.z, tolerance)
|
||||
}
|
||||
}
|
||||
|
||||
impl Zero for Vec3 {
|
||||
#[inline]
|
||||
fn zero() -> Self {
|
||||
Vec3::ZERO
|
||||
}
|
||||
}
|
||||
|
||||
pub trait NearlyZero<Tolerance = Self> {
|
||||
const DEFAULT_TOLERANCE: Tolerance;
|
||||
|
||||
#[inline]
|
||||
fn nearly_zero(self) -> bool
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.nearly_zero_within(Self::DEFAULT_TOLERANCE)
|
||||
}
|
||||
|
||||
fn nearly_zero_within(self, tolerance: Tolerance) -> bool;
|
||||
}
|
||||
|
||||
impl<T, Tolerance> NearlyZero<Tolerance> for T
|
||||
where
|
||||
T: Zero,
|
||||
T: NearlyEq<T, Tolerance>,
|
||||
{
|
||||
const DEFAULT_TOLERANCE: Tolerance = <Self as NearlyEq<T, Tolerance>>::DEFAULT_TOLERANCE;
|
||||
|
||||
#[inline]
|
||||
fn nearly_zero_within(self, tolerance: Tolerance) -> bool {
|
||||
self.nearly_eq_within(<Self as Zero>::zero(), tolerance)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Helpers to make it easier to implement traits for both `f32` and `f64`
|
||||
//! without needing to copy and paste identical implementations.
|
||||
|
||||
#[rustfmt::skip] pub(super) trait Zero { fn zero() -> Self; }
|
||||
#[rustfmt::skip] impl Zero for f32 { #[inline] fn zero() -> Self { 0_f32 } }
|
||||
#[rustfmt::skip] impl Zero for f64 { #[inline] fn zero() -> Self { 0_f64 } }
|
||||
|
||||
#[rustfmt::skip] pub(super) trait One { fn one() -> Self; }
|
||||
#[rustfmt::skip] impl One for f32 { #[inline] fn one() -> Self { 1_f32 } }
|
||||
#[rustfmt::skip] impl One for f64 { #[inline] fn one() -> Self { 1_f64 } }
|
||||
|
||||
pub(super) trait Clamp {
|
||||
fn clamp(self, min: Self, max: Self) -> Self;
|
||||
}
|
||||
|
||||
impl<T> Clamp for T
|
||||
where
|
||||
T: Sized + Copy + PartialOrd + core::fmt::Debug,
|
||||
{
|
||||
#[inline]
|
||||
fn clamp(mut self, min: Self, max: Self) -> Self {
|
||||
assert!(
|
||||
min <= max,
|
||||
"min > max, or either was NaN. min = {min:?}, max = {max:?}"
|
||||
);
|
||||
if self < min {
|
||||
self = min
|
||||
}
|
||||
if self > max {
|
||||
self = max
|
||||
}
|
||||
self
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
use bevy::prelude::*;
|
||||
|
||||
use crate::math::{NearlyEq, NearlyZero};
|
||||
|
||||
use super::{CurveSin, Easing, Lerp};
|
||||
|
||||
pub struct InterpolationPlugin;
|
||||
|
||||
#[derive(SystemSet, Clone, Copy, PartialEq, Eq, Hash, Debug)]
|
||||
pub struct InterpolationSystem;
|
||||
|
||||
impl Plugin for InterpolationPlugin {
|
||||
fn build(&self, app: &mut App) {
|
||||
app.add_systems(
|
||||
Update,
|
||||
(interp_velocity, interp_position).in_set(InterpolationSystem),
|
||||
)
|
||||
.add_systems(PostUpdate, cleanup_finished_interps);
|
||||
}
|
||||
}
|
||||
|
||||
/// A `newtype` component for a `t` interpolation parameter.
|
||||
#[derive(Component, Deref, DerefMut, Debug, Clone, Copy)]
|
||||
pub struct Interp(pub f32);
|
||||
|
||||
pub struct Interpolation<T>
|
||||
where
|
||||
f32: Lerp<T>,
|
||||
{
|
||||
pub initial: T,
|
||||
pub target: T,
|
||||
pub t: f32,
|
||||
pub easing: Easing,
|
||||
}
|
||||
|
||||
impl<T> Interpolation<T>
|
||||
where
|
||||
f32: Lerp<T>,
|
||||
T: Copy,
|
||||
{
|
||||
fn tick(&mut self, dt: f32) -> T {
|
||||
self.t = (self.t + dt).clamp(0., 1.);
|
||||
self.current()
|
||||
}
|
||||
|
||||
pub fn current(&self) -> T {
|
||||
if self.t.nearly_zero() {
|
||||
self.initial
|
||||
} else if self.t.nearly_eq(1.) {
|
||||
self.target
|
||||
} else {
|
||||
let t = match self.easing {
|
||||
Easing::Linear => self.t,
|
||||
Easing::Sine => self.t.curve_sin(),
|
||||
};
|
||||
|
||||
t.lerp(self.initial, self.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Default for Interpolation<T>
|
||||
where
|
||||
f32: Lerp<T>,
|
||||
T: Default,
|
||||
{
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
initial: default(),
|
||||
target: default(),
|
||||
t: 0.,
|
||||
easing: Easing::Linear,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
pub struct InterpVelocity {
|
||||
#[deref]
|
||||
pub inner: Interpolation<Vec3>,
|
||||
pub accel_zero2max: f32,
|
||||
}
|
||||
|
||||
// TODO: Does this belong here?
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
pub struct Velocity(pub Vec3);
|
||||
|
||||
#[derive(Component, Deref, DerefMut)]
|
||||
pub struct InterpPosition {
|
||||
#[deref]
|
||||
pub inner: Interpolation<Vec3>,
|
||||
pub duration: f32,
|
||||
}
|
||||
|
||||
fn interp_velocity(time: Res<Time>, mut q_interp: Query<(&mut Velocity, &mut InterpVelocity)>) {
|
||||
let delta_seconds = time.delta_seconds();
|
||||
|
||||
for (mut velocity, mut interp) in q_interp.iter_mut() {
|
||||
let dt = delta_seconds / interp.accel_zero2max;
|
||||
**velocity = interp.tick(dt);
|
||||
}
|
||||
}
|
||||
|
||||
fn interp_position(time: Res<Time>, mut q_interp: Query<&mut InterpPosition>) {
|
||||
let delta_seconds = time.delta_seconds();
|
||||
|
||||
for mut interp in q_interp.iter_mut() {
|
||||
let dt = delta_seconds / interp.duration;
|
||||
interp.tick(dt);
|
||||
}
|
||||
}
|
||||
|
||||
fn cleanup_finished_interps(
|
||||
mut cmd: Commands,
|
||||
q_interp_velocity: Query<(Entity, &InterpVelocity)>,
|
||||
q_interp_position: Query<(Entity, &InterpPosition)>,
|
||||
) {
|
||||
for (ent, interp) in q_interp_velocity.iter() {
|
||||
if interp.t.nearly_eq(1.) {
|
||||
cmd.entity(ent).remove::<InterpVelocity>();
|
||||
}
|
||||
}
|
||||
|
||||
for (ent, interp) in q_interp_position.iter() {
|
||||
if interp.t.nearly_eq(1.) {
|
||||
cmd.entity(ent).remove::<InterpPosition>();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
//! Helpers for interpolating between values.
|
||||
|
||||
pub use ecs::*;
|
||||
pub use traits::*;
|
||||
|
||||
mod ecs;
|
||||
mod traits;
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum Easing {
|
||||
Linear,
|
||||
Sine,
|
||||
}
|
||||
|
||||
impl Default for Easing {
|
||||
fn default() -> Self {
|
||||
Self::Linear
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
use std::ops;
|
||||
|
||||
use bevy::prelude::Vec3;
|
||||
|
||||
use crate::math::{
|
||||
internal::{Clamp, One, Zero},
|
||||
NearlyZero,
|
||||
};
|
||||
|
||||
pub trait Lerp<Operand> {
|
||||
/// Interpolate linearly between `a` and `b`, using `self` as the `t` parameter.
|
||||
fn lerp(self, a: Operand, b: Operand) -> Operand;
|
||||
}
|
||||
|
||||
pub trait InvLerp<T: Sized> {
|
||||
/// Derive a `t` interpolation parameter for `self`, where `a` is the
|
||||
/// "origin" and `b` is the "destination".
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use math::NearlyEq;
|
||||
///
|
||||
/// let t = 48_f32.inv_lerp(32., 64.);
|
||||
/// // The expected result is 0.5, because 48 is halfway between 32 and 64.
|
||||
/// assert!(t.nearly_eq(0.5));
|
||||
/// ```
|
||||
fn inv_lerp(self, a: Self, b: Self) -> T;
|
||||
}
|
||||
|
||||
pub trait Remap<T: Sized>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
/// Re-map a value from its interpolated position in the range `from`, to
|
||||
/// the equivalent position in the range `to`.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use math::NearlyEq;
|
||||
///
|
||||
/// let x = 48_f32.remap((32., 64.), (32., 128.));
|
||||
/// // The expected result is 80, because 48 is halfway between 32 and 64,
|
||||
/// // and 80 is halfway between 32 and 128.
|
||||
/// assert!(x.nearly_eq_within(80., 1e-3));
|
||||
/// ```
|
||||
fn remap(self, from: (Self, Self), to: (Self, Self)) -> Self;
|
||||
}
|
||||
|
||||
pub trait CurveSin {
|
||||
/// Fit a `t` interpolation parameter to a sine curve. Mostly useful for
|
||||
/// "easing" between steps in an animation.
|
||||
///
|
||||
/// https://www.desmos.com/calculator/k9x4c4cr5y
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// // This is fine, but it will feel "jerky" because the velocity is
|
||||
/// // constant from start to end.
|
||||
/// fn update_position(dt: f32, start: Vec3, end: Vec3, t: f32) -> Vec3 {
|
||||
/// (t + dt).clamp(0., 1.).lerp(start, end)
|
||||
/// }
|
||||
///
|
||||
/// // This will smoothly accelerate from a standstill at t=0, and
|
||||
/// // smoothly decelerate back to a stop at t=1.
|
||||
/// fn update_position_eased(dt: f32, start: Vec3, end: Vec3, t: f32) -> Vec3 {
|
||||
/// (t + dt)
|
||||
/// .clamp(0., 1.)
|
||||
/// .curve_sin()
|
||||
/// .lerp(start, end)
|
||||
/// }
|
||||
/// ```
|
||||
fn curve_sin(self) -> Self;
|
||||
}
|
||||
|
||||
impl<T, Operand> Lerp<Operand> for T
|
||||
where
|
||||
T: Copy,
|
||||
T: One,
|
||||
T: ops::Sub<Output = T>,
|
||||
Operand: ops::Mul<T, Output = Operand>,
|
||||
Operand: ops::Add<Output = Operand>,
|
||||
{
|
||||
#[inline]
|
||||
fn lerp(self, a: Operand, b: Operand) -> Operand {
|
||||
a * (Self::one() - self) + b * self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> InvLerp<T> for T
|
||||
where
|
||||
T: One + Zero,
|
||||
T: Clamp,
|
||||
T: NearlyZero,
|
||||
T: Copy,
|
||||
T: ops::Sub<Output = T>,
|
||||
T: ops::Div<Output = T>,
|
||||
{
|
||||
#[inline]
|
||||
fn inv_lerp(self, a: T, b: T) -> T {
|
||||
let dividend = b - a;
|
||||
if !dividend.nearly_zero() {
|
||||
((self - a) / dividend).clamp(T::zero(), T::one())
|
||||
} else {
|
||||
T::one()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Vector inverse lerp requires a special implementation, yoinked from here:
|
||||
// https://discussions.unity.com/t/inverselerp-for-vector3/177038
|
||||
impl InvLerp<f32> for Vec3 {
|
||||
#[inline]
|
||||
fn inv_lerp(self, a: Vec3, b: Vec3) -> f32 {
|
||||
let ab = b - a;
|
||||
let av = self - a;
|
||||
if !ab.nearly_zero() {
|
||||
av.dot(ab) / ab.length_squared()
|
||||
} else {
|
||||
1.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Operand> Remap<T> for Operand
|
||||
where
|
||||
Operand: InvLerp<T>,
|
||||
T: Lerp<Operand>,
|
||||
{
|
||||
#[inline]
|
||||
fn remap(self, from: (Self, Self), to: (Self, Self)) -> Self {
|
||||
self.inv_lerp(from.0, from.1).lerp(to.0, to.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl CurveSin for f32 {
|
||||
fn curve_sin(self) -> Self {
|
||||
use std::f32::consts::PI;
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
}
|
||||
|
||||
impl CurveSin for f64 {
|
||||
fn curve_sin(self) -> Self {
|
||||
use std::f64::consts::PI;
|
||||
|
||||
-1. * ((self * PI).cos() / 2.) + 0.5
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
mod float_comparison;
|
||||
mod internal;
|
||||
pub mod interp;
|
||||
|
||||
pub use float_comparison::*;
|
||||
pub use interp::*;
|
||||
Reference in New Issue
Block a user