From df68f0714553ee32c4d448fc43c6026653088a15 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 25 Sep 2024 17:06:09 -0400 Subject: [PATCH 01/22] Allow curves to animate transforms, properties, and morph weights --- crates/bevy_animation/src/animatable.rs | 86 +-- crates/bevy_animation/src/animation_curves.rs | 552 ++++++++++++++++ crates/bevy_animation/src/gltf_curves.rs | 420 ++++++++++++ crates/bevy_animation/src/keyframes.rs | 598 ------------------ crates/bevy_animation/src/lib.rs | 508 +++------------ crates/bevy_gltf/src/loader.rs | 186 +++++- crates/bevy_math/src/curve/cores.rs | 196 +++++- crates/bevy_math/src/curve/iterable.rs | 55 ++ crates/bevy_math/src/curve/mod.rs | 1 + examples/animation/animated_transform.rs | 100 +-- examples/animation/animated_ui.rs | 30 +- 11 files changed, 1525 insertions(+), 1207 deletions(-) create mode 100644 crates/bevy_animation/src/animation_curves.rs create mode 100644 crates/bevy_animation/src/gltf_curves.rs delete mode 100644 crates/bevy_animation/src/keyframes.rs create mode 100644 crates/bevy_math/src/curve/iterable.rs diff --git a/crates/bevy_animation/src/animatable.rs b/crates/bevy_animation/src/animatable.rs index 1a0cce53bfeec..298d0125a208c 100644 --- a/crates/bevy_animation/src/animatable.rs +++ b/crates/bevy_animation/src/animatable.rs @@ -1,6 +1,6 @@ //! Traits and type for interpolating between values. -use crate::{util, AnimationEvaluationError, Interpolation}; +use crate::util; use bevy_color::{Laba, LinearRgba, Oklaba, Srgba, Xyza}; use bevy_math::*; use bevy_reflect::Reflect; @@ -188,93 +188,11 @@ impl Animatable for Quat { } } -/// An abstraction over a list of keyframes. -/// -/// Using this abstraction instead of `Vec` enables more flexibility in how -/// keyframes are stored. In particular, morph weights use this trait in order -/// to flatten the keyframes for all morph weights into a single vector instead -/// of nesting vectors. -pub(crate) trait GetKeyframe { - /// The type of the property to be animated. - type Output; - /// Retrieves the value of the keyframe at the given index. - fn get_keyframe(&self, index: usize) -> Option<&Self::Output>; -} - -/// Interpolates between keyframes and stores the result in `dest`. -/// -/// This is factored out so that it can be shared between implementations of -/// [`crate::keyframes::Keyframes`]. -pub(crate) fn interpolate_keyframes( - dest: &mut T, - keyframes: &(impl GetKeyframe + ?Sized), - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, -) -> Result<(), AnimationEvaluationError> -where - T: Animatable + Clone, -{ - let value = match interpolation { - Interpolation::Step => { - let Some(start_keyframe) = keyframes.get_keyframe(step_start) else { - return Err(AnimationEvaluationError::KeyframeNotPresent(step_start)); - }; - (*start_keyframe).clone() - } - - Interpolation::Linear => { - let (Some(start_keyframe), Some(end_keyframe)) = ( - keyframes.get_keyframe(step_start), - keyframes.get_keyframe(step_start + 1), - ) else { - return Err(AnimationEvaluationError::KeyframeNotPresent(step_start + 1)); - }; - - T::interpolate(start_keyframe, end_keyframe, time) - } - - Interpolation::CubicSpline => { - let ( - Some(start_keyframe), - Some(start_tangent_keyframe), - Some(end_tangent_keyframe), - Some(end_keyframe), - ) = ( - keyframes.get_keyframe(step_start * 3 + 1), - keyframes.get_keyframe(step_start * 3 + 2), - keyframes.get_keyframe(step_start * 3 + 3), - keyframes.get_keyframe(step_start * 3 + 4), - ) - else { - return Err(AnimationEvaluationError::KeyframeNotPresent( - step_start * 3 + 4, - )); - }; - - interpolate_with_cubic_bezier( - start_keyframe, - start_tangent_keyframe, - end_tangent_keyframe, - end_keyframe, - time, - duration, - ) - } - }; - - *dest = T::interpolate(dest, &value, weight); - - Ok(()) -} - /// Evaluates a cubic Bézier curve at a value `t`, given two endpoints and the /// derivatives at those endpoints. /// /// The derivatives are linearly scaled by `duration`. -fn interpolate_with_cubic_bezier(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T +pub fn interpolate_with_cubic_bezier(p0: &T, d0: &T, d3: &T, p3: &T, t: f32, duration: f32) -> T where T: Animatable + Clone, { diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs new file mode 100644 index 0000000000000..8c4fcaade1f99 --- /dev/null +++ b/crates/bevy_animation/src/animation_curves.rs @@ -0,0 +1,552 @@ +//! The [`AnimationCurve`] trait and adaptors that allow curves to implement it. +//! +//! # Overview +//! +//! The flow of curves into the animation system generally begins with something that +//! implements the [`Curve`] trait. Let's imagine, for example, that we have some +//! `Curve` that we want to use to animate something. That could be defined in +//! a number of different ways, but let's imagine that we've defined it [using a function]: +//! +//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_math::vec3; +//! let wobble_curve = function_curve( +//! Interval::UNIT, +//! |t| vec3(t.cos(), 0.0, 0.0) +//! ); +//! +//! Okay, so we have a curve, but the animation system also needs to know, in some way, +//! how the values from this curve should actually be used. That is, it needs to know what +//! to animate! That's what [`AnimationCurve`] is for. In particular, what we need to do +//! is take our curve and turn it into an `AnimationCurve` which will be usable by the +//! animation system. +//! +//! For instance, let's imagine that we want to imagine that we want to use the `Vec3` output +//! from our curve to animate the [translation component of a `Transform`]. For this, there is +//! the adaptor [`TranslationCurve`], which wraps any `Curve` and turns it into an +//! [`AnimationCurve`] that will use the given curve to animate the entity's translation: +//! +//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_math::vec3; +//! # let wobble_curve = function_curve( +//! # Interval::UNIT, +//! # |t| vec3(t.cos(), 0.0, 0.0) +//! # ); +//! let wobble_animation = TranslationCurve(wobble_curve); +//! +//! And finally, this `AnimationCurve` needs to be added to an [`AnimationClip`] in order to +//! actually animate something. This is what that looks like: +//! +//! # use bevy_math::curve::{Curve, Interval, function_curve}; +//! # use bevy_animation::{AnimationClip, AnimationTargetId}; +//! # use bevy_core::Name; +//! # use bevy_math::vec3; +//! # let wobble_curve = function_curve( +//! # Interval::UNIT, +//! # |t| vec3(t.cos(), 0.0, 0.0) +//! # ); +//! # let wobble_animation = TranslationCurve(wobble_curve); +//! # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); +//! let mut animation_clip = AnimationClip::default(); +//! animation_clip.add_curve_to_target( +//! animation_target_id, +//! wobble_animation, +//! ); +//! +//! # Making animation curves +//! +//! The overview showed one example, but in general there are a few different ways of going from +//! a [`Curve`], which produces time-related data of some kind, to an [`AnimationCurve`], which +//! knows how to apply that data to an entity. +//! +//! ## `Transform` +//! +//! [`Transform`] is special and has its own adaptors: +//! - [`TranslationCurve`], which uses `Vec3` output to animate [`Transform::translation`] +//! - [`RotationCurve`], which uses `Quat` output to animate [`Transform::rotation`] +//! - [`ScaleCurve`], which uses `Vec3` output to animate [`Transform::scale`] +//! - [`TransformCurve`], which uses `Transform` output to animate the entire `Transform` +//! +//! ## Animatable properties +//! +//! Animation of arbitrary components can be accomplished using [`AnimatableProperty`] in +//! conjunction with [`AnimatableCurve`]. See the documentation [there] for details. +//! +//! [using a function]: bevy_math::curve::function_curve +//! [translation component of a `Transform`]: bevy_transform::prelude::Transform::translation +//! [`AnimationClip`]: crate::AnimationClip +//! [there]: AnimatableProperty + +use std::{ + any::TypeId, + fmt::{self, Debug, Formatter}, + marker::PhantomData, +}; + +use bevy_asset::Handle; +use bevy_ecs::{ + component::Component, + world::{EntityMutExcept, Mut}, +}; +use bevy_math::{ + curve::{ + cores::{UnevenCore, UnevenCoreError}, + iterable::IterableCurve, + Curve, Interval, + }, + FloatExt, Quat, Vec3, +}; +use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, Reflectable, TypePath, Typed}; +use bevy_render::mesh::morph::MorphWeights; +use bevy_transform::prelude::Transform; + +use crate::{ + graph::AnimationGraph, prelude::Animatable, AnimationEvaluationError, AnimationPlayer, +}; + +/// A value on a component that Bevy can animate. +/// +/// You can implement this trait on a unit struct in order to support animating +/// custom components other than transforms and morph weights. Use that type in +/// conjunction with [`AnimatableCurve`] (and perhaps [`AnimatableKeyframeCurve`] +/// to define the animation itself). For example, in order to animate font size of a +/// text section from 24 pt. to 80 pt., you might use: +/// +/// # use bevy_animation::prelude::AnimatableProperty; +/// # use bevy_reflect::Reflect; +/// # use bevy_text::Text; +/// #[derive(Reflect)] +/// struct FontSizeProperty; +/// +/// impl AnimatableProperty for FontSizeProperty { +/// type Component = Text; +/// type Property = f32; +/// fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { +/// Some(&mut component.sections.get_mut(0)?.style.font_size) +/// } +/// } +/// +/// You can then create an [`AnimationClip`] to animate this property like so: +/// +/// # use bevy_animation::{AnimationClip, AnimationTargetId, VariableCurve}; +/// # use bevy_animation::prelude::{AnimatableProperty, AnimatableKeyframeCurve, AnimatableCurve}; +/// # use bevy_core::Name; +/// # use bevy_reflect::Reflect; +/// # use bevy_text::Text; +/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); +/// # #[derive(Reflect)] +/// # struct FontSizeProperty; +/// # impl AnimatableProperty for FontSizeProperty { +/// # type Component = Text; +/// # type Property = f32; +/// # fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { +/// # Some(&mut component.sections.get_mut(0)?.style.font_size) +/// # } +/// # } +/// let mut animation_clip = AnimationClip::default(); +/// animation_clip.add_curve_to_target( +/// animation_target_id, +/// AnimatableKeyframeCurve::new( +/// [ +/// (0.0, 24.0), +/// (1.0, 80.0), +/// ] +/// ) +/// .map(AnimatableCurve::::from_curve) +/// .expect("Failed to create font size curve") +/// ); +/// +/// Here, the use of `AnimatableKeyframeCurve` creates a curve out of the given keyframe time-value +/// pairs, using the `Animatable` implementation of `f32` to interpolate between then. The +/// invocation of [`AnimatableCurve::from_curve`] with `FontSizeProperty` indicates that the `f32` +/// output from that curve is to be used to animate the font size of a `Text` component (as +/// configured above). +/// +/// [`AnimationClip`]: crate::AnimationClip +pub trait AnimatableProperty: Reflect + TypePath { + /// The type of the component that the property lives on. + type Component: Component; + + /// The type of the property to be animated. + type Property: Animatable + + FromReflect + + GetTypeRegistration + + Reflect + + TypePath + + Typed + + Clone + + Sync + + Debug; + + /// Given a reference to the component, returns a reference to the property. + /// + /// If the property couldn't be found, returns `None`. + fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property>; +} + +/// This trait collects the additional requirements on top of [`Curve`] needed for a +/// curve to be used as an [`AnimationCurve`]. +pub trait InnerAnimationCurve: Curve + Debug + Clone + Reflectable {} + +impl InnerAnimationCurve for C where C: Curve + Debug + Clone + Reflectable + FromReflect +{} + +/// This type allows the conversion of a [curve] valued in the [property type] of an +/// [`AnimatableProperty`] into an [`AnimationCurve`] which animates that property. +/// +/// [curve]: Curve +/// [property type]: AnimatableProperty::Property +#[derive(Reflect)] +#[reflect(from_reflect = false)] +pub struct AnimatableCurve { + curve: C, + #[reflect(ignore)] + _phantom: PhantomData

, +} + +impl AnimatableCurve +where + P: AnimatableProperty, + C: InnerAnimationCurve, +{ + /// Create an [`AnimatableCurve`] (and thus an [`AnimationCurve`]) from a curve + /// valued in an [animatable property]. + /// + /// [animatable property]: AnimatableProperty::Property + pub fn from_curve(curve: C) -> Self { + Self { + curve, + _phantom: PhantomData, + } + } +} + +impl Clone for AnimatableCurve +where + C: Clone, +{ + fn clone(&self) -> Self { + Self { + curve: self.curve.clone(), + _phantom: PhantomData, + } + } +} + +impl Debug for AnimatableCurve +where + C: Debug, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + f.debug_struct("AnimatableCurve") + .field("curve", &self.curve) + .finish() + } +} + +impl AnimationCurve for AnimatableCurve +where + P: AnimatableProperty, + C: InnerAnimationCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.curve.domain() + } + + fn apply<'a>( + &self, + t: f32, + _transform: Option>, + mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = entity.get_mut::().ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let property = P::get_mut(&mut component) + .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; + let value = self.curve.sample_clamped(t); + *property = ::interpolate(property, &value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Vec3` to become an [`AnimationCurve`] that animates +/// the translation component of a transform. +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect)] +#[reflect(from_reflect = false)] +pub struct TranslationCurve(pub C); + +impl AnimationCurve for TranslationCurve +where + C: InnerAnimationCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + component.translation = + ::interpolate(&component.translation, &new_value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Quat` to become an [`AnimationCurve`] that animates +/// the rotation component of a transform. +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect)] +#[reflect(from_reflect = false)] +pub struct RotationCurve(pub C); + +impl AnimationCurve for RotationCurve +where + C: InnerAnimationCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + component.rotation = + ::interpolate(&component.rotation, &new_value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Vec3` to become an [`AnimationCurve`] that animates +/// the scale component of a transform. +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect)] +#[reflect(from_reflect = false)] +pub struct ScaleCurve(pub C); + +impl AnimationCurve for ScaleCurve +where + C: InnerAnimationCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + component.scale = ::interpolate(&component.scale, &new_value, weight); + Ok(()) + } +} + +/// This type allows a [curve] valued in `Transform` to become an [`AnimationCurve`] that animates +/// a transform. +/// +/// This exists primarily as a convenience to animate entities using the entire transform at once +/// instead of splitting it into pieces and animating each part (translation, rotation, scale). +/// +/// [curve]: Curve +#[derive(Debug, Clone, Reflect)] +#[reflect(from_reflect = false)] +pub struct TransformCurve(pub C); + +impl AnimationCurve for TransformCurve +where + C: InnerAnimationCurve, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + transform: Option>, + _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut component = transform.ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + let new_value = self.0.sample_clamped(t); + *component = ::interpolate(&component, &new_value, weight); + Ok(()) + } +} + +/// This type allows an [`IterableCurve`] valued in `f32` to be used as an [`AnimationCurve`] +/// that animates [morph weights]. +/// +/// [morph weights]: MorphWeights +#[derive(Debug, Clone, Reflect)] +#[reflect(from_reflect = false)] +pub struct WeightsCurve(pub C); + +impl AnimationCurve for WeightsCurve +where + C: IterableCurve + Debug + Clone + Reflectable, +{ + fn clone_value(&self) -> Box { + Box::new(self.clone()) + } + + fn domain(&self) -> Interval { + self.0.domain() + } + + fn apply<'a>( + &self, + t: f32, + _transform: Option>, + mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError> { + let mut dest = entity.get_mut::().ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })?; + lerp_morph_weights(dest.weights_mut(), self.0.sample_iter_clamped(t), weight); + Ok(()) + } +} + +/// Update `morph_weights` based on weights in `incoming_weights` with a linear interpolation +/// on `lerp_weight`. +fn lerp_morph_weights( + morph_weights: &mut [f32], + incoming_weights: impl Iterator, + lerp_weight: f32, +) { + let zipped = morph_weights.iter_mut().zip(incoming_weights); + for (morph_weight, incoming_weights) in zipped { + *morph_weight = morph_weight.lerp(incoming_weights, lerp_weight); + } +} + +/// A low-level trait that provides control over how curves are actually applied to entities +/// by the animation system. +/// +/// Typically, this will not need to be implemented manually, since it is automatically +/// implemented by [`AnimatableCurve`] and other curves used by the animation system +/// (e.g. those that animate parts of transforms or morph weights). However, this can be +/// implemented manually when `AnimatableCurve` is not sufficiently expressive. +/// +/// In many respects, this behaves like a type-erased form of [`Curve`], where the output +/// type of the curve is remembered only in the components that are mutated in the +/// implementation of [`apply`]. +/// +/// [`apply`]: AnimationCurve::apply +pub trait AnimationCurve: Reflect + Debug + Send + Sync { + /// Returns a boxed clone of this value. + fn clone_value(&self) -> Box; + + /// The range of times for which this animation is defined. + fn domain(&self) -> Interval; + + /// Write the value of sampling this curve at time `t` into `transform` or `entity`, + /// as appropriate, interpolating between the existing value and the sampled value + /// using the given `weight`. + fn apply<'a>( + &self, + t: f32, + transform: Option>, + entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + weight: f32, + ) -> Result<(), AnimationEvaluationError>; +} + +/// A [curve] defined by keyframes with values in an [animatable] type. +/// +/// The keyframes are interpolated using the type's [`Animatable::interpolate`] implementation. +/// +/// [curve]: Curve +/// [animatable]: Animatable +#[derive(Debug, Clone, Reflect)] +pub struct AnimatableKeyframeCurve { + core: UnevenCore, +} + +impl Curve for AnimatableKeyframeCurve +where + T: Animatable + Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core.sample_with(t, ::interpolate) + } + + #[inline] + fn sample_clamped(&self, t: f32) -> T { + // Sampling by keyframes is automatically clamped to the keyframe bounds. + self.sample_unchecked(t) + } +} + +impl AnimatableKeyframeCurve +where + T: Animatable, +{ + /// Create a new [`AnimatableKeyframeCurve`] from the given `keyframes`. The values of this + /// curve are interpolated from the keyframes using the output type's implementation of + /// [`Animatable::interpolate`]. + /// + /// There must be at least two samples in order for this method to succeed. + pub fn new(keyframes: impl IntoIterator) -> Result { + Ok(Self { + core: UnevenCore::new(keyframes)?, + }) + } +} diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs new file mode 100644 index 0000000000000..c324b660cd168 --- /dev/null +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -0,0 +1,420 @@ +//! Concrete curve structures used to load glTF curves into the animation system. + +use bevy_math::{ + curve::{cores::*, iterable::IterableCurve, *}, + FloatPow, Quat, Vec4, VectorSpace, +}; +use bevy_reflect::Reflect; +use thiserror::Error; + +/// A keyframe-defined curve that "interpolates" by stepping at `t = 1.0` to the next keyframe. +#[derive(Debug, Clone, Reflect)] +pub struct SteppedKeyframeCurve { + core: UnevenCore, +} + +impl Curve for SteppedKeyframeCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, |x, y, t| if t >= 1.0 { y.clone() } else { x.clone() }) + } +} + +impl SteppedKeyframeCurve { + /// Create a new [`SteppedKeyframeCurve`]. If the curve could not be constructed from the + /// given data, an error is returned. + #[inline] + pub fn new(timed_samples: impl IntoIterator) -> Result { + Ok(Self { + core: UnevenCore::new(timed_samples)?, + }) + } +} + +/// A keyframe-defined curve that uses cubic spline interpolation, backed by a contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct CubicKeyframeCurve { + // Note: the sample width here should be 3. + core: ChunkedUnevenCore, +} + +impl Curve for CubicKeyframeCurve +where + V: VectorSpace, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> V { + match self.core.sample_interp_timed(t) { + // In all the cases where only one frame matters, defer to the position within it. + InterpolationDatum::Exact((_, v)) + | InterpolationDatum::LeftTail((_, v)) + | InterpolationDatum::RightTail((_, v)) => v[1], + + InterpolationDatum::Between((t0, u), (t1, v), s) => { + cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0) + } + } + } +} + +impl CubicKeyframeCurve { + /// Create a new [`CubicKeyframeCurve`] from keyframe `times` and their associated `values`. + /// Because 3 values are needed to perform cubic interpolation, `values` must have triple the + /// length of `times` — each consecutive triple `a_k, v_k, b_k` associated to time `t_k` + /// consists of: + /// - The in-tangent `a_k` for the sample at time `t_k` + /// - The actual value `v_k` for the sample at time `t_k` + /// - The out-tangent `b_k` for the sample at time `t_k` + /// + /// For example, for a curve built from two keyframes, the inputs would have the following form: + /// - `times`: `[t_0, t_1]` + /// - `values`: `[a_0, v_0, b_0, a_1, v_1, b_1]` + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new(times, values, 3)?, + }) + } +} + +// NOTE: We can probably delete `CubicRotationCurve` once we improve the `Reflect` implementations +// for the `Curve` API adaptors; this is basically a `CubicKeyframeCurve` composed with `map`. + +/// A keyframe-defined curve that uses cubic spline interpolation, special-cased for quaternions +/// since it uses `Vec4` internally. +#[derive(Debug, Clone, Reflect)] +pub struct CubicRotationCurve { + // Note: The sample width here should be 3. + core: ChunkedUnevenCore, +} + +impl Curve for CubicRotationCurve { + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> Quat { + let vec = match self.core.sample_interp_timed(t) { + // In all the cases where only one frame matters, defer to the position within it. + InterpolationDatum::Exact((_, v)) + | InterpolationDatum::LeftTail((_, v)) + | InterpolationDatum::RightTail((_, v)) => v[1], + + InterpolationDatum::Between((t0, u), (t1, v), s) => { + cubic_spline_interpolation(u[1], u[2], v[0], v[1], s, t1 - t0) + } + }; + Quat::from_vec4(vec.normalize()) + } +} + +impl CubicRotationCurve { + /// Create a new [`CubicRotationCurve`] from keyframe `times` and their associated `values`. + /// Because 3 values are needed to perform cubic interpolation, `values` must have triple the + /// length of `times` — each consecutive triple `a_k, v_k, b_k` associated to time `t_k` + /// consists of: + /// - The in-tangent `a_k` for the sample at time `t_k` + /// - The actual value `v_k` for the sample at time `t_k` + /// - The out-tangent `b_k` for the sample at time `t_k` + /// + /// For example, for a curve built from two keyframes, the inputs would have the following form: + /// - `times`: `[t_0, t_1]` + /// - `values`: `[a_0, v_0, b_0, a_1, v_1, b_1]` + /// + /// To sample quaternions from this curve, the resulting interpolated `Vec4` output is normalized + /// and interpreted as a quaternion. + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new(times, values, 3)?, + }) + } +} + +/// A keyframe-defined curve that uses linear interpolation over many samples at once, backed +/// by a contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct WideLinearKeyframeCurve { + // Here the sample width is the number of things to simultaneously interpolate. + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideLinearKeyframeCurve +where + T: VectorSpace, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + match self.core.sample_interp(t) { + InterpolationDatum::Exact(v) + | InterpolationDatum::LeftTail(v) + | InterpolationDatum::RightTail(v) => TwoIterators::Left(v.iter().copied()), + + InterpolationDatum::Between(u, v, s) => { + let interpolated = u.iter().zip(v.iter()).map(move |(x, y)| x.lerp(*y, s)); + TwoIterators::Right(interpolated) + } + } + } +} + +impl WideLinearKeyframeCurve { + /// Create a new [`WideLinearKeyframeCurve`]. An error will be returned if: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by that of `times` (once sorted, filtered, + /// and deduplicated). + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) + } +} + +/// A keyframe-defined curve that uses stepped "interpolation" over many samples at once, backed +/// by a contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct WideSteppedKeyframeCurve { + // Here the sample width is the number of things to simultaneously interpolate. + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideSteppedKeyframeCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + match self.core.sample_interp(t) { + InterpolationDatum::Exact(v) + | InterpolationDatum::LeftTail(v) + | InterpolationDatum::RightTail(v) => TwoIterators::Left(v.iter().cloned()), + + InterpolationDatum::Between(u, v, s) => { + let interpolated = + u.iter() + .zip(v.iter()) + .map(move |(x, y)| if s >= 1.0 { y.clone() } else { x.clone() }); + TwoIterators::Right(interpolated) + } + } + } +} + +impl WideSteppedKeyframeCurve { + /// Create a new [`WideSteppedKeyframeCurve`]. An error will be returned if: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by that of `times` (once sorted, filtered, + /// and deduplicated). + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) + } +} + +/// A keyframe-defined curve that uses cubic interpolation over many samples at once, backed by a +/// contiguous buffer. +#[derive(Debug, Clone, Reflect)] +pub struct WideCubicKeyframeCurve { + core: ChunkedUnevenCore, +} + +impl IterableCurve for WideCubicKeyframeCurve +where + T: VectorSpace, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + match self.core.sample_interp_timed(t) { + InterpolationDatum::Exact((_, v)) + | InterpolationDatum::LeftTail((_, v)) + | InterpolationDatum::RightTail((_, v)) => { + // Pick out the part of this that actually represents the position (instead of tangents), + // which is the middle third. + let width = self.core.width(); + TwoIterators::Left(v[width..(width * 2)].iter().copied()) + } + + InterpolationDatum::Between((t0, u), (t1, v), s) => TwoIterators::Right( + cubic_spline_interpolate_slices(self.core.width() / 3, u, v, s, t1 - t0), + ), + } + } +} + +/// An error indicating that a multisampling keyframe curve could not be constructed. +#[derive(Debug, Error)] +#[error("Unable to construct a curve using this data")] +pub enum WideKeyframeCurveError { + /// The number of given values was not divisible by a multiple of the number of keyframes. + #[error("The number of values ({values_given}) was expected to be divisible by {divisor}")] + LengthMismatch { + /// The number of values given. + values_given: usize, + /// The number that `values_given` was supposed to be divisible by. + divisor: usize, + }, + + /// An error was returned by the internal core constructor. + CoreError(#[from] ChunkedUnevenCoreError), +} + +impl WideCubicKeyframeCurve { + /// Create a new [`WideCubicKeyframeCurve`]. An error will be returned if: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by three times that of `times` (once sorted, + /// filtered, and deduplicated). + #[inline] + pub fn new( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + let times: Vec = times.into_iter().collect(); + let values: Vec = values.into_iter().collect(); + let divisor = times.len() * 3; + + if values.len() % divisor != 0 { + return Err(WideKeyframeCurveError::LengthMismatch { + values_given: values.len(), + divisor, + }); + } + + Ok(Self { + core: ChunkedUnevenCore::new_width_inferred(times, values)?, + }) + } +} + +/// A curve specifying the [`MorphWeights`] for a mesh in animation. The variants are broken +/// down by interpolation mode (with the exception of `Constant`, which never interpolates). +/// +/// This type is, itself, a `Curve>`; however, in order to avoid allocation, it is +/// recommended to use its implementation of the [`IterableCurve`] trait, which allows iterating +/// directly over information derived from the curve without allocating. +/// +/// [`MorphWeights`]: bevy_render::prelude::MorphWeights +#[derive(Debug, Clone, Reflect)] +pub enum WeightsCurve { + /// A curve which takes a constant value over its domain. Notably, this is how animations with + /// only a single keyframe are interpreted. + Constant(ConstantCurve>), + + /// A curve which interpolates weights linearly between keyframes. + Linear(WideLinearKeyframeCurve), + + /// A curve which interpolates weights between keyframes in steps. + Step(WideSteppedKeyframeCurve), + + /// A curve which interpolates between keyframes by using auxiliary tangent data to join + /// adjacent keyframes with a cubic Hermite spline, which is then sampled. + CubicSpline(WideCubicKeyframeCurve), +} + +//---------// +// HELPERS // +//---------// + +enum TwoIterators { + Left(A), + Right(B), +} + +impl Iterator for TwoIterators +where + A: Iterator, + B: Iterator, +{ + type Item = T; + + fn next(&mut self) -> Option { + match self { + TwoIterators::Left(a) => a.next(), + TwoIterators::Right(b) => b.next(), + } + } +} + +/// Helper function for cubic spline interpolation. +fn cubic_spline_interpolation( + value_start: T, + tangent_out_start: T, + tangent_in_end: T, + value_end: T, + lerp: f32, + step_duration: f32, +) -> T +where + T: VectorSpace, +{ + value_start * (2.0 * lerp.cubed() - 3.0 * lerp.squared() + 1.0) + + tangent_out_start * (step_duration) * (lerp.cubed() - 2.0 * lerp.squared() + lerp) + + value_end * (-2.0 * lerp.cubed() + 3.0 * lerp.squared()) + + tangent_in_end * step_duration * (lerp.cubed() - lerp.squared()) +} + +fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( + width: usize, + first: &'a [T], + second: &'a [T], + s: f32, + step_between: f32, +) -> impl Iterator + 'a { + (0..width).map(move |idx| { + cubic_spline_interpolation( + first[idx + width], + first[idx + (width * 2)], + second[idx + width], + second[idx], + s, + step_between, + ) + }) +} diff --git a/crates/bevy_animation/src/keyframes.rs b/crates/bevy_animation/src/keyframes.rs deleted file mode 100644 index bed12e1908723..0000000000000 --- a/crates/bevy_animation/src/keyframes.rs +++ /dev/null @@ -1,598 +0,0 @@ -//! Keyframes of animation clips. - -use std::{ - any::TypeId, - fmt::{self, Debug, Formatter}, -}; - -use bevy_asset::Handle; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::{ - component::Component, - world::{EntityMutExcept, Mut}, -}; -use bevy_math::{Quat, Vec3}; -use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, TypePath, Typed}; -use bevy_render::mesh::morph::MorphWeights; -use bevy_transform::prelude::Transform; - -use crate::{ - animatable, - graph::AnimationGraph, - prelude::{Animatable, GetKeyframe}, - AnimationEvaluationError, AnimationPlayer, Interpolation, -}; - -/// A value on a component that Bevy can animate. -/// -/// You can implement this trait on a unit struct in order to support animating -/// custom components other than transforms and morph weights. Use that type in -/// conjunction with [`AnimatablePropertyKeyframes`]. For example, in order to -/// animate font size of a text section from 24 pt. to 80 pt., you might use: -/// -/// # use bevy_animation::prelude::AnimatableProperty; -/// # use bevy_reflect::Reflect; -/// # use bevy_text::Text; -/// #[derive(Reflect)] -/// struct FontSizeProperty; -/// -/// impl AnimatableProperty for FontSizeProperty { -/// type Component = Text; -/// type Property = f32; -/// fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// Some(&mut component.sections.get_mut(0)?.style.font_size) -/// } -/// } -/// -/// You can then create a [`crate::AnimationClip`] to animate this property like so: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation, VariableCurve}; -/// # use bevy_animation::prelude::{AnimatableProperty, AnimatablePropertyKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_reflect::Reflect; -/// # use bevy_text::Text; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// # #[derive(Reflect)] -/// # struct FontSizeProperty; -/// # impl AnimatableProperty for FontSizeProperty { -/// # type Component = Text; -/// # type Property = f32; -/// # fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// # Some(&mut component.sections.get_mut(0)?.style.font_size) -/// # } -/// # } -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::>( -/// [0.0, 1.0], -/// [24.0, 80.0], -/// ), -/// ); -pub trait AnimatableProperty: Reflect + TypePath + 'static { - /// The type of the component that the property lives on. - type Component: Component; - - /// The type of the property to be animated. - type Property: Animatable - + FromReflect - + GetTypeRegistration - + Reflect - + TypePath - + Typed - + Clone - + Sync - + Debug - + 'static; - - /// Given a reference to the component, returns a reference to the property. - /// - /// If the property couldn't be found, returns `None`. - fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property>; -} - -/// Keyframes in a [`crate::VariableCurve`] that animate an -/// [`AnimatableProperty`]. -/// -/// This is the a generic type of [`Keyframes`] that can animate any -/// [`AnimatableProperty`]. See the documentation for [`AnimatableProperty`] for -/// more information as to how to use this type. -/// -/// If you're animating scale, rotation, or translation of a [`Transform`], -/// [`ScaleKeyframes`], [`RotationKeyframes`], and [`TranslationKeyframes`] are -/// faster, and simpler, alternatives to this type. -#[derive(Reflect, Deref, DerefMut)] -pub struct AnimatablePropertyKeyframes

(pub Vec) -where - P: AnimatableProperty; - -impl

Clone for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -impl

Debug for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - f.debug_tuple("AnimatablePropertyKeyframes") - .field(&self.0) - .finish() - } -} - -/// A low-level trait for use in [`crate::VariableCurve`] that provides fine -/// control over how animations are evaluated. -/// -/// You can implement this trait when the generic -/// [`AnimatablePropertyKeyframes`] isn't sufficiently-expressive for your -/// needs. For example, [`MorphWeights`] implements this trait instead of using -/// [`AnimatablePropertyKeyframes`] because it needs to animate arbitrarily many -/// weights at once, which can't be done with [`Animatable`] as that works on -/// fixed-size values only. -pub trait Keyframes: Reflect + Debug + Send + Sync { - /// Returns a boxed clone of this value. - fn clone_value(&self) -> Box; - - /// Interpolates between the existing value and the value of the first - /// keyframe, and writes the value into `transform` and/or `entity` as - /// appropriate. - /// - /// Arguments: - /// - /// * `transform`: The transform of the entity, if present. - /// - /// * `entity`: Allows access to the rest of the components of the entity. - /// - /// * `weight`: The blend weight between the existing component value (0.0) - /// and the one computed from the keyframes (1.0). - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - weight: f32, - ) -> Result<(), AnimationEvaluationError>; - - /// Interpolates between the existing value and the value of the two nearest - /// keyframes, and writes the value into `transform` and/or `entity` as - /// appropriate. - /// - /// Arguments: - /// - /// * `transform`: The transform of the entity, if present. - /// - /// * `entity`: Allows access to the rest of the components of the entity. - /// - /// * `interpolation`: The type of interpolation to use. - /// - /// * `step_start`: The index of the first keyframe. - /// - /// * `time`: The blend weight between the first keyframe (0.0) and the next - /// keyframe (1.0). - /// - /// * `weight`: The blend weight between the existing component value (0.0) - /// and the one computed from the keyframes (1.0). - /// - /// If `interpolation` is `Interpolation::Linear`, then pseudocode for this - /// function could be `property = lerp(property, lerp(keyframes[step_start], - /// keyframes[step_start + 1], time), weight)`. - #[allow(clippy::too_many_arguments)] - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError>; -} - -/// Keyframes for animating [`Transform::translation`]. -/// -/// An example of a [`crate::AnimationClip`] that animates translation: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; -/// # use bevy_animation::{VariableCurve, prelude::TranslationKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_math::Vec3; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::( -/// [0.0, 1.0], -/// [Vec3::ZERO, Vec3::ONE], -/// ), -/// ); -#[derive(Clone, Reflect, Debug, Deref, DerefMut)] -pub struct TranslationKeyframes(pub Vec); - -/// Keyframes for animating [`Transform::scale`]. -/// -/// An example of a [`crate::AnimationClip`] that animates translation: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; -/// # use bevy_animation::{VariableCurve, prelude::ScaleKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_math::Vec3; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::( -/// [0.0, 1.0], -/// [Vec3::ONE, Vec3::splat(2.0)], -/// ), -/// ); -#[derive(Clone, Reflect, Debug, Deref, DerefMut)] -pub struct ScaleKeyframes(pub Vec); - -/// Keyframes for animating [`Transform::rotation`]. -/// -/// An example of a [`crate::AnimationClip`] that animates translation: -/// -/// # use bevy_animation::{AnimationClip, AnimationTargetId, Interpolation}; -/// # use bevy_animation::{VariableCurve, prelude::RotationKeyframes}; -/// # use bevy_core::Name; -/// # use bevy_math::Quat; -/// # use std::f32::consts::FRAC_PI_2; -/// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); -/// let mut animation_clip = AnimationClip::default(); -/// animation_clip.add_curve_to_target( -/// animation_target_id, -/// VariableCurve::linear::( -/// [0.0, 1.0], -/// [Quat::from_rotation_x(FRAC_PI_2), Quat::from_rotation_y(FRAC_PI_2)], -/// ), -/// ); -#[derive(Clone, Reflect, Debug, Deref, DerefMut)] -pub struct RotationKeyframes(pub Vec); - -/// Keyframes for animating [`MorphWeights`]. -#[derive(Clone, Debug, Reflect)] -pub struct MorphWeightsKeyframes { - /// The total number of morph weights. - pub morph_target_count: usize, - - /// The morph weights. - /// - /// The length of this vector should be the total number of morph weights - /// times the number of keyframes. - pub weights: Vec, -} - -impl From for TranslationKeyframes -where - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl Keyframes for TranslationKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - component.translation = Animatable::interpolate(&component.translation, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - animatable::interpolate_keyframes( - &mut component.translation, - &(*self)[..], - interpolation, - step_start, - time, - weight, - duration, - ) - } -} - -impl From for ScaleKeyframes -where - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl Keyframes for ScaleKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - component.scale = Animatable::interpolate(&component.scale, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - animatable::interpolate_keyframes( - &mut component.scale, - &(*self)[..], - interpolation, - step_start, - time, - weight, - duration, - ) - } -} - -impl From for RotationKeyframes -where - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl Keyframes for RotationKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - transform: Option>, - _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - component.rotation = Animatable::interpolate(&component.rotation, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - transform: Option>, - _: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = transform.ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - animatable::interpolate_keyframes( - &mut component.rotation, - &(*self)[..], - interpolation, - step_start, - time, - weight, - duration, - ) - } -} - -impl From for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, - T: Into>, -{ - fn from(value: T) -> Self { - Self(value.into()) - } -} - -impl

