bevy/crates/bevy_math/src/common_traits.rs

164 lines
4.9 KiB
Rust
Raw Normal View History

use glam::{Vec2, Vec3, Vec3A, Vec4};
Move `Point` out of cubic splines module and expand it (#12747) # Objective Previously, the `Point` trait, which abstracts all of the operations of a real vector space, was sitting in the submodule of `bevy_math` for cubic splines. However, the trait has broader applications than merely cubic splines, and we should use it when possible to avoid code duplication when performing vector operations. ## Solution `Point` has been moved into a new submodule in `bevy_math` named `common_traits`. Furthermore, it has been renamed to `VectorSpace`, which is more descriptive, and an additional trait `NormedVectorSpace` has been introduced to expand the API to cover situations involving geometry in addition to algebra. Additionally, `VectorSpace` itself now requires a `ZERO` constant and `Neg`. It also supports a `lerp` function as an automatic trait method. Here is what that looks like: ```rust /// A type that supports the mathematical operations of a real vector space, irrespective of dimension. /// In particular, this means that the implementing type supports: /// - Scalar multiplication and division on the right by elements of `f32` /// - Negation /// - Addition and subtraction /// - Zero /// /// Within the limitations of floating point arithmetic, all the following are required to hold: /// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`. /// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`. /// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`. /// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`. /// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`. /// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`. /// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`. /// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait VectorSpace: Mul<f32, Output = Self> + Div<f32, Output = Self> + Add<Self, Output = Self> + Sub<Self, Output = Self> + Neg + Default + Debug + Clone + Copy { /// The zero vector, which is the identity of addition for the vector space type. const ZERO: Self; /// Perform vector space linear interpolation between this element and another, based /// on the parameter `t`. When `t` is `0`, `self` is recovered. When `t` is `1`, `rhs` /// is recovered. /// /// Note that the value of `t` is not clamped by this function, so interpolating outside /// of the interval `[0,1]` is allowed. #[inline] fn lerp(&self, rhs: Self, t: f32) -> Self { *self * (1. - t) + rhs * t } } ``` ```rust /// A type that supports the operations of a normed vector space; i.e. a norm operation in addition /// to those of [`VectorSpace`]. Specifically, the implementor must guarantee that the following /// relationships hold, within the limitations of floating point arithmetic: /// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`. /// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`. /// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`. /// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`. /// /// Note that, because implementing types use floating point arithmetic, they are not required to actually /// implement `PartialEq` or `Eq`. pub trait NormedVectorSpace: VectorSpace { /// The size of this element. The return value should always be nonnegative. fn norm(self) -> f32; /// The squared norm of this element. Computing this is often faster than computing /// [`NormedVectorSpace::norm`]. #[inline] fn norm_squared(self) -> f32 { self.norm() * self.norm() } /// The distance between this element and another, as determined by the norm. #[inline] fn distance(self, rhs: Self) -> f32 { (rhs - self).norm() } /// The squared distance between this element and another, as determined by the norm. Note that /// this is often faster to compute in practice than [`NormedVectorSpace::distance`]. #[inline] fn distance_squared(self, rhs: Self) -> f32 { (rhs - self).norm_squared() } } ``` Furthermore, this PR also demonstrates the use of the `NormedVectorSpace` combined API to implement `ShapeSample` for `Triangle2d` and `Triangle3d` simultaneously. Such deduplication is one of the drivers for developing these APIs. --- ## Changelog - `Point` from `cubic_splines` becomes `VectorSpace`, exported as `bevy::math::VectorSpace`. - `VectorSpace` requires `Neg` and `VectorSpace::ZERO` in addition to its existing prerequisites. - Introduced public traits `bevy::math::NormedVectorSpace` for generic geometry tasks involving vectors. - Implemented `ShapeSample` for `Triangle2d` and `Triangle3d`. ## Migration Guide Since `Point` no longer exists, any projects using it must switch to `bevy::math::VectorSpace`. Additionally, third-party implementations of this trait now require the `Neg` trait; the constant `VectorSpace::ZERO` must be provided as well. --- ## Discussion ### Design considerations Originally, the `NormedVectorSpace::norm` method was part of a separate trait `Normed`. However, I think that was probably too broad and, more importantly, the semantics of having it in `NormedVectorSpace` are much clearer. As it currently stands, the API exposed here is pretty minimal, and there is definitely a lot more that we could do, but there are more questions to answer along the way. As a silly example, we could implement `NormedVectorSpace::length` as an alias for `NormedVectorSpace::norm`, but this overlaps with methods in all of the glam types, so we would want to make sure that the implementations are effectively identical (for what it's worth, I think they are already). ### Future directions One example of something that could belong in the `NormedVectorSpace` API is normalization. Actually, such a thing previously existed on this branch before I decided to shelve it because of concerns with namespace collision. It looked like this: ```rust /// This element, but normalized to norm 1 if possible. Returns an error when the reciprocal of /// the element's norm is not finite. #[inline] #[must_use] fn normalize(&self) -> Result<Self, NonNormalizableError> { let reciprocal = 1.0 / self.norm(); if reciprocal.is_finite() { Ok(*self * reciprocal) } else { Err(NonNormalizableError { reciprocal }) } } /// An error indicating that an element of a [`NormedVectorSpace`] was non-normalizable due to having /// non-finite norm-reciprocal. #[derive(Debug, Error)] #[error("Element with norm reciprocal {reciprocal} cannot be normalized")] pub struct NonNormalizableError { reciprocal: f32 } ``` With this kind of thing in hand, it might be worth considering eventually making the passage from vectors to directions fully generic by employing a wrapper type. (Of course, for our concrete types, we would leave the existing names in place as aliases.) That is, something like: ```rust pub struct NormOne<T> where T: NormedVectorSpace { //... } ``` Utterly separately, the reason that I implemented `ShapeSample` for `Triangle2d`/`Triangle3d` was to prototype uniform sampling of abstract meshes, so that's also a future direction. --------- Co-authored-by: Zachary Harrold <zac@harrold.com.au>
2024-03-28 13:40:26 +00:00
use std::fmt::Debug;
use std::ops::{Add, Div, Mul, Neg, Sub};
/// A type that supports the mathematical operations of a real vector space, irrespective of dimension.
/// In particular, this means that the implementing type supports:
/// - Scalar multiplication and division on the right by elements of `f32`
/// - Negation
/// - Addition and subtraction
/// - Zero
///
/// Within the limitations of floating point arithmetic, all the following are required to hold:
/// - (Associativity of addition) For all `u, v, w: Self`, `(u + v) + w == u + (v + w)`.
/// - (Commutativity of addition) For all `u, v: Self`, `u + v == v + u`.
/// - (Additive identity) For all `v: Self`, `v + Self::ZERO == v`.
/// - (Additive inverse) For all `v: Self`, `v - v == v + (-v) == Self::ZERO`.
/// - (Compatibility of multiplication) For all `a, b: f32`, `v: Self`, `v * (a * b) == (v * a) * b`.
/// - (Multiplicative identity) For all `v: Self`, `v * 1.0 == v`.
/// - (Distributivity for vector addition) For all `a: f32`, `u, v: Self`, `(u + v) * a == u * a + v * a`.
/// - (Distributivity for scalar addition) For all `a, b: f32`, `v: Self`, `v * (a + b) == v * a + v * b`.
///
/// Note that, because implementing types use floating point arithmetic, they are not required to actually
/// implement `PartialEq` or `Eq`.
pub trait VectorSpace:
Mul<f32, Output = Self>
+ Div<f32, Output = Self>
+ Add<Self, Output = Self>
+ Sub<Self, Output = Self>
+ Neg
+ Default
+ Debug
+ Clone
+ Copy
{
/// The zero vector, which is the identity of addition for the vector space type.
const ZERO: Self;
/// Perform vector space linear interpolation between this element and another, based
/// on the parameter `t`. When `t` is `0`, `self` is recovered. When `t` is `1`, `rhs`
/// is recovered.
///
/// Note that the value of `t` is not clamped by this function, so interpolating outside
/// of the interval `[0,1]` is allowed.
#[inline]
fn lerp(&self, rhs: Self, t: f32) -> Self {
*self * (1. - t) + rhs * t
}
}
impl VectorSpace for Vec4 {
const ZERO: Self = Vec4::ZERO;
}
impl VectorSpace for Vec3 {
const ZERO: Self = Vec3::ZERO;
}
impl VectorSpace for Vec3A {
const ZERO: Self = Vec3A::ZERO;
}
impl VectorSpace for Vec2 {
const ZERO: Self = Vec2::ZERO;
}
impl VectorSpace for f32 {
const ZERO: Self = 0.0;
}
/// A type that supports the operations of a normed vector space; i.e. a norm operation in addition
/// to those of [`VectorSpace`]. Specifically, the implementor must guarantee that the following
/// relationships hold, within the limitations of floating point arithmetic:
/// - (Nonnegativity) For all `v: Self`, `v.norm() >= 0.0`.
/// - (Positive definiteness) For all `v: Self`, `v.norm() == 0.0` implies `v == Self::ZERO`.
/// - (Absolute homogeneity) For all `c: f32`, `v: Self`, `(v * c).norm() == v.norm() * c.abs()`.
/// - (Triangle inequality) For all `v, w: Self`, `(v + w).norm() <= v.norm() + w.norm()`.
///
/// Note that, because implementing types use floating point arithmetic, they are not required to actually
/// implement `PartialEq` or `Eq`.
pub trait NormedVectorSpace: VectorSpace {
/// The size of this element. The return value should always be nonnegative.
fn norm(self) -> f32;
/// The squared norm of this element. Computing this is often faster than computing
/// [`NormedVectorSpace::norm`].
#[inline]
fn norm_squared(self) -> f32 {
self.norm() * self.norm()
}
/// The distance between this element and another, as determined by the norm.
#[inline]
fn distance(self, rhs: Self) -> f32 {
(rhs - self).norm()
}
/// The squared distance between this element and another, as determined by the norm. Note that
/// this is often faster to compute in practice than [`NormedVectorSpace::distance`].
#[inline]
fn distance_squared(self, rhs: Self) -> f32 {
(rhs - self).norm_squared()
}
}
impl NormedVectorSpace for Vec4 {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for Vec3 {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for Vec3A {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for Vec2 {
#[inline]
fn norm(self) -> f32 {
self.length()
}
#[inline]
fn norm_squared(self) -> f32 {
self.length_squared()
}
}
impl NormedVectorSpace for f32 {
#[inline]
fn norm(self) -> f32 {
self.abs()
}
#[inline]
fn norm_squared(self) -> f32 {
self * self
}
}