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:
Danny McGee
2023-10-31 16:39:10 -04:00
committed by GitHub
parent 4b9ea61495
commit ce6db7fc3c
11 changed files with 811 additions and 399 deletions
+81
View File
@@ -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)
}
}
+34
View File
@@ -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
}
}
+129
View File
@@ -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>();
}
}
}
+19
View File
@@ -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
}
}
+152
View File
@@ -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
}
}
+6
View File
@@ -0,0 +1,6 @@
mod float_comparison;
mod internal;
pub mod interp;
pub use float_comparison::*;
pub use interp::*;