Keyframes for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - fn clone_value(&self) -> Box { - Box::new((*self).clone()) - } - - fn apply_single_keyframe<'a>( - &self, - _: Option>, - mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let property = P::get_mut(&mut component) - .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; - let value = self - .first() - .ok_or(AnimationEvaluationError::KeyframeNotPresent(0))?; - ::interpolate(property, value, weight); - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - _: Option>, - mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut component = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - let property = P::get_mut(&mut component) - .ok_or_else(|| AnimationEvaluationError::PropertyNotPresent(TypeId::of::

()))?; - animatable::interpolate_keyframes( - property, - self, - interpolation, - step_start, - time, - weight, - duration, - )?; - Ok(()) - } -} - -impl GetKeyframe for [A] -where - A: Animatable, -{ - type Output = A; - - fn get_keyframe(&self, index: usize) -> Option<&Self::Output> { - self.get(index) - } -} - -impl

GetKeyframe for AnimatablePropertyKeyframes

-where - P: AnimatableProperty, -{ - type Output = P::Property; - - fn get_keyframe(&self, index: usize) -> Option<&Self::Output> { - self.get(index) - } -} - -/// Information needed to look up morph weight values in the flattened morph -/// weight keyframes vector. -struct GetMorphWeightKeyframe<'k> { - /// The morph weights keyframe structure that we're animating. - keyframes: &'k MorphWeightsKeyframes, - /// The index of the morph target in that structure. - morph_target_index: usize, -} - -impl Keyframes for MorphWeightsKeyframes { - fn clone_value(&self) -> Box { - Box::new(self.clone()) - } - - fn apply_single_keyframe<'a>( - &self, - _: Option>, - mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - weight: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut dest = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - - // TODO: Go 4 weights at a time to make better use of SIMD. - for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() { - *morph_weight = - f32::interpolate(morph_weight, &self.weights[morph_target_index], weight); - } - - Ok(()) - } - - fn apply_tweened_keyframes<'a>( - &self, - _: Option>, - mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, - interpolation: Interpolation, - step_start: usize, - time: f32, - weight: f32, - duration: f32, - ) -> Result<(), AnimationEvaluationError> { - let mut dest = entity.get_mut::().ok_or_else(|| { - AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) - })?; - - // TODO: Go 4 weights at a time to make better use of SIMD. - for (morph_target_index, morph_weight) in dest.weights_mut().iter_mut().enumerate() { - animatable::interpolate_keyframes( - morph_weight, - &GetMorphWeightKeyframe { - keyframes: self, - morph_target_index, - }, - interpolation, - step_start, - time, - weight, - duration, - )?; - } - - Ok(()) - } -} - -impl GetKeyframe for GetMorphWeightKeyframe<'_> { - type Output = f32; - - fn get_keyframe(&self, keyframe_index: usize) -> Option<&Self::Output> { - self.keyframes - .weights - .as_slice() - .get(keyframe_index * self.keyframes.morph_target_count + self.morph_target_index) - } -} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index e6d8b3294a773..c34621d9a8fc9 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -8,8 +8,9 @@ //! Animation for the game engine Bevy pub mod animatable; +pub mod animation_curves; +pub mod gltf_curves; pub mod graph; -pub mod keyframes; pub mod transition; mod util; @@ -28,12 +29,11 @@ use bevy_core::Name; use bevy_ecs::{ entity::MapEntities, prelude::*, reflect::ReflectMapEntities, world::EntityMutExcept, }; -use bevy_math::FloatExt; use bevy_reflect::{ - prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicStruct, FieldIter, - FromReflect, FromType, GetTypeRegistration, NamedField, PartialReflect, Reflect, - ReflectFromPtr, ReflectKind, ReflectMut, ReflectOwned, ReflectRef, Struct, StructInfo, - TypeInfo, TypePath, TypeRegistration, Typed, + prelude::ReflectDefault, utility::NonGenericTypeInfoCell, ApplyError, DynamicTupleStruct, + FromReflect, FromType, GetTypeRegistration, PartialReflect, Reflect, ReflectFromPtr, + ReflectKind, ReflectMut, ReflectOwned, ReflectRef, TupleStruct, TupleStructFieldIter, + TupleStructInfo, TypeInfo, TypePath, TypeRegistration, Typed, UnnamedField, }; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; @@ -56,14 +56,14 @@ use uuid::Uuid; pub mod prelude { #[doc(hidden)] pub use crate::{ - animatable::*, graph::*, keyframes::*, transition::*, AnimationClip, AnimationPlayer, - AnimationPlugin, Interpolation, VariableCurve, + animatable::*, animation_curves::*, graph::*, transition::*, AnimationClip, + AnimationPlayer, AnimationPlugin, VariableCurve, }; } use crate::{ + animation_curves::AnimationCurve, graph::{AnimationGraph, AnimationGraphAssetLoader, AnimationNodeIndex}, - keyframes::Keyframes, transition::{advance_transitions, expire_completed_transitions, AnimationTransitions}, }; @@ -72,167 +72,29 @@ use crate::{ /// [UUID namespace]: https://en.wikipedia.org/wiki/Universally_unique_identifier#Versions_3_and_5_(namespace_name-based) pub static ANIMATION_TARGET_NAMESPACE: Uuid = Uuid::from_u128(0x3179f519d9274ff2b5966fd077023911); -/// Describes how an attribute of a [`Transform`] or -/// [`bevy_render::mesh::morph::MorphWeights`] should be animated. +/// Contains an [animation curve] which is used to animate entities. /// -/// `keyframe_timestamps` and `keyframes` should have the same length. +/// [animation curve]: AnimationCurve #[derive(Debug, TypePath)] -pub struct VariableCurve { - /// Timestamp for each of the keyframes. - pub keyframe_timestamps: Vec, - /// List of the keyframes. - /// - /// The representation will depend on the interpolation type of this curve: - /// - /// - for `Interpolation::Step` and `Interpolation::Linear`, each keyframe is a single value - /// - for `Interpolation::CubicSpline`, each keyframe is made of three values for `tangent_in`, - /// `keyframe_value` and `tangent_out` - pub keyframes: Box, - /// Interpolation method to use between keyframes. - pub interpolation: Interpolation, -} +pub struct VariableCurve(pub Box); impl Clone for VariableCurve { fn clone(&self) -> Self { - VariableCurve { - keyframe_timestamps: self.keyframe_timestamps.clone(), - keyframes: Keyframes::clone_value(&*self.keyframes), - interpolation: self.interpolation, - } + Self(AnimationCurve::clone_value(&*self.0)) } } impl VariableCurve { - /// Creates a new curve from timestamps, keyframes, and interpolation type. - /// - /// The two arrays must have the same length. - pub fn new( - keyframe_timestamps: Vec, - keyframes: impl Into, - interpolation: Interpolation, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve { - keyframe_timestamps, - keyframes: Box::new(keyframes.into()), - interpolation, - } - } - - /// Creates a new curve from timestamps and keyframes with no interpolation. - /// - /// The two arrays must have the same length. - pub fn step( - keyframe_timestamps: impl Into>, - keyframes: impl Into, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Step) - } - - /// Creates a new curve from timestamps and keyframes with linear - /// interpolation. - /// - /// The two arrays must have the same length. - pub fn linear( - keyframe_timestamps: impl Into>, - keyframes: impl Into, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve::new(keyframe_timestamps.into(), keyframes, Interpolation::Linear) - } - - /// Creates a new curve from timestamps and keyframes with no interpolation. - /// - /// The two arrays must have the same length. - pub fn cubic_spline( - keyframe_timestamps: impl Into>, - keyframes: impl Into, - ) -> VariableCurve - where - K: Keyframes, - { - VariableCurve::new( - keyframe_timestamps.into(), - keyframes, - Interpolation::CubicSpline, - ) - } - - /// Find the index of the keyframe at or before the current time. + /// Create a new [`VariableCurve`] from an [animation curve]. /// - /// Returns [`None`] if the curve is finished or not yet started. - /// To be more precise, this returns [`None`] if the frame is at or past the last keyframe: - /// we cannot get the *next* keyframe to interpolate to in that case. - pub fn find_current_keyframe(&self, seek_time: f32) -> Option { - // An Ok(keyframe_index) result means an exact result was found by binary search - // An Err result means the keyframe was not found, and the index is the keyframe - // PERF: finding the current keyframe can be optimised - let search_result = self - .keyframe_timestamps - .binary_search_by(|probe| probe.partial_cmp(&seek_time).unwrap()); - - // Subtract one for zero indexing! - let last_keyframe = self.keyframe_timestamps.len() - 1; - - // We want to find the index of the keyframe before the current time - // If the keyframe is past the second-to-last keyframe, the animation cannot be interpolated. - let step_start = match search_result { - // An exact match was found, and it is the last keyframe (or something has gone terribly wrong). - // This means that the curve is finished. - Ok(n) if n >= last_keyframe => return None, - // An exact match was found, and it is not the last keyframe. - Ok(i) => i, - // No exact match was found, and the seek_time is before the start of the animation. - // This occurs because the binary search returns the index of where we could insert a value - // without disrupting the order of the vector. - // If the value is less than the first element, the index will be 0. - Err(0) => return None, - // No exact match was found, and it was after the last keyframe. - // The curve is finished. - Err(n) if n > last_keyframe => return None, - // No exact match was found, so return the previous keyframe to interpolate from. - Err(i) => i - 1, - }; - - // Consumers need to be able to interpolate between the return keyframe and the next - assert!(step_start < self.keyframe_timestamps.len()); - - Some(step_start) - } - - /// Find the index of the keyframe at or before the current time. - /// - /// Returns the first keyframe if the `seek_time` is before the first keyframe, and - /// the second-to-last keyframe if the `seek_time` is after the last keyframe. - /// Panics if there are less than 2 keyframes. - pub fn find_interpolation_start_keyframe(&self, seek_time: f32) -> usize { - // An Ok(keyframe_index) result means an exact result was found by binary search - // An Err result means the keyframe was not found, and the index is the keyframe - // PERF: finding the current keyframe can be optimised - let search_result = self - .keyframe_timestamps - .binary_search_by(|probe| probe.partial_cmp(&seek_time).unwrap()); - - // We want to find the index of the keyframe before the current time - // If the keyframe is past the second-to-last keyframe, the animation cannot be interpolated. - match search_result { - // An exact match was found - Ok(i) => i.clamp(0, self.keyframe_timestamps.len() - 2), - // No exact match was found, so return the previous keyframe to interpolate from. - Err(i) => (i.saturating_sub(1)).clamp(0, self.keyframe_timestamps.len() - 2), - } + /// [animation curve]: AnimationCurve + pub fn new(animation_curve: impl AnimationCurve) -> Self { + Self(Box::new(animation_curve)) } } // We have to implement `PartialReflect` manually because of the embedded -// `Box`, which can't be automatically derived yet. +// `Box`, which can't be automatically derived yet. impl PartialReflect for VariableCurve { #[inline] fn get_represented_type_info(&self) -> Option<&'static TypeInfo> { @@ -269,32 +131,31 @@ impl PartialReflect for VariableCurve { } fn try_apply(&mut self, value: &dyn PartialReflect) -> Result<(), ApplyError> { - if let ReflectRef::Struct(struct_value) = value.reflect_ref() { - for (i, value) in struct_value.iter_fields().enumerate() { - let name = struct_value.name_at(i).unwrap(); - if let Some(v) = self.field_mut(name) { + if let ReflectRef::TupleStruct(tuple_value) = value.reflect_ref() { + for (i, value) in tuple_value.iter_fields().enumerate() { + if let Some(v) = self.field_mut(i) { v.try_apply(value)?; } } } else { return Err(ApplyError::MismatchedKinds { from_kind: value.reflect_kind(), - to_kind: ReflectKind::Struct, + to_kind: ReflectKind::TupleStruct, }); } Ok(()) } fn reflect_ref(&self) -> ReflectRef { - ReflectRef::Struct(self) + ReflectRef::TupleStruct(self) } fn reflect_mut(&mut self) -> ReflectMut { - ReflectMut::Struct(self) + ReflectMut::TupleStruct(self) } fn reflect_owned(self: Box) -> ReflectOwned { - ReflectOwned::Struct(self) + ReflectOwned::TupleStruct(self) } fn clone_value(&self) -> Box { @@ -303,7 +164,7 @@ impl PartialReflect for VariableCurve { } // We have to implement `Reflect` manually because of the embedded `Box`, which can't be automatically derived yet. +// AnimationCurve>`, which can't be automatically derived yet. impl Reflect for VariableCurve { #[inline] fn into_any(self: Box) -> Box { @@ -342,79 +203,38 @@ impl Reflect for VariableCurve { } } -// We have to implement `Struct` manually because of the embedded `Box`, which can't be automatically derived yet. -impl Struct for VariableCurve { - fn field(&self, name: &str) -> Option<&dyn PartialReflect> { - match name { - "keyframe_timestamps" => Some(&self.keyframe_timestamps), - "keyframes" => Some(self.keyframes.as_partial_reflect()), - "interpolation" => Some(&self.interpolation), - _ => None, - } - } - - fn field_mut(&mut self, name: &str) -> Option<&mut dyn PartialReflect> { - match name { - "keyframe_timestamps" => Some(&mut self.keyframe_timestamps), - "keyframes" => Some(self.keyframes.as_partial_reflect_mut()), - "interpolation" => Some(&mut self.interpolation), - _ => None, - } - } - - fn field_at(&self, index: usize) -> Option<&dyn PartialReflect> { +// We have to implement `TupleStruct` manually because of the embedded `Box`, which can't be automatically derived yet. +impl TupleStruct for VariableCurve { + fn field(&self, index: usize) -> Option<&dyn PartialReflect> { match index { - 0 => Some(&self.keyframe_timestamps), - 1 => Some(self.keyframes.as_partial_reflect()), - 2 => Some(&self.interpolation), + 0 => Some(self.0.as_partial_reflect()), _ => None, } } - fn field_at_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { + fn field_mut(&mut self, index: usize) -> Option<&mut dyn PartialReflect> { match index { - 0 => Some(&mut self.keyframe_timestamps), - 1 => Some(self.keyframes.as_partial_reflect_mut()), - 2 => Some(&mut self.interpolation), - _ => None, - } - } - - fn name_at(&self, index: usize) -> Option<&str> { - match index { - 0 => Some("keyframe_timestamps"), - 1 => Some("keyframes"), - 2 => Some("interpolation"), + 0 => Some(self.0.as_partial_reflect_mut()), _ => None, } } fn field_len(&self) -> usize { - 3 + 1 } - fn iter_fields(&self) -> FieldIter { - FieldIter::new(self) + fn iter_fields(&self) -> TupleStructFieldIter { + TupleStructFieldIter::new(self) } - fn clone_dynamic(&self) -> DynamicStruct { - DynamicStruct::from_iter([ - ( - "keyframe_timestamps", - Box::new(self.keyframe_timestamps.clone()) as Box, - ), - ("keyframes", PartialReflect::clone_value(&*self.keyframes)), - ( - "interpolation", - Box::new(self.interpolation) as Box, - ), - ]) + fn clone_dynamic(&self) -> DynamicTupleStruct { + DynamicTupleStruct::from_iter([PartialReflect::clone_value(&*self.0)]) } } // We have to implement `FromReflect` manually because of the embedded `Box`, which can't be automatically derived yet. +// AnimationCurve>`, which can't be automatically derived yet. impl FromReflect for VariableCurve { fn from_reflect(reflect: &dyn PartialReflect) -> Option { Some(reflect.try_downcast_ref::()?.clone()) @@ -422,7 +242,7 @@ impl FromReflect for VariableCurve { } // We have to implement `GetTypeRegistration` manually because of the embedded -// `Box`, which can't be automatically derived yet. +// `Box`, which can't be automatically derived yet. impl GetTypeRegistration for VariableCurve { fn get_type_registration() -> TypeRegistration { let mut registration = TypeRegistration::of::(); @@ -432,32 +252,16 @@ impl GetTypeRegistration for VariableCurve { } // We have to implement `Typed` manually because of the embedded `Box`, which can't be automatically derived yet. +// AnimationCurve>`, which can't be automatically derived yet. impl Typed for VariableCurve { fn type_info() -> &'static TypeInfo { static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new(); CELL.get_or_set(|| { - TypeInfo::Struct(StructInfo::new::(&[ - NamedField::new::>("keyframe_timestamps"), - NamedField::new::<()>("keyframes"), - NamedField::new::("interpolation"), - ])) + TypeInfo::TupleStruct(TupleStructInfo::new::(&[UnnamedField::new::<()>(0)])) }) } } -/// Interpolation method to use between keyframes. -#[derive(Reflect, Clone, Copy, Debug)] -pub enum Interpolation { - /// Linear interpolation between the two closest keyframes. - Linear, - /// Step interpolation, the value of the start keyframe is used. - Step, - /// Cubic spline interpolation. The value of the two closest keyframes is used, with the out - /// tangent of the start keyframe and the in tangent of the end keyframe. - CubicSpline, -} - /// A list of [`VariableCurve`]s and the [`AnimationTargetId`]s to which they /// apply. /// @@ -584,18 +388,46 @@ impl AnimationClip { self.duration = duration_sec; } - /// Adds a [`VariableCurve`] to an [`AnimationTarget`] named by an + /// Adds an [`AnimationCurve`] to an [`AnimationTarget`] named by an /// [`AnimationTargetId`]. /// /// If the curve extends beyond the current duration of this clip, this /// method lengthens this clip to include the entire time span that the /// curve covers. - pub fn add_curve_to_target(&mut self, target_id: AnimationTargetId, curve: VariableCurve) { + pub fn add_curve_to_target( + &mut self, + target_id: AnimationTargetId, + curve: impl AnimationCurve, + ) { // Update the duration of the animation by this curve duration if it's longer - self.duration = self - .duration - .max(*curve.keyframe_timestamps.last().unwrap_or(&0.0)); - self.curves.entry(target_id).or_default().push(curve); + let end = curve.domain().end(); + if end.is_finite() { + self.duration = self.duration.max(end); + } + self.curves + .entry(target_id) + .or_default() + .push(VariableCurve::new(curve)); + } + + /// Like [`add_curve_to_target`], but adding a [`VariableCurve`] directly. + /// + /// Under normal circumstances, that method is generally more convenient. + /// + /// [`add_curve_to_target`]: AnimationClip::add_curve_to_target + pub fn add_variable_curve_to_target( + &mut self, + target_id: AnimationTargetId, + variable_curve: VariableCurve, + ) { + let end = variable_curve.0.domain().end(); + if end.is_finite() { + self.duration = self.duration.max(end); + } + self.curves + .entry(target_id) + .or_default() + .push(variable_curve); } } @@ -614,15 +446,6 @@ pub enum RepeatAnimation { /// Why Bevy failed to evaluate an animation. #[derive(Clone, Debug)] pub enum AnimationEvaluationError { - /// The `keyframes` array is too small. - /// - /// For curves with `Interpolation::Step` or `Interpolation::Linear`, the - /// `keyframes` array must have at least as many elements as keyframe - /// timestamps. For curves with `Interpolation::CubicBezier`, the - /// `keyframes` array must have at least 3× the number of elements as - /// keyframe timestamps, in order to account for the tangents. - KeyframeNotPresent(usize), - /// The component to be animated isn't present on the animation target. /// /// To fix this error, make sure the entity to be animated contains all @@ -1188,36 +1011,11 @@ pub fn animate_targets( let seek_time = active_animation.seek_time; for curve in curves { - // Some curves have only one keyframe used to set a transform - if curve.keyframe_timestamps.len() == 1 { - if let Err(err) = curve.keyframes.apply_single_keyframe( - transform.as_mut().map(|transform| transform.reborrow()), - entity_mut.reborrow(), - weight, - ) { - warn!("Animation application failed: {:?}", err); - } - - continue; - } - - // Find the best keyframe to interpolate from - let step_start = curve.find_interpolation_start_keyframe(seek_time); - - let timestamp_start = curve.keyframe_timestamps[step_start]; - let timestamp_end = curve.keyframe_timestamps[step_start + 1]; - // Compute how far we are through the keyframe, normalized to [0, 1] - let lerp = f32::inverse_lerp(timestamp_start, timestamp_end, seek_time) - .clamp(0.0, 1.0); - - if let Err(err) = curve.keyframes.apply_tweened_keyframes( + if let Err(err) = curve.0.apply( + seek_time, transform.as_mut().map(|transform| transform.reborrow()), entity_mut.reborrow(), - curve.interpolation, - step_start, - lerp, weight, - timestamp_end - timestamp_start, ) { warn!("Animation application failed: {:?}", err); } @@ -1311,153 +1109,3 @@ impl AnimationGraphEvaluator { .extend(iter::repeat(EvaluatedAnimationGraphNode::default()).take(node_count)); } } - -#[cfg(test)] -mod tests { - use crate::{prelude::TranslationKeyframes, VariableCurve}; - use bevy_math::Vec3; - - // Returns the curve and the keyframe count. - fn test_variable_curve() -> (VariableCurve, usize) { - let keyframe_timestamps = vec![1.0, 2.0, 3.0, 4.0]; - let keyframes = vec![ - Vec3::ONE * 0.0, - Vec3::ONE * 3.0, - Vec3::ONE * 6.0, - Vec3::ONE * 9.0, - ]; - let interpolation = crate::Interpolation::Linear; - - assert_eq!(keyframe_timestamps.len(), keyframes.len()); - let keyframe_count = keyframes.len(); - - let variable_curve = VariableCurve::new::( - keyframe_timestamps, - keyframes, - interpolation, - ); - - // f32 doesn't impl Ord so we can't easily sort it - let mut maybe_last_timestamp = None; - for current_timestamp in &variable_curve.keyframe_timestamps { - assert!(current_timestamp.is_finite()); - - if let Some(last_timestamp) = maybe_last_timestamp { - assert!(current_timestamp > last_timestamp); - } - maybe_last_timestamp = Some(current_timestamp); - } - - (variable_curve, keyframe_count) - } - - #[test] - fn find_current_keyframe_is_in_bounds() { - let curve = test_variable_curve().0; - let min_time = *curve.keyframe_timestamps.first().unwrap(); - // We will always get none at times at or past the second last keyframe - let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - let max_time = curve.keyframe_timestamps[second_last_keyframe]; - let elapsed_time = max_time - min_time; - - let n_keyframes = curve.keyframe_timestamps.len(); - let n_test_points = 5; - - for i in 0..=n_test_points { - // Get a value between 0 and 1 - let normalized_time = i as f32 / n_test_points as f32; - let seek_time = min_time + normalized_time * elapsed_time; - assert!(seek_time >= min_time); - assert!(seek_time <= max_time); - - let maybe_current_keyframe = curve.find_current_keyframe(seek_time); - assert!( - maybe_current_keyframe.is_some(), - "Seek time: {seek_time}, Min time: {min_time}, Max time: {max_time}" - ); - - // We cannot return the last keyframe, - // because we want to interpolate between the current and next keyframe - assert!(maybe_current_keyframe.unwrap() < n_keyframes); - } - } - - #[test] - fn find_current_keyframe_returns_none_on_unstarted_animations() { - let curve = test_variable_curve().0; - let min_time = *curve.keyframe_timestamps.first().unwrap(); - let seek_time = 0.0; - assert!(seek_time < min_time); - - let maybe_keyframe = curve.find_current_keyframe(seek_time); - assert!( - maybe_keyframe.is_none(), - "Seek time: {seek_time}, Minimum time: {min_time}" - ); - } - - #[test] - fn find_current_keyframe_returns_none_on_finished_animation() { - let curve = test_variable_curve().0; - let max_time = *curve.keyframe_timestamps.last().unwrap(); - - assert!(max_time < f32::INFINITY); - let maybe_keyframe = curve.find_current_keyframe(f32::INFINITY); - assert!(maybe_keyframe.is_none()); - - let maybe_keyframe = curve.find_current_keyframe(max_time); - assert!(maybe_keyframe.is_none()); - } - - #[test] - fn second_last_keyframe_is_found_correctly() { - let curve = test_variable_curve().0; - - // Exact time match - let second_last_keyframe = curve.keyframe_timestamps.len() - 2; - let second_last_time = curve.keyframe_timestamps[second_last_keyframe]; - let maybe_keyframe = curve.find_current_keyframe(second_last_time); - assert!(maybe_keyframe.unwrap() == second_last_keyframe); - - // Inexact match, between the last and second last frames - let seek_time = second_last_time + 0.001; - let last_time = curve.keyframe_timestamps[second_last_keyframe + 1]; - assert!(seek_time < last_time); - - let maybe_keyframe = curve.find_current_keyframe(seek_time); - assert!(maybe_keyframe.unwrap() == second_last_keyframe); - } - - #[test] - fn exact_keyframe_matches_are_found_correctly() { - let (curve, keyframe_count) = test_variable_curve(); - let second_last_keyframe = keyframe_count - 2; - - for i in 0..=second_last_keyframe { - let seek_time = curve.keyframe_timestamps[i]; - - let keyframe = curve.find_current_keyframe(seek_time).unwrap(); - assert!(keyframe == i); - } - } - - #[test] - fn exact_and_inexact_keyframes_correspond() { - let (curve, keyframe_count) = test_variable_curve(); - let second_last_keyframe = keyframe_count - 2; - - for i in 0..=second_last_keyframe { - let seek_time = curve.keyframe_timestamps[i]; - - let exact_keyframe = curve.find_current_keyframe(seek_time).unwrap(); - - let inexact_seek_time = seek_time + 0.0001; - let final_time = *curve.keyframe_timestamps.last().unwrap(); - assert!(inexact_seek_time < final_time); - - let inexact_keyframe = curve.find_current_keyframe(inexact_seek_time).unwrap(); - - assert!(exact_keyframe == inexact_keyframe); - } - } -} diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index e9ca9562da596..1d7718eb45d35 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -3,11 +3,11 @@ use crate::{ GltfMeshExtras, GltfNode, GltfSceneExtras, GltfSkin, }; -use bevy_animation::prelude::{ - Keyframes, MorphWeightsKeyframes, RotationKeyframes, ScaleKeyframes, TranslationKeyframes, -}; #[cfg(feature = "bevy_animation")] -use bevy_animation::{AnimationTarget, AnimationTargetId}; +use bevy_animation::{ + animation_curves::{RotationCurve, ScaleCurve, TranslationCurve, WeightsCurve}, + AnimationTarget, AnimationTargetId, VariableCurve, +}; use bevy_asset::{ io::Reader, AssetLoadError, AssetLoader, Handle, LoadContext, ReadAssetBytesError, }; @@ -270,7 +270,9 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { - use bevy_animation::Interpolation; + use bevy_animation::gltf_curves::*; + use bevy_math::curve::{constant_curve, Interval, UnevenSampleAutoCurve}; + use bevy_math::{Quat, Vec4}; use gltf::animation::util::ReadOutputs; let mut animations = vec![]; let mut named_animations = HashMap::default(); @@ -278,12 +280,8 @@ async fn load_gltf<'a, 'b, 'c>( for animation in gltf.animations() { let mut animation_clip = bevy_animation::AnimationClip::default(); for channel in animation.channels() { - let interpolation = match channel.sampler().interpolation() { - gltf::animation::Interpolation::Linear => Interpolation::Linear, - gltf::animation::Interpolation::Step => Interpolation::Step, - gltf::animation::Interpolation::CubicSpline => Interpolation::CubicSpline, - }; let node = channel.target().node(); + let interpolation = channel.sampler().interpolation(); let reader = channel.reader(|buffer| Some(&buffer_data[buffer.index()])); let keyframe_timestamps: Vec = if let Some(inputs) = reader.read_inputs() { match inputs { @@ -298,26 +296,150 @@ async fn load_gltf<'a, 'b, 'c>( return Err(GltfError::MissingAnimationSampler(animation.index())); }; - let keyframes = if let Some(outputs) = reader.read_outputs() { + if keyframe_timestamps.is_empty() { + warn!("Tried to load animation with no keyframe timestamps"); + continue; + } + + let maybe_curve: Option = if let Some(outputs) = + reader.read_outputs() + { match outputs { ReadOutputs::Translations(tr) => { - Box::new(TranslationKeyframes(tr.map(Vec3::from).collect())) - as Box + let translations: Vec = tr.map(Vec3::from).collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, translations[0])) + .map(TranslationCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(translations), + ) + .ok() + .map(TranslationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(translations), + ) + .ok() + .map(TranslationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + CubicKeyframeCurve::new(keyframe_timestamps, translations) + .ok() + .map(TranslationCurve) + .map(VariableCurve::new) + } + } + } + } + ReadOutputs::Rotations(rots) => { + let rotations: Vec = + rots.into_f32().map(Quat::from_array).collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, rotations[0])) + .map(RotationCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(rotations), + ) + .ok() + .map(RotationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(rotations), + ) + .ok() + .map(RotationCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + CubicRotationCurve::new( + keyframe_timestamps, + rotations.into_iter().map(Vec4::from), + ) + .ok() + .map(RotationCurve) + .map(VariableCurve::new) + } + } + } } - ReadOutputs::Rotations(rots) => Box::new(RotationKeyframes( - rots.into_f32().map(bevy_math::Quat::from_array).collect(), - )) - as Box, ReadOutputs::Scales(scale) => { - Box::new(ScaleKeyframes(scale.map(Vec3::from).collect())) - as Box + let scales: Vec = scale.map(Vec3::from).collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, scales[0])) + .map(ScaleCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + UnevenSampleAutoCurve::new( + keyframe_timestamps.into_iter().zip(scales), + ) + .ok() + .map(ScaleCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + SteppedKeyframeCurve::new( + keyframe_timestamps.into_iter().zip(scales), + ) + .ok() + .map(ScaleCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + CubicKeyframeCurve::new(keyframe_timestamps, scales) + .ok() + .map(ScaleCurve) + .map(VariableCurve::new) + } + } + } } ReadOutputs::MorphTargetWeights(weights) => { - let weights: Vec<_> = weights.into_f32().collect(); - Box::new(MorphWeightsKeyframes { - morph_target_count: weights.len() / keyframe_timestamps.len(), - weights, - }) as Box + let weights: Vec = weights.into_f32().collect(); + if keyframe_timestamps.len() == 1 { + #[allow(clippy::unnecessary_map_on_constructor)] + Some(constant_curve(Interval::EVERYWHERE, weights)) + .map(WeightsCurve) + .map(VariableCurve::new) + } else { + match interpolation { + gltf::animation::Interpolation::Linear => { + WideLinearKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(WeightsCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::Step => { + WideSteppedKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(WeightsCurve) + .map(VariableCurve::new) + } + gltf::animation::Interpolation::CubicSpline => { + WideCubicKeyframeCurve::new(keyframe_timestamps, weights) + .ok() + .map(WeightsCurve) + .map(VariableCurve::new) + } + } + } } } } else { @@ -325,15 +447,19 @@ async fn load_gltf<'a, 'b, 'c>( return Err(GltfError::MissingAnimationSampler(animation.index())); }; + let Some(curve) = maybe_curve else { + warn!( + "Invalid keyframe data for node {}; curve could not be constructed", + node.index() + ); + continue; + }; + if let Some((root_index, path)) = paths.get(&node.index()) { animation_roots.insert(*root_index); - animation_clip.add_curve_to_target( + animation_clip.add_variable_curve_to_target( AnimationTargetId::from_names(path.iter()), - bevy_animation::VariableCurve { - keyframe_timestamps, - keyframes, - interpolation, - }, + curve, ); } else { warn!( diff --git a/crates/bevy_math/src/curve/cores.rs b/crates/bevy_math/src/curve/cores.rs index 92ad2fdc71a3c..6e637c4ba78a0 100644 --- a/crates/bevy_math/src/curve/cores.rs +++ b/crates/bevy_math/src/curve/cores.rs @@ -496,6 +496,15 @@ pub enum ChunkedUnevenCoreError { /// The actual length of the value buffer. actual: usize, }, + + /// Tried to infer the width, but the ratio of lengths wasn't an integer, so no such length exists. + #[error("The length of the list of values ({values_len}) was not divisible by that of the list of times ({times_len})")] + NonDivisibleLengths { + /// The length of the value buffer. + values_len: usize, + /// The length of the time buffer. + times_len: usize, + }, } impl ChunkedUnevenCore { @@ -504,17 +513,17 @@ impl ChunkedUnevenCore { /// /// Produces an error in any of the following circumstances: /// - `width` is zero. - /// - `times` has less than `2` valid unique entries. + /// - `times` has less than `2` unique valid entries. /// - `values` has the incorrect length relative to `times`. /// /// [type-level documentation]: ChunkedUnevenCore pub fn new( - times: impl Into>, - values: impl Into>, + times: impl IntoIterator, + values: impl IntoIterator, width: usize, ) -> Result { - let times: Vec = times.into(); - let values: Vec = values.into(); + let times = times.into_iter().collect_vec(); + let values = values.into_iter().collect_vec(); if width == 0 { return Err(ChunkedUnevenCoreError::ZeroWidth); @@ -538,6 +547,52 @@ impl ChunkedUnevenCore { Ok(Self { times, values }) } + /// Create a new [`ChunkedUnevenCore`], inferring the width from the sizes of the inputs. + /// The given `times` are sorted, filtered to finite times, and deduplicated. See the + /// [type-level documentation] for more information about this type. Prefer using [`new`] + /// if possible, since that constructor has richer error checking. + /// + /// Produces an error in any of the following circumstances: + /// - `values` has length zero. + /// - `times` has less than `2` unique valid entries. + /// - The length of `values` is not divisible by that of `times` (once sorted, filtered, + /// and deduplicated). + /// + /// The [width] is implicitly taken to be the length of `values` divided by that of `times` + /// (once sorted, filtered, and deduplicated). + /// + /// [type-level documentation]: ChunkedUnevenCore + /// [`new`]: ChunkedUnevenCore::new + /// [width]: ChunkedUnevenCore::width + pub fn new_width_inferred( + times: impl IntoIterator, + values: impl IntoIterator, + ) -> Result { + let times = times.into_iter().collect_vec(); + let values = values.into_iter().collect_vec(); + + let times = filter_sort_dedup_times(times); + + if times.len() < 2 { + return Err(ChunkedUnevenCoreError::NotEnoughSamples { + samples: times.len(), + }); + } + + if values.len() % times.len() != 0 { + return Err(ChunkedUnevenCoreError::NonDivisibleLengths { + values_len: values.len(), + times_len: times.len(), + }); + } + + if values.is_empty() { + return Err(ChunkedUnevenCoreError::ZeroWidth); + } + + Ok(Self { times, values }) + } + /// The domain of the curve derived from this core. /// /// # Panics @@ -626,3 +681,134 @@ pub fn uneven_interp(times: &[f32], t: f32) -> InterpolationDatum { } } } + +#[cfg(test)] +mod tests { + use super::{ChunkedUnevenCore, EvenCore, UnevenCore}; + use crate::curve::{cores::InterpolationDatum, interval}; + use approx::{assert_abs_diff_eq, AbsDiffEq}; + + fn approx_between(datum: InterpolationDatum, start: T, end: T, p: f32) -> bool + where + T: PartialEq, + { + if let InterpolationDatum::Between(m_start, m_end, m_p) = datum { + m_start == start && m_end == end && m_p.abs_diff_eq(&p, 1e-6) + } else { + false + } + } + + fn is_left_tail(datum: InterpolationDatum) -> bool { + matches!(datum, InterpolationDatum::LeftTail(_)) + } + + fn is_right_tail(datum: InterpolationDatum) -> bool { + matches!(datum, InterpolationDatum::RightTail(_)) + } + + fn is_exact(datum: InterpolationDatum, target: T) -> bool + where + T: PartialEq, + { + if let InterpolationDatum::Exact(v) = datum { + v == target + } else { + false + } + } + + #[test] + fn even_sample_interp() { + let even_core = EvenCore::::new( + interval(0.0, 1.0).unwrap(), + // 11 entries -> 10 segments + vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0], + ) + .expect("Failed to construct test core"); + + let datum = even_core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = even_core.sample_interp(0.0); + assert!(is_left_tail(datum)); + let datum = even_core.sample_interp(1.0); + assert!(is_right_tail(datum)); + let datum = even_core.sample_interp(2.0); + assert!(is_right_tail(datum)); + + let datum = even_core.sample_interp(0.05); + let InterpolationDatum::Between(0.0, 1.0, p) = datum else { + panic!("Sample did not lie in the correct subinterval") + }; + assert_abs_diff_eq!(p, 0.5); + + let datum = even_core.sample_interp(0.05); + assert!(approx_between(datum, &0.0, &1.0, 0.5)); + let datum = even_core.sample_interp(0.33); + assert!(approx_between(datum, &3.0, &4.0, 0.3)); + let datum = even_core.sample_interp(0.78); + assert!(approx_between(datum, &7.0, &8.0, 0.8)); + + let datum = even_core.sample_interp(0.5); + assert!(approx_between(datum, &4.0, &5.0, 1.0) || approx_between(datum, &5.0, &6.0, 0.0)); + let datum = even_core.sample_interp(0.7); + assert!(approx_between(datum, &6.0, &7.0, 1.0) || approx_between(datum, &7.0, &8.0, 0.0)); + } + + #[test] + fn uneven_sample_interp() { + let uneven_core = UnevenCore::::new(vec![ + (0.0, 0.0), + (1.0, 3.0), + (2.0, 9.0), + (4.0, 10.0), + (8.0, -5.0), + ]) + .expect("Failed to construct test core"); + + let datum = uneven_core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = uneven_core.sample_interp(0.0); + assert!(is_exact(datum, &0.0)); + let datum = uneven_core.sample_interp(8.0); + assert!(is_exact(datum, &(-5.0))); + let datum = uneven_core.sample_interp(9.0); + assert!(is_right_tail(datum)); + + let datum = uneven_core.sample_interp(0.5); + assert!(approx_between(datum, &0.0, &3.0, 0.5)); + let datum = uneven_core.sample_interp(2.5); + assert!(approx_between(datum, &9.0, &10.0, 0.25)); + let datum = uneven_core.sample_interp(7.0); + assert!(approx_between(datum, &10.0, &(-5.0), 0.75)); + + let datum = uneven_core.sample_interp(2.0); + assert!(is_exact(datum, &9.0)); + let datum = uneven_core.sample_interp(4.0); + assert!(is_exact(datum, &10.0)); + } + + #[test] + fn chunked_uneven_sample_interp() { + let core = + ChunkedUnevenCore::new(vec![0.0, 2.0, 8.0], vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0], 2) + .expect("Failed to construct test core"); + + let datum = core.sample_interp(-1.0); + assert!(is_left_tail(datum)); + let datum = core.sample_interp(0.0); + assert!(is_exact(datum, &[0.0, 1.0])); + let datum = core.sample_interp(8.0); + assert!(is_exact(datum, &[4.0, 5.0])); + let datum = core.sample_interp(10.0); + assert!(is_right_tail(datum)); + + let datum = core.sample_interp(1.0); + assert!(approx_between(datum, &[0.0, 1.0], &[2.0, 3.0], 0.5)); + let datum = core.sample_interp(3.0); + assert!(approx_between(datum, &[2.0, 3.0], &[4.0, 5.0], 1.0 / 6.0)); + + let datum = core.sample_interp(2.0); + assert!(is_exact(datum, &[2.0, 3.0])); + } +} diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs new file mode 100644 index 0000000000000..7e0ccd1a98b85 --- /dev/null +++ b/crates/bevy_math/src/curve/iterable.rs @@ -0,0 +1,55 @@ +//! Iterable curves, which sample in the form of an iterator in order to support `Vec`-like +//! output whose length cannot be known statically. + +use super::{ConstantCurve, Interval}; + +/// A curve which provides samples in the form of [`Iterator`]s. +/// +/// This is an abstraction that provides an interface for curves which look like `Curve>` +/// but side-stepping issues with allocation on sampling. This happens when the size of an output +/// array cannot be known statically. +pub trait IterableCurve { + /// The interval over which this curve is parametrized. + fn domain(&self) -> Interval; + + /// Sample a point on this curve at the parameter value `t`, producing an iterator over values. + /// This is the unchecked version of sampling, which should only be used if the sample time `t` + /// is already known to lie within the curve's domain. + /// + /// Values sampled from outside of a curve's domain are generally considered invalid; data which + /// is nonsensical or otherwise useless may be returned in such a circumstance, and extrapolation + /// beyond a curve's domain should not be relied upon. + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator; + // where + // Self: 'a; + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + /// The parameter `t` is clamped to the domain of the curve. + fn sample_iter_clamped(&self, t: f32) -> impl Iterator { + let t_clamped = self.domain().clamp(t); + self.sample_iter_unchecked(t_clamped) + } + + /// Sample this curve at a specified time `t`, producing an iterator over sampled values. + /// If the parameter `t` does not lie in the curve's domain, `None` is returned. + fn sample_iter(&self, t: f32) -> Option> { + if self.domain().contains(t) { + Some(self.sample_iter_unchecked(t)) + } else { + None + } + } +} + +impl IterableCurve for ConstantCurve> +where + T: Clone, +{ + fn domain(&self) -> Interval { + self.domain + } + + fn sample_iter_unchecked(&self, _t: f32) -> impl Iterator { + self.value.iter().cloned() + } +} diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 3dfb9284ade54..35da4402b52c3 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,6 +4,7 @@ pub mod cores; pub mod interval; +pub mod iterable; pub use interval::{interval, Interval}; use itertools::Itertools; diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index d5eb9780c9e4d..e2857006f6431 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -49,79 +49,79 @@ fn setup( // Creating the animation let mut animation = AnimationClip::default(); - // A curve can modify a single part of a transform, here the translation + // A curve can modify a single part of a transform: here, the translation. let planet_animation_target_id = AnimationTargetId::from_name(&planet); animation.add_curve_to_target( planet_animation_target_id, - VariableCurve::linear::( - [0.0, 1.0, 2.0, 3.0, 4.0], - [ - Vec3::new(1.0, 0.0, 1.0), - Vec3::new(-1.0, 0.0, 1.0), - Vec3::new(-1.0, 0.0, -1.0), - Vec3::new(1.0, 0.0, -1.0), - // in case seamless looping is wanted, the last keyframe should - // be the same as the first one - Vec3::new(1.0, 0.0, 1.0), - ], - ), + UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + Vec3::new(1.0, 0.0, 1.0), + Vec3::new(-1.0, 0.0, 1.0), + Vec3::new(-1.0, 0.0, -1.0), + Vec3::new(1.0, 0.0, -1.0), + // in case seamless looping is wanted, the last keyframe should + // be the same as the first one + Vec3::new(1.0, 0.0, 1.0), + ])) + .map(TranslationCurve) + .expect("Failed to build translation curve"), ); // Or it can modify the rotation of the transform. // To find the entity to modify, the hierarchy will be traversed looking for - // an entity with the right name at each level + // an entity with the right name at each level. let orbit_controller_animation_target_id = AnimationTargetId::from_names([planet.clone(), orbit_controller.clone()].iter()); animation.add_curve_to_target( orbit_controller_animation_target_id, - VariableCurve::linear::( - [0.0, 1.0, 2.0, 3.0, 4.0], - [ - Quat::IDENTITY, - Quat::from_axis_angle(Vec3::Y, PI / 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), - Quat::IDENTITY, - ], - ), + UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + Quat::IDENTITY, + Quat::from_axis_angle(Vec3::Y, PI / 2.), + Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), + Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), + Quat::IDENTITY, + ])) + .map(RotationCurve) + .expect("Failed to build rotation curve"), ); // If a curve in an animation is shorter than the other, it will not repeat // until all other curves are finished. In that case, another animation should - // be created for each part that would have a different duration / period + // be created for each part that would have a different duration / period. let satellite_animation_target_id = AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ); animation.add_curve_to_target( satellite_animation_target_id, - VariableCurve::linear::( - [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0], - [ - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - Vec3::splat(1.2), - Vec3::splat(0.8), - ], - ), + UnevenSampleAutoCurve::new( + [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5, 4.0] + .into_iter() + .zip([ + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + Vec3::splat(1.2), + Vec3::splat(0.8), + ]), + ) + .map(ScaleCurve) + .expect("Failed to build scale curve"), ); - // There can be more than one curve targeting the same entity path + // There can be more than one curve targeting the same entity path. animation.add_curve_to_target( AnimationTargetId::from_names( [planet.clone(), orbit_controller.clone(), satellite.clone()].iter(), ), - VariableCurve::linear::( - [0.0, 1.0, 2.0, 3.0, 4.0], - [ - Quat::IDENTITY, - Quat::from_axis_angle(Vec3::Y, PI / 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), - Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), - Quat::IDENTITY, - ], - ), + UnevenSampleAutoCurve::new([0.0, 1.0, 2.0, 3.0, 4.0].into_iter().zip([ + Quat::IDENTITY, + Quat::from_axis_angle(Vec3::Y, PI / 2.), + Quat::from_axis_angle(Vec3::Y, PI / 2. * 2.), + Quat::from_axis_angle(Vec3::Y, PI / 2. * 3.), + Quat::IDENTITY, + ])) + .map(RotationCurve) + .expect("Failed to build rotation curve"), ); // Create the animation graph diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index ad55bac900358..fd9c356cb6e4b 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -77,24 +77,34 @@ impl AnimationInfo { // Create a curve that animates font size. // - // `VariableCurve::linear` is just a convenience constructor; it's also - // possible to initialize the structure manually. + // The curve itself is a `Curve`, and `f32` is `FontSizeProperty::Property`, + // which is required by `AnimatableCurve::from_curve`. animation_clip.add_curve_to_target( animation_target_id, - VariableCurve::linear::>( - [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0], - [24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0], - ), + AnimatableKeyframeCurve::new( + [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0] + .into_iter() + .zip([24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0]), + ) + .map(AnimatableCurve::::from_curve) + .expect("Failed to build font size curve"), ); // Create a curve that animates font color. Note that this should have // the same time duration as the previous curve. + // + // Similar to the above, the curve itself is a `Curve`, and `Srgba` is + // `TextColorProperty::Property`, which is required by the `from_curve` method. animation_clip.add_curve_to_target( animation_target_id, - VariableCurve::linear::>( - [0.0, 1.0, 2.0, 3.0], - [Srgba::RED, Srgba::GREEN, Srgba::BLUE, Srgba::RED], - ), + AnimatableKeyframeCurve::new([0.0, 1.0, 2.0, 3.0].into_iter().zip([ + Srgba::RED, + Srgba::GREEN, + Srgba::BLUE, + Srgba::RED, + ])) + .map(AnimatableCurve::::from_curve) + .expect("Failed to build text color curve"), ); // Save our animation clip as an asset. From 423e4b008a4a4504334f17ac38554ad0f48854de Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Wed, 25 Sep 2024 17:10:05 -0400 Subject: [PATCH 02/22] Move some imports around in glTF loader --- crates/bevy_gltf/src/loader.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 1d7718eb45d35..73e92741657fa 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -4,10 +4,7 @@ use crate::{ }; #[cfg(feature = "bevy_animation")] -use bevy_animation::{ - animation_curves::{RotationCurve, ScaleCurve, TranslationCurve, WeightsCurve}, - AnimationTarget, AnimationTargetId, VariableCurve, -}; +use bevy_animation::{AnimationTarget, AnimationTargetId}; use bevy_asset::{ io::Reader, AssetLoadError, AssetLoader, Handle, LoadContext, ReadAssetBytesError, }; @@ -270,7 +267,7 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { - use bevy_animation::gltf_curves::*; + use bevy_animation::{animation_curves::*, gltf_curves::*, VariableCurve}; use bevy_math::curve::{constant_curve, Interval, UnevenSampleAutoCurve}; use bevy_math::{Quat, Vec4}; use gltf::animation::util::ReadOutputs; From 39f7f0a0211850f502d69bca8ed6f7fb4d1023df Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Thu, 26 Sep 2024 17:02:51 -0400 Subject: [PATCH 03/22] Drastically improve reflection trait implementations of curve adaptors --- crates/bevy_animation/src/animation_curves.rs | 34 +- crates/bevy_math/src/curve/adaptors.rs | 522 ++++++++++++++++++ crates/bevy_math/src/curve/iterable.rs | 2 - crates/bevy_math/src/curve/mod.rs | 296 +--------- 4 files changed, 537 insertions(+), 317 deletions(-) create mode 100644 crates/bevy_math/src/curve/adaptors.rs diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 8c4fcaade1f99..254f61ab23025 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -11,7 +11,7 @@ //! # use bevy_math::vec3; //! let wobble_curve = function_curve( //! Interval::UNIT, -//! |t| vec3(t.cos(), 0.0, 0.0) +//! |t| { vec3(t.cos(), 0.0, 0.0) }, //! ); //! //! Okay, so we have a curve, but the animation system also needs to know, in some way, @@ -27,6 +27,7 @@ //! //! # use bevy_math::curve::{Curve, Interval, function_curve}; //! # use bevy_math::vec3; +//! # use bevy_animation::animation_curves::*; //! # let wobble_curve = function_curve( //! # Interval::UNIT, //! # |t| vec3(t.cos(), 0.0, 0.0) @@ -37,12 +38,12 @@ //! actually animate something. This is what that looks like: //! //! # use bevy_math::curve::{Curve, Interval, function_curve}; -//! # use bevy_animation::{AnimationClip, AnimationTargetId}; +//! # use bevy_animation::{AnimationClip, AnimationTargetId, animation_curves::*}; //! # use bevy_core::Name; //! # use bevy_math::vec3; //! # let wobble_curve = function_curve( //! # Interval::UNIT, -//! # |t| vec3(t.cos(), 0.0, 0.0) +//! # |t| { vec3(t.cos(), 0.0, 0.0) }, //! # ); //! # let wobble_animation = TranslationCurve(wobble_curve); //! # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); @@ -95,7 +96,7 @@ use bevy_math::{ }, FloatExt, Quat, Vec3, }; -use bevy_reflect::{FromReflect, GetTypeRegistration, Reflect, Reflectable, TypePath, Typed}; +use bevy_reflect::{FromReflect, Reflect, Reflectable, TypePath}; use bevy_render::mesh::morph::MorphWeights; use bevy_transform::prelude::Transform; @@ -167,15 +168,7 @@ pub trait AnimatableProperty: Reflect + TypePath { type Component: Component; /// The type of the property to be animated. - type Property: Animatable - + FromReflect - + GetTypeRegistration - + Reflect - + TypePath - + Typed - + Clone - + Sync - + Debug; + type Property: Animatable + FromReflect + Reflectable + Clone + Sync + Debug; /// Given a reference to the component, returns a reference to the property. /// @@ -187,15 +180,14 @@ pub trait AnimatableProperty: Reflect + TypePath { /// curve to be used as an [`AnimationCurve`]. pub trait InnerAnimationCurve: Curve + Debug + Clone + Reflectable {} -impl InnerAnimationCurve for C where C: Curve + Debug + Clone + Reflectable + FromReflect -{} +impl InnerAnimationCurve for C where C: Curve + Debug + Clone + Reflectable {} /// This type allows the conversion of a [curve] valued in the [property type] of an /// [`AnimatableProperty`] into an [`AnimationCurve`] which animates that property. /// /// [curve]: Curve /// [property type]: AnimatableProperty::Property -#[derive(Reflect)] +#[derive(Reflect, FromReflect)] #[reflect(from_reflect = false)] pub struct AnimatableCurve { curve: C, @@ -278,7 +270,7 @@ where /// the translation component of a transform. /// /// [curve]: Curve -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, FromReflect)] #[reflect(from_reflect = false)] pub struct TranslationCurve(pub C); @@ -315,7 +307,7 @@ where /// the rotation component of a transform. /// /// [curve]: Curve -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, FromReflect)] #[reflect(from_reflect = false)] pub struct RotationCurve(pub C); @@ -352,7 +344,7 @@ where /// the scale component of a transform. /// /// [curve]: Curve -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, FromReflect)] #[reflect(from_reflect = false)] pub struct ScaleCurve(pub C); @@ -391,7 +383,7 @@ where /// instead of splitting it into pieces and animating each part (translation, rotation, scale). /// /// [curve]: Curve -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, FromReflect)] #[reflect(from_reflect = false)] pub struct TransformCurve(pub C); @@ -427,7 +419,7 @@ where /// that animates [morph weights]. /// /// [morph weights]: MorphWeights -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, FromReflect)] #[reflect(from_reflect = false)] pub struct WeightsCurve(pub C); diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs new file mode 100644 index 0000000000000..5315687a414bd --- /dev/null +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -0,0 +1,522 @@ +//! Adaptors used by the Curve API for transforming and combining curves together. + +use super::interval::*; +use super::Curve; + +use std::any::type_name; +use std::fmt::{self, Debug}; +use std::marker::PhantomData; + +use bevy_reflect::utility::GenericTypePathCell; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{Reflect, TypePath}; + +const THIS_MODULE: &str = "bevy_math::curve::adaptors"; +const THIS_CRATE: &str = "bevy_math"; + +// NOTE ON REFLECTION: +// +// Function members of structs pose an obstacle for reflection, because they don't implement +// reflection traits themselves. Some of these are more problematic than others; for example, +// `FromReflect` is basically hopeless for function members regardless, so function-containing +// adaptors will just never be `FromReflect` (at least until function item types implement +// Default, if that ever happens). Similarly, they do not implement `TypePath`, and as a result, +// those adaptors also need custom `TypePath` adaptors which use `type_name` instead. +// +// The sum total weirdness of the `Reflect` implementations amounts to this; those adaptors: +// - are currently never `FromReflect`; +// - have custom `TypePath` implementations which are not fully stable; +// - have custom `Debug` implementations which display the function only by type name. + +/// A curve with a constant value over its domain. +/// +/// This is a curve that holds an inner value and always produces a clone of that value when sampled. +#[derive(Clone, Copy, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ConstantCurve { + pub(crate) domain: Interval, + pub(crate) value: T, +} + +impl ConstantCurve +where + T: Clone, +{ + /// Create a constant curve, which has the given `domain` and always produces the given `value` + /// when sampled. + pub fn new(domain: Interval, value: T) -> Self { + Self { domain, value } + } +} + +impl Curve for ConstantCurve +where + T: Clone, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, _t: f32) -> T { + self.value.clone() + } +} + +/// A curve defined by a function together with a fixed domain. +/// +/// This is a curve that holds an inner function `f` which takes numbers (`f32`) as input and produces +/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where T: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct FunctionCurve { + pub(crate) domain: Interval, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) f: F, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Debug for FunctionCurve { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("FunctionCurve") + .field("domain", &self.domain) + .field("f", &type_name::()) + .field("_phantom", &self._phantom) + .finish() + } +} + +#[cfg(feature = "bevy_reflect")] +impl TypePath for FunctionCurve +where + T: TypePath, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::FunctionCurve<{},{}>", + THIS_MODULE, + T::type_path(), + type_name::() + ) + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "FunctionCurve<{},{}>", + T::short_type_path(), + type_name::() + ) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("FunctionCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(THIS_MODULE) + } +} + +impl FunctionCurve +where + F: Fn(f32) -> T, +{ + /// Create a new curve with the given `domain` from the given `function`. When sampled, the + /// `function` is evaluated at the sample time to compute the output. + pub fn new(domain: Interval, function: F) -> Self { + FunctionCurve { + domain, + f: function, + _phantom: PhantomData, + } + } +} + +impl Curve for FunctionCurve +where + F: Fn(f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + (self.f)(t) + } +} + +/// A curve whose samples are defined by mapping samples from another curve through a +/// given function. Curves of this type are produced by [`Curve::map`]. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where S: TypePath, T: TypePath, C: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct MapCurve { + pub(crate) preimage: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) f: F, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData<(S, T)>, +} + +impl Debug for MapCurve +where + C: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("MapCurve") + .field("preimage", &self.preimage) + .field("f", &type_name::()) + .field("_phantom", &self._phantom) + .finish() + } +} + +#[cfg(feature = "bevy_reflect")] +impl TypePath for MapCurve +where + S: TypePath, + T: TypePath, + C: TypePath, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::MapCurve<{},{},{},{}>", + THIS_MODULE, + S::type_path(), + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "MapCurve<{},{},{},{}>", + S::type_path(), + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("MapCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(THIS_MODULE) + } +} + +impl Curve for MapCurve +where + C: Curve, + F: Fn(S) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.preimage.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + (self.f)(self.preimage.sample_unchecked(t)) + } +} + +/// A curve whose sample space is mapped onto that of some base curve's before sampling. +/// Curves of this type are produced by [`Curve::reparametrize`]. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where T: TypePath, C: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct ReparamCurve { + pub(crate) domain: Interval, + pub(crate) base: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) f: F, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Debug for ReparamCurve +where + C: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("ReparamCurve") + .field("domain", &self.domain) + .field("base", &self.base) + .field("f", &type_name::()) + .field("_phantom", &self._phantom) + .finish() + } +} + +#[cfg(feature = "bevy_reflect")] +impl TypePath for ReparamCurve +where + T: TypePath, + C: TypePath, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::ReparamCurve<{},{},{}>", + THIS_MODULE, + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "ReparamCurve<{},{},{}>", + T::type_path(), + C::type_path(), + type_name::() + ) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("ReparamCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(THIS_MODULE) + } +} + +impl Curve for ReparamCurve +where + C: Curve, + F: Fn(f32) -> f32, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.base.sample_unchecked((self.f)(t)) + } +} + +/// A curve that has had its domain changed by a linear reparametrization (stretching and scaling). +/// Curves of this type are produced by [`Curve::reparametrize_linear`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(from_reflect = false) +)] +pub struct LinearReparamCurve { + /// Invariants: The domain of this curve must always be bounded. + pub(crate) base: C, + /// Invariants: This interval must always be bounded. + pub(crate) new_domain: Interval, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Curve for LinearReparamCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.new_domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + // The invariants imply this unwrap always succeeds. + let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); + self.base.sample_unchecked(f(t)) + } +} + +/// A curve that has been reparametrized by another curve, using that curve to transform the +/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct CurveReparamCurve { + pub(crate) base: C, + pub(crate) reparam_curve: D, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Curve for CurveReparamCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.reparam_curve.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + let sample_time = self.reparam_curve.sample_unchecked(t); + self.base.sample_unchecked(sample_time) + } +} + +/// A curve that is the graph of another curve over its parameter space. Curves of this type are +/// produced by [`Curve::graph`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct GraphCurve { + pub(crate) base: C, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Curve<(f32, T)> for GraphCurve +where + C: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.base.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> (f32, T) { + (t, self.base.sample_unchecked(t)) + } +} + +/// A curve that combines the output data from two constituent curves into a tuple output. Curves +/// of this type are produced by [`Curve::zip`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ProductCurve { + pub(crate) domain: Interval, + pub(crate) first: C, + pub(crate) second: D, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData<(S, T)>, +} + +impl Curve<(S, T)> for ProductCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + self.domain + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> (S, T) { + ( + self.first.sample_unchecked(t), + self.second.sample_unchecked(t), + ) + } +} + +/// The curve that results from chaining one curve with another. The second curve is +/// effectively reparametrized so that its start is at the end of the first. +/// +/// For this to be well-formed, the first curve's domain must be right-finite and the second's +/// must be left-finite. +/// +/// Curves of this type are produced by [`Curve::chain`]. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct ChainCurve { + pub(crate) first: C, + pub(crate) second: D, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) _phantom: PhantomData, +} + +impl Curve for ChainCurve +where + C: Curve, + D: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + // This unwrap always succeeds because `first` has a valid Interval as its domain and the + // length of `second` cannot be NAN. It's still fine if it's infinity. + Interval::new( + self.first.domain().start(), + self.first.domain().end() + self.second.domain().length(), + ) + .unwrap() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + if t > self.first.domain().end() { + self.second.sample_unchecked( + // `t - first.domain.end` computes the offset into the domain of the second. + t - self.first.domain().end() + self.second.domain().start(), + ) + } else { + self.first.sample_unchecked(t) + } + } +} diff --git a/crates/bevy_math/src/curve/iterable.rs b/crates/bevy_math/src/curve/iterable.rs index 7e0ccd1a98b85..b8adcd30d5c45 100644 --- a/crates/bevy_math/src/curve/iterable.rs +++ b/crates/bevy_math/src/curve/iterable.rs @@ -20,8 +20,6 @@ pub trait IterableCurve { /// is nonsensical or otherwise useless may be returned in such a circumstance, and extrapolation /// beyond a curve's domain should not be relied upon. fn sample_iter_unchecked(&self, t: f32) -> impl Iterator; - // where - // Self: 'a; /// Sample this curve at a specified time `t`, producing an iterator over sampled values. /// The parameter `t` is clamped to the domain of the curve. diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 35da4402b52c3..9a370216ab5d9 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -2,10 +2,12 @@ //! contains the [`Interval`] type, along with a selection of core data structures used to back //! curves that are interpolated from samples. +pub mod adaptors; pub mod cores; pub mod interval; pub mod iterable; +pub use adaptors::*; pub use interval::{interval, Interval}; use itertools::Itertools; @@ -512,300 +514,6 @@ pub enum ResamplingError { UnboundedDomain, } -/// A curve with a constant value over its domain. -/// -/// This is a curve that holds an inner value and always produces a clone of that value when sampled. -#[derive(Clone, Copy, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct ConstantCurve { - domain: Interval, - value: T, -} - -impl ConstantCurve -where - T: Clone, -{ - /// Create a constant curve, which has the given `domain` and always produces the given `value` - /// when sampled. - pub fn new(domain: Interval, value: T) -> Self { - Self { domain, value } - } -} - -impl Curve for ConstantCurve -where - T: Clone, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, _t: f32) -> T { - self.value.clone() - } -} - -/// A curve defined by a function together with a fixed domain. -/// -/// This is a curve that holds an inner function `f` which takes numbers (`f32`) as input and produces -/// output of type `T`. The value of this curve when sampled at time `t` is just `f(t)`. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct FunctionCurve { - domain: Interval, - f: F, - _phantom: PhantomData, -} - -impl FunctionCurve -where - F: Fn(f32) -> T, -{ - /// Create a new curve with the given `domain` from the given `function`. When sampled, the - /// `function` is evaluated at the sample time to compute the output. - pub fn new(domain: Interval, function: F) -> Self { - FunctionCurve { - domain, - f: function, - _phantom: PhantomData, - } - } -} - -impl Curve for FunctionCurve -where - F: Fn(f32) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - (self.f)(t) - } -} - -/// A curve whose samples are defined by mapping samples from another curve through a -/// given function. Curves of this type are produced by [`Curve::map`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct MapCurve { - preimage: C, - f: F, - _phantom: PhantomData<(S, T)>, -} - -impl Curve for MapCurve -where - C: Curve, - F: Fn(S) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.preimage.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - (self.f)(self.preimage.sample_unchecked(t)) - } -} - -/// A curve whose sample space is mapped onto that of some base curve's before sampling. -/// Curves of this type are produced by [`Curve::reparametrize`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct ReparamCurve { - domain: Interval, - base: C, - f: F, - _phantom: PhantomData, -} - -impl Curve for ReparamCurve -where - C: Curve, - F: Fn(f32) -> f32, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - self.base.sample_unchecked((self.f)(t)) - } -} - -/// A curve that has had its domain changed by a linear reparametrization (stretching and scaling). -/// Curves of this type are produced by [`Curve::reparametrize_linear`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct LinearReparamCurve { - /// Invariants: The domain of this curve must always be bounded. - base: C, - /// Invariants: This interval must always be bounded. - new_domain: Interval, - _phantom: PhantomData, -} - -impl Curve for LinearReparamCurve -where - C: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.new_domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - // The invariants imply this unwrap always succeeds. - let f = self.new_domain.linear_map_to(self.base.domain()).unwrap(); - self.base.sample_unchecked(f(t)) - } -} - -/// A curve that has been reparametrized by another curve, using that curve to transform the -/// sample times before sampling. Curves of this type are produced by [`Curve::reparametrize_by_curve`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct CurveReparamCurve { - base: C, - reparam_curve: D, - _phantom: PhantomData, -} - -impl Curve for CurveReparamCurve -where - C: Curve, - D: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.reparam_curve.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - let sample_time = self.reparam_curve.sample_unchecked(t); - self.base.sample_unchecked(sample_time) - } -} - -/// A curve that is the graph of another curve over its parameter space. Curves of this type are -/// produced by [`Curve::graph`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct GraphCurve { - base: C, - _phantom: PhantomData, -} - -impl Curve<(f32, T)> for GraphCurve -where - C: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.base.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> (f32, T) { - (t, self.base.sample_unchecked(t)) - } -} - -/// A curve that combines the output data from two constituent curves into a tuple output. Curves -/// of this type are produced by [`Curve::zip`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct ProductCurve { - domain: Interval, - first: C, - second: D, - _phantom: PhantomData<(S, T)>, -} - -impl Curve<(S, T)> for ProductCurve -where - C: Curve, - D: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - self.domain - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> (S, T) { - ( - self.first.sample_unchecked(t), - self.second.sample_unchecked(t), - ) - } -} - -/// The curve that results from chaining one curve with another. The second curve is -/// effectively reparametrized so that its start is at the end of the first. -/// -/// For this to be well-formed, the first curve's domain must be right-finite and the second's -/// must be left-finite. -/// -/// Curves of this type are produced by [`Curve::chain`]. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct ChainCurve { - first: C, - second: D, - _phantom: PhantomData, -} - -impl Curve for ChainCurve -where - C: Curve, - D: Curve, -{ - #[inline] - fn domain(&self) -> Interval { - // This unwrap always succeeds because `first` has a valid Interval as its domain and the - // length of `second` cannot be NAN. It's still fine if it's infinity. - Interval::new( - self.first.domain().start(), - self.first.domain().end() + self.second.domain().length(), - ) - .unwrap() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - if t > self.first.domain().end() { - self.second.sample_unchecked( - // `t - first.domain.end` computes the offset into the domain of the second. - t - self.first.domain().end() + self.second.domain().start(), - ) - } else { - self.first.sample_unchecked(t) - } - } -} - /// A curve that is defined by explicit neighbor interpolation over a set of samples. #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] From 6db9e53a68613affb0bcd0012af389410d4f3a5c Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Fri, 27 Sep 2024 09:01:03 -0400 Subject: [PATCH 04/22] Resolve new std -> core lints --- crates/bevy_animation/src/animation_curves.rs | 2 +- crates/bevy_math/src/curve/adaptors.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 254f61ab23025..b771b68cb5ee6 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -77,7 +77,7 @@ //! [`AnimationClip`]: crate::AnimationClip //! [there]: AnimatableProperty -use std::{ +use core::{ any::TypeId, fmt::{self, Debug, Formatter}, marker::PhantomData, diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 5315687a414bd..b61e72c3d46d9 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -3,9 +3,9 @@ use super::interval::*; use super::Curve; -use std::any::type_name; -use std::fmt::{self, Debug}; -use std::marker::PhantomData; +use core::any::type_name; +use core::fmt::{self, Debug}; +use core::marker::PhantomData; use bevy_reflect::utility::GenericTypePathCell; #[cfg(feature = "bevy_reflect")] From eee7a23b11a553a2c755ef7851a3e57134f991a3 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 08:24:46 -0400 Subject: [PATCH 05/22] Fix doc typo --- crates/bevy_animation/src/animation_curves.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index b771b68cb5ee6..887bedc1ac613 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -157,7 +157,7 @@ use crate::{ /// ); /// /// Here, the use of `AnimatableKeyframeCurve` creates a curve out of the given keyframe time-value -/// pairs, using the `Animatable` implementation of `f32` to interpolate between then. The +/// pairs, using the `Animatable` implementation of `f32` to interpolate between them. The /// invocation of [`AnimatableCurve::from_curve`] with `FontSizeProperty` indicates that the `f32` /// output from that curve is to be used to animate the font size of a `Text` component (as /// configured above). From 8c366e4611fcd6461fbc18df14e27b9d8e7b5ce0 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 08:38:11 -0400 Subject: [PATCH 06/22] Fix compile issue without bevy_reflect feature --- crates/bevy_math/src/curve/adaptors.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index b61e72c3d46d9..7382225806ab9 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -7,9 +7,8 @@ use core::any::type_name; use core::fmt::{self, Debug}; use core::marker::PhantomData; -use bevy_reflect::utility::GenericTypePathCell; #[cfg(feature = "bevy_reflect")] -use bevy_reflect::{Reflect, TypePath}; +use bevy_reflect::{utility::GenericTypePathCell, Reflect, TypePath}; const THIS_MODULE: &str = "bevy_math::curve::adaptors"; const THIS_CRATE: &str = "bevy_math"; From e350c7d8754c2659beae75fc4fddb2e58d15cda6 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 09:46:23 -0400 Subject: [PATCH 07/22] Improve reflection implementations for sample-based curves with explicit interpolation --- crates/bevy_math/src/curve/mod.rs | 201 +---------- crates/bevy_math/src/curve/sample_curves.rs | 365 ++++++++++++++++++++ 2 files changed, 371 insertions(+), 195 deletions(-) create mode 100644 crates/bevy_math/src/curve/sample_curves.rs diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 608027ba33f3a..23fc743ede4d2 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,14 +4,18 @@ pub mod cores; pub mod interval; +pub mod sample_curves; +// bevy_math::curve re-exports all commonly-needed curve-related items. pub use interval::{interval, Interval}; -use itertools::Itertools; +pub use sample_curves::*; + +use cores::{EvenCore, UnevenCore}; use crate::StableInterpolate; use core::{marker::PhantomData, ops::Deref}; -use cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; use interval::InvalidIntervalError; +use itertools::Itertools; use thiserror::Error; #[cfg(feature = "bevy_reflect")] @@ -805,199 +809,6 @@ where } } -/// A curve that is defined by explicit neighbor interpolation over a set of samples. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct SampleCurve { - core: EvenCore, - interpolation: I, -} - -impl Curve for SampleCurve -where - T: Clone, - I: Fn(&T, &T, f32) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - self.core.sample_with(t, &self.interpolation) - } -} - -impl SampleCurve { - /// Create a new [`SampleCurve`] using the specified `interpolation` to interpolate between - /// the given `samples`. An error is returned if there are not at least 2 samples or if the - /// given `domain` is unbounded. - /// - /// The interpolation takes two values by reference together with a scalar parameter and - /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and - /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. - pub fn new( - domain: Interval, - samples: impl IntoIterator, - interpolation: I, - ) -> Result - where - I: Fn(&T, &T, f32) -> T, - { - Ok(Self { - core: EvenCore::new(domain, samples)?, - interpolation, - }) - } -} - -/// A curve that is defined by neighbor interpolation over a set of samples. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct SampleAutoCurve { - core: EvenCore, -} - -impl Curve for SampleAutoCurve -where - T: StableInterpolate, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - self.core - .sample_with(t, ::interpolate_stable) - } -} - -impl SampleAutoCurve { - /// Create a new [`SampleCurve`] using type-inferred interpolation to interpolate between - /// the given `samples`. An error is returned if there are not at least 2 samples or if the - /// given `domain` is unbounded. - pub fn new( - domain: Interval, - samples: impl IntoIterator, - ) -> Result { - Ok(Self { - core: EvenCore::new(domain, samples)?, - }) - } -} - -/// A curve that is defined by interpolation over unevenly spaced samples with explicit -/// interpolation. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct UnevenSampleCurve { - core: UnevenCore, - interpolation: I, -} - -impl Curve for UnevenSampleCurve -where - T: Clone, - I: Fn(&T, &T, f32) -> T, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - self.core.sample_with(t, &self.interpolation) - } -} - -impl UnevenSampleCurve { - /// Create a new [`UnevenSampleCurve`] using the provided `interpolation` to interpolate - /// between adjacent `timed_samples`. The given samples are filtered to finite times and - /// sorted internally; if there are not at least 2 valid timed samples, an error will be - /// returned. - /// - /// The interpolation takes two values by reference together with a scalar parameter and - /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and - /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. - pub fn new( - timed_samples: impl IntoIterator, - interpolation: I, - ) -> Result { - Ok(Self { - core: UnevenCore::new(timed_samples)?, - interpolation, - }) - } - - /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. - /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], - /// but the function inputs to each are inverses of one another. - /// - /// The samples are re-sorted by time after mapping and deduplicated by output time, so - /// the function `f` should generally be injective over the sample times of the curve. - pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { - Self { - core: self.core.map_sample_times(f), - interpolation: self.interpolation, - } - } -} - -/// A curve that is defined by interpolation over unevenly spaced samples. -#[derive(Clone, Debug)] -#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct UnevenSampleAutoCurve { - core: UnevenCore, -} - -impl Curve for UnevenSampleAutoCurve -where - T: StableInterpolate, -{ - #[inline] - fn domain(&self) -> Interval { - self.core.domain() - } - - #[inline] - fn sample_unchecked(&self, t: f32) -> T { - self.core - .sample_with(t, ::interpolate_stable) - } -} - -impl UnevenSampleAutoCurve { - /// Create a new [`UnevenSampleAutoCurve`] from a given set of timed samples, interpolated - /// using the The samples are filtered to finite times and - /// sorted internally; if there are not at least 2 valid timed samples, an error will be - /// returned. - pub fn new(timed_samples: impl IntoIterator) -> Result { - Ok(Self { - core: UnevenCore::new(timed_samples)?, - }) - } - - /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. - /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], - /// but the function inputs to each are inverses of one another. - /// - /// The samples are re-sorted by time after mapping and deduplicated by output time, so - /// the function `f` should generally be injective over the sample times of the curve. - pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { - Self { - core: self.core.map_sample_times(f), - } - } -} - /// Create a [`Curve`] that constantly takes the given `value` over the given `domain`. pub fn constant_curve(domain: Interval, value: T) -> ConstantCurve { ConstantCurve { domain, value } diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs new file mode 100644 index 0000000000000..ec7c31c839a4f --- /dev/null +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -0,0 +1,365 @@ +//! Sample-interpolated curves constructed using the [`Curve`] API. + +use super::cores::{EvenCore, EvenCoreError, UnevenCore, UnevenCoreError}; +use super::{Curve, Interval}; + +use crate::StableInterpolate; +use core::any::type_name; +use core::fmt::{self, Debug}; + +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::{utility::GenericTypePathCell, Reflect, TypePath}; + +const THIS_MODULE: &str = "bevy_math::curve::sample_curves"; +const THIS_CRATE: &str = "bevy_math"; + +/// A curve that is defined by explicit neighbor interpolation over a set of evenly-spaced samples. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where T: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct SampleCurve { + pub(crate) core: EvenCore, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) interpolation: I, +} + +impl Debug for SampleCurve +where + EvenCore: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SampleCurve") + .field("core", &self.core) + .field("interpolation", &type_name::()) + .finish() + } +} + +impl TypePath for SampleCurve +where + T: TypePath, + I: 'static, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::SampleCurve<{},{}>", + THIS_MODULE, + T::type_path(), + type_name::() + ) + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!("SampleCurve<{},{}>", T::type_path(), type_name::()) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("SampleCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(THIS_MODULE) + } +} + +impl Curve for SampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core.sample_with(t, &self.interpolation) + } +} + +impl SampleCurve { + /// Create a new [`SampleCurve`] using the specified `interpolation` to interpolate between + /// the given `samples`. An error is returned if there are not at least 2 samples or if the + /// given `domain` is unbounded. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + pub fn new( + domain: Interval, + samples: impl IntoIterator, + interpolation: I, + ) -> Result + where + I: Fn(&T, &T, f32) -> T, + { + Ok(Self { + core: EvenCore::new(domain, samples)?, + interpolation, + }) + } +} + +/// A curve that is defined by neighbor interpolation over a set of evenly-spaced samples, +/// interpolated automatically using [a particularly well-behaved interpolation]. +/// +/// [a particularly well-behaved interpolation]: StableInterpolate +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct SampleAutoCurve { + pub(crate) core: EvenCore, +} + +impl Curve for SampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl SampleAutoCurve { + /// Create a new [`SampleCurve`] using type-inferred interpolation to interpolate between + /// the given `samples`. An error is returned if there are not at least 2 samples or if the + /// given `domain` is unbounded. + pub fn new( + domain: Interval, + samples: impl IntoIterator, + ) -> Result { + Ok(Self { + core: EvenCore::new(domain, samples)?, + }) + } +} + +/// A curve that is defined by interpolation over unevenly spaced samples with explicit +/// interpolation. +#[derive(Clone)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr( + feature = "bevy_reflect", + derive(Reflect), + reflect(where T: TypePath), + reflect(from_reflect = false, type_path = false), +)] +pub struct UnevenSampleCurve { + pub(crate) core: UnevenCore, + #[cfg_attr(feature = "bevy_reflect", reflect(ignore))] + pub(crate) interpolation: I, +} + +impl Debug for UnevenSampleCurve +where + UnevenCore: Debug, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("SampleCurve") + .field("core", &self.core) + .field("interpolation", &type_name::()) + .finish() + } +} + +impl TypePath for UnevenSampleCurve +where + T: TypePath, + I: 'static, +{ + fn type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!( + "{}::UnevenSampleCurve<{},{}>", + THIS_MODULE, + T::type_path(), + type_name::() + ) + }) + } + + fn short_type_path() -> &'static str { + static CELL: GenericTypePathCell = GenericTypePathCell::new(); + CELL.get_or_insert::(|| { + format!("UnevenSampleCurve<{},{}>", T::type_path(), type_name::()) + }) + } + + fn type_ident() -> Option<&'static str> { + Some("UnevenSampleCurve") + } + + fn crate_name() -> Option<&'static str> { + Some(THIS_CRATE) + } + + fn module_path() -> Option<&'static str> { + Some(THIS_MODULE) + } +} + +impl Curve for UnevenSampleCurve +where + T: Clone, + I: Fn(&T, &T, f32) -> T, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core.sample_with(t, &self.interpolation) + } +} + +impl UnevenSampleCurve { + /// Create a new [`UnevenSampleCurve`] using the provided `interpolation` to interpolate + /// between adjacent `timed_samples`. The given samples are filtered to finite times and + /// sorted internally; if there are not at least 2 valid timed samples, an error will be + /// returned. + /// + /// The interpolation takes two values by reference together with a scalar parameter and + /// produces an owned value. The expectation is that `interpolation(&x, &y, 0.0)` and + /// `interpolation(&x, &y, 1.0)` are equivalent to `x` and `y` respectively. + pub fn new( + timed_samples: impl IntoIterator, + interpolation: I, + ) -> Result { + Ok(Self { + core: UnevenCore::new(timed_samples)?, + interpolation, + }) + } + + /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. + /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// but the function inputs to each are inverses of one another. + /// + /// The samples are re-sorted by time after mapping and deduplicated by output time, so + /// the function `f` should generally be injective over the sample times of the curve. + pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleCurve { + Self { + core: self.core.map_sample_times(f), + interpolation: self.interpolation, + } + } +} + +/// A curve that is defined by interpolation over unevenly spaced samples, +/// interpolated automatically using [a particularly well-behaved interpolation]. +/// +/// [a particularly well-behaved interpolation]: StableInterpolate +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +pub struct UnevenSampleAutoCurve { + pub(crate) core: UnevenCore, +} + +impl Curve for UnevenSampleAutoCurve +where + T: StableInterpolate, +{ + #[inline] + fn domain(&self) -> Interval { + self.core.domain() + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.core + .sample_with(t, ::interpolate_stable) + } +} + +impl UnevenSampleAutoCurve { + /// Create a new [`UnevenSampleAutoCurve`] from a given set of timed samples, interpolated + /// using the The samples are filtered to finite times and + /// sorted internally; if there are not at least 2 valid timed samples, an error will be + /// returned. + pub fn new(timed_samples: impl IntoIterator) -> Result { + Ok(Self { + core: UnevenCore::new(timed_samples)?, + }) + } + + /// This [`UnevenSampleAutoCurve`], but with the sample times moved by the map `f`. + /// In principle, when `f` is monotone, this is equivalent to [`Curve::reparametrize`], + /// but the function inputs to each are inverses of one another. + /// + /// The samples are re-sorted by time after mapping and deduplicated by output time, so + /// the function `f` should generally be injective over the sample times of the curve. + pub fn map_sample_times(self, f: impl Fn(f32) -> f32) -> UnevenSampleAutoCurve { + Self { + core: self.core.map_sample_times(f), + } + } +} + +#[cfg(test)] +mod tests { + //! These tests should guarantee (by even compiling) that SampleCurve and UnevenSampleCurve + //! can be Reflect under reasonable circumstances where their interpolation is defined by: + //! - function items + //! - 'static closures + //! - function pointers + use super::{SampleCurve, UnevenSampleCurve}; + use crate::{curve::Interval, VectorSpace}; + use bevy_reflect::Reflect; + + #[test] + fn reflect_sample_curve() { + fn foo(x: &f32, y: &f32, t: f32) -> f32 { + x.lerp(*y, t) + } + let bar = |x: &f32, y: &f32, t: f32| x.lerp(*y, t); + let baz: fn(&f32, &f32, f32) -> f32 = bar; + + let samples = [0.0, 1.0, 2.0]; + + let _: Box = Box::new(SampleCurve::new(Interval::UNIT, samples, foo).unwrap()); + let _: Box = Box::new(SampleCurve::new(Interval::UNIT, samples, bar).unwrap()); + let _: Box = Box::new(SampleCurve::new(Interval::UNIT, samples, baz).unwrap()); + } + + #[test] + fn reflect_uneven_sample_curve() { + fn foo(x: &f32, y: &f32, t: f32) -> f32 { + x.lerp(*y, t) + } + let bar = |x: &f32, y: &f32, t: f32| x.lerp(*y, t); + let baz: fn(&f32, &f32, f32) -> f32 = bar; + + let keyframes = [(0.0, 1.0), (1.0, 0.0), (2.0, -1.0)]; + + let _: Box = Box::new(UnevenSampleCurve::new(keyframes, foo).unwrap()); + let _: Box = Box::new(UnevenSampleCurve::new(keyframes, bar).unwrap()); + let _: Box = Box::new(UnevenSampleCurve::new(keyframes, baz).unwrap()); + } +} From bbb5b81601301b9f83c8120621eec156570f138a Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 10:09:10 -0400 Subject: [PATCH 08/22] Lint --- crates/bevy_math/src/curve/sample_curves.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index ec7c31c839a4f..e12bed8933b96 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -324,8 +324,8 @@ impl UnevenSampleAutoCurve { #[cfg(test)] mod tests { - //! These tests should guarantee (by even compiling) that SampleCurve and UnevenSampleCurve - //! can be Reflect under reasonable circumstances where their interpolation is defined by: + //! These tests should guarantee (by even compiling) that `SampleCurve` and `UnevenSampleCurve` + //! can be `Reflect` under reasonable circumstances where their interpolation is defined by: //! - function items //! - 'static closures //! - function pointers From 2089667f61c000006553535ad01ee844b25f79cc Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 10:12:48 -0400 Subject: [PATCH 09/22] Wrangle features --- crates/bevy_math/src/curve/sample_curves.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index e12bed8933b96..1e35d6ba628a2 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -40,6 +40,7 @@ where } } +#[cfg(feature = "bevy_reflect")] impl TypePath for SampleCurve where T: TypePath, @@ -185,6 +186,7 @@ where } } +#[cfg(feature = "bevy_reflect")] impl TypePath for UnevenSampleCurve where T: TypePath, From 7c4fd9d58c4c8b9dde06561f6881ae5cd6dd56d8 Mon Sep 17 00:00:00 2001 From: Matty Date: Sat, 28 Sep 2024 13:18:27 -0400 Subject: [PATCH 10/22] Apply `#[reflect(Debug)]` Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> --- crates/bevy_math/src/curve/sample_curves.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index 1e35d6ba628a2..766352a0f5117 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -21,6 +21,7 @@ const THIS_CRATE: &str = "bevy_math"; derive(Reflect), reflect(where T: TypePath), reflect(from_reflect = false, type_path = false), + reflect(Debug), )] pub struct SampleCurve { pub(crate) core: EvenCore, @@ -123,7 +124,7 @@ impl SampleCurve { /// [a particularly well-behaved interpolation]: StableInterpolate #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct SampleAutoCurve { pub(crate) core: EvenCore, } @@ -167,6 +168,7 @@ impl SampleAutoCurve { derive(Reflect), reflect(where T: TypePath), reflect(from_reflect = false, type_path = false), + reflect(Debug), )] pub struct UnevenSampleCurve { pub(crate) core: UnevenCore, @@ -279,7 +281,7 @@ impl UnevenSampleCurve { /// [a particularly well-behaved interpolation]: StableInterpolate #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] pub struct UnevenSampleAutoCurve { pub(crate) core: UnevenCore, } From fd0709b70b260ea640b3c1ee967f456c807f96cc Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 13:24:36 -0400 Subject: [PATCH 11/22] Hide paths behind bevy_reflect feature --- crates/bevy_math/src/curve/sample_curves.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index 766352a0f5117..c0228c879e6cb 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -10,8 +10,11 @@ use core::fmt::{self, Debug}; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{utility::GenericTypePathCell, Reflect, TypePath}; -const THIS_MODULE: &str = "bevy_math::curve::sample_curves"; -const THIS_CRATE: &str = "bevy_math"; +#[cfg(feature = "bevy_reflect")] +mod paths { + pub(super) const THIS_MODULE: &str = "bevy_math::curve::sample_curves"; + pub(super) const THIS_CRATE: &str = "bevy_math"; +} /// A curve that is defined by explicit neighbor interpolation over a set of evenly-spaced samples. #[derive(Clone)] @@ -52,7 +55,7 @@ where CELL.get_or_insert::(|| { format!( "{}::SampleCurve<{},{}>", - THIS_MODULE, + paths::THIS_MODULE, T::type_path(), type_name::() ) @@ -71,11 +74,11 @@ where } fn crate_name() -> Option<&'static str> { - Some(THIS_CRATE) + Some(paths::THIS_CRATE) } fn module_path() -> Option<&'static str> { - Some(THIS_MODULE) + Some(paths::THIS_MODULE) } } @@ -199,7 +202,7 @@ where CELL.get_or_insert::(|| { format!( "{}::UnevenSampleCurve<{},{}>", - THIS_MODULE, + paths::THIS_MODULE, T::type_path(), type_name::() ) @@ -218,11 +221,11 @@ where } fn crate_name() -> Option<&'static str> { - Some(THIS_CRATE) + Some(paths::THIS_CRATE) } fn module_path() -> Option<&'static str> { - Some(THIS_MODULE) + Some(paths::THIS_MODULE) } } From 74165492c7be834747b5a02425d67c2f80352c17 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 13:28:29 -0400 Subject: [PATCH 12/22] Revert nonfunctional Debug reflects --- crates/bevy_math/src/curve/sample_curves.rs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index c0228c879e6cb..58190f4d169a2 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -24,7 +24,6 @@ mod paths { derive(Reflect), reflect(where T: TypePath), reflect(from_reflect = false, type_path = false), - reflect(Debug), )] pub struct SampleCurve { pub(crate) core: EvenCore, @@ -127,7 +126,7 @@ impl SampleCurve { /// [a particularly well-behaved interpolation]: StableInterpolate #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct SampleAutoCurve { pub(crate) core: EvenCore, } @@ -171,7 +170,6 @@ impl SampleAutoCurve { derive(Reflect), reflect(where T: TypePath), reflect(from_reflect = false, type_path = false), - reflect(Debug), )] pub struct UnevenSampleCurve { pub(crate) core: UnevenCore, @@ -284,7 +282,7 @@ impl UnevenSampleCurve { /// [a particularly well-behaved interpolation]: StableInterpolate #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct UnevenSampleAutoCurve { pub(crate) core: UnevenCore, } From a88109e8dee106a9ad380b0d872ddcab1f208375 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 13:31:32 -0400 Subject: [PATCH 13/22] Added comments on instability of TypeName implementations --- crates/bevy_math/src/curve/sample_curves.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index 58190f4d169a2..255fa84d10275 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -43,6 +43,8 @@ where } } +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. #[cfg(feature = "bevy_reflect")] impl TypePath for SampleCurve where @@ -189,6 +191,8 @@ where } } +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. #[cfg(feature = "bevy_reflect")] impl TypePath for UnevenSampleCurve where From a80ac911b52c1655ad8988b1b3dbe317af4fb538 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sat, 28 Sep 2024 14:31:19 -0400 Subject: [PATCH 14/22] Tweaks to curve adaptor reflection --- crates/bevy_math/src/curve/adaptors.rs | 40 +++++++++++++++++--------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index 7382225806ab9..bb56c81f9c6e5 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -10,8 +10,11 @@ use core::marker::PhantomData; #[cfg(feature = "bevy_reflect")] use bevy_reflect::{utility::GenericTypePathCell, Reflect, TypePath}; -const THIS_MODULE: &str = "bevy_math::curve::adaptors"; -const THIS_CRATE: &str = "bevy_math"; +#[cfg(feature = "bevy_reflect")] +mod paths { + pub(super) const THIS_MODULE: &str = "bevy_math::curve::adaptors"; + pub(super) const THIS_CRATE: &str = "bevy_math"; +} // NOTE ON REFLECTION: // @@ -94,17 +97,20 @@ impl Debug for FunctionCurve { } } +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. #[cfg(feature = "bevy_reflect")] -impl TypePath for FunctionCurve +impl TypePath for FunctionCurve where T: TypePath, + F: 'static, { fn type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); CELL.get_or_insert::(|| { format!( "{}::FunctionCurve<{},{}>", - THIS_MODULE, + paths::THIS_MODULE, T::type_path(), type_name::() ) @@ -127,11 +133,11 @@ where } fn crate_name() -> Option<&'static str> { - Some(THIS_CRATE) + Some(paths::THIS_CRATE) } fn module_path() -> Option<&'static str> { - Some(THIS_MODULE) + Some(paths::THIS_MODULE) } } @@ -196,19 +202,22 @@ where } } +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. #[cfg(feature = "bevy_reflect")] -impl TypePath for MapCurve +impl TypePath for MapCurve where S: TypePath, T: TypePath, C: TypePath, + F: 'static, { fn type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); CELL.get_or_insert::(|| { format!( "{}::MapCurve<{},{},{},{}>", - THIS_MODULE, + paths::THIS_MODULE, S::type_path(), T::type_path(), C::type_path(), @@ -235,11 +244,11 @@ where } fn crate_name() -> Option<&'static str> { - Some(THIS_CRATE) + Some(paths::THIS_CRATE) } fn module_path() -> Option<&'static str> { - Some(THIS_MODULE) + Some(paths::THIS_MODULE) } } @@ -292,18 +301,21 @@ where } } +/// Note: This is not a fully stable implementation of `TypePath` due to usage of `type_name` +/// for function members. #[cfg(feature = "bevy_reflect")] -impl TypePath for ReparamCurve +impl TypePath for ReparamCurve where T: TypePath, C: TypePath, + F: 'static, { fn type_path() -> &'static str { static CELL: GenericTypePathCell = GenericTypePathCell::new(); CELL.get_or_insert::(|| { format!( "{}::ReparamCurve<{},{},{}>", - THIS_MODULE, + paths::THIS_MODULE, T::type_path(), C::type_path(), type_name::() @@ -328,11 +340,11 @@ where } fn crate_name() -> Option<&'static str> { - Some(THIS_CRATE) + Some(paths::THIS_CRATE) } fn module_path() -> Option<&'static str> { - Some(THIS_MODULE) + Some(paths::THIS_MODULE) } } From f12555df7f170c3473463df636d727abf1e9e2e4 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 29 Sep 2024 17:20:52 -0400 Subject: [PATCH 15/22] Remove PhantomData from Debug, use AnimationEntityMut --- crates/bevy_animation/src/animation_curves.rs | 40 ++++++++----------- crates/bevy_animation/src/gltf_curves.rs | 1 - crates/bevy_math/src/curve/adaptors.rs | 3 -- 3 files changed, 17 insertions(+), 27 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 887bedc1ac613..c186133781edf 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -83,11 +83,7 @@ use core::{ marker::PhantomData, }; -use bevy_asset::Handle; -use bevy_ecs::{ - component::Component, - world::{EntityMutExcept, Mut}, -}; +use bevy_ecs::{component::Component, world::Mut}; use bevy_math::{ curve::{ cores::{UnevenCore, UnevenCoreError}, @@ -100,9 +96,7 @@ use bevy_reflect::{FromReflect, Reflect, Reflectable, TypePath}; use bevy_render::mesh::morph::MorphWeights; use bevy_transform::prelude::Transform; -use crate::{ - graph::AnimationGraph, prelude::Animatable, AnimationEvaluationError, AnimationPlayer, -}; +use crate::{prelude::Animatable, AnimationEntityMut, AnimationEvaluationError}; /// A value on a component that Bevy can animate. /// @@ -178,9 +172,9 @@ pub trait AnimatableProperty: Reflect + TypePath { /// This trait collects the additional requirements on top of [`Curve`] needed for a /// curve to be used as an [`AnimationCurve`]. -pub trait InnerAnimationCurve: Curve + Debug + Clone + Reflectable {} +pub trait AnimationCompatibleCurve: Curve + Debug + Clone + Reflectable {} -impl InnerAnimationCurve for C where C: Curve + Debug + Clone + Reflectable {} +impl AnimationCompatibleCurve for C where C: Curve + Debug + Clone + Reflectable {} /// This type allows the conversion of a [curve] valued in the [property type] of an /// [`AnimatableProperty`] into an [`AnimationCurve`] which animates that property. @@ -198,7 +192,7 @@ pub struct AnimatableCurve { impl AnimatableCurve where P: AnimatableProperty, - C: InnerAnimationCurve, + C: AnimationCompatibleCurve, { /// Create an [`AnimatableCurve`] (and thus an [`AnimationCurve`]) from a curve /// valued in an [animatable property]. @@ -238,7 +232,7 @@ where impl AnimationCurve for AnimatableCurve where P: AnimatableProperty, - C: InnerAnimationCurve, + C: AnimationCompatibleCurve, { fn clone_value(&self) -> Box { Box::new(self.clone()) @@ -252,7 +246,7 @@ where &self, t: f32, _transform: Option>, - mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + mut entity: AnimationEntityMut<'a>, weight: f32, ) -> Result<(), AnimationEvaluationError> { let mut component = entity.get_mut::().ok_or_else(|| { @@ -276,7 +270,7 @@ pub struct TranslationCurve(pub C); impl AnimationCurve for TranslationCurve where - C: InnerAnimationCurve, + C: AnimationCompatibleCurve, { fn clone_value(&self) -> Box { Box::new(self.clone()) @@ -290,7 +284,7 @@ where &self, t: f32, transform: Option>, - _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + _entity: AnimationEntityMut<'a>, weight: f32, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { @@ -313,7 +307,7 @@ pub struct RotationCurve(pub C); impl AnimationCurve for RotationCurve where - C: InnerAnimationCurve, + C: AnimationCompatibleCurve, { fn clone_value(&self) -> Box { Box::new(self.clone()) @@ -327,7 +321,7 @@ where &self, t: f32, transform: Option>, - _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + _entity: AnimationEntityMut<'a>, weight: f32, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { @@ -350,7 +344,7 @@ pub struct ScaleCurve(pub C); impl AnimationCurve for ScaleCurve where - C: InnerAnimationCurve, + C: AnimationCompatibleCurve, { fn clone_value(&self) -> Box { Box::new(self.clone()) @@ -364,7 +358,7 @@ where &self, t: f32, transform: Option>, - _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + _entity: AnimationEntityMut<'a>, weight: f32, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { @@ -389,7 +383,7 @@ pub struct TransformCurve(pub C); impl AnimationCurve for TransformCurve where - C: InnerAnimationCurve, + C: AnimationCompatibleCurve, { fn clone_value(&self) -> Box { Box::new(self.clone()) @@ -403,7 +397,7 @@ where &self, t: f32, transform: Option>, - _entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + _entity: AnimationEntityMut<'a>, weight: f32, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { @@ -439,7 +433,7 @@ where &self, t: f32, _transform: Option>, - mut entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + mut entity: AnimationEntityMut<'a>, weight: f32, ) -> Result<(), AnimationEvaluationError> { let mut dest = entity.get_mut::().ok_or_else(|| { @@ -490,7 +484,7 @@ pub trait AnimationCurve: Reflect + Debug + Send + Sync { &self, t: f32, transform: Option>, - entity: EntityMutExcept<'a, (Transform, AnimationPlayer, Handle)>, + entity: AnimationEntityMut<'a>, weight: f32, ) -> Result<(), AnimationEvaluationError>; } diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index c324b660cd168..b922bda4e8ecf 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -269,7 +269,6 @@ where self.core.domain() } - #[inline] fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { match self.core.sample_interp_timed(t) { InterpolationDatum::Exact((_, v)) diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index bb56c81f9c6e5..e89ac568aa9a5 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -92,7 +92,6 @@ impl Debug for FunctionCurve { f.debug_struct("FunctionCurve") .field("domain", &self.domain) .field("f", &type_name::()) - .field("_phantom", &self._phantom) .finish() } } @@ -197,7 +196,6 @@ where f.debug_struct("MapCurve") .field("preimage", &self.preimage) .field("f", &type_name::()) - .field("_phantom", &self._phantom) .finish() } } @@ -296,7 +294,6 @@ where .field("domain", &self.domain) .field("base", &self.base) .field("f", &type_name::()) - .field("_phantom", &self._phantom) .finish() } } From 8e480d6e3da444c2d0e53c367d390ec21cc44921 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Sun, 29 Sep 2024 18:53:33 -0400 Subject: [PATCH 16/22] Improve cubic spline interpolation function used by gltf curves --- crates/bevy_animation/src/gltf_curves.rs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index b922bda4e8ecf..284428d1b0a28 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -2,7 +2,7 @@ use bevy_math::{ curve::{cores::*, iterable::IterableCurve, *}, - FloatPow, Quat, Vec4, VectorSpace, + vec4, Quat, Vec4, VectorSpace, }; use bevy_reflect::Reflect; use thiserror::Error; @@ -393,10 +393,11 @@ fn cubic_spline_interpolation( where T: VectorSpace, { - value_start * (2.0 * lerp.cubed() - 3.0 * lerp.squared() + 1.0) - + tangent_out_start * (step_duration) * (lerp.cubed() - 2.0 * lerp.squared() + lerp) - + value_end * (-2.0 * lerp.cubed() + 3.0 * lerp.squared()) - + tangent_in_end * step_duration * (lerp.cubed() - lerp.squared()) + let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp; + value_start * (coeffs.x + 1.0) + + tangent_out_start * step_duration * lerp * coeffs.y + + value_end * coeffs.z + + tangent_in_end * step_duration * coeffs.w } fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( From 766db9b5fd589eab4daa21dd77003f5bb2903323 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 30 Sep 2024 04:40:01 -0400 Subject: [PATCH 17/22] Fix cubic interpolation formula used by gltf curves --- crates/bevy_animation/src/gltf_curves.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index 284428d1b0a28..836c66b5b5dfc 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -394,10 +394,10 @@ where T: VectorSpace, { let coeffs = (vec4(2.0, 1.0, -2.0, 1.0) * lerp + vec4(-3.0, -2.0, 3.0, -1.0)) * lerp; - value_start * (coeffs.x + 1.0) - + tangent_out_start * step_duration * lerp * coeffs.y - + value_end * coeffs.z - + tangent_in_end * step_duration * coeffs.w + value_start * (coeffs.x * lerp + 1.0) + + tangent_out_start * step_duration * lerp * (coeffs.y + 1.0) + + value_end * lerp * coeffs.z + + tangent_in_end * step_duration * lerp * coeffs.w } fn cubic_spline_interpolate_slices<'a, T: VectorSpace>( From f104a4947d0fe057fbd21d8ef22679dedf5ec1a2 Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 30 Sep 2024 14:41:52 -0400 Subject: [PATCH 18/22] Update doc comments Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com> --- crates/bevy_animation/src/animation_curves.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index c186133781edf..746b6f8b9dd1e 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -20,7 +20,7 @@ //! is take our curve and turn it into an `AnimationCurve` which will be usable by the //! animation system. //! -//! For instance, let's imagine that we want to imagine that we want to use the `Vec3` output +//! For instance, let's imagine that we want to use the `Vec3` output //! from our curve to animate the [translation component of a `Transform`]. For this, there is //! the adaptor [`TranslationCurve`], which wraps any `Curve` and turns it into an //! [`AnimationCurve`] that will use the given curve to animate the entity's translation: @@ -150,8 +150,8 @@ use crate::{prelude::Animatable, AnimationEntityMut, AnimationEvaluationError}; /// .expect("Failed to create font size curve") /// ); /// -/// Here, the use of `AnimatableKeyframeCurve` creates a curve out of the given keyframe time-value -/// pairs, using the `Animatable` implementation of `f32` to interpolate between them. The +/// Here, the use of [`AnimatableKeyframeCurve`] creates a curve out of the given keyframe time-value +/// pairs, using the [`Animatable`] implementation of `f32` to interpolate between them. The /// invocation of [`AnimatableCurve::from_curve`] with `FontSizeProperty` indicates that the `f32` /// output from that curve is to be used to animate the font size of a `Text` component (as /// configured above). From e697a0b845569cc2ca5108c0e1aae0f3b7b1282d Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 30 Sep 2024 14:46:01 -0400 Subject: [PATCH 19/22] Apply suggestions from code review Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com> --- crates/bevy_animation/src/gltf_curves.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index 836c66b5b5dfc..f32ddc4ab0176 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -289,10 +289,10 @@ where /// An error indicating that a multisampling keyframe curve could not be constructed. #[derive(Debug, Error)] -#[error("Unable to construct a curve using this data")] +#[error("unable to construct a curve using this data")] pub enum WideKeyframeCurveError { /// The number of given values was not divisible by a multiple of the number of keyframes. - #[error("The number of values ({values_given}) was expected to be divisible by {divisor}")] + #[error("number of values ({values_given}) is not divisible by {divisor}")] LengthMismatch { /// The number of values given. values_given: usize, @@ -305,7 +305,9 @@ pub enum WideKeyframeCurveError { } impl WideCubicKeyframeCurve { - /// Create a new [`WideCubicKeyframeCurve`]. An error will be returned if: + /// Create a new [`WideCubicKeyframeCurve`]. + /// + /// An error will be returned if: /// - `values` has length zero. /// - `times` has less than `2` unique valid entries. /// - The length of `values` is not divisible by three times that of `times` (once sorted, From 934d44e4514c930f0e040b275375767821b887b6 Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 30 Sep 2024 14:46:49 -0400 Subject: [PATCH 20/22] More error formatting Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com> --- examples/animation/animated_transform.rs | 4 ++-- examples/animation/animated_ui.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index e2857006f6431..14c0c9fce5ded 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -63,7 +63,7 @@ fn setup( Vec3::new(1.0, 0.0, 1.0), ])) .map(TranslationCurve) - .expect("Failed to build translation curve"), + .expect("should be able to build translation curve because we pass in valid samples"), ); // Or it can modify the rotation of the transform. // To find the entity to modify, the hierarchy will be traversed looking for @@ -121,7 +121,7 @@ fn setup( Quat::IDENTITY, ])) .map(RotationCurve) - .expect("Failed to build rotation curve"), + .expect("should be able to build translation curve because we pass in valid samples"), ); // Create the animation graph diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index fd9c356cb6e4b..448f692e0962c 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -87,7 +87,7 @@ impl AnimationInfo { .zip([24.0, 80.0, 24.0, 80.0, 24.0, 80.0, 24.0]), ) .map(AnimatableCurve::::from_curve) - .expect("Failed to build font size curve"), + .expect("should be able to build translation curve because we pass in valid samples"), ); // Create a curve that animates font color. Note that this should have @@ -104,7 +104,7 @@ impl AnimationInfo { Srgba::RED, ])) .map(AnimatableCurve::::from_curve) - .expect("Failed to build text color curve"), + .expect("should be able to build translation curve because we pass in valid samples"), ); // Save our animation clip as an asset. From 07e80c5e060a0baafa7ead8f8e1446f58bb4fc67 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 30 Sep 2024 14:54:46 -0400 Subject: [PATCH 21/22] Rename ProductCurve -> ZipCurve --- crates/bevy_math/src/curve/adaptors.rs | 4 ++-- crates/bevy_math/src/curve/mod.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/bevy_math/src/curve/adaptors.rs b/crates/bevy_math/src/curve/adaptors.rs index e89ac568aa9a5..385345ddd947a 100644 --- a/crates/bevy_math/src/curve/adaptors.rs +++ b/crates/bevy_math/src/curve/adaptors.rs @@ -456,7 +456,7 @@ where #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] -pub struct ProductCurve { +pub struct ZipCurve { pub(crate) domain: Interval, pub(crate) first: C, pub(crate) second: D, @@ -464,7 +464,7 @@ pub struct ProductCurve { pub(crate) _phantom: PhantomData<(S, T)>, } -impl Curve<(S, T)> for ProductCurve +impl Curve<(S, T)> for ZipCurve where C: Curve, D: Curve, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index f5f9bc256b85d..5755892d498df 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -232,13 +232,13 @@ pub trait Curve { /// time `t` and `y` is the sample of `other` at time `t`. The domain of the new curve is the /// intersection of the domains of its constituents. If the domain intersection would be empty, /// an error is returned. - fn zip(self, other: C) -> Result, InvalidIntervalError> + fn zip(self, other: C) -> Result, InvalidIntervalError> where Self: Sized, C: Curve + Sized, { let domain = self.domain().intersect(other.domain())?; - Ok(ProductCurve { + Ok(ZipCurve { domain, first: self, second: other, From 926aa7bc7d08c1246b4a70ce47d47e205f022fe9 Mon Sep 17 00:00:00 2001 From: Matthew Weatherley Date: Mon, 30 Sep 2024 15:25:16 -0400 Subject: [PATCH 22/22] Rebase again --- crates/bevy_asset/src/server/mod.rs | 8 +- .../src/auto_exposure/buffers.rs | 5 +- .../src/core_2d/camera_2d.rs | 5 + crates/bevy_core_pipeline/src/core_2d/mod.rs | 7 +- .../src/core_3d/camera_3d.rs | 4 + crates/bevy_core_pipeline/src/core_3d/mod.rs | 23 +- crates/bevy_core_pipeline/src/dof/mod.rs | 4 +- crates/bevy_core_pipeline/src/taa/mod.rs | 23 +- crates/bevy_dev_tools/src/fps_overlay.rs | 22 +- crates/bevy_ecs/src/component.rs | 39 ++- crates/bevy_ecs/src/system/system.rs | 4 +- crates/bevy_ecs/src/world/mod.rs | 65 +++- crates/bevy_gizmos/src/lib.rs | 3 +- crates/bevy_input/src/button_input.rs | 3 +- crates/bevy_input/src/keyboard.rs | 2 +- crates/bevy_math/src/primitives/dim2.rs | 34 ++ .../bevy_math/src/sampling/shape_sampling.rs | 3 +- crates/bevy_mikktspace/Cargo.toml | 9 +- crates/bevy_mikktspace/src/generated.rs | 69 +++- crates/bevy_mikktspace/src/lib.rs | 3 + crates/bevy_pbr/src/bundle.rs | 7 + crates/bevy_pbr/src/cluster/mod.rs | 14 +- crates/bevy_pbr/src/lib.rs | 3 + crates/bevy_pbr/src/light_probe/mod.rs | 14 +- .../bevy_pbr/src/meshlet/instance_manager.rs | 6 +- crates/bevy_pbr/src/prepass/mod.rs | 4 +- crates/bevy_pbr/src/render/light.rs | 287 +++++++++++------ crates/bevy_pbr/src/ssao/mod.rs | 8 +- crates/bevy_pbr/src/volumetric_fog/render.rs | 13 +- crates/bevy_render/src/camera/camera.rs | 14 +- crates/bevy_render/src/extract_component.rs | 32 +- crates/bevy_render/src/lib.rs | 34 +- crates/bevy_render/src/pipelined_rendering.rs | 14 +- crates/bevy_render/src/world_sync.rs | 268 ++++++++++++++++ crates/bevy_sprite/src/bundle.rs | 3 + crates/bevy_sprite/src/mesh2d/mesh.rs | 8 - crates/bevy_sprite/src/render/mod.rs | 14 +- crates/bevy_ui/src/render/mod.rs | 95 +++--- .../src/render/ui_material_pipeline.rs | 14 +- crates/bevy_ui/src/stack.rs | 294 ++++++++++-------- crates/bevy_ui/src/ui_node.rs | 31 +- crates/bevy_winit/src/lib.rs | 3 +- crates/bevy_winit/src/state.rs | 20 +- crates/bevy_winit/src/system.rs | 23 +- examples/3d/fog_volumes.rs | 5 +- examples/3d/shadow_biases.rs | 18 +- examples/shader/custom_post_processing.rs | 6 +- examples/stress_tests/bevymark.rs | 18 +- examples/ui/z_index.rs | 56 ++-- 49 files changed, 1173 insertions(+), 488 deletions(-) create mode 100644 crates/bevy_render/src/world_sync.rs diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index f5f424c2dccd9..a527a7bd2a22f 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -131,9 +131,9 @@ impl AssetServer { } /// Retrieves the [`AssetSource`] for the given `source`. - pub fn get_source<'a>( + pub fn get_source<'a, 'b>( &'a self, - source: impl Into>, + source: impl Into>, ) -> Result<&'a AssetSource, MissingAssetSourceError> { self.data.sources.get(source.into()) } @@ -218,9 +218,9 @@ impl AssetServer { } /// Retrieves the default [`AssetLoader`] for the given path, if one can be found. - pub async fn get_path_asset_loader<'a>( + pub async fn get_path_asset_loader<'a, 'b>( &self, - path: impl Into>, + path: impl Into>, ) -> Result, MissingAssetLoaderForExtensionError> { let path = path.into(); diff --git a/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs b/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs index 836c18b3a75aa..4991a0925bee9 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/buffers.rs @@ -2,6 +2,7 @@ use bevy_ecs::prelude::*; use bevy_render::{ render_resource::{StorageBuffer, UniformBuffer}, renderer::{RenderDevice, RenderQueue}, + world_sync::RenderEntity, Extract, }; use bevy_utils::{Entry, HashMap}; @@ -26,13 +27,13 @@ pub(super) struct ExtractedStateBuffers { pub(super) fn extract_buffers( mut commands: Commands, - changed: Extract>>, + changed: Extract>>, mut removed: Extract>, ) { commands.insert_resource(ExtractedStateBuffers { changed: changed .iter() - .map(|(entity, settings)| (entity, settings.clone())) + .map(|(entity, settings)| (entity.id(), settings.clone())) .collect(), removed: removed.read().collect(), }); diff --git a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs index 682c15cd035ee..03ddf5c073915 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -4,6 +4,7 @@ use crate::{ }; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_render::world_sync::SyncToRenderWorld; use bevy_render::{ camera::{ Camera, CameraMainTextureUsages, CameraProjection, CameraRenderGraph, @@ -35,6 +36,8 @@ pub struct Camera2dBundle { pub deband_dither: DebandDither, pub main_texture_usages: CameraMainTextureUsages, pub msaa: Msaa, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } impl Default for Camera2dBundle { @@ -55,6 +58,7 @@ impl Default for Camera2dBundle { deband_dither: DebandDither::Disabled, main_texture_usages: Default::default(), msaa: Default::default(), + sync: Default::default(), } } } @@ -88,6 +92,7 @@ impl Camera2dBundle { deband_dither: DebandDither::Disabled, main_texture_usages: Default::default(), msaa: Default::default(), + sync: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/core_2d/mod.rs b/crates/bevy_core_pipeline/src/core_2d/mod.rs index 6bd3b3e324bcd..9f07f19bf8a81 100644 --- a/crates/bevy_core_pipeline/src/core_2d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_2d/mod.rs @@ -57,6 +57,7 @@ use bevy_render::{ renderer::RenderDevice, texture::TextureCache, view::{Msaa, ViewDepthTexture}, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; @@ -357,11 +358,10 @@ impl CachedRenderPipelinePhaseItem for Transparent2d { } pub fn extract_core_2d_camera_phases( - mut commands: Commands, mut transparent_2d_phases: ResMut>, mut opaque_2d_phases: ResMut>, mut alpha_mask_2d_phases: ResMut>, - cameras_2d: Extract>>, + cameras_2d: Extract>>, mut live_entities: Local, ) { live_entities.clear(); @@ -370,8 +370,7 @@ pub fn extract_core_2d_camera_phases( if !camera.is_active { continue; } - - commands.get_or_spawn(entity); + let entity = entity.id(); transparent_2d_phases.insert_or_clear(entity); opaque_2d_phases.insert_or_clear(entity); alpha_mask_2d_phases.insert_or_clear(entity); diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index ed748c52bc328..454892a30684c 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -10,6 +10,7 @@ use bevy_render::{ primitives::Frustum, render_resource::{LoadOp, TextureUsages}, view::{ColorGrading, Msaa, VisibleEntities}, + world_sync::SyncToRenderWorld, }; use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; @@ -153,6 +154,8 @@ pub struct Camera3dBundle { pub exposure: Exposure, pub main_texture_usages: CameraMainTextureUsages, pub msaa: Msaa, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } // NOTE: ideally Perspective and Orthographic defaults can share the same impl, but sadly it breaks rust's type inference @@ -173,6 +176,7 @@ impl Default for Camera3dBundle { main_texture_usages: Default::default(), deband_dither: DebandDither::Enabled, msaa: Default::default(), + sync: Default::default(), } } } diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index fc066e2d9ea7f..71f1f031979d5 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -91,6 +91,7 @@ use bevy_render::{ renderer::RenderDevice, texture::{BevyDefault, ColorAttachment, Image, TextureCache}, view::{ExtractedView, ViewDepthTexture, ViewTarget}, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{tracing::warn, HashMap}; @@ -504,23 +505,21 @@ impl CachedRenderPipelinePhaseItem for Transparent3d { } pub fn extract_core_3d_camera_phases( - mut commands: Commands, mut opaque_3d_phases: ResMut>, mut alpha_mask_3d_phases: ResMut>, mut transmissive_3d_phases: ResMut>, mut transparent_3d_phases: ResMut>, - cameras_3d: Extract>>, + cameras_3d: Extract>>, mut live_entities: Local, ) { live_entities.clear(); - for (entity, camera) in &cameras_3d { + for (render_entity, camera) in &cameras_3d { if !camera.is_active { continue; } - commands.get_or_spawn(entity); - + let entity = render_entity.id(); opaque_3d_phases.insert_or_clear(entity); alpha_mask_3d_phases.insert_or_clear(entity); transmissive_3d_phases.insert_or_clear(entity); @@ -545,7 +544,7 @@ pub fn extract_camera_prepass_phase( cameras_3d: Extract< Query< ( - Entity, + &RenderEntity, &Camera, Has, Has, @@ -559,13 +558,20 @@ pub fn extract_camera_prepass_phase( ) { live_entities.clear(); - for (entity, camera, depth_prepass, normal_prepass, motion_vector_prepass, deferred_prepass) in - cameras_3d.iter() + for ( + render_entity, + camera, + depth_prepass, + normal_prepass, + motion_vector_prepass, + deferred_prepass, + ) in cameras_3d.iter() { if !camera.is_active { continue; } + let entity = render_entity.id(); if depth_prepass || normal_prepass || motion_vector_prepass { opaque_3d_prepass_phases.insert_or_clear(entity); alpha_mask_3d_prepass_phases.insert_or_clear(entity); @@ -581,7 +587,6 @@ pub fn extract_camera_prepass_phase( opaque_3d_deferred_phases.remove(&entity); alpha_mask_3d_deferred_phases.remove(&entity); } - live_entities.insert(entity); commands diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index ba31dfb279653..100b7f20ffc4d 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -51,6 +51,7 @@ use bevy_render::{ prepare_view_targets, ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, }, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{info_once, prelude::default, warn_once}; @@ -809,7 +810,7 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { /// Extracts all [`DepthOfField`] components into the render world. fn extract_depth_of_field_settings( mut commands: Commands, - mut query: Extract>, + mut query: Extract>, ) { if !DEPTH_TEXTURE_SAMPLING_SUPPORTED { info_once!( @@ -819,6 +820,7 @@ fn extract_depth_of_field_settings( } for (entity, depth_of_field, projection) in query.iter_mut() { + let entity = entity.id(); // Depth of field is nonsensical without a perspective projection. let Projection::Perspective(ref perspective_projection) = *projection else { continue; diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 56dcd6a69bb4d..3ca33a21d6502 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -32,6 +32,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice}, texture::{BevyDefault, CachedTexture, TextureCache}, view::{ExtractedView, Msaa, ViewTarget}, + world_sync::RenderEntity, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; use bevy_utils::tracing::warn; @@ -351,20 +352,26 @@ impl SpecializedRenderPipeline for TaaPipeline { } fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut) { - let mut cameras_3d = main_world - .query_filtered::<(Entity, &Camera, &Projection, &mut TemporalAntiAliasing), ( - With, - With, - With, - With, - )>(); + let mut cameras_3d = main_world.query_filtered::<( + &RenderEntity, + &Camera, + &Projection, + &mut TemporalAntiAliasing, + ), ( + With, + With, + With, + With, + )>(); for (entity, camera, camera_projection, mut taa_settings) in cameras_3d.iter_mut(&mut main_world) { let has_perspective_projection = matches!(camera_projection, Projection::Perspective(_)); if camera.is_active && has_perspective_projection { - commands.get_or_spawn(entity).insert(taa_settings.clone()); + commands + .get_or_spawn(entity.id()) + .insert(taa_settings.clone()); taa_settings.reset = false; } } diff --git a/crates/bevy_dev_tools/src/fps_overlay.rs b/crates/bevy_dev_tools/src/fps_overlay.rs index d97af217ba81a..97d0c3989a79d 100644 --- a/crates/bevy_dev_tools/src/fps_overlay.rs +++ b/crates/bevy_dev_tools/src/fps_overlay.rs @@ -16,11 +16,11 @@ use bevy_render::view::Visibility; use bevy_text::{Font, Text, TextSection, TextStyle}; use bevy_ui::{ node_bundles::{NodeBundle, TextBundle}, - PositionType, Style, ZIndex, + GlobalZIndex, PositionType, Style, }; use bevy_utils::default; -/// Global [`ZIndex`] used to render the fps overlay. +/// [`GlobalZIndex`] used to render the fps overlay. /// /// We use a number slightly under `i32::MAX` so you can render on top of it if you really need to. pub const FPS_OVERLAY_ZINDEX: i32 = i32::MAX - 32; @@ -83,16 +83,18 @@ struct FpsText; fn setup(mut commands: Commands, overlay_config: Res) { commands - .spawn(NodeBundle { - style: Style { - // We need to make sure the overlay doesn't affect the position of other UI nodes - position_type: PositionType::Absolute, + .spawn(( + NodeBundle { + style: Style { + // We need to make sure the overlay doesn't affect the position of other UI nodes + position_type: PositionType::Absolute, + ..default() + }, + // Render overlay on top of everything ..default() }, - // Render overlay on top of everything - z_index: ZIndex::Global(FPS_OVERLAY_ZINDEX), - ..default() - }) + GlobalZIndex(FPS_OVERLAY_ZINDEX), + )) .with_children(|c| { c.spawn(( TextBundle::from_sections([ diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index c3599842031d3..d461fa4ba7709 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -836,7 +836,7 @@ pub struct Components { } impl Components { - /// Registers a component of type `T` with this instance. + /// Registers a [`Component`] of type `T` with this instance. /// If a component of this type has already been registered, this will return /// the ID of the pre-existing component. /// @@ -876,9 +876,9 @@ impl Components { /// Registers a component described by `descriptor`. /// - /// ## Note + /// # Note /// - /// If this method is called multiple times with identical descriptors, a distinct `ComponentId` + /// If this method is called multiple times with identical descriptors, a distinct [`ComponentId`] /// will be created for each one. /// /// # See also @@ -1034,6 +1034,7 @@ impl Components { /// # See also /// /// * [`Components::resource_id()`] + /// * [`Components::register_resource_with_descriptor()`] #[inline] pub fn register_resource(&mut self) -> ComponentId { // SAFETY: The [`ComponentDescriptor`] matches the [`TypeId`] @@ -1044,6 +1045,24 @@ impl Components { } } + /// Registers a [`Resource`] described by `descriptor`. + /// + /// # Note + /// + /// If this method is called multiple times with identical descriptors, a distinct [`ComponentId`] + /// will be created for each one. + /// + /// # See also + /// + /// * [`Components::resource_id()`] + /// * [`Components::register_resource()`] + pub fn register_resource_with_descriptor( + &mut self, + descriptor: ComponentDescriptor, + ) -> ComponentId { + Components::register_resource_inner(&mut self.components, descriptor) + } + /// Registers a [non-send resource](crate::system::NonSend) of type `T` with this instance. /// If a resource of this type has already been registered, this will return /// the ID of the pre-existing resource. @@ -1069,12 +1088,20 @@ impl Components { let components = &mut self.components; *self.resource_indices.entry(type_id).or_insert_with(|| { let descriptor = func(); - let component_id = ComponentId(components.len()); - components.push(ComponentInfo::new(component_id, descriptor)); - component_id + Components::register_resource_inner(components, descriptor) }) } + #[inline] + fn register_resource_inner( + components: &mut Vec, + descriptor: ComponentDescriptor, + ) -> ComponentId { + let component_id = ComponentId(components.len()); + components.push(ComponentInfo::new(component_id, descriptor)); + component_id + } + /// Gets an iterator over all components registered with this instance. pub fn iter(&self) -> impl Iterator + '_ { self.components.iter() diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 689c9f725f208..60d3c58f62d47 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -50,7 +50,7 @@ pub trait System: Send + Sync + 'static { /// Returns true if the system must be run exclusively. fn is_exclusive(&self) -> bool; - /// Returns true if system as deferred buffers + /// Returns true if system has deferred buffers. fn has_deferred(&self) -> bool; /// Runs the system with the given input in the world. Unlike [`System::run`], this function @@ -106,7 +106,7 @@ pub trait System: Send + Sync + 'static { /// should provide their own safety mechanism to prevent undefined behavior. /// /// This method has to be called directly before [`System::run_unsafe`] with no other (relevant) - /// world mutations inbetween. Otherwise, while it won't lead to any undefined behavior, + /// world mutations in between. Otherwise, while it won't lead to any undefined behavior, /// the validity of the param may change. /// /// # Safety diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 15e4c61464eac..554bb607a55ac 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -336,19 +336,20 @@ impl World { /// Registers a new [`Resource`] type and returns the [`ComponentId`] created for it. /// - /// Note that the resource doesn't have a value in world, it's only registered. - /// if you want to insert the [`Resource`] in the [`World`], use [`World::init_resource`] or [`World::insert_resource`] instead. + /// The [`Resource`] doesn't have a value in the [`World`], it's only registered. If you want + /// to insert the [`Resource`] in the [`World`], use [`World::init_resource`] or + /// [`World::insert_resource`] instead. pub fn register_resource(&mut self) -> ComponentId { self.components.register_resource::() } /// Returns the [`ComponentId`] of the given [`Resource`] type `T`. /// - /// The returned `ComponentId` is specific to the `World` instance - /// it was retrieved from and should not be used with another `World` instance. + /// The returned [`ComponentId`] is specific to the [`World`] instance it was retrieved from + /// and should not be used with another [`World`] instance. /// - /// Returns [`None`] if the `Resource` type has not yet been initialized within - /// the `World` using [`World::register_resource`], [`World::init_resource`] or [`World::insert_resource`]. + /// Returns [`None`] if the [`Resource`] type has not yet been initialized within the + /// [`World`] using [`World::register_resource`], [`World::init_resource`] or [`World::insert_resource`]. pub fn resource_id(&self) -> Option { self.components.get_resource_id(TypeId::of::()) } @@ -1320,6 +1321,23 @@ impl World { .map(Into::into) } + /// Registers a new [`Resource`] type and returns the [`ComponentId`] created for it. + /// + /// This enables the dynamic registration of new [`Resource`] definitions at runtime for + /// advanced use cases. + /// + /// # Note + /// + /// Registering a [`Resource`] does not insert it into [`World`]. For insertion, you could use + /// [`World::insert_resource_by_id`]. + pub fn register_resource_with_descriptor( + &mut self, + descriptor: ComponentDescriptor, + ) -> ComponentId { + self.components + .register_resource_with_descriptor(descriptor) + } + /// Initializes a new resource and returns the [`ComponentId`] created for it. /// /// If the resource already exists, nothing happens. @@ -3248,6 +3266,39 @@ mod tests { ); } + #[test] + fn dynamic_resource() { + let mut world = World::new(); + + let descriptor = ComponentDescriptor::new_resource::(); + + let component_id = world.register_resource_with_descriptor(descriptor); + + let value = 0; + OwningPtr::make(value, |ptr| { + // SAFETY: value is valid for the layout of `TestResource` + unsafe { + world.insert_resource_by_id( + component_id, + ptr, + #[cfg(feature = "track_change_detection")] + panic::Location::caller(), + ); + } + }); + + // SAFETY: We know that the resource is of type `TestResource` + let resource = unsafe { + world + .get_resource_by_id(component_id) + .unwrap() + .deref::() + }; + assert_eq!(resource.0, 0); + + assert!(world.remove_resource_by_id(component_id).is_some()); + } + #[test] fn custom_resource_with_layout() { static DROP_COUNT: AtomicU32 = AtomicU32::new(0); @@ -3268,7 +3319,7 @@ mod tests { ) }; - let component_id = world.register_component_with_descriptor(descriptor); + let component_id = world.register_resource_with_descriptor(descriptor); let value: [u8; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; OwningPtr::make(value, |ptr| { diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 0c3bd81d67a10..0fdf881bb1393 100644 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -103,6 +103,7 @@ use { ShaderStages, ShaderType, VertexFormat, }, renderer::RenderDevice, + world_sync::TemporaryRenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }, bytemuck::cast_slice, @@ -113,7 +114,6 @@ use { any(feature = "bevy_pbr", feature = "bevy_sprite"), ))] use bevy_render::render_resource::{VertexAttribute, VertexBufferLayout, VertexStepMode}; - use bevy_time::Fixed; use bevy_utils::TypeIdMap; use config::{ @@ -459,6 +459,7 @@ fn extract_gizmo_data( (*handle).clone_weak(), #[cfg(any(feature = "bevy_pbr", feature = "bevy_sprite"))] config::GizmoMeshConfig::from(config), + TemporaryRenderEntity, )); } } diff --git a/crates/bevy_input/src/button_input.rs b/crates/bevy_input/src/button_input.rs index 31c27f7330074..d329470d8be70 100644 --- a/crates/bevy_input/src/button_input.rs +++ b/crates/bevy_input/src/button_input.rs @@ -62,8 +62,7 @@ use { /// /// `ButtonInput` is tied to window focus. For example, if the user holds a button /// while the window loses focus, [`ButtonInput::just_released`] will be triggered. Similarly if the window -/// regains focus, [`ButtonInput::just_pressed`] will be triggered. Currently this happens even if the -/// focus switches from one Bevy window to another (for example because a new window was just spawned). +/// regains focus, [`ButtonInput::just_pressed`] will be triggered. /// /// `ButtonInput` is independent of window focus. /// diff --git a/crates/bevy_input/src/keyboard.rs b/crates/bevy_input/src/keyboard.rs index 223003c85c0a0..018c2feb1fec6 100644 --- a/crates/bevy_input/src/keyboard.rs +++ b/crates/bevy_input/src/keyboard.rs @@ -134,7 +134,7 @@ pub struct KeyboardFocusLost; /// ## Differences /// /// The main difference between the [`KeyboardInput`] event and the [`ButtonInput`] resources is that -/// the latter have convenient functions such as [`ButtonInput::pressed`], [`ButtonInput::just_pressed`] and [`ButtonInput::just_released`]. +/// the latter has convenient functions such as [`ButtonInput::pressed`], [`ButtonInput::just_pressed`] and [`ButtonInput::just_released`] and is window id agnostic. pub fn keyboard_input_system( mut key_input: ResMut>, mut keyboard_input_events: EventReader, diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index 6e44d8bc045ec..597fe8e8916ca 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -1817,6 +1817,28 @@ impl Capsule2d { half_length: length / 2.0, } } + + /// Get the part connecting the semicircular ends of the capsule as a [`Rectangle`] + #[inline] + pub fn to_inner_rectangle(&self) -> Rectangle { + Rectangle::new(self.radius * 2.0, self.half_length * 2.0) + } +} + +impl Measured2d for Capsule2d { + /// Get the area of the capsule + #[inline] + fn area(&self) -> f32 { + // pi*r^2 + (2r)*l + PI * self.radius.squared() + self.to_inner_rectangle().area() + } + + /// Get the perimeter of the capsule + #[inline] + fn perimeter(&self) -> f32 { + // 2pi*r + 2l + 2.0 * PI * self.radius + 4.0 * self.half_length + } } #[cfg(test)] @@ -1892,6 +1914,18 @@ mod tests { assert_eq!(circle.perimeter(), 18.849556, "incorrect perimeter"); } + #[test] + fn capsule_math() { + let capsule = Capsule2d::new(2.0, 9.0); + assert_eq!( + capsule.to_inner_rectangle(), + Rectangle::new(4.0, 9.0), + "rectangle wasn't created correctly from a capsule" + ); + assert_eq!(capsule.area(), 48.566371, "incorrect area"); + assert_eq!(capsule.perimeter(), 30.566371, "incorrect perimeter"); + } + #[test] fn annulus_math() { let annulus = Annulus::new(2.5, 3.5); diff --git a/crates/bevy_math/src/sampling/shape_sampling.rs b/crates/bevy_math/src/sampling/shape_sampling.rs index 7e8c672a6bf7c..e1fb0aef7d209 100644 --- a/crates/bevy_math/src/sampling/shape_sampling.rs +++ b/crates/bevy_math/src/sampling/shape_sampling.rs @@ -450,8 +450,7 @@ impl ShapeSample for Capsule2d { if capsule_area > 0.0 { // Check if the random point should be inside the rectangle if rng.gen_bool((rectangle_area / capsule_area) as f64) { - let rectangle = Rectangle::new(self.radius * 2.0, self.half_length * 2.0); - rectangle.sample_interior(rng) + self.to_inner_rectangle().sample_interior(rng) } else { let circle = Circle::new(self.radius); let point = circle.sample_interior(rng); diff --git a/crates/bevy_mikktspace/Cargo.toml b/crates/bevy_mikktspace/Cargo.toml index 2207a1ca2af88..6145bcf9dde87 100644 --- a/crates/bevy_mikktspace/Cargo.toml +++ b/crates/bevy_mikktspace/Cargo.toml @@ -15,8 +15,15 @@ license = "Zlib AND (MIT OR Apache-2.0)" keywords = ["bevy", "3D", "graphics", "algorithm", "tangent"] rust-version = "1.76.0" +[features] +default = ["std"] + +std = ["glam/std"] +libm = ["glam/libm", "dep:libm"] + [dependencies] -glam = "0.29" +glam = { version = "0.29.0", default-features = false } +libm = { version = "0.2", default-features = false, optional = true } [[example]] name = "generate" diff --git a/crates/bevy_mikktspace/src/generated.rs b/crates/bevy_mikktspace/src/generated.rs index b4bf42275a66c..bd3242a459803 100644 --- a/crates/bevy_mikktspace/src/generated.rs +++ b/crates/bevy_mikktspace/src/generated.rs @@ -45,6 +45,7 @@ unsafe_code )] +use alloc::{vec, vec::Vec}; use core::ptr::{self, null_mut}; use glam::Vec3; @@ -211,7 +212,7 @@ pub unsafe fn genTangSpace(geometry: &mut I, fAngularThreshold: f32 let mut index = 0; let iNrFaces = geometry.num_faces(); let mut bRes: bool = false; - let fThresCos = fAngularThreshold.to_radians().cos(); + let fThresCos = cos(fAngularThreshold.to_radians()); f = 0; while f < iNrFaces { let verts = geometry.num_vertices_of_face(f); @@ -630,7 +631,7 @@ unsafe fn VNotZero(v: Vec3) -> bool { } unsafe fn NotZero(fX: f32) -> bool { - fX.abs() > 1.17549435e-38f32 + abs(fX) > 1.17549435e-38f32 } unsafe fn EvalTspace( @@ -724,7 +725,7 @@ unsafe fn EvalTspace( } else { fCos }; - fAngle = (fCos as f64).acos() as f32; + fAngle = acosf64(fCos as f64) as f32; fMagS = (*pTriInfos.offset(f as isize)).fMagS; fMagT = (*pTriInfos.offset(f as isize)).fMagT; res.vOs = res.vOs + (fAngle * vOs); @@ -1010,7 +1011,7 @@ unsafe fn InitTriInfo( 0i32 }; if NotZero(fSignedAreaSTx2) { - let fAbsArea: f32 = fSignedAreaSTx2.abs(); + let fAbsArea: f32 = abs(fSignedAreaSTx2); let fLenOs: f32 = vOs.length(); let fLenOt: f32 = vOt.length(); let fS: f32 = if (*pTriInfos.offset(f as isize)).iFlag & 8i32 == 0i32 { @@ -1808,3 +1809,63 @@ unsafe fn GenerateInitialVerticesIndexList( } return iTSpacesOffs; } + +fn cos(value: f32) -> f32 { + #[cfg(feature = "std")] + { + value.cos() + } + #[cfg(all(not(feature = "std"), feature = "libm"))] + { + libm::cosf(value) + } + #[cfg(all(not(feature = "std"), not(feature = "libm")))] + { + compile_error!("Require either 'libm' or 'std' for `cos`") + } +} + +fn acos(value: f32) -> f32 { + #[cfg(feature = "std")] + { + value.acos() + } + #[cfg(all(not(feature = "std"), feature = "libm"))] + { + libm::acosf(value) + } + #[cfg(all(not(feature = "std"), not(feature = "libm")))] + { + compile_error!("Require either 'libm' or 'std' for `acos`") + } +} + +fn abs(value: f32) -> f32 { + #[cfg(feature = "std")] + { + value.abs() + } + #[cfg(all(not(feature = "std"), feature = "libm"))] + { + libm::fabsf(value) + } + #[cfg(all(not(feature = "std"), not(feature = "libm")))] + { + compile_error!("Require either 'libm' or 'std' for `abs`") + } +} + +fn acosf64(value: f64) -> f64 { + #[cfg(feature = "std")] + { + value.acos() + } + #[cfg(all(not(feature = "std"), feature = "libm"))] + { + libm::acos(value) + } + #[cfg(all(not(feature = "std"), not(feature = "libm")))] + { + compile_error!("Require either 'libm' or 'std' for `acos`") + } +} diff --git a/crates/bevy_mikktspace/src/lib.rs b/crates/bevy_mikktspace/src/lib.rs index 50526edab2dba..fe514e208eb47 100644 --- a/crates/bevy_mikktspace/src/lib.rs +++ b/crates/bevy_mikktspace/src/lib.rs @@ -11,6 +11,9 @@ html_logo_url = "https://bevyengine.org/assets/icon.png", html_favicon_url = "https://bevyengine.org/assets/icon.png" )] +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; use glam::{Vec2, Vec3}; diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 7c6b9fef9fe30..d5ffebf981f43 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -15,6 +15,7 @@ use bevy_render::{ mesh::Mesh, primitives::{CascadesFrusta, CubemapFrusta, Frustum}, view::{InheritedVisibility, ViewVisibility, Visibility}, + world_sync::SyncToRenderWorld, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -108,6 +109,8 @@ pub struct PointLightBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } /// A component bundle for spot light entities @@ -124,6 +127,8 @@ pub struct SpotLightBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } /// A component bundle for [`DirectionalLight`] entities. @@ -142,4 +147,6 @@ pub struct DirectionalLightBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index b0933a3fd2fc2..9c77a02149fc3 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -20,6 +20,7 @@ use bevy_render::{ UniformBuffer, }, renderer::{RenderDevice, RenderQueue}, + world_sync::RenderEntity, Extract, }; use bevy_utils::{hashbrown::HashSet, tracing::warn}; @@ -525,7 +526,8 @@ pub(crate) fn clusterable_object_order( /// Extracts clusters from the main world from the render world. pub fn extract_clusters( mut commands: Commands, - views: Extract>, + views: Extract>, + mapper: Extract>, ) { for (entity, clusters, camera) in &views { if !camera.is_active { @@ -544,13 +546,15 @@ pub fn extract_clusters( cluster_objects.spot_light_count as u32, )); for clusterable_entity in &cluster_objects.entities { - data.push(ExtractedClusterableObjectElement::ClusterableObjectEntity( - *clusterable_entity, - )); + if let Ok(entity) = mapper.get(*clusterable_entity) { + data.push(ExtractedClusterableObjectElement::ClusterableObjectEntity( + entity.id(), + )); + } } } - commands.get_or_spawn(entity).insert(( + commands.get_or_spawn(entity.id()).insert(( ExtractedClusterableObjects { data }, ExtractedClusterConfig { near: clusters.near, diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index feb6b3d4fccf0..c48601b889da3 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -439,6 +439,9 @@ impl Plugin for PbrPlugin { ) .init_resource::(); + render_app.world_mut().observe(add_light_view_entities); + render_app.world_mut().observe(remove_light_view_entities); + let shadow_pass_node = ShadowPassNode::new(render_app.world_mut()); let mut graph = render_app.world_mut().resource_mut::(); let draw_3d_graph = graph.get_sub_graph_mut(Core3d).unwrap(); diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index f110dc6f82c2f..eb1b4ccec4f91 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -23,6 +23,7 @@ use bevy_render::{ settings::WgpuFeatures, texture::{FallbackImage, GpuImage, Image}, view::ExtractedView, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_transform::{components::Transform, prelude::GlobalTransform}; @@ -371,7 +372,7 @@ impl Plugin for LightProbePlugin { /// Compared to the `ExtractComponentPlugin`, this implementation will create a default instance /// if one does not already exist. fn gather_environment_map_uniform( - view_query: Extract), With>>, + view_query: Extract), With>>, mut commands: Commands, ) { for (view_entity, environment_map_light) in view_query.iter() { @@ -385,7 +386,7 @@ fn gather_environment_map_uniform( EnvironmentMapUniform::default() }; commands - .get_or_spawn(view_entity) + .get_or_spawn(view_entity.id()) .insert(environment_map_uniform); } } @@ -395,7 +396,9 @@ fn gather_environment_map_uniform( fn gather_light_probes( image_assets: Res>, light_probe_query: Extract>>, - view_query: Extract), With>>, + view_query: Extract< + Query<(&RenderEntity, &GlobalTransform, &Frustum, Option<&C>), With>, + >, mut reflection_probes: Local>>, mut view_reflection_probes: Local>>, mut commands: Commands, @@ -433,14 +436,15 @@ fn gather_light_probes( // Gather up the light probes in the list. render_view_light_probes.maybe_gather_light_probes(&view_reflection_probes); + let entity = view_entity.id(); // Record the per-view light probes. if render_view_light_probes.is_empty() { commands - .get_or_spawn(view_entity) + .get_or_spawn(entity) .remove::>(); } else { commands - .get_or_spawn(view_entity) + .get_or_spawn(entity) .insert(render_view_light_probes); } } diff --git a/crates/bevy_pbr/src/meshlet/instance_manager.rs b/crates/bevy_pbr/src/meshlet/instance_manager.rs index 161cbf7f8b963..4c609848a600e 100644 --- a/crates/bevy_pbr/src/meshlet/instance_manager.rs +++ b/crates/bevy_pbr/src/meshlet/instance_manager.rs @@ -178,21 +178,21 @@ pub fn extract_meshlet_mesh_entities( Res, ResMut>, EventReader>, - &Entities, )>, >, >, + render_entities: &Entities, ) { // Get instances query if system_state.is_none() { *system_state = Some(SystemState::new(&mut main_world)); } let system_state = system_state.as_mut().unwrap(); - let (instances_query, asset_server, mut assets, mut asset_events, entities) = + let (instances_query, asset_server, mut assets, mut asset_events) = system_state.get_mut(&mut main_world); // Reset per-frame data - instance_manager.reset(entities); + instance_manager.reset(render_entities); // Free GPU buffer space for any modified or dropped MeshletMesh assets for asset_event in asset_events.read() { diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 6cf3d8f6419f1..166ef0c2cc1c1 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -4,6 +4,7 @@ use bevy_render::{ mesh::{MeshVertexBufferLayoutRef, RenderMesh}, render_resource::binding_types::uniform_buffer, view::WithMesh, + world_sync::RenderEntity, }; pub use prepass_bindings::*; @@ -580,10 +581,11 @@ where // Extract the render phases for the prepass pub fn extract_camera_previous_view_data( mut commands: Commands, - cameras_3d: Extract), With>>, + cameras_3d: Extract), With>>, ) { for (entity, camera, maybe_previous_view_data) in cameras_3d.iter() { if camera.is_active { + let entity = entity.id(); let entity = commands.get_or_spawn(entity); if let Some(previous_view_data) = maybe_previous_view_data { diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index b3f83eb5a5621..212737e058be3 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,12 +1,14 @@ use bevy_asset::UntypedAssetId; use bevy_color::ColorToComponents; use bevy_core_pipeline::core_3d::{Camera3d, CORE_3D_DEPTH_FORMAT}; +use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ entity::{EntityHashMap, EntityHashSet}, prelude::*, system::lifetimeless::Read, }; use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; +use bevy_render::world_sync::RenderEntity; use bevy_render::{ diagnostic::RecordDiagnostics, mesh::RenderMesh, @@ -192,6 +194,7 @@ pub fn extract_lights( global_point_lights: Extract>, point_lights: Extract< Query<( + &RenderEntity, &PointLight, &CubemapVisibleEntities, &GlobalTransform, @@ -202,6 +205,7 @@ pub fn extract_lights( >, spot_lights: Extract< Query<( + &RenderEntity, &SpotLight, &VisibleMeshEntities, &GlobalTransform, @@ -213,7 +217,7 @@ pub fn extract_lights( directional_lights: Extract< Query< ( - Entity, + &RenderEntity, &DirectionalLight, &CascadesVisibleEntities, &Cascades, @@ -227,6 +231,7 @@ pub fn extract_lights( Without, >, >, + mapper: Extract>, mut previous_point_lights_len: Local, mut previous_spot_lights_len: Local, ) { @@ -250,6 +255,7 @@ pub fn extract_lights( let mut point_lights_values = Vec::with_capacity(*previous_point_lights_len); for entity in global_point_lights.iter().copied() { let Ok(( + render_entity, point_light, cubemap_visible_entities, transform, @@ -287,7 +293,7 @@ pub fn extract_lights( volumetric: volumetric_light.is_some(), }; point_lights_values.push(( - entity, + render_entity.id(), ( extracted_point_light, render_cubemap_visible_entities, @@ -301,6 +307,7 @@ pub fn extract_lights( let mut spot_lights_values = Vec::with_capacity(*previous_spot_lights_len); for entity in global_point_lights.iter().copied() { if let Ok(( + render_entity, spot_light, visible_entities, transform, @@ -319,7 +326,7 @@ pub fn extract_lights( 2.0 * ops::tan(spot_light.outer_angle) / directional_light_shadow_map.size as f32; spot_lights_values.push(( - entity, + render_entity.id(), ( ExtractedPointLight { color: spot_light.color.into(), @@ -370,9 +377,33 @@ pub fn extract_lights( continue; } - // TODO: As above - let render_visible_entities = visible_entities.clone(); - commands.get_or_spawn(entity).insert(( + // TODO: update in place instead of reinserting. + let mut extracted_cascades = EntityHashMap::default(); + let mut extracted_frusta = EntityHashMap::default(); + let mut cascade_visible_entities = EntityHashMap::default(); + for (e, v) in cascades.cascades.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_cascades.insert(entity.id(), v.clone()); + } else { + break; + } + } + for (e, v) in frusta.frusta.iter() { + if let Ok(entity) = mapper.get(*e) { + extracted_frusta.insert(entity.id(), v.clone()); + } else { + break; + } + } + for (e, v) in visible_entities.entities.iter() { + if let Ok(entity) = mapper.get(*e) { + cascade_visible_entities.insert(entity.id(), v.clone()); + } else { + break; + } + } + + commands.get_or_spawn(entity.id()).insert(( ExtractedDirectionalLight { color: directional_light.color.into(), illuminance: directional_light.illuminance, @@ -385,15 +416,44 @@ pub fn extract_lights( shadow_normal_bias: directional_light.shadow_normal_bias * core::f32::consts::SQRT_2, cascade_shadow_config: cascade_config.clone(), - cascades: cascades.cascades.clone(), - frusta: frusta.frusta.clone(), + cascades: extracted_cascades, + frusta: extracted_frusta, render_layers: maybe_layers.unwrap_or_default().clone(), }, - render_visible_entities, + CascadesVisibleEntities { + entities: cascade_visible_entities, + }, )); } } +#[derive(Component, Default, Deref, DerefMut)] +pub struct LightViewEntities(Vec); + +// TODO: using required component +pub(crate) fn add_light_view_entities( + trigger: Trigger, + mut commands: Commands, +) { + commands + .get_entity(trigger.entity()) + .map(|v| v.insert(LightViewEntities::default())); +} + +pub(crate) fn remove_light_view_entities( + trigger: Trigger, + query: Query<&LightViewEntities>, + mut commands: Commands, +) { + if let Ok(entities) = query.get(trigger.entity()) { + for e in entities.0.iter().copied() { + if let Some(v) = commands.get_entity(e) { + v.despawn(); + } + } + } +} + pub(crate) struct CubeMapFace { pub(crate) target: Vec3, pub(crate) up: Vec3, @@ -564,14 +624,17 @@ pub fn prepare_lights( point_light_shadow_map: Res, directional_light_shadow_map: Res, mut shadow_render_phases: ResMut>, - mut max_directional_lights_warning_emitted: Local, - mut max_cascades_per_light_warning_emitted: Local, + (mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): ( + Local, + Local, + ), point_lights: Query<( Entity, &ExtractedPointLight, AnyOf<(&CubemapFrusta, &Frustum)>, )>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, + mut light_view_entities: Query<&mut LightViewEntities>, mut live_shadow_mapping_lights: Local, ) { let views_iter = views.iter(); @@ -862,8 +925,9 @@ pub fn prepare_lights( live_shadow_mapping_lights.clear(); + let mut dir_light_view_offset = 0; // set up light data for each view - for (entity, extracted_view, clusters, maybe_layers) in &views { + for (offset, (entity, extracted_view, clusters, maybe_layers)) in views.iter().enumerate() { let point_light_depth_texture = texture_cache.get( &render_device, TextureDescriptor { @@ -949,15 +1013,25 @@ pub fn prepare_lights( // and ignore rotation because we want the shadow map projections to align with the axes let view_translation = GlobalTransform::from_translation(light.transform.translation()); + let Ok(mut light_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + + // for each face of a cube and each view we spawn a light entity + while light_entities.len() < 6 * (offset + 1) { + light_entities.push(commands.spawn_empty().id()); + } + let cube_face_projection = Mat4::perspective_infinite_reverse_rh( core::f32::consts::FRAC_PI_2, 1.0, light.shadow_map_near_z, ); - for (face_index, (view_rotation, frustum)) in cube_face_rotations + for (face_index, ((view_rotation, frustum), view_light_entity)) in cube_face_rotations .iter() .zip(&point_light_frusta.unwrap().frusta) + .zip(light_entities.iter().skip(6 * offset).copied()) .enumerate() { let depth_texture_view = @@ -974,36 +1048,35 @@ pub fn prepare_lights( array_layer_count: Some(1u32), }); - let view_light_entity = commands - .spawn(( - ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), - pass_name: format!( - "shadow pass point light {} {}", - light_index, - face_index_to_name(face_index) - ), - }, - ExtractedView { - viewport: UVec4::new( - 0, - 0, - point_light_shadow_map.size as u32, - point_light_shadow_map.size as u32, - ), - world_from_view: view_translation * *view_rotation, - clip_from_world: None, - clip_from_view: cube_face_projection, - hdr: false, - color_grading: Default::default(), - }, - *frustum, - LightEntity::Point { - light_entity, - face_index, - }, - )) - .id(); + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + pass_name: format!( + "shadow pass point light {} {}", + light_index, + face_index_to_name(face_index) + ), + }, + ExtractedView { + viewport: UVec4::new( + 0, + 0, + point_light_shadow_map.size as u32, + point_light_shadow_map.size as u32, + ), + world_from_view: view_translation * *view_rotation, + clip_from_world: None, + clip_from_view: cube_face_projection, + hdr: false, + color_grading: Default::default(), + }, + *frustum, + LightEntity::Point { + light_entity, + face_index, + }, + )); + view_lights.push(view_light_entity); shadow_render_phases.insert_or_clear(view_light_entity); @@ -1021,6 +1094,10 @@ pub fn prepare_lights( let spot_world_from_view = spot_light_world_from_view(&light.transform); let spot_world_from_view = spot_world_from_view.into(); + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; + let angle = light.spot_light_angles.expect("lights should be sorted so that \ [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); @@ -1039,29 +1116,33 @@ pub fn prepare_lights( array_layer_count: Some(1u32), }); - let view_light_entity = commands - .spawn(( - ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), - pass_name: format!("shadow pass spot light {light_index}"), - }, - ExtractedView { - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - world_from_view: spot_world_from_view, - clip_from_view: spot_projection, - clip_from_world: None, - hdr: false, - color_grading: Default::default(), - }, - *spot_light_frustum.unwrap(), - LightEntity::Spot { light_entity }, - )) - .id(); + while light_view_entities.len() < offset + 1 { + light_view_entities.push(commands.spawn_empty().id()); + } + + let view_light_entity = light_view_entities[offset]; + + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + pass_name: format!("shadow pass spot light {light_index}"), + }, + ExtractedView { + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: spot_world_from_view, + clip_from_view: spot_projection, + clip_from_world: None, + hdr: false, + color_grading: Default::default(), + }, + *spot_light_frustum.unwrap(), + LightEntity::Spot { light_entity }, + )); view_lights.push(view_light_entity); @@ -1079,6 +1160,9 @@ pub fn prepare_lights( { let gpu_light = &mut gpu_lights.directional_lights[light_index]; + let Ok(mut light_view_entities) = light_view_entities.get_mut(light_entity) else { + continue; + }; // Check if the light intersects with the view. if !view_layers.intersects(&light.render_layers) { gpu_light.skip = 1u32; @@ -1102,9 +1186,22 @@ pub fn prepare_lights( .unwrap() .iter() .take(MAX_CASCADES_PER_LIGHT); - for (cascade_index, ((cascade, frustum), bound)) in cascades + + let iter = cascades .zip(frusta) - .zip(&light.cascade_shadow_config.bounds) + .zip(&light.cascade_shadow_config.bounds); + + while light_view_entities.len() < dir_light_view_offset + iter.len() { + light_view_entities.push(commands.spawn_empty().id()); + } + + for (cascade_index, (((cascade, frustum), bound), view_light_entity)) in iter + .zip( + light_view_entities + .iter() + .skip(dir_light_view_offset) + .copied(), + ) .enumerate() { gpu_lights.directional_lights[light_index].cascades[cascade_index] = @@ -1134,37 +1231,37 @@ pub fn prepare_lights( frustum.half_spaces[4] = HalfSpace::new(frustum.half_spaces[4].normal().extend(f32::INFINITY)); - let view_light_entity = commands - .spawn(( - ShadowView { - depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), - pass_name: format!( - "shadow pass directional light {light_index} cascade {cascade_index}"), - }, - ExtractedView { - viewport: UVec4::new( - 0, - 0, - directional_light_shadow_map.size as u32, - directional_light_shadow_map.size as u32, - ), - world_from_view: GlobalTransform::from(cascade.world_from_cascade), - clip_from_view: cascade.clip_from_cascade, - clip_from_world: Some(cascade.clip_from_world), - hdr: false, - color_grading: Default::default(), - }, - frustum, - LightEntity::Directional { - light_entity, - cascade_index, - }, - )) - .id(); + commands.entity(view_light_entity).insert(( + ShadowView { + depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), + pass_name: format!( + "shadow pass directional light {light_index} cascade {cascade_index}" + ), + }, + ExtractedView { + viewport: UVec4::new( + 0, + 0, + directional_light_shadow_map.size as u32, + directional_light_shadow_map.size as u32, + ), + world_from_view: GlobalTransform::from(cascade.world_from_cascade), + clip_from_view: cascade.clip_from_cascade, + clip_from_world: Some(cascade.clip_from_world), + hdr: false, + color_grading: Default::default(), + }, + frustum, + LightEntity::Directional { + light_entity, + cascade_index, + }, + )); view_lights.push(view_light_entity); shadow_render_phases.insert_or_clear(view_light_entity); live_shadow_mapping_lights.insert(view_light_entity); + dir_light_view_offset += 1; } } diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index ffbd7584583af..7495960391306 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -30,6 +30,7 @@ use bevy_render::{ renderer::{RenderAdapter, RenderContext, RenderDevice, RenderQueue}, texture::{CachedTexture, TextureCache}, view::{Msaa, ViewUniform, ViewUniformOffset, ViewUniforms}, + world_sync::RenderEntity, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_utils::{ @@ -488,7 +489,7 @@ fn extract_ssao_settings( mut commands: Commands, cameras: Extract< Query< - (Entity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa), + (&RenderEntity, &Camera, &ScreenSpaceAmbientOcclusion, &Msaa), (With, With, With), >, >, @@ -501,9 +502,10 @@ fn extract_ssao_settings( ); return; } - if camera.is_active { - commands.get_or_spawn(entity).insert(ssao_settings.clone()); + commands + .get_or_spawn(entity.id()) + .insert(ssao_settings.clone()); } } } diff --git a/crates/bevy_pbr/src/volumetric_fog/render.rs b/crates/bevy_pbr/src/volumetric_fog/render.rs index 126d2fddf7dd3..b833197d4ac99 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -38,6 +38,7 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice, RenderQueue}, texture::{BevyDefault as _, GpuImage, Image}, view::{ExtractedView, Msaa, ViewDepthTexture, ViewTarget, ViewUniformOffset}, + world_sync::RenderEntity, Extract, }; use bevy_transform::components::GlobalTransform; @@ -270,27 +271,27 @@ impl FromWorld for VolumetricFogPipeline { /// from the main world to the render world. pub fn extract_volumetric_fog( mut commands: Commands, - view_targets: Extract>, - fog_volumes: Extract>, - volumetric_lights: Extract>, + view_targets: Extract>, + fog_volumes: Extract>, + volumetric_lights: Extract>, ) { if volumetric_lights.is_empty() { return; } for (entity, volumetric_fog) in view_targets.iter() { - commands.get_or_spawn(entity).insert(*volumetric_fog); + commands.get_or_spawn(entity.id()).insert(*volumetric_fog); } for (entity, fog_volume, fog_transform) in fog_volumes.iter() { commands - .get_or_spawn(entity) + .get_or_spawn(entity.id()) .insert((*fog_volume).clone()) .insert(*fog_transform); } for (entity, volumetric_light) in volumetric_lights.iter() { - commands.get_or_spawn(entity).insert(*volumetric_light); + commands.get_or_spawn(entity.id()).insert(*volumetric_light); } } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 3fd905bdcf104..1cd6238a5073c 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -10,6 +10,7 @@ use crate::{ view::{ ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, RenderLayers, VisibleEntities, }, + world_sync::RenderEntity, Extract, }; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; @@ -935,7 +936,7 @@ pub fn extract_cameras( mut commands: Commands, query: Extract< Query<( - Entity, + &RenderEntity, &Camera, &CameraRenderGraph, &GlobalTransform, @@ -954,7 +955,7 @@ pub fn extract_cameras( ) { let primary_window = primary_window.iter().next(); for ( - entity, + render_entity, camera, camera_render_graph, transform, @@ -968,11 +969,10 @@ pub fn extract_cameras( gpu_culling, ) in query.iter() { - let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone(); - if !camera.is_active { continue; } + let color_grading = color_grading.unwrap_or(&ColorGrading::default()).clone(); if let ( Some(URect { @@ -990,7 +990,8 @@ pub fn extract_cameras( continue; } - let mut commands = commands.get_or_spawn(entity).insert(( + let mut commands = commands.entity(render_entity.id()); + commands = commands.insert(( ExtractedCamera { target: camera.target.normalize(primary_window), viewport: camera.viewport.clone(), @@ -1036,7 +1037,6 @@ pub fn extract_cameras( if let Some(perspective) = projection { commands = commands.insert(perspective.clone()); } - if gpu_culling { if *gpu_preprocessing_support == GpuPreprocessingSupport::Culling { commands.insert(GpuCulling); @@ -1046,7 +1046,7 @@ pub fn extract_cameras( ); } } - } + }; } } diff --git a/crates/bevy_render/src/extract_component.rs b/crates/bevy_render/src/extract_component.rs index 01b6b06ceacaa..cfbdaa4f1d078 100644 --- a/crates/bevy_render/src/extract_component.rs +++ b/crates/bevy_render/src/extract_component.rs @@ -2,6 +2,7 @@ use crate::{ render_resource::{encase::internal::WriteInto, DynamicUniformBuffer, ShaderType}, renderer::{RenderDevice, RenderQueue}, view::ViewVisibility, + world_sync::{RenderEntity, SyncToRenderWorld}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin}; @@ -11,6 +12,7 @@ use bevy_ecs::{ prelude::*, query::{QueryFilter, QueryItem, ReadOnlyQueryData}, system::lifetimeless::Read, + world::OnAdd, }; use core::{marker::PhantomData, ops::Deref}; @@ -155,10 +157,18 @@ fn prepare_uniform_components( commands.insert_or_spawn_batch(entities); } -/// This plugin extracts the components into the "render world". +/// This plugin extracts the components into the render world for synced entities. /// -/// Therefore it sets up the [`ExtractSchedule`] step -/// for the specified [`ExtractComponent`]. +/// To do so, it sets up the [`ExtractSchedule`] step for the specified [`ExtractComponent`]. +/// +/// # Warning +/// +/// Be careful when removing the [`ExtractComponent`] from an entity. When an [`ExtractComponent`] +/// is added to an entity, that entity is automatically synced with the render world (see also +/// [`WorldSyncPlugin`](crate::world_sync::WorldSyncPlugin)). When removing the entity in the main +/// world, the synced entity also gets removed. However, if only the [`ExtractComponent`] is removed +/// this *doesn't* happen, and the synced entity stays around with the old extracted data. +/// We recommend despawning the entire entity, instead of only removing [`ExtractComponent`]. pub struct ExtractComponentPlugin { only_extract_visible: bool, marker: PhantomData (C, F)>, @@ -184,6 +194,10 @@ impl ExtractComponentPlugin { impl Plugin for ExtractComponentPlugin { fn build(&self, app: &mut App) { + // TODO: use required components + app.observe(|trigger: Trigger, mut commands: Commands| { + commands.entity(trigger.entity()).insert(SyncToRenderWorld); + }); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { if self.only_extract_visible { render_app.add_systems(ExtractSchedule, extract_visible_components::); @@ -205,33 +219,33 @@ impl ExtractComponent for Handle { } } -/// This system extracts all components of the corresponding [`ExtractComponent`] type. +/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are synced via [`SyncToRenderWorld`]. fn extract_components( mut commands: Commands, mut previous_len: Local, - query: Extract>, + query: Extract>, ) { let mut values = Vec::with_capacity(*previous_len); for (entity, query_item) in &query { if let Some(component) = C::extract_component(query_item) { - values.push((entity, component)); + values.push((entity.id(), component)); } } *previous_len = values.len(); commands.insert_or_spawn_batch(values); } -/// This system extracts all visible components of the corresponding [`ExtractComponent`] type. +/// This system extracts all components of the corresponding [`ExtractComponent`], for entities that are visible and synced via [`SyncToRenderWorld`]. fn extract_visible_components( mut commands: Commands, mut previous_len: Local, - query: Extract>, + query: Extract>, ) { let mut values = Vec::with_capacity(*previous_len); for (entity, view_visibility, query_item) in &query { if view_visibility.get() { if let Some(component) = C::extract_component(query_item) { - values.push((entity, component)); + values.push((entity.id(), component)); } } } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index c69ddac032e63..51bd01c612d30 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -40,6 +40,7 @@ mod spatial_bundle; pub mod storage; pub mod texture; pub mod view; +pub mod world_sync; /// The render prelude. /// @@ -73,6 +74,9 @@ use extract_resource::ExtractResourcePlugin; use globals::GlobalsPlugin; use render_asset::RenderAssetBytesPerFrame; use renderer::{RenderAdapter, RenderAdapterInfo, RenderDevice, RenderQueue}; +use world_sync::{ + despawn_temporary_render_entities, entity_sync_system, SyncToRenderWorld, WorldSyncPlugin, +}; use crate::gpu_readback::GpuReadbackPlugin; use crate::{ @@ -364,6 +368,7 @@ impl Plugin for RenderPlugin { GlobalsPlugin, MorphPlugin, BatchingPlugin, + WorldSyncPlugin, StoragePlugin, GpuReadbackPlugin::default(), )); @@ -377,7 +382,8 @@ impl Plugin for RenderPlugin { .register_type::() .register_type::() .register_type::() - .register_type::(); + .register_type::() + .register_type::(); } fn ready(&self, app: &App) -> bool { @@ -484,35 +490,15 @@ unsafe fn initialize_render_app(app: &mut App) { render_system, ) .in_set(RenderSet::Render), - World::clear_entities.in_set(RenderSet::PostCleanup), + despawn_temporary_render_entities.in_set(RenderSet::PostCleanup), ), ); render_app.set_extract(|main_world, render_world| { - #[cfg(feature = "trace")] - let _render_span = bevy_utils::tracing::info_span!("extract main app to render subapp").entered(); { #[cfg(feature = "trace")] - let _stage_span = - bevy_utils::tracing::info_span!("reserve_and_flush") - .entered(); - - // reserve all existing main world entities for use in render_app - // they can only be spawned using `get_or_spawn()` - let total_count = main_world.entities().total_count(); - - assert_eq!( - render_world.entities().len(), - 0, - "An entity was spawned after the entity list was cleared last frame and before the extract schedule began. This is not supported", - ); - - // SAFETY: This is safe given the clear_entities call in the past frame and the assert above - unsafe { - render_world - .entities_mut() - .flush_and_reserve_invalid_assuming_no_entities(total_count); - } + let _stage_span = bevy_utils::tracing::info_span!("entity_sync").entered(); + entity_sync_system(main_world, render_world); } // run extract schedule diff --git a/crates/bevy_render/src/pipelined_rendering.rs b/crates/bevy_render/src/pipelined_rendering.rs index 7abca128457aa..f17209665b7b5 100644 --- a/crates/bevy_render/src/pipelined_rendering.rs +++ b/crates/bevy_render/src/pipelined_rendering.rs @@ -84,13 +84,15 @@ impl Drop for RenderAppChannels { /// A single frame of execution looks something like below /// /// ```text -/// |--------------------------------------------------------------------| -/// | | RenderExtractApp schedule | winit events | main schedule | -/// | extract |----------------------------------------------------------| -/// | | extract commands | rendering schedule | -/// |--------------------------------------------------------------------| +/// |---------------------------------------------------------------------------| +/// | | | RenderExtractApp schedule | winit events | main schedule | +/// | sync | extract |----------------------------------------------------------| +/// | | | extract commands | rendering schedule | +/// |---------------------------------------------------------------------------| /// ``` /// +/// - `sync` is the step where the entity-entity mapping between the main and render world is updated. +/// This is run on the main app's thread. For more information checkout [`WorldSyncPlugin`]. /// - `extract` is the step where data is copied from the main world to the render world. /// This is run on the main app's thread. /// - On the render thread, we first apply the `extract commands`. This is not run during extract, so the @@ -101,6 +103,8 @@ impl Drop for RenderAppChannels { /// - Next all the `winit events` are processed. /// - And finally the `main app schedule` is run. /// - Once both the `main app schedule` and the `render schedule` are finished running, `extract` is run again. +/// +/// [`WorldSyncPlugin`]: crate::world_sync::WorldSyncPlugin #[derive(Default)] pub struct PipelinedRenderingPlugin; diff --git a/crates/bevy_render/src/world_sync.rs b/crates/bevy_render/src/world_sync.rs new file mode 100644 index 0000000000000..cec1ce748c386 --- /dev/null +++ b/crates/bevy_render/src/world_sync.rs @@ -0,0 +1,268 @@ +use bevy_app::Plugin; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::Trigger, + query::With, + reflect::ReflectComponent, + system::{Local, Query, ResMut, Resource, SystemState}, + world::{Mut, OnAdd, OnRemove, World}, +}; +use bevy_reflect::Reflect; + +/// A plugin that synchronizes entities with [`SyncToRenderWorld`] between the main world and the render world. +/// +/// Bevy's renderer is architected independently from the main app. +/// It operates in its own separate ECS [`World`], so the renderer logic can run in parallel with the main world logic. +/// This is called "Pipelined Rendering", see [`PipelinedRenderingPlugin`] for more information. +/// +/// [`WorldSyncPlugin`] is the first thing that runs every frame and it maintains an entity-to-entity mapping +/// between the main world and the render world. +/// It does so by spawning and despawning entities in the render world, to match spawned and despawned entities in the main world. +/// The link between synced entities is maintained by the [`RenderEntity`] and [`MainEntity`] components. +/// The [`RenderEntity`] contains the corresponding render world entity of a main world entity, while [`MainEntity`] contains +/// the corresponding main world entity of a render world entity. +/// The entities can be accessed by calling `.id()` on either component. +/// +/// Synchronization is necessary preparation for extraction ([`ExtractSchedule`](crate::ExtractSchedule)), which copies over component data from the main +/// to the render world for these entities. +/// +/// ```text +/// |--------------------------------------------------------------------| +/// | | | Main world update | +/// | sync | extract |---------------------------------------------------| +/// | | | Render world update | +/// |--------------------------------------------------------------------| +/// ``` +/// +/// An example for synchronized main entities 1v1 and 18v1 +/// +/// ```text +/// |---------------------------Main World------------------------------| +/// | Entity | Component | +/// |-------------------------------------------------------------------| +/// | ID: 1v1 | PointLight | RenderEntity(ID: 3V1) | SyncToRenderWorld | +/// | ID: 18v1 | PointLight | RenderEntity(ID: 5V1) | SyncToRenderWorld | +/// |-------------------------------------------------------------------| +/// +/// |----------Render World-----------| +/// | Entity | Component | +/// |---------------------------------| +/// | ID: 3v1 | MainEntity(ID: 1V1) | +/// | ID: 5v1 | MainEntity(ID: 18V1) | +/// |---------------------------------| +/// +/// ``` +/// +/// Note that this effectively establishes a link between the main world entity and the render world entity. +/// Not every entity needs to be synchronized, however; only entities with the [`SyncToRenderWorld`] component are synced. +/// Adding [`SyncToRenderWorld`] to a main world component will establish such a link. +/// Once a synchronized main entity is despawned, its corresponding render entity will be automatically +/// despawned in the next `sync`. +/// +/// The sync step does not copy any of component data between worlds, since its often not necessary to transfer over all +/// the components of a main world entity. +/// The render world probably cares about a `Position` component, but not a `Velocity` component. +/// The extraction happens in its own step, independently from, and after synchronization. +/// +/// Moreover, [`WorldSyncPlugin`] only synchronizes *entities*. [`RenderAsset`](crate::render_asset::RenderAsset)s like meshes and textures are handled +/// differently. +/// +/// [`PipelinedRenderingPlugin`]: crate::pipelined_rendering::PipelinedRenderingPlugin +#[derive(Default)] +pub struct WorldSyncPlugin; + +impl Plugin for WorldSyncPlugin { + fn build(&self, app: &mut bevy_app::App) { + app.init_resource::(); + app.observe( + |trigger: Trigger, mut pending: ResMut| { + pending.push(EntityRecord::Added(trigger.entity())); + }, + ); + app.observe( + |trigger: Trigger, + mut pending: ResMut, + query: Query<&RenderEntity>| { + if let Ok(e) = query.get(trigger.entity()) { + pending.push(EntityRecord::Removed(e.id())); + }; + }, + ); + } +} +/// Marker component that indicates that its entity needs to be synchronized to the render world +/// +/// NOTE: This component should persist throughout the entity's entire lifecycle. +/// If this component is removed from its entity, the entity will be despawned. +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect[Component]] +#[component(storage = "SparseSet")] +pub struct SyncToRenderWorld; + +/// Component added on the main world entities that are synced to the Render World in order to keep track of the corresponding render world entity +#[derive(Component, Deref, Clone, Debug, Copy)] +pub struct RenderEntity(Entity); +impl RenderEntity { + #[inline] + pub fn id(&self) -> Entity { + self.0 + } +} + +/// Component added on the render world entities to keep track of the corresponding main world entity +#[derive(Component, Deref, Clone, Debug)] +pub struct MainEntity(Entity); +impl MainEntity { + #[inline] + pub fn id(&self) -> Entity { + self.0 + } +} + +/// Marker component that indicates that its entity needs to be despawned at the end of the frame. +#[derive(Component, Clone, Debug, Default, Reflect)] +#[component(storage = "SparseSet")] +pub struct TemporaryRenderEntity; + +/// A record enum to what entities with [`SyncToRenderWorld`] have been added or removed. +pub(crate) enum EntityRecord { + /// When an entity is spawned on the main world, notify the render world so that it can spawn a corresponding + /// entity. This contains the main world entity. + Added(Entity), + /// When an entity is despawned on the main world, notify the render world so that the corresponding entity can be + /// despawned. This contains the render world entity. + Removed(Entity), +} + +// Entity Record in MainWorld pending to Sync +#[derive(Resource, Default, Deref, DerefMut)] +pub(crate) struct PendingSyncEntity { + records: Vec, +} + +pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut World) { + main_world.resource_scope(|world, mut pending: Mut| { + // TODO : batching record + for record in pending.drain(..) { + match record { + EntityRecord::Added(e) => { + if let Some(mut entity) = world.get_entity_mut(e) { + match entity.entry::() { + bevy_ecs::world::Entry::Occupied(_) => { + panic!("Attempting to synchronize an entity that has already been synchronized!"); + } + bevy_ecs::world::Entry::Vacant(entry) => { + let id = render_world.spawn(MainEntity(e)).id(); + + entry.insert(RenderEntity(id)); + } + }; + } + } + EntityRecord::Removed(e) => { + if let Some(ec) = render_world.get_entity_mut(e) { + ec.despawn(); + }; + } + } + } + }); +} + +pub(crate) fn despawn_temporary_render_entities( + world: &mut World, + state: &mut SystemState>>, + mut local: Local>, +) { + let query = state.get(world); + + local.extend(query.iter()); + + // Ensure next frame allocation keeps order + local.sort_unstable_by_key(|e| e.index()); + for e in local.drain(..).rev() { + world.despawn(e); + } +} + +#[cfg(test)] +mod tests { + use bevy_ecs::{ + component::Component, + entity::Entity, + observer::Trigger, + query::With, + system::{Query, ResMut}, + world::{OnAdd, OnRemove, World}, + }; + + use super::{ + entity_sync_system, EntityRecord, MainEntity, PendingSyncEntity, RenderEntity, + SyncToRenderWorld, + }; + + #[derive(Component)] + struct RenderDataComponent; + + #[test] + fn world_sync() { + let mut main_world = World::new(); + let mut render_world = World::new(); + main_world.init_resource::(); + + main_world.observe( + |trigger: Trigger, mut pending: ResMut| { + pending.push(EntityRecord::Added(trigger.entity())); + }, + ); + main_world.observe( + |trigger: Trigger, + mut pending: ResMut, + query: Query<&RenderEntity>| { + if let Ok(e) = query.get(trigger.entity()) { + pending.push(EntityRecord::Removed(e.id())); + }; + }, + ); + + // spawn some empty entities for test + for _ in 0..99 { + main_world.spawn_empty(); + } + + // spawn + let main_entity = main_world + .spawn(RenderDataComponent) + // indicates that its entity needs to be synchronized to the render world + .insert(SyncToRenderWorld) + .id(); + + entity_sync_system(&mut main_world, &mut render_world); + + let mut q = render_world.query_filtered::>(); + + // Only one synchronized entity + assert!(q.iter(&render_world).count() == 1); + + let render_entity = q.get_single(&render_world).unwrap(); + let render_entity_component = main_world.get::(main_entity).unwrap(); + + assert!(render_entity_component.id() == render_entity); + + let main_entity_component = render_world + .get::(render_entity_component.id()) + .unwrap(); + + assert!(main_entity_component.id() == main_entity); + + // despawn + main_world.despawn(main_entity); + + entity_sync_system(&mut main_world, &mut render_world); + + // Only one synchronized entity + assert!(q.iter(&render_world).count() == 0); + } +} diff --git a/crates/bevy_sprite/src/bundle.rs b/crates/bevy_sprite/src/bundle.rs index ca962c40b1b2c..df9e99e622dee 100644 --- a/crates/bevy_sprite/src/bundle.rs +++ b/crates/bevy_sprite/src/bundle.rs @@ -4,6 +4,7 @@ use bevy_ecs::bundle::Bundle; use bevy_render::{ texture::Image, view::{InheritedVisibility, ViewVisibility, Visibility}, + world_sync::SyncToRenderWorld, }; use bevy_transform::components::{GlobalTransform, Transform}; @@ -30,4 +31,6 @@ pub struct SpriteBundle { pub inherited_visibility: InheritedVisibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, + /// Marker component that indicates that its entity needs to be synchronized to the render world + pub sync: SyncToRenderWorld, } diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index 0f70232afcdf8..a0e6229a6af1f 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -221,8 +221,6 @@ pub struct RenderMesh2dInstances(EntityHashMap); pub struct Mesh2d; pub fn extract_mesh2d( - mut commands: Commands, - mut previous_len: Local, mut render_mesh_instances: ResMut, query: Extract< Query<( @@ -235,15 +233,11 @@ pub fn extract_mesh2d( >, ) { render_mesh_instances.clear(); - let mut entities = Vec::with_capacity(*previous_len); for (entity, view_visibility, transform, handle, no_automatic_batching) in &query { if !view_visibility.get() { continue; } - // FIXME: Remove this - it is just a workaround to enable rendering to work as - // render commands require an entity to exist at the moment. - entities.push((entity, Mesh2d)); render_mesh_instances.insert( entity, RenderMesh2dInstance { @@ -257,8 +251,6 @@ pub fn extract_mesh2d( }, ); } - *previous_len = entities.len(); - commands.insert_or_spawn_batch(entities); } #[derive(Resource, Clone)] diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 1efa866ec7e64..63b025d5a6747 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -39,6 +39,7 @@ use bevy_render::{ ExtractedView, Msaa, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms, ViewVisibility, VisibleEntities, }, + world_sync::{RenderEntity, TemporaryRenderEntity}, Extract, }; use bevy_transform::components::GlobalTransform; @@ -372,6 +373,7 @@ pub fn extract_sprites( sprite_query: Extract< Query<( Entity, + &RenderEntity, &ViewVisibility, &Sprite, &GlobalTransform, @@ -382,7 +384,9 @@ pub fn extract_sprites( >, ) { extracted_sprites.sprites.clear(); - for (entity, view_visibility, sprite, transform, handle, sheet, slices) in sprite_query.iter() { + for (original_entity, entity, view_visibility, sprite, transform, handle, sheet, slices) in + sprite_query.iter() + { if !view_visibility.get() { continue; } @@ -390,8 +394,8 @@ pub fn extract_sprites( if let Some(slices) = slices { extracted_sprites.sprites.extend( slices - .extract_sprites(transform, entity, sprite, handle) - .map(|e| (commands.spawn_empty().id(), e)), + .extract_sprites(transform, original_entity, sprite, handle) + .map(|e| (commands.spawn(TemporaryRenderEntity).id(), e)), ); } else { let atlas_rect = @@ -410,7 +414,7 @@ pub fn extract_sprites( // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive extracted_sprites.sprites.insert( - entity, + entity.id(), ExtractedSprite { color: sprite.color.into(), transform: *transform, @@ -421,7 +425,7 @@ pub fn extract_sprites( flip_y: sprite.flip_y, image_handle_id: handle.id(), anchor: sprite.anchor.as_vec(), - original_entity: None, + original_entity: Some(original_entity), }, ); } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 0026ecbe23a82..75cf7b771b564 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -33,6 +33,7 @@ use bevy_render::{ render_phase::{PhaseItem, PhaseItemExtraIndex}, texture::GpuImage, view::ViewVisibility, + world_sync::{RenderEntity, TemporaryRenderEntity}, ExtractSchedule, Render, }; use bevy_sprite::TextureAtlasLayout; @@ -188,12 +189,13 @@ pub struct ExtractedUiNodes { pub uinodes: EntityHashMap, } +#[allow(clippy::too_many_arguments)] pub fn extract_uinode_background_colors( + mut commands: Commands, mut extracted_uinodes: ResMut, default_ui_camera: Extract, uinode_query: Extract< Query<( - Entity, &Node, &GlobalTransform, &ViewVisibility, @@ -202,22 +204,25 @@ pub fn extract_uinode_background_colors( &BackgroundColor, )>, >, + mapping: Extract>, ) { - for (entity, uinode, transform, view_visibility, clip, camera, background_color) in - &uinode_query - { + for (uinode, transform, view_visibility, clip, camera, background_color) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { continue; }; + let Ok(&camera_entity) = mapping.get(camera_entity) else { + continue; + }; + // Skip invisible backgrounds if !view_visibility.get() || background_color.0.is_fully_transparent() { continue; } extracted_uinodes.uinodes.insert( - entity, + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: uinode.stack_index, transform: transform.compute_matrix(), @@ -231,7 +236,7 @@ pub fn extract_uinode_background_colors( atlas_scaling: None, flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border: uinode.border(), border_radius: uinode.border_radius(), node_type: NodeType::Rect, @@ -260,6 +265,7 @@ pub fn extract_uinode_images( Without, >, >, + mapping: Extract>, ) { for (uinode, transform, view_visibility, clip, camera, image, atlas) in &uinode_query { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) @@ -267,6 +273,10 @@ pub fn extract_uinode_images( continue; }; + let Ok(render_camera_entity) = mapping.get(camera_entity) else { + continue; + }; + // Skip invisible images if !view_visibility.get() || image.color.is_fully_transparent() @@ -303,7 +313,7 @@ pub fn extract_uinode_images( }; extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: uinode.stack_index, transform: transform.compute_matrix(), @@ -314,7 +324,7 @@ pub fn extract_uinode_images( atlas_scaling, flip_x: image.flip_x, flip_y: image.flip_y, - camera_entity, + camera_entity: render_camera_entity.id(), border: uinode.border, border_radius: uinode.border_radius, node_type: NodeType::Rect, @@ -337,6 +347,7 @@ pub fn extract_uinode_borders( AnyOf<(&BorderColor, &Outline)>, )>, >, + mapping: Extract>, ) { let image = AssetId::::default(); @@ -356,6 +367,10 @@ pub fn extract_uinode_borders( continue; }; + let Ok(&camera_entity) = mapping.get(camera_entity) else { + continue; + }; + // Skip invisible borders if !view_visibility.get() || maybe_border_color.is_some_and(|border_color| border_color.0.is_fully_transparent()) @@ -368,7 +383,7 @@ pub fn extract_uinode_borders( if !uinode.is_empty() && uinode.border() != BorderRect::ZERO { if let Some(border_color) = maybe_border_color { extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: uinode.stack_index, transform: global_transform.compute_matrix(), @@ -382,7 +397,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border_radius: uinode.border_radius(), border: uinode.border(), node_type: NodeType::Border, @@ -408,7 +423,7 @@ pub fn extract_uinode_borders( clip: maybe_clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border: BorderRect::square(uinode.outline_width()), border_radius: uinode.outline_radius(), node_type: NodeType::Border, @@ -438,7 +453,7 @@ pub fn extract_default_ui_camera_view( mut transparent_render_phases: ResMut>, ui_scale: Extract>, query: Extract< - Query<(Entity, &Camera, Option<&UiAntiAlias>), Or<(With, With)>>, + Query<(&RenderEntity, &Camera, Option<&UiAntiAlias>), Or<(With, With)>>, >, mut live_entities: Local, ) { @@ -463,6 +478,7 @@ pub fn extract_default_ui_camera_view( camera.physical_viewport_rect(), camera.physical_viewport_size(), ) { + let entity = entity.id(); // use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection let projection_matrix = Mat4::orthographic_rh( 0.0, @@ -473,23 +489,26 @@ pub fn extract_default_ui_camera_view( UI_CAMERA_FAR, ); let default_camera_view = commands - .spawn(ExtractedView { - clip_from_view: projection_matrix, - world_from_view: GlobalTransform::from_xyz( - 0.0, - 0.0, - UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, - ), - clip_from_world: None, - hdr: camera.hdr, - viewport: UVec4::new( - physical_origin.x, - physical_origin.y, - physical_size.x, - physical_size.y, - ), - color_grading: Default::default(), - }) + .spawn(( + ExtractedView { + clip_from_view: projection_matrix, + world_from_view: GlobalTransform::from_xyz( + 0.0, + 0.0, + UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, + ), + clip_from_world: None, + hdr: camera.hdr, + viewport: UVec4::new( + physical_origin.x, + physical_origin.y, + physical_size.x, + physical_size.y, + ), + color_grading: Default::default(), + }, + TemporaryRenderEntity, + )) .id(); let entity_commands = commands .get_or_spawn(entity) @@ -507,10 +526,11 @@ pub fn extract_default_ui_camera_view( } #[cfg(feature = "bevy_text")] +#[allow(clippy::too_many_arguments)] pub fn extract_uinode_text( mut commands: Commands, mut extracted_uinodes: ResMut, - camera_query: Extract>, + camera_query: Extract>, default_ui_camera: Extract, texture_atlases: Extract>>, ui_scale: Extract>, @@ -525,12 +545,13 @@ pub fn extract_uinode_text( &TextLayoutInfo, )>, >, + mapping: Extract>, ) { + let default_ui_camera = default_ui_camera.get(); for (uinode, global_transform, view_visibility, clip, camera, text, text_layout_info) in &uinode_query { - let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) - else { + let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera) else { continue; }; @@ -542,11 +563,14 @@ pub fn extract_uinode_text( let scale_factor = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.target_scaling_factor()) + .and_then(Camera::target_scaling_factor) .unwrap_or(1.0) * ui_scale.0; let inverse_scale_factor = scale_factor.recip(); + let Ok(&camera_entity) = mapping.get(camera_entity) else { + continue; + }; // Align the text to the nearest physical pixel: // * Translate by minus the text node's half-size // (The transform translates to the center of the node but the text coordinates are relative to the node's top left corner) @@ -581,8 +605,9 @@ pub fn extract_uinode_text( let mut rect = atlas.textures[atlas_info.location.glyph_index].as_rect(); rect.min *= inverse_scale_factor; rect.max *= inverse_scale_factor; + let id = commands.spawn(TemporaryRenderEntity).id(); extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + id, ExtractedUiNode { stack_index: uinode.stack_index, transform: transform @@ -594,7 +619,7 @@ pub fn extract_uinode_text( clip: clip.map(|clip| clip.clip), flip_x: false, flip_y: false, - camera_entity, + camera_entity: camera_entity.id(), border: BorderRect::ZERO, border_radius: ResolvedBorderRadius::ZERO, node_type: NodeType::Rect, diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 9c814ab843a12..71cbb2dcb4fb0 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -20,6 +20,7 @@ use bevy_render::{ renderer::{RenderDevice, RenderQueue}, texture::BevyDefault, view::*, + world_sync::{RenderEntity, TemporaryRenderEntity}, Extract, ExtractSchedule, Render, RenderSet, }; use bevy_transform::prelude::GlobalTransform; @@ -354,13 +355,13 @@ impl Default for ExtractedUiMaterialNodes { } pub fn extract_ui_material_nodes( + mut commands: Commands, mut extracted_uinodes: ResMut>, materials: Extract>>, default_ui_camera: Extract, uinode_query: Extract< Query< ( - Entity, &Node, &GlobalTransform, &Handle, @@ -371,15 +372,20 @@ pub fn extract_ui_material_nodes( Without, >, >, + render_entity_lookup: Extract>, ) { // If there is only one camera, we use it as default let default_single_camera = default_ui_camera.get(); - for (entity, uinode, transform, handle, view_visibility, clip, camera) in uinode_query.iter() { + for (uinode, transform, handle, view_visibility, clip, camera) in uinode_query.iter() { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_single_camera) else { continue; }; + let Ok(&camera_entity) = render_entity_lookup.get(camera_entity) else { + continue; + }; + // skip invisible nodes if !view_visibility.get() { continue; @@ -398,7 +404,7 @@ pub fn extract_ui_material_nodes( ]; extracted_uinodes.uinodes.insert( - entity, + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiMaterialNode { stack_index: uinode.stack_index, transform: transform.compute_matrix(), @@ -409,7 +415,7 @@ pub fn extract_ui_material_nodes( }, border, clip: clip.map(|clip| clip.clip), - camera_entity, + camera_entity: camera_entity.id(), }, ); } diff --git a/crates/bevy_ui/src/stack.rs b/crates/bevy_ui/src/stack.rs index 30b6c8e44addd..1d1b28df8c763 100644 --- a/crates/bevy_ui/src/stack.rs +++ b/crates/bevy_ui/src/stack.rs @@ -3,7 +3,7 @@ use bevy_ecs::prelude::*; use bevy_hierarchy::prelude::*; -use crate::{Node, ZIndex}; +use crate::{GlobalZIndex, Node, ZIndex}; /// The current UI stack, which contains all UI nodes ordered by their depth (back-to-front). /// @@ -15,70 +15,76 @@ pub struct UiStack { pub uinodes: Vec, } -/// Caches stacking context buffers for use in [`ui_stack_system`]. #[derive(Default)] -pub(crate) struct StackingContextCache { - inner: Vec, +pub(crate) struct ChildBufferCache { + pub inner: Vec>, } -impl StackingContextCache { - fn pop(&mut self) -> StackingContext { +impl ChildBufferCache { + fn pop(&mut self) -> Vec<(Entity, i32)> { self.inner.pop().unwrap_or_default() } - fn push(&mut self, mut context: StackingContext) { - for entry in context.entries.drain(..) { - self.push(entry.stack); - } - self.inner.push(context); + fn push(&mut self, vec: Vec<(Entity, i32)>) { + self.inner.push(vec); } } -#[derive(Default)] -struct StackingContext { - entries: Vec, -} - -struct StackingContextEntry { - z_index: i32, - entity: Entity, - stack: StackingContext, -} - /// Generates the render stack for UI nodes. /// -/// First generate a UI node tree (`StackingContext`) based on z-index. -/// Then flatten that tree into back-to-front ordered `UiStack`. -pub(crate) fn ui_stack_system( - mut cache: Local, +/// Create a list of root nodes from unparented entities and entities with a `GlobalZIndex` component. +/// Then build the `UiStack` from a walk of the existing layout trees starting from each root node, +/// filtering branches by `Without`so that we don't revisit nodes. +#[allow(clippy::too_many_arguments)] +pub fn ui_stack_system( + mut cache: Local, + mut root_nodes: Local>, mut ui_stack: ResMut, - root_node_query: Query, Without)>, - zindex_query: Query<&ZIndex, With>, + root_node_query: Query< + (Entity, Option<&GlobalZIndex>, Option<&ZIndex>), + (With, Without), + >, + zindex_global_node_query: Query< + (Entity, &GlobalZIndex, Option<&ZIndex>), + (With, With), + >, children_query: Query<&Children>, + zindex_query: Query, (With, Without)>, mut update_query: Query<&mut Node>, ) { - // Generate `StackingContext` tree - let mut global_context = cache.pop(); - let mut total_entry_count: usize = 0; + ui_stack.uinodes.clear(); + for (id, global_zindex, maybe_zindex) in zindex_global_node_query.iter() { + root_nodes.push(( + id, + ( + global_zindex.0, + maybe_zindex.map(|zindex| zindex.0).unwrap_or(0), + ), + )); + } + + for (id, maybe_global_zindex, maybe_zindex) in root_node_query.iter() { + root_nodes.push(( + id, + ( + maybe_global_zindex.map(|zindex| zindex.0).unwrap_or(0), + maybe_zindex.map(|zindex| zindex.0).unwrap_or(0), + ), + )); + } + + root_nodes.sort_by_key(|(_, z)| *z); - for entity in &root_node_query { - insert_context_hierarchy( + for (root_entity, _) in root_nodes.drain(..) { + update_uistack_recursive( &mut cache, - &zindex_query, + root_entity, &children_query, - entity, - &mut global_context, - None, - &mut total_entry_count, + &zindex_query, + &mut ui_stack.uinodes, ); } - // Flatten `StackingContext` into `UiStack` - ui_stack.uinodes.clear(); - ui_stack.uinodes.reserve(total_entry_count); - fill_stack_recursively(&mut cache, &mut ui_stack.uinodes, &mut global_context); - cache.push(global_context); - for (i, entity) in ui_stack.uinodes.iter().enumerate() { if let Ok(mut node) = update_query.get_mut(*entity) { node.bypass_change_detection().stack_index = i as u32; @@ -86,67 +92,28 @@ pub(crate) fn ui_stack_system( } } -/// Generate z-index based UI node tree -fn insert_context_hierarchy( - cache: &mut StackingContextCache, - zindex_query: &Query<&ZIndex, With>, +fn update_uistack_recursive( + cache: &mut ChildBufferCache, + node_entity: Entity, children_query: &Query<&Children>, - entity: Entity, - global_context: &mut StackingContext, - parent_context: Option<&mut StackingContext>, - total_entry_count: &mut usize, + zindex_query: &Query, (With, Without)>, + ui_stack: &mut Vec, ) { - let mut new_context = cache.pop(); - - if let Ok(children) = children_query.get(entity) { - // Reserve space for all children. In practice, some may not get pushed since - // nodes with `ZIndex::Global` are pushed to the global (root) context. - new_context.entries.reserve_exact(children.len()); - - for entity in children { - insert_context_hierarchy( - cache, - zindex_query, - children_query, - *entity, - global_context, - Some(&mut new_context), - total_entry_count, - ); + ui_stack.push(node_entity); + + if let Ok(children) = children_query.get(node_entity) { + let mut child_buffer = cache.pop(); + child_buffer.extend(children.iter().filter_map(|child_entity| { + zindex_query + .get(*child_entity) + .ok() + .map(|zindex| (*child_entity, zindex.map(|zindex| zindex.0).unwrap_or(0))) + })); + child_buffer.sort_by_key(|k| k.1); + for (child_entity, _) in child_buffer.drain(..) { + update_uistack_recursive(cache, child_entity, children_query, zindex_query, ui_stack); } - } - - // The node will be added either to global/parent based on its z-index type: global/local. - let z_index = zindex_query.get(entity).unwrap_or(&ZIndex::Local(0)); - let (entity_context, z_index) = match z_index { - ZIndex::Local(value) => (parent_context.unwrap_or(global_context), *value), - ZIndex::Global(value) => (global_context, *value), - }; - - *total_entry_count += 1; - entity_context.entries.push(StackingContextEntry { - z_index, - entity, - stack: new_context, - }); -} - -/// Flatten `StackingContext` (z-index based UI node tree) into back-to-front entities list -fn fill_stack_recursively( - cache: &mut StackingContextCache, - result: &mut Vec, - stack: &mut StackingContext, -) { - // Sort entries by ascending z_index, while ensuring that siblings - // with the same local z_index will keep their ordering. This results - // in `back-to-front` ordering, low z_index = back; high z_index = front. - stack.entries.sort_by_key(|e| e.z_index); - - for mut entry in stack.entries.drain(..) { - // Parent node renders before/behind child nodes - result.push(entry.entity); - fill_stack_recursively(cache, result, &mut entry.stack); - cache.push(entry.stack); + cache.push(child_buffer); } } @@ -160,15 +127,35 @@ mod tests { }; use bevy_hierarchy::{BuildChildren, ChildBuild}; - use crate::{Node, UiStack, ZIndex}; + use crate::{GlobalZIndex, Node, UiStack, ZIndex}; use super::ui_stack_system; #[derive(Component, PartialEq, Debug, Clone)] struct Label(&'static str); - fn node_with_zindex(name: &'static str, z_index: ZIndex) -> (Label, Node, ZIndex) { - (Label(name), Node::default(), z_index) + fn node_with_global_and_local_zindex( + name: &'static str, + global_zindex: i32, + local_zindex: i32, + ) -> (Label, Node, GlobalZIndex, ZIndex) { + ( + Label(name), + Node::default(), + GlobalZIndex(global_zindex), + ZIndex(local_zindex), + ) + } + + fn node_with_global_zindex( + name: &'static str, + global_zindex: i32, + ) -> (Label, Node, GlobalZIndex) { + (Label(name), Node::default(), GlobalZIndex(global_zindex)) + } + + fn node_with_zindex(name: &'static str, zindex: i32) -> (Label, Node, ZIndex) { + (Label(name), Node::default(), ZIndex(zindex)) } fn node_without_zindex(name: &'static str) -> (Label, Node) { @@ -188,24 +175,24 @@ mod tests { let mut queue = CommandQueue::default(); let mut commands = Commands::new(&mut queue, &world); - commands.spawn(node_with_zindex("0", ZIndex::Global(2))); + commands.spawn(node_with_global_zindex("0", 2)); commands - .spawn(node_with_zindex("1", ZIndex::Local(1))) + .spawn(node_with_zindex("1", 1)) .with_children(|parent| { parent .spawn(node_without_zindex("1-0")) .with_children(|parent| { parent.spawn(node_without_zindex("1-0-0")); parent.spawn(node_without_zindex("1-0-1")); - parent.spawn(node_with_zindex("1-0-2", ZIndex::Local(-1))); + parent.spawn(node_with_zindex("1-0-2", -1)); }); parent.spawn(node_without_zindex("1-1")); parent - .spawn(node_with_zindex("1-2", ZIndex::Global(-1))) + .spawn(node_with_global_zindex("1-2", -1)) .with_children(|parent| { parent.spawn(node_without_zindex("1-2-0")); - parent.spawn(node_with_zindex("1-2-1", ZIndex::Global(-3))); + parent.spawn(node_with_global_zindex("1-2-1", -3)); parent .spawn(node_without_zindex("1-2-2")) .with_children(|_| ()); @@ -227,7 +214,7 @@ mod tests { }); }); - commands.spawn(node_with_zindex("3", ZIndex::Global(-2))); + commands.spawn(node_with_global_zindex("3", -2)); queue.apply(&mut world); @@ -243,25 +230,74 @@ mod tests { .map(|entity| query.get(&world, *entity).unwrap().clone()) .collect::>(); let expected_result = vec![ - Label("1-2-1"), // ZIndex::Global(-3) - Label("3"), // ZIndex::Global(-2) - Label("1-2"), // ZIndex::Global(-1) - Label("1-2-0"), - Label("1-2-2"), - Label("1-2-3"), - Label("2"), - Label("2-0"), - Label("2-1"), - Label("2-1-0"), - Label("1"), // ZIndex::Local(1) - Label("1-0"), - Label("1-0-2"), // ZIndex::Local(-1) - Label("1-0-0"), - Label("1-0-1"), - Label("1-1"), - Label("1-3"), - Label("0"), // ZIndex::Global(2) + (Label("1-2-1")), // GlobalZIndex(-3) + (Label("3")), // GlobalZIndex(-2) + (Label("1-2")), // GlobalZIndex(-1) + (Label("1-2-0")), + (Label("1-2-2")), + (Label("1-2-3")), + (Label("2")), + (Label("2-0")), + (Label("2-1")), + (Label("2-1-0")), + (Label("1")), // ZIndex(1) + (Label("1-0")), + (Label("1-0-2")), // ZIndex(-1) + (Label("1-0-0")), + (Label("1-0-1")), + (Label("1-1")), + (Label("1-3")), + (Label("0")), // GlobalZIndex(2) ]; assert_eq!(actual_result, expected_result); } + + #[test] + fn test_with_equal_global_zindex_zindex_decides_order() { + let mut world = World::default(); + world.init_resource::(); + + let mut queue = CommandQueue::default(); + let mut commands = Commands::new(&mut queue, &world); + commands.spawn(node_with_global_and_local_zindex("0", -1, 1)); + commands.spawn(node_with_global_and_local_zindex("1", -1, 2)); + commands.spawn(node_with_global_and_local_zindex("2", 1, 3)); + commands.spawn(node_with_global_and_local_zindex("3", 1, -3)); + commands + .spawn(node_without_zindex("4")) + .with_children(|builder| { + builder.spawn(node_with_global_and_local_zindex("5", 0, -1)); + builder.spawn(node_with_global_and_local_zindex("6", 0, 1)); + builder.spawn(node_with_global_and_local_zindex("7", -1, -1)); + builder.spawn(node_with_global_zindex("8", 1)); + }); + + queue.apply(&mut world); + + let mut schedule = Schedule::default(); + schedule.add_systems(ui_stack_system); + schedule.run(&mut world); + + let mut query = world.query::<&Label>(); + let ui_stack = world.resource::(); + let actual_result = ui_stack + .uinodes + .iter() + .map(|entity| query.get(&world, *entity).unwrap().clone()) + .collect::>(); + + let expected_result = vec![ + (Label("7")), + (Label("0")), + (Label("1")), + (Label("5")), + (Label("4")), + (Label("6")), + (Label("3")), + (Label("8")), + (Label("2")), + ]; + + assert_eq!(actual_result, expected_result); + } } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index c4af28e4c788e..dac55d0dd4826 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2052,26 +2052,21 @@ pub struct CalculatedClip { /// appear in the UI hierarchy. In such a case, the last node to be added to its parent /// will appear in front of its siblings. /// -/// Internally, nodes with a global z-index share the stacking context of root UI nodes -/// (nodes that have no parent). Because of this, there is no difference between using -/// `ZIndex::Local(n)` and `ZIndex::Global(n)` for root nodes. -/// -/// Nodes without this component will be treated as if they had a value of `ZIndex::Local(0)`. -#[derive(Component, Copy, Clone, Debug, PartialEq, Eq, Reflect)] + +/// Nodes without this component will be treated as if they had a value of [`ZIndex(0)`]. +#[derive(Component, Copy, Clone, Debug, Default, PartialEq, Eq, Reflect)] #[reflect(Component, Default, Debug, PartialEq)] -pub enum ZIndex { - /// Indicates the order in which this node should be rendered relative to its siblings. - Local(i32), - /// Indicates the order in which this node should be rendered relative to root nodes and - /// all other nodes that have a global z-index. - Global(i32), -} +pub struct ZIndex(pub i32); -impl Default for ZIndex { - fn default() -> Self { - Self::Local(0) - } -} +/// `GlobalZIndex` allows a [`Node`] entity anywhere in the UI hierarchy to escape the implicit draw ordering of the UI's layout tree and +/// be rendered above or below other UI nodes. +/// Nodes with a `GlobalZIndex` of greater than 0 will be drawn on top of nodes without a `GlobalZIndex` or nodes with a lower `GlobalZIndex`. +/// Nodes with a `GlobalZIndex` of less than 0 will be drawn below nodes without a `GlobalZIndex` or nodes with a greater `GlobalZIndex`. +/// +/// If two Nodes have the same `GlobalZIndex`, the node with the greater [`ZIndex`] will be drawn on top. +#[derive(Component, Copy, Clone, Debug, Default, PartialEq, Eq, Reflect)] +#[reflect(Component, Default, Debug, PartialEq)] +pub struct GlobalZIndex(pub i32); /// Used to add rounded corners to a UI node. You can set a UI node to have uniformly /// rounded corners or specify different radii for each corner. If a given radius exceeds half diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index bd1cee59c3379..9313abf6071e0 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -27,7 +27,7 @@ use bevy_ecs::prelude::*; use bevy_window::{exit_on_all_closed, Window, WindowCreated}; pub use converters::convert_system_cursor_icon; pub use state::{CursorSource, CustomCursorCache, CustomCursorCacheKey, PendingCursor}; -use system::{changed_windows, despawn_windows}; +use system::{changed_windows, check_keyboard_focus_lost, despawn_windows}; pub use system::{create_monitors, create_windows}; #[cfg(all(target_family = "wasm", target_os = "unknown"))] pub use winit::platform::web::CustomCursorExtWebSys; @@ -133,6 +133,7 @@ impl Plugin for WinitPlugin { // so we don't need to care about its ordering relative to `changed_windows` changed_windows.ambiguous_with(exit_on_all_closed), despawn_windows, + check_keyboard_focus_lost, ) .chain(), ); diff --git a/crates/bevy_winit/src/state.rs b/crates/bevy_winit/src/state.rs index 53c21e71ec8eb..56006e56a3e31 100644 --- a/crates/bevy_winit/src/state.rs +++ b/crates/bevy_winit/src/state.rs @@ -10,7 +10,6 @@ use bevy_ecs::{ }; use bevy_input::{ gestures::*, - keyboard::KeyboardFocusLost, mouse::{MouseButtonInput, MouseMotion, MouseScrollUnit, MouseWheel}, }; use bevy_log::{error, trace, warn}; @@ -267,18 +266,14 @@ impl ApplicationHandler for WinitAppRunnerState { .send(WindowCloseRequested { window }), WindowEvent::KeyboardInput { ref event, - is_synthetic, + // On some platforms, winit sends "synthetic" key press events when the window + // gains or loses focus. These should not be handled, so we only process key + // events if they are not synthetic key presses. + is_synthetic: false, .. } => { - // Winit sends "synthetic" key press events when the window gains focus. These - // should not be handled, so we only process key events if they are not synthetic - // key presses. "synthetic" key release events should still be handled though, for - // properly releasing keys when the window loses focus. - if !(is_synthetic && event.state.is_pressed()) { - // Process the keyboard input event, as long as it's not a synthetic key press. - self.bevy_window_events - .send(converters::convert_keyboard_input(event, window)); - } + self.bevy_window_events + .send(converters::convert_keyboard_input(event, window)); } WindowEvent::CursorMoved { position, .. } => { let physical_position = DVec2::new(position.x, position.y); @@ -354,9 +349,6 @@ impl ApplicationHandler for WinitAppRunnerState { win.focused = focused; self.bevy_window_events .send(WindowFocused { window, focused }); - if !focused { - self.bevy_window_events.send(KeyboardFocusLost); - } } WindowEvent::Occluded(occluded) => { self.bevy_window_events diff --git a/crates/bevy_winit/src/system.rs b/crates/bevy_winit/src/system.rs index 2fa86d1e5171e..34a4dddc51eb1 100644 --- a/crates/bevy_winit/src/system.rs +++ b/crates/bevy_winit/src/system.rs @@ -6,10 +6,11 @@ use bevy_ecs::{ removal_detection::RemovedComponents, system::{Local, NonSendMut, Query, SystemParamItem}, }; +use bevy_input::keyboard::KeyboardFocusLost; use bevy_utils::tracing::{error, info, warn}; use bevy_window::{ ClosingWindow, Monitor, PrimaryMonitor, RawHandleWrapper, VideoMode, Window, WindowClosed, - WindowClosing, WindowCreated, WindowMode, WindowResized, WindowWrapper, + WindowClosing, WindowCreated, WindowFocused, WindowMode, WindowResized, WindowWrapper, }; use winit::{ @@ -122,6 +123,26 @@ pub fn create_windows( } } +/// Check whether keyboard focus was lost. This is different from window +/// focus in that swapping between Bevy windows keeps window focus. +pub(crate) fn check_keyboard_focus_lost( + mut focus_events: EventReader, + mut keyboard_focus: EventWriter, +) { + let mut focus_lost = false; + let mut focus_gained = false; + for e in focus_events.read() { + if e.focused { + focus_gained = true; + } else { + focus_lost = true; + } + } + if focus_lost & !focus_gained { + keyboard_focus.send(KeyboardFocusLost); + } +} + /// Synchronize available monitors as reported by [`winit`] with [`Monitor`] entities in the world. pub fn create_monitors( event_loop: &ActiveEventLoop, diff --git a/examples/3d/fog_volumes.rs b/examples/3d/fog_volumes.rs index 551d719e68cd2..8ebc9233322dd 100644 --- a/examples/3d/fog_volumes.rs +++ b/examples/3d/fog_volumes.rs @@ -9,6 +9,7 @@ use bevy::{ math::vec3, pbr::{FogVolume, VolumetricFog, VolumetricLight}, prelude::*, + render::world_sync::SyncToRenderWorld, }; /// Entry point. @@ -43,7 +44,9 @@ fn setup(mut commands: Commands, asset_server: Res) { // up. scattering: 1.0, ..default() - }); + }) + // indicates that this fog volume needs to be Synchronized to the render world + .insert(SyncToRenderWorld); // Spawn a bright directional light that illuminates the fog well. commands diff --git a/examples/3d/shadow_biases.rs b/examples/3d/shadow_biases.rs index e0bb9cde1eabc..ffcc1e70e6585 100644 --- a/examples/3d/shadow_biases.rs +++ b/examples/3d/shadow_biases.rs @@ -116,16 +116,18 @@ fn setup( let style = TextStyle::default(); commands - .spawn(NodeBundle { - style: Style { - position_type: PositionType::Absolute, - padding: UiRect::all(Val::Px(5.0)), + .spawn(( + NodeBundle { + style: Style { + position_type: PositionType::Absolute, + padding: UiRect::all(Val::Px(5.0)), + ..default() + }, + background_color: Color::BLACK.with_alpha(0.75).into(), ..default() }, - z_index: ZIndex::Global(i32::MAX), - background_color: Color::BLACK.with_alpha(0.75).into(), - ..default() - }) + GlobalZIndex(i32::MAX), + )) .with_children(|c| { c.spawn(TextBundle::from_sections([ TextSection::new("Controls:\n", style.clone()), diff --git a/examples/shader/custom_post_processing.rs b/examples/shader/custom_post_processing.rs index a81001dedb6b5..96d56259d5374 100644 --- a/examples/shader/custom_post_processing.rs +++ b/examples/shader/custom_post_processing.rs @@ -263,7 +263,7 @@ impl FromWorld for PostProcessPipeline { let pipeline_id = world .resource_mut::() - // This will add the pipeline to the cache and queue it's creation + // This will add the pipeline to the cache and queue its creation .queue_render_pipeline(RenderPipelineDescriptor { label: Some("post_process_pipeline".into()), layout: vec![layout.clone()], @@ -282,7 +282,7 @@ impl FromWorld for PostProcessPipeline { })], }), // All of the following properties are not important for this effect so just use the default values. - // This struct doesn't have the Default trait implemented because not all field can have a default value. + // This struct doesn't have the Default trait implemented because not all fields can have a default value. primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState::default(), @@ -374,7 +374,7 @@ fn update_settings(mut settings: Query<&mut PostProcessSettings>, time: Res