mirror of
https://github.com/storytold/storyteller-bevy.git
synced 2026-10-17 00:13:17 +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,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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user