From ca8dd061467d44da358ae116d1b6da03e917aaa6 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 2 Oct 2024 17:36:42 -0700 Subject: [PATCH 001/546] Impose a more sensible ordering for animation graph evaluation. (#15589) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is an updated version of #15530. Review comments were addressed. This commit changes the animation graph evaluation to be operate in a more sensible order and updates the semantics of blend nodes to conform to [the animation composition RFC]. Prior to this patch, a node graph like this: ``` ┌─────┐ │ │ │ 1 │ │ │ └──┬──┘ │ ┌───────┴───────┐ │ │ ▼ ▼ ┌─────┐ ┌─────┐ │ │ │ │ │ 2 │ │ 3 │ │ │ │ │ └──┬──┘ └──┬──┘ │ │ ┌───┴───┐ ┌───┴───┐ │ │ │ │ ▼ ▼ ▼ ▼ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ │ │ │ │ │ │ │ │ │ 4 │ │ 6 │ │ 5 │ │ 7 │ │ │ │ │ │ │ │ │ └─────┘ └─────┘ └─────┘ └─────┘ ``` Would be evaluated as (((4 ⊕ 5) ⊕ 6) ⊕ 7), with the blend (lerp/slerp) operation notated as ⊕. As quaternion multiplication isn't commutative, this is very counterintuitive and will especially lead to trouble with the forthcoming additive blending feature (#15198). This patch fixes the issue by changing the evaluation order to postorder, with children of a node evaluated in ascending order by node index. To do so, this patch revamps `AnimationCurve` to be based on an *evaluation stack* and a *blend register*. During target evaluation, the graph evaluator traverses the graph in postorder. When encountering a clip node, the evaluator pushes the possibly-interpolated value onto the evaluation stack. When encountering a blend node, the evaluator pops values off the stack into the blend register, accumulating weights as appropriate. When the graph is completely evaluated, the top element on the stack is *committed* to the property of the component. A new system, the *graph threading* system, is added in order to cache the sorted postorder traversal to avoid the overhead of sorting children at animation evaluation time. Mask evaluation has been moved to this system so that the graph only has to be traversed at most once per frame. Unlike the `ActiveAnimation` list, the *threaded graph* is cached from frame to frame and only has to be regenerated when the animation graph asset changes. This patch currently regresses the `animate_target` performance in `many_foxes` by around 50%, resulting in an FPS loss of about 2-3 FPS. I'd argue that this is an acceptable price to pay for a much more intuitive system. In the future, we can mitigate the regression with a fast path that avoids consulting the graph if only one animation is playing. However, in the interest of keeping this patch simple, I didn't do so here. [the animation composition RFC]: https://github.com/bevyengine/rfcs/blob/main/rfcs/51-animation-composition.md # Objective - Describe the objective or issue this PR addresses. - If you're fixing a specific issue, say "Fixes #X". ## Solution - Describe the solution used to achieve the objective above. ## Testing - Did you test these changes? If so, how? - Are there any parts that need more testing? - How can other people (reviewers) test your changes? Is there anything specific they need to know? - If relevant, what platforms did you test these changes on, and are there any important ones you can't test? --- ## Showcase > This section is optional. If this PR does not include a visual change or does not add a new feature, you can delete this section. - Help others understand the result of this PR by showcasing your awesome work! - If this PR adds a new feature or public API, consider adding a brief pseudo-code snippet of it in action - If this PR includes a visual change, consider adding a screenshot, GIF, or video - If you want, you could even include a before/after comparison! - If the Migration Guide adequately covers the changes, you can delete this section While a showcase should aim to be brief and digestible, you can use a toggleable section to save space on longer showcases:
Click to view showcase ```rust println!("My super cool code."); ```
## Migration Guide > This section is optional. If there are no breaking changes, you can delete this section. - If this PR is a breaking change (relative to the last release of Bevy), describe how a user might need to migrate their code to support these changes - Simply adding new functionality is not a breaking change. - Fixing behavior that was definitely a bug, rather than a questionable design choice is not a breaking change. --------- Co-authored-by: Alice Cecile --- crates/bevy_animation/Cargo.toml | 2 +- crates/bevy_animation/src/animation_curves.rs | 668 ++++++++++++++++-- crates/bevy_animation/src/graph.rs | 219 +++++- crates/bevy_animation/src/lib.rs | 329 +++++---- examples/animation/animation_graph.rs | 14 +- 5 files changed, 1030 insertions(+), 202 deletions(-) diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index ae1e8ee23cdc9e..66d20c49fe8728 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -33,7 +33,6 @@ bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev", features = [ bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } # other -fixedbitset = "0.5" petgraph = { version = "0.6", features = ["serde-1"] } ron = "0.8" serde = "1" @@ -41,6 +40,7 @@ blake3 = { version = "1.0" } thiserror = "1" thread_local = "1" uuid = { version = "1.7", features = ["v4"] } +smallvec = "1" [lints] workspace = true diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index c7825e9066f9f0..26589b8e6e56f9 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -89,13 +89,15 @@ use bevy_math::{ iterable::IterableCurve, Curve, Interval, }, - FloatExt, Quat, Vec3, + Quat, Vec3, }; use bevy_reflect::{FromReflect, Reflect, Reflectable, TypePath}; use bevy_render::mesh::morph::MorphWeights; use bevy_transform::prelude::Transform; -use crate::{prelude::Animatable, AnimationEntityMut, AnimationEvaluationError}; +use crate::{ + graph::AnimationNodeIndex, prelude::Animatable, AnimationEntityMut, AnimationEvaluationError, +}; /// A value on a component that Bevy can animate. /// @@ -188,6 +190,21 @@ pub struct AnimatableCurve { _phantom: PhantomData

, } +/// An [`AnimatableCurveEvaluator`] for [`AnimatableProperty`] instances. +/// +/// You shouldn't ordinarily need to instantiate one of these manually. Bevy +/// will automatically do so when you use an [`AnimatableCurve`] instance. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct AnimatableCurveEvaluator

+where + P: AnimatableProperty, +{ + evaluator: BasicAnimationCurveEvaluator, + #[reflect(ignore)] + phantom: PhantomData

, +} + impl AnimatableCurve where P: AnimatableProperty, @@ -241,20 +258,72 @@ where self.curve.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::>() + } + + fn create_evaluator(&self) -> Box { + Box::new(AnimatableCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + phantom: PhantomData::

, + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - _transform: Option>, - mut entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::>() + .unwrap(); + let value = self.curve.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl

AnimationCurveEvaluator for AnimatableCurveEvaluator

+where + P: AnimatableProperty, +{ + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + _: Option>, + mut entity: AnimationEntityMut<'a>, ) -> 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); + *property = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::>)? + .value; Ok(()) } } @@ -267,6 +336,16 @@ where #[reflect(from_reflect = false)] pub struct TranslationCurve(pub C); +/// An [`AnimationCurveEvaluator`] for use with [`TranslationCurve`]s. +/// +/// You shouldn't need to instantiate this manually; Bevy will automatically do +/// so. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct TranslationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator, +} + impl AnimationCurve for TranslationCurve where C: AnimationCompatibleCurve, @@ -279,19 +358,66 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(TranslationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - transform: Option>, - _entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + let value = self.0.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl AnimationCurveEvaluator for TranslationCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + transform: Option>, + _: AnimationEntityMut<'a>, ) -> 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); + component.translation = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; Ok(()) } } @@ -304,6 +430,16 @@ where #[reflect(from_reflect = false)] pub struct RotationCurve(pub C); +/// An [`AnimationCurveEvaluator`] for use with [`RotationCurve`]s. +/// +/// You shouldn't need to instantiate this manually; Bevy will automatically do +/// so. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct RotationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator, +} + impl AnimationCurve for RotationCurve where C: AnimationCompatibleCurve, @@ -316,19 +452,66 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(RotationCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - transform: Option>, - _entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + let value = self.0.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl AnimationCurveEvaluator for RotationCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + transform: Option>, + _: AnimationEntityMut<'a>, ) -> 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); + component.rotation = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; Ok(()) } } @@ -341,6 +524,16 @@ where #[reflect(from_reflect = false)] pub struct ScaleCurve(pub C); +/// An [`AnimationCurveEvaluator`] for use with [`ScaleCurve`]s. +/// +/// You shouldn't need to instantiate this manually; Bevy will automatically do +/// so. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +pub struct ScaleCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator, +} + impl AnimationCurve for ScaleCurve where C: AnimationCompatibleCurve, @@ -353,18 +546,66 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(ScaleCurveEvaluator { + evaluator: BasicAnimationCurveEvaluator::default(), + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - transform: Option>, - _entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + let value = self.0.sample_clamped(t); + curve_evaluator + .evaluator + .stack + .push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + Ok(()) + } +} + +impl AnimationCurveEvaluator for ScaleCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) + } + + fn commit<'a>( + &mut self, + transform: Option>, + _: AnimationEntityMut<'a>, ) -> 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); + component.scale = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; Ok(()) } } @@ -377,6 +618,43 @@ where #[reflect(from_reflect = false)] pub struct WeightsCurve(pub C); +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +struct WeightsCurveEvaluator { + /// The values of the stack, in which each element is a list of morph target + /// weights. + /// + /// The stack elements are concatenated and tightly packed together. + /// + /// The number of elements in this stack will always be a multiple of + /// [`Self::morph_target_count`]. + stack_morph_target_weights: Vec, + + /// The blend weights and graph node indices for each element of the stack. + /// + /// This should have as many elements as there are stack nodes. In other + /// words, `Self::stack_morph_target_weights.len() * + /// Self::morph_target_counts as usize == + /// Self::stack_blend_weights_and_graph_nodes`. + stack_blend_weights_and_graph_nodes: Vec<(f32, AnimationNodeIndex)>, + + /// The morph target weights in the blend register, if any. + /// + /// This field should be ignored if [`Self::blend_register_blend_weight`] is + /// `None`. If non-empty, it will always have [`Self::morph_target_count`] + /// elements in it. + blend_register_morph_target_weights: Vec, + + /// The weight in the blend register. + /// + /// This will be `None` if the blend register is empty. In that case, + /// [`Self::blend_register_morph_target_weights`] will be empty. + blend_register_blend_weight: Option, + + /// The number of morph targets that are to be animated. + morph_target_count: Option, +} + impl AnimationCurve for WeightsCurve where C: IterableCurve + Debug + Clone + Reflectable, @@ -389,45 +667,222 @@ where self.0.domain() } - fn apply<'a>( + fn evaluator_type(&self) -> TypeId { + TypeId::of::() + } + + fn create_evaluator(&self) -> Box { + Box::new(WeightsCurveEvaluator { + stack_morph_target_weights: vec![], + stack_blend_weights_and_graph_nodes: vec![], + blend_register_morph_target_weights: vec![], + blend_register_blend_weight: None, + morph_target_count: None, + }) + } + + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, - _transform: Option>, - mut entity: AnimationEntityMut<'a>, weight: f32, + graph_node: AnimationNodeIndex, ) -> 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); + let curve_evaluator = (*Reflect::as_any_mut(curve_evaluator)) + .downcast_mut::() + .unwrap(); + + let prev_morph_target_weights_len = curve_evaluator.stack_morph_target_weights.len(); + curve_evaluator + .stack_morph_target_weights + .extend(self.0.sample_iter_clamped(t)); + curve_evaluator.morph_target_count = Some( + (curve_evaluator.stack_morph_target_weights.len() - prev_morph_target_weights_len) + as u32, + ); + + curve_evaluator + .stack_blend_weights_and_graph_nodes + .push((weight, graph_node)); 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); +impl AnimationCurveEvaluator for WeightsCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + let Some(&(_, top_graph_node)) = self.stack_blend_weights_and_graph_nodes.last() else { + return Ok(()); + }; + if top_graph_node != graph_node { + return Ok(()); + } + + let (weight_to_blend, _) = self.stack_blend_weights_and_graph_nodes.pop().unwrap(); + let stack_iter = self.stack_morph_target_weights.drain( + (self.stack_morph_target_weights.len() - self.morph_target_count.unwrap() as usize).., + ); + + match self.blend_register_blend_weight { + None => { + self.blend_register_blend_weight = Some(weight_to_blend); + self.blend_register_morph_target_weights.clear(); + self.blend_register_morph_target_weights.extend(stack_iter); + } + + Some(ref mut current_weight) => { + *current_weight += weight_to_blend; + for (dest, src) in self + .blend_register_morph_target_weights + .iter_mut() + .zip(stack_iter) + { + *dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight); + } + } + } + + Ok(()) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + if self.blend_register_blend_weight.take().is_some() { + self.stack_morph_target_weights + .append(&mut self.blend_register_morph_target_weights); + self.stack_blend_weights_and_graph_nodes + .push((weight, graph_node)); + } + Ok(()) + } + + fn commit<'a>( + &mut self, + _: Option>, + mut entity: AnimationEntityMut<'a>, + ) -> Result<(), AnimationEvaluationError> { + if self.stack_morph_target_weights.is_empty() { + return Ok(()); + } + + // Compute the index of the first morph target in the last element of + // the stack. + let index_of_first_morph_target = + self.stack_morph_target_weights.len() - self.morph_target_count.unwrap() as usize; + + for (dest, src) in entity + .get_mut::() + .ok_or_else(|| { + AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) + })? + .weights_mut() + .iter_mut() + .zip(self.stack_morph_target_weights[index_of_first_morph_target..].iter()) + { + *dest = *src; + } + self.stack_morph_target_weights.clear(); + self.stack_blend_weights_and_graph_nodes.clear(); + Ok(()) } } -/// A low-level trait that provides control over how curves are actually applied to entities -/// by the animation system. +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +struct BasicAnimationCurveEvaluator +where + A: Animatable, +{ + stack: Vec>, + blend_register: Option<(A, f32)>, +} + +#[derive(Reflect, FromReflect)] +#[reflect(from_reflect = false)] +struct BasicAnimationCurveEvaluatorStackElement +where + A: Animatable, +{ + value: A, + weight: f32, + graph_node: AnimationNodeIndex, +} + +impl Default for BasicAnimationCurveEvaluator +where + A: Animatable, +{ + fn default() -> Self { + BasicAnimationCurveEvaluator { + stack: vec![], + blend_register: None, + } + } +} + +impl BasicAnimationCurveEvaluator +where + A: Animatable, +{ + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + let Some(top) = self.stack.last() else { + return Ok(()); + }; + if top.graph_node != graph_node { + return Ok(()); + } + + let BasicAnimationCurveEvaluatorStackElement { + value: value_to_blend, + weight: weight_to_blend, + graph_node: _, + } = self.stack.pop().unwrap(); + + match self.blend_register { + None => self.blend_register = Some((value_to_blend, weight_to_blend)), + Some((ref mut current_value, ref mut current_weight)) => { + *current_weight += weight_to_blend; + *current_value = A::interpolate( + current_value, + &value_to_blend, + weight_to_blend / *current_weight, + ); + } + } + + Ok(()) + } + + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + if let Some((value, _)) = self.blend_register.take() { + self.stack.push(BasicAnimationCurveEvaluatorStackElement { + value, + weight, + graph_node, + }); + } + Ok(()) + } +} + +/// 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. +/// 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`]. +/// 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 { @@ -437,15 +892,111 @@ pub trait AnimationCurve: Reflect + Debug + Send + Sync { /// 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>( + /// Returns the type ID of the [`AnimationCurveEvaluator`]. + /// + /// This must match the type returned by [`Self::create_evaluator`]. It must + /// be a single type that doesn't depend on the type of the curve. + fn evaluator_type(&self) -> TypeId; + + /// Returns a newly-instantiated [`AnimationCurveEvaluator`] for use with + /// this curve. + /// + /// All curve types must return the same type of + /// [`AnimationCurveEvaluator`]. The returned value must match the type + /// returned by [`Self::evaluator_type`]. + fn create_evaluator(&self) -> Box; + + /// Samples the curve at the given time `t`, and pushes the sampled value + /// onto the evaluation stack of the `curve_evaluator`. + /// + /// The `curve_evaluator` parameter points to the value returned by + /// [`Self::create_evaluator`], upcast to an `&mut dyn + /// AnimationCurveEvaluator`. Typically, implementations of [`Self::apply`] + /// will want to downcast the `curve_evaluator` parameter to the concrete + /// type [`Self::evaluator_type`] in order to push values of the appropriate + /// type onto its evaluation stack. + /// + /// Be sure not to confuse the `t` and `weight` values. The former + /// determines the position at which the *curve* is sampled, while `weight` + /// ultimately determines how much the *stack values* will be blended + /// together (see the definition of [`AnimationCurveEvaluator::blend`]). + fn apply( &self, + curve_evaluator: &mut dyn AnimationCurveEvaluator, t: f32, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError>; +} + +/// 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 [`AnimatableCurveEvaluator`] +/// isn't sufficiently-expressive for your needs. For example, [`MorphWeights`] +/// implements this trait instead of using [`AnimatableCurveEvaluator`] 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. +/// +/// If you implement this trait, you should also implement [`AnimationCurve`] on +/// your curve type, as that trait allows creating instances of this one. +/// +/// Implementations of [`AnimatableCurveEvaluator`] should maintain a *stack* of +/// (value, weight, node index) triples, as well as a *blend register*, which is +/// either a (value, weight) pair or empty. *Value* here refers to an instance +/// of the value being animated: for example, [`Vec3`] in the case of +/// translation keyframes. The stack stores intermediate values generated while +/// evaluating the [`crate::graph::AnimationGraph`], while the blend register +/// stores the result of a blend operation. +pub trait AnimationCurveEvaluator: Reflect { + /// Blends the top element of the stack with the blend register. + /// + /// The semantics of this method are as follows: + /// + /// 1. Pop the top element of the stack. Call its value vₘ and its weight + /// wₘ. If the stack was empty, return success. + /// + /// 2. If the blend register is empty, set the blend register value to vₘ + /// and the blend register weight to wₘ; then, return success. + /// + /// 3. If the blend register is nonempty, call its current value vₙ and its + /// current weight wₙ. Then, set the value of the blend register to + /// `interpolate(vₙ, vₘ, wₘ / (wₘ + wₙ))`, and set the weight of the blend + /// register to wₘ + wₙ. + /// + /// 4. Return success. + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>; + + /// Pushes the current value of the blend register onto the stack. + /// + /// If the blend register is empty, this method does nothing successfully. + /// Otherwise, this method pushes the current value of the blend register + /// onto the stack, alongside the weight and graph node supplied to this + /// function. The weight present in the blend register is discarded; only + /// the weight parameter to this function is pushed onto the stack. The + /// blend register is emptied after this process. + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError>; + + /// Pops the top value off the stack and writes it into the appropriate + /// component. + /// + /// If the stack is empty, this method does nothing successfully. Otherwise, + /// it pops the top value off the stack, fetches the associated component + /// from either the `transform` or `entity` values as appropriate, and + /// updates the appropriate property with the value popped from the stack. + /// The weight and node index associated with the popped stack element are + /// discarded. After doing this, the stack is emptied. + /// + /// The property on the component must be overwritten with the value from + /// the stack, not blended with it. + fn commit<'a>( + &mut self, transform: Option>, entity: AnimationEntityMut<'a>, - weight: f32, ) -> Result<(), AnimationEvaluationError>; } @@ -496,3 +1047,10 @@ where }) } } + +fn inconsistent

() -> AnimationEvaluationError +where + P: 'static + ?Sized, +{ + AnimationEvaluationError::InconsistentEvaluatorImplementation(TypeId::of::

()) +} diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 5264cf9a235520..22c0e1a60842c8 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -1,14 +1,25 @@ //! The animation graph, which allows animations to be blended together. -use core::ops::{Index, IndexMut}; +use core::iter; +use core::ops::{Index, IndexMut, Range}; use std::io::{self, Write}; -use bevy_asset::{io::Reader, Asset, AssetId, AssetLoader, AssetPath, Handle, LoadContext}; +use bevy_asset::{ + io::Reader, Asset, AssetEvent, AssetId, AssetLoader, AssetPath, Assets, Handle, LoadContext, +}; +use bevy_ecs::{ + event::EventReader, + system::{Res, ResMut, Resource}, +}; use bevy_reflect::{Reflect, ReflectSerialize}; use bevy_utils::HashMap; -use petgraph::graph::{DiGraph, NodeIndex}; +use petgraph::{ + graph::{DiGraph, NodeIndex}, + Direction, +}; use ron::de::SpannedError; use serde::{Deserialize, Serialize}; +use smallvec::SmallVec; use thiserror::Error; use crate::{AnimationClip, AnimationTargetId}; @@ -172,6 +183,99 @@ pub enum AnimationGraphLoadError { SpannedRon(#[from] SpannedError), } +/// Acceleration structures for animation graphs that allows Bevy to evaluate +/// them quickly. +/// +/// These are kept up to date as [`AnimationGraph`] instances are added, +/// modified, and removed. +#[derive(Default, Reflect, Resource)] +pub struct ThreadedAnimationGraphs( + pub(crate) HashMap, ThreadedAnimationGraph>, +); + +/// An acceleration structure for an animation graph that allows Bevy to +/// evaluate it quickly. +/// +/// This is kept up to date as the associated [`AnimationGraph`] instance is +/// added, modified, or removed. +#[derive(Default, Reflect)] +pub struct ThreadedAnimationGraph { + /// A cached postorder traversal of the graph. + /// + /// The node indices here are stored in postorder. Siblings are stored in + /// descending order. This is because the + /// [`crate::animation_curves::AnimationCurveEvaluator`] uses a stack for + /// evaluation. Consider this graph: + /// + /// ```text + /// ┌─────┐ + /// │ │ + /// │ 1 │ + /// │ │ + /// └──┬──┘ + /// │ + /// ┌───────┼───────┐ + /// │ │ │ + /// ▼ ▼ ▼ + /// ┌─────┐ ┌─────┐ ┌─────┐ + /// │ │ │ │ │ │ + /// │ 2 │ │ 3 │ │ 4 │ + /// │ │ │ │ │ │ + /// └──┬──┘ └─────┘ └─────┘ + /// │ + /// ┌───┴───┐ + /// │ │ + /// ▼ ▼ + /// ┌─────┐ ┌─────┐ + /// │ │ │ │ + /// │ 5 │ │ 6 │ + /// │ │ │ │ + /// └─────┘ └─────┘ + /// ``` + /// + /// The postorder traversal in this case will be (4, 3, 6, 5, 2, 1). + /// + /// The fact that the children of each node are sorted in reverse ensures + /// that, at each level, the order of blending proceeds in ascending order + /// by node index, as we guarantee. To illustrate this, consider the way + /// the graph above is evaluated. (Interpolation is represented with the ⊕ + /// symbol.) + /// + /// | Step | Node | Operation | Stack (after operation) | Blend Register | + /// | ---- | ---- | ---------- | ----------------------- | -------------- | + /// | 1 | 4 | Push | 4 | | + /// | 2 | 3 | Push | 4 3 | | + /// | 3 | 6 | Push | 4 3 6 | | + /// | 4 | 5 | Push | 4 3 6 5 | | + /// | 5 | 2 | Blend 5 | 4 3 6 | 5 | + /// | 6 | 2 | Blend 6 | 4 3 | 5 ⊕ 6 | + /// | 7 | 2 | Push Blend | 4 3 2 | | + /// | 8 | 1 | Blend 2 | 4 3 | 2 | + /// | 9 | 1 | Blend 3 | 4 | 2 ⊕ 3 | + /// | 10 | 1 | Blend 4 | | 2 ⊕ 3 ⊕ 4 | + /// | 11 | 1 | Push Blend | 1 | | + /// | 12 | | Commit | | | + pub threaded_graph: Vec, + + /// A mapping from each parent node index to the range within + /// [`Self::sorted_edges`]. + /// + /// This allows for quick lookup of the children of each node, sorted in + /// ascending order of node index, without having to sort the result of the + /// `petgraph` traversal functions every frame. + pub sorted_edge_ranges: Vec>, + + /// A list of the children of each node, sorted in ascending order. + pub sorted_edges: Vec, + + /// A mapping from node index to a bitfield specifying the mask groups that + /// this node masks *out* (i.e. doesn't animate). + /// + /// A 1 in bit position N indicates that this node doesn't animate any + /// targets of mask group N. + pub computed_masks: Vec, +} + /// A version of [`AnimationGraph`] suitable for serializing as an asset. /// /// Animation nodes can refer to external animation clips, and the [`AssetId`] @@ -571,3 +675,112 @@ impl From for SerializedAnimationGraph { } } } + +/// A system that creates, updates, and removes [`ThreadedAnimationGraph`] +/// structures for every changed [`AnimationGraph`]. +/// +/// The [`ThreadedAnimationGraph`] contains acceleration structures that allow +/// for quick evaluation of that graph's animations. +pub(crate) fn thread_animation_graphs( + mut threaded_animation_graphs: ResMut, + animation_graphs: Res>, + mut animation_graph_asset_events: EventReader>, +) { + for animation_graph_asset_event in animation_graph_asset_events.read() { + match *animation_graph_asset_event { + AssetEvent::Added { id } + | AssetEvent::Modified { id } + | AssetEvent::LoadedWithDependencies { id } => { + // Fetch the animation graph. + let Some(animation_graph) = animation_graphs.get(id) else { + continue; + }; + + // Reuse the allocation if possible. + let mut threaded_animation_graph = + threaded_animation_graphs.0.remove(&id).unwrap_or_default(); + threaded_animation_graph.clear(); + + // Recursively thread the graph in postorder. + threaded_animation_graph.init(animation_graph); + threaded_animation_graph.build_from( + &animation_graph.graph, + animation_graph.root, + 0, + ); + + // Write in the threaded graph. + threaded_animation_graphs + .0 + .insert(id, threaded_animation_graph); + } + + AssetEvent::Removed { id } => { + threaded_animation_graphs.0.remove(&id); + } + AssetEvent::Unused { .. } => {} + } + } +} + +impl ThreadedAnimationGraph { + /// Removes all the data in this [`ThreadedAnimationGraph`], keeping the + /// memory around for later reuse. + fn clear(&mut self) { + self.threaded_graph.clear(); + self.sorted_edge_ranges.clear(); + self.sorted_edges.clear(); + } + + /// Prepares the [`ThreadedAnimationGraph`] for recursion. + fn init(&mut self, animation_graph: &AnimationGraph) { + let node_count = animation_graph.graph.node_count(); + let edge_count = animation_graph.graph.edge_count(); + + self.threaded_graph.reserve(node_count); + self.sorted_edges.reserve(edge_count); + + self.sorted_edge_ranges.clear(); + self.sorted_edge_ranges + .extend(iter::repeat(0..0).take(node_count)); + + self.computed_masks.clear(); + self.computed_masks.extend(iter::repeat(0).take(node_count)); + } + + /// Recursively constructs the [`ThreadedAnimationGraph`] for the subtree + /// rooted at the given node. + /// + /// `mask` specifies the computed mask of the parent node. (It could be + /// fetched from the [`Self::computed_masks`] field, but we pass it + /// explicitly as a micro-optimization.) + fn build_from( + &mut self, + graph: &AnimationDiGraph, + node_index: AnimationNodeIndex, + mut mask: u64, + ) { + // Accumulate the mask. + mask |= graph.node_weight(node_index).unwrap().mask; + self.computed_masks.insert(node_index.index(), mask); + + // Gather up the indices of our children, and sort them. + let mut kids: SmallVec<[AnimationNodeIndex; 8]> = graph + .neighbors_directed(node_index, Direction::Outgoing) + .collect(); + kids.sort_unstable(); + + // Write in the list of kids. + self.sorted_edge_ranges[node_index.index()] = + (self.sorted_edges.len() as u32)..((self.sorted_edges.len() + kids.len()) as u32); + self.sorted_edges.extend_from_slice(&kids); + + // Recurse. (This is a postorder traversal.) + for kid in kids.into_iter().rev() { + self.build_from(graph, kid, mask); + } + + // Finally, push our index. + self.threaded_graph.push(node_index); + } +} diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 39a8349d812126..23585432041e6a 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -16,7 +16,6 @@ pub mod graph; pub mod transition; mod util; -use alloc::collections::BTreeMap; use core::{ any::{Any, TypeId}, cell::RefCell, @@ -24,6 +23,9 @@ use core::{ hash::{Hash, Hasher}, iter, }; +use prelude::AnimationCurveEvaluator; + +use crate::graph::ThreadedAnimationGraphs; use bevy_app::{App, Plugin, PostUpdate}; use bevy_asset::{Asset, AssetApp, Assets, Handle}; @@ -46,11 +48,9 @@ use bevy_ui::UiSystem; use bevy_utils::{ hashbrown::HashMap, tracing::{trace, warn}, - NoOpHash, + NoOpHash, TypeIdMap, }; -use fixedbitset::FixedBitSet; -use graph::AnimationMask; -use petgraph::{graph::NodeIndex, Direction}; +use petgraph::graph::NodeIndex; use serde::{Deserialize, Serialize}; use thread_local::ThreadLocal; use uuid::Uuid; @@ -461,6 +461,14 @@ pub enum AnimationEvaluationError { /// The component to be animated was present, but the property on the /// component wasn't present. PropertyNotPresent(TypeId), + + /// An internal error occurred in the implementation of + /// [`AnimationCurveEvaluator`]. + /// + /// You shouldn't ordinarily see this error unless you implemented + /// [`AnimationCurveEvaluator`] yourself. The contained [`TypeId`] is the ID + /// of the curve evaluator. + InconsistentEvaluatorImplementation(TypeId), } /// An animation that an [`AnimationPlayer`] is currently either playing or was @@ -471,12 +479,8 @@ pub enum AnimationEvaluationError { pub struct ActiveAnimation { /// The factor by which the weight from the [`AnimationGraph`] is multiplied. weight: f32, - /// The actual weight of this animation this frame, taking the - /// [`AnimationGraph`] into account. - computed_weight: f32, /// The mask groups that are masked out (i.e. won't be animated) this frame, /// taking the `AnimationGraph` into account. - computed_mask: AnimationMask, repeat: RepeatAnimation, speed: f32, /// Total time the animation has been played. @@ -497,8 +501,6 @@ impl Default for ActiveAnimation { fn default() -> Self { Self { weight: 1.0, - computed_weight: 1.0, - computed_mask: 0, repeat: RepeatAnimation::default(), speed: 1.0, elapsed: 0.0, @@ -658,9 +660,7 @@ impl ActiveAnimation { #[derive(Component, Default, Reflect)] #[reflect(Component, Default)] pub struct AnimationPlayer { - /// We use a `BTreeMap` instead of a `HashMap` here to ensure a consistent - /// ordering when applying the animations. - active_animations: BTreeMap, + active_animations: HashMap, blend_weights: HashMap, } @@ -679,27 +679,29 @@ impl Clone for AnimationPlayer { } } -/// Information needed during the traversal of the animation graph in -/// [`advance_animations`]. +/// Temporary data that the [`animate_targets`] system maintains. #[derive(Default)] -pub struct AnimationGraphEvaluator { - /// The stack used for the depth-first search of the graph. - dfs_stack: Vec, - /// The list of visited nodes during the depth-first traversal. - dfs_visited: FixedBitSet, - /// Accumulated weights and masks for each node. - nodes: Vec, -} - -/// The accumulated weight and computed mask for a single node. -#[derive(Clone, Copy, Default, Debug)] -struct EvaluatedAnimationGraphNode { - /// The weight that has been accumulated for this node, taking its - /// ancestors' weights into account. - weight: f32, - /// The mask that has been computed for this node, taking its ancestors' - /// masks into account. - mask: AnimationMask, +pub struct AnimationEvaluationState { + /// Stores all [`AnimationCurveEvaluator`]s corresponding to properties that + /// we've seen so far. + /// + /// This is a mapping from the type ID of an animation curve evaluator to + /// the animation curve evaluator itself. + /// + /// For efficiency's sake, the [`AnimationCurveEvaluator`]s are cached from + /// frame to frame and animation target to animation target. Therefore, + /// there may be entries in this list corresponding to properties that the + /// current [`AnimationPlayer`] doesn't animate. To iterate only over the + /// properties that are currently being animated, consult the + /// [`Self::current_curve_evaluator_types`] set. + curve_evaluators: TypeIdMap>, + + /// The set of [`AnimationCurveEvaluator`] types that the current + /// [`AnimationPlayer`] is animating. + /// + /// This is built up as new curve evaluators are encountered during graph + /// traversal. + current_curve_evaluator_types: TypeIdMap<()>, } impl AnimationPlayer { @@ -845,7 +847,6 @@ pub fn advance_animations( animation_clips: Res>, animation_graphs: Res>, mut players: Query<(&mut AnimationPlayer, &Handle)>, - animation_graph_evaluator: Local>>, ) { let delta_seconds = time.delta_seconds(); players @@ -856,40 +857,15 @@ pub fn advance_animations( }; // Tick animations, and schedule them. - // - // We use a thread-local here so we can reuse allocations across - // frames. - let mut evaluator = animation_graph_evaluator.get_or_default().borrow_mut(); let AnimationPlayer { ref mut active_animations, - ref blend_weights, .. } = *player; - // Reset our state. - evaluator.reset(animation_graph.root, animation_graph.graph.node_count()); - - while let Some(node_index) = evaluator.dfs_stack.pop() { - // Skip if we've already visited this node. - if evaluator.dfs_visited.put(node_index.index()) { - continue; - } - + for node_index in animation_graph.graph.node_indices() { let node = &animation_graph[node_index]; - // Calculate weight and mask from the graph. - let (mut weight, mut mask) = (node.weight, node.mask); - for parent_index in animation_graph - .graph - .neighbors_directed(node_index, Direction::Incoming) - { - let evaluated_parent = &evaluator.nodes[parent_index.index()]; - weight *= evaluated_parent.weight; - mask |= evaluated_parent.mask; - } - evaluator.nodes[node_index.index()] = EvaluatedAnimationGraphNode { weight, mask }; - if let Some(active_animation) = active_animations.get_mut(&node_index) { // Tick the animation if necessary. if !active_animation.paused { @@ -899,24 +875,7 @@ pub fn advance_animations( } } } - - weight *= active_animation.weight; - } else if let Some(&blend_weight) = blend_weights.get(&node_index) { - weight *= blend_weight; - } - - // Write in the computed weight and mask for this node. - if let Some(active_animation) = active_animations.get_mut(&node_index) { - active_animation.computed_weight = weight; - active_animation.computed_mask = mask; } - - // Push children. - evaluator.dfs_stack.extend( - animation_graph - .graph - .neighbors_directed(node_index, Direction::Outgoing), - ); } }); } @@ -937,13 +896,15 @@ pub type AnimationEntityMut<'w> = EntityMutExcept< pub fn animate_targets( clips: Res>, graphs: Res>, + threaded_animation_graphs: Res, players: Query<(&AnimationPlayer, &Handle)>, mut targets: Query<(&AnimationTarget, Option<&mut Transform>, AnimationEntityMut)>, + animation_evaluation_state: Local>>, ) { // Evaluate all animation targets in parallel. targets .par_iter_mut() - .for_each(|(target, mut transform, mut entity_mut)| { + .for_each(|(target, transform, entity_mut)| { let &AnimationTarget { id: target_id, player: player_id, @@ -955,7 +916,7 @@ pub fn animate_targets( } else { trace!( "Either an animation player {:?} or a graph was missing for the target \ - entity {:?} ({:?}); no animations will play this frame", + entity {:?} ({:?}); no animations will play this frame", player_id, entity_mut.id(), entity_mut.get::(), @@ -968,6 +929,12 @@ pub fn animate_targets( return; }; + let Some(threaded_animation_graph) = + threaded_animation_graphs.0.get(&animation_graph_id) + else { + return; + }; + // Determine which mask groups this animation target belongs to. let target_mask = animation_graph .mask_groups @@ -975,63 +942,104 @@ pub fn animate_targets( .cloned() .unwrap_or_default(); - // Apply the animations one after another. The way we accumulate - // weights ensures that the order we apply them in doesn't matter. - // - // Proof: Consider three animations A₀, A₁, A₂, … with weights w₀, - // w₁, w₂, … respectively. We seek the value: - // - // A₀w₀ + A₁w₁ + A₂w₂ + ⋯ - // - // Defining lerp(a, b, t) = a + t(b - a), we have: - // - // ⎛ ⎛ w₁ ⎞ w₂ ⎞ - // A₀w₀ + A₁w₁ + A₂w₂ + ⋯ = ⋯ lerp⎜lerp⎜A₀, A₁, ⎯⎯⎯⎯⎯⎯⎯⎯⎟, A₂, ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎟ ⋯ - // ⎝ ⎝ w₀ + w₁⎠ w₀ + w₁ + w₂⎠ - // - // Each step of the following loop corresponds to one of the lerp - // operations above. - let mut total_weight = 0.0; - for (&animation_graph_node_index, active_animation) in - animation_player.active_animations.iter() - { - // If the weight is zero or the current animation target is - // masked out, stop here. - if active_animation.weight == 0.0 - || (target_mask & active_animation.computed_mask) != 0 - { - continue; - } + let mut evaluation_state = animation_evaluation_state.get_or_default().borrow_mut(); + let evaluation_state = &mut *evaluation_state; - let Some(clip) = animation_graph - .get(animation_graph_node_index) - .and_then(|animation_graph_node| animation_graph_node.clip.as_ref()) - .and_then(|animation_clip_handle| clips.get(animation_clip_handle)) + // Evaluate the graph. + for &animation_graph_node_index in threaded_animation_graph.threaded_graph.iter() { + let Some(animation_graph_node) = animation_graph.get(animation_graph_node_index) else { continue; }; - let Some(curves) = clip.curves_for_target(target_id) else { - continue; - }; + match animation_graph_node.clip { + None => { + // This is a blend node. + for edge_index in threaded_animation_graph.sorted_edge_ranges + [animation_graph_node_index.index()] + .clone() + { + if let Err(err) = evaluation_state.blend_all( + threaded_animation_graph.sorted_edges[edge_index as usize], + ) { + warn!("Failed to blend animation: {:?}", err); + } + } - let weight = active_animation.computed_weight; - total_weight += weight; + if let Err(err) = evaluation_state.push_blend_register_all( + animation_graph_node.weight, + animation_graph_node_index, + ) { + warn!("Animation blending failed: {:?}", err); + } + } - let weight = weight / total_weight; - let seek_time = active_animation.seek_time; + Some(ref animation_clip_handle) => { + // This is a clip node. + let Some(active_animation) = animation_player + .active_animations + .get(&animation_graph_node_index) + else { + continue; + }; + + // If the weight is zero or the current animation target is + // masked out, stop here. + if active_animation.weight == 0.0 + || (target_mask + & threaded_animation_graph.computed_masks + [animation_graph_node_index.index()]) + != 0 + { + continue; + } - for curve in curves { - if let Err(err) = curve.0.apply( - seek_time, - transform.as_mut().map(|transform| transform.reborrow()), - entity_mut.reborrow(), - weight, - ) { - warn!("Animation application failed: {:?}", err); + let Some(clip) = clips.get(animation_clip_handle) else { + continue; + }; + + let Some(curves) = clip.curves_for_target(target_id) else { + continue; + }; + + let weight = active_animation.weight; + let seek_time = active_animation.seek_time; + + for curve in curves { + // Fetch the curve evaluator. Curve evaluator types + // are unique to each property, but shared among all + // curve types. For example, given two curve types A + // and B, `RotationCurve` and `RotationCurve` + // will both yield a `RotationCurveEvaluator` and + // therefore will share the same evaluator in this + // table. + let curve_evaluator_type_id = (*curve.0).evaluator_type(); + let curve_evaluator = evaluation_state + .curve_evaluators + .entry(curve_evaluator_type_id) + .or_insert_with(|| curve.0.create_evaluator()); + + evaluation_state + .current_curve_evaluator_types + .insert(curve_evaluator_type_id, ()); + + if let Err(err) = AnimationCurve::apply( + &*curve.0, + &mut **curve_evaluator, + seek_time, + weight, + animation_graph_node_index, + ) { + warn!("Animation application failed: {:?}", err); + } + } } } } + + if let Err(err) = evaluation_state.commit_all(transform, entity_mut) { + warn!("Animation application failed: {:?}", err); + } }); } @@ -1050,9 +1058,12 @@ impl Plugin for AnimationPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .init_resource::() .add_systems( PostUpdate, ( + graph::thread_animation_graphs, advance_transitions, advance_animations, // TODO: `animate_targets` can animate anything, so @@ -1100,17 +1111,63 @@ impl From<&Name> for AnimationTargetId { } } -impl AnimationGraphEvaluator { - // Starts a new depth-first search. - fn reset(&mut self, root: AnimationNodeIndex, node_count: usize) { - self.dfs_stack.clear(); - self.dfs_stack.push(root); +impl AnimationEvaluationState { + /// Calls [`AnimationCurveEvaluator::blend`] on all curve evaluator types + /// that we've been building up for a single target. + /// + /// The given `node_index` is the node that we're evaluating. + fn blend_all( + &mut self, + node_index: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + for curve_evaluator_type in self.current_curve_evaluator_types.keys() { + self.curve_evaluators + .get_mut(curve_evaluator_type) + .unwrap() + .blend(node_index)?; + } + Ok(()) + } - self.dfs_visited.grow(node_count); - self.dfs_visited.clear(); + /// Calls [`AnimationCurveEvaluator::push_blend_register`] on all curve + /// evaluator types that we've been building up for a single target. + /// + /// The `weight` parameter is the weight that should be pushed onto the + /// stack, while the `node_index` parameter is the node that we're + /// evaluating. + fn push_blend_register_all( + &mut self, + weight: f32, + node_index: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + for curve_evaluator_type in self.current_curve_evaluator_types.keys() { + self.curve_evaluators + .get_mut(curve_evaluator_type) + .unwrap() + .push_blend_register(weight, node_index)?; + } + Ok(()) + } - self.nodes.clear(); - self.nodes - .extend(iter::repeat(EvaluatedAnimationGraphNode::default()).take(node_count)); + /// Calls [`AnimationCurveEvaluator::commit`] on all curve evaluator types + /// that we've been building up for a single target. + /// + /// This is the call that actually writes the computed values into the + /// components being animated. + fn commit_all( + &mut self, + mut transform: Option>, + mut entity_mut: AnimationEntityMut, + ) -> Result<(), AnimationEvaluationError> { + for (curve_evaluator_type, _) in self.current_curve_evaluator_types.drain() { + self.curve_evaluators + .get_mut(&curve_evaluator_type) + .unwrap() + .commit( + transform.as_mut().map(|transform| transform.reborrow()), + entity_mut.reborrow(), + )?; + } + Ok(()) } } diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 33a30a88792cd4..4336151fefa64a 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -47,24 +47,24 @@ static NODE_RECTS: [NodeRect; 5] = [ NodeRect::new(10.00, 10.00, 97.64, 48.41), NodeRect::new(10.00, 78.41, 97.64, 48.41), NodeRect::new(286.08, 78.41, 97.64, 48.41), - NodeRect::new(148.04, 44.20, 97.64, 48.41), + NodeRect::new(148.04, 112.61, 97.64, 48.41), // was 44.20 NodeRect::new(10.00, 146.82, 97.64, 48.41), ]; /// The positions of the horizontal lines in the UI. static HORIZONTAL_LINES: [Line; 6] = [ - Line::new(107.64, 34.21, 20.20), + Line::new(107.64, 34.21, 158.24), Line::new(107.64, 102.61, 20.20), - Line::new(107.64, 171.02, 158.24), - Line::new(127.84, 68.41, 20.20), - Line::new(245.68, 68.41, 20.20), + Line::new(107.64, 171.02, 20.20), + Line::new(127.84, 136.82, 20.20), + Line::new(245.68, 136.82, 20.20), Line::new(265.88, 102.61, 20.20), ]; /// The positions of the vertical lines in the UI. static VERTICAL_LINES: [Line; 2] = [ - Line::new(127.83, 34.21, 68.40), - Line::new(265.88, 68.41, 102.61), + Line::new(127.83, 102.61, 68.40), + Line::new(265.88, 34.21, 102.61), ]; /// Initializes the app. From acea4e7e6fe163c0e519c26764785ab7bd37b189 Mon Sep 17 00:00:00 2001 From: MiniaczQ Date: Thu, 3 Oct 2024 15:16:55 +0200 Subject: [PATCH 002/546] Better warnings about invalid parameters (#15500) # Objective System param validation warnings should be configurable and default to "warn once" (per system). Fixes: #15391 ## Solution `SystemMeta` is given a new `ParamWarnPolicy` field. The policy decides whether warnings will be emitted by each system param when it fails validation. The policy is updated by the system after param validation fails. Example warning: ``` 2024-09-30T18:10:04.740749Z WARN bevy_ecs::system::function_system: System fallible_params::do_nothing_fail_validation will not run because it requested inaccessible system parameter Single<(), (With, With)> ``` Currently, only the first invalid parameter is displayed. Warnings can be disabled on function systems using `.param_never_warn()`. (there is also `.with_param_warn_policy(policy)`) ## Testing Ran `fallible_params` example. --------- Co-authored-by: SpecificProtagonist --- crates/bevy_ecs/src/lib.rs | 2 +- crates/bevy_ecs/src/schedule/executor/mod.rs | 12 --- .../src/schedule/executor/multi_threaded.rs | 5 +- .../bevy_ecs/src/schedule/executor/simple.rs | 5 - .../src/schedule/executor/single_threaded.rs | 5 - crates/bevy_ecs/src/system/adapter_system.rs | 5 +- crates/bevy_ecs/src/system/combinator.rs | 10 +- .../src/system/exclusive_function_system.rs | 3 +- crates/bevy_ecs/src/system/function_system.rs | 101 +++++++++++++++++- crates/bevy_ecs/src/system/system.rs | 2 +- crates/bevy_ecs/src/system/system_param.rs | 52 ++++++--- crates/bevy_render/src/extract_param.rs | 3 +- examples/ecs/fallible_params.rs | 29 +++-- 13 files changed, 176 insertions(+), 58 deletions(-) diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 4dda0d4ea9e385..c481da091a77ef 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -60,7 +60,7 @@ pub mod prelude { Commands, Deferred, EntityCommand, EntityCommands, In, InMut, InRef, IntoSystem, Local, NonSend, NonSendMut, ParallelCommands, ParamSet, Populated, Query, ReadOnlySystem, Res, ResMut, Resource, Single, System, SystemIn, SystemInput, SystemParamBuilder, - SystemParamFunction, + SystemParamFunction, WithParamWarnPolicy, }, world::{ Command, EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, diff --git a/crates/bevy_ecs/src/schedule/executor/mod.rs b/crates/bevy_ecs/src/schedule/executor/mod.rs index 24113aee2bee05..37fd9ff7246c8a 100644 --- a/crates/bevy_ecs/src/schedule/executor/mod.rs +++ b/crates/bevy_ecs/src/schedule/executor/mod.rs @@ -180,18 +180,6 @@ mod __rust_begin_short_backtrace { } } -#[macro_export] -/// Emits a warning about system being skipped. -macro_rules! warn_system_skipped { - ($ty:literal, $sys:expr) => { - bevy_utils::tracing::warn!( - "{} {} was skipped due to inaccessible system parameters.", - $ty, - $sys - ) - }; -} - #[cfg(test)] mod tests { use crate::{ diff --git a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs index 8782793e32f715..53453240dcb887 100644 --- a/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/multi_threaded.rs @@ -19,7 +19,6 @@ use crate::{ query::Access, schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, system::BoxedSystem, - warn_system_skipped, world::{unsafe_world_cell::UnsafeWorldCell, World}, }; @@ -524,7 +523,7 @@ impl ExecutorState { unsafe fn should_run( &mut self, system_index: usize, - system: &BoxedSystem, + system: &mut BoxedSystem, conditions: &mut Conditions, world: UnsafeWorldCell, ) -> bool { @@ -575,7 +574,6 @@ impl ExecutorState { // - `update_archetype_component_access` has been called for system. let valid_params = unsafe { system.validate_param_unsafe(world) }; if !valid_params { - warn_system_skipped!("System", system.name()); self.skipped_systems.insert(system_index); } should_run &= valid_params; @@ -751,7 +749,6 @@ unsafe fn evaluate_and_fold_conditions( // required by the condition. // - `update_archetype_component_access` has been called for condition. if !unsafe { condition.validate_param_unsafe(world) } { - warn_system_skipped!("Condition", condition.name()); return false; } // SAFETY: diff --git a/crates/bevy_ecs/src/schedule/executor/simple.rs b/crates/bevy_ecs/src/schedule/executor/simple.rs index 171125342cd787..508f1fbffd07a0 100644 --- a/crates/bevy_ecs/src/schedule/executor/simple.rs +++ b/crates/bevy_ecs/src/schedule/executor/simple.rs @@ -7,7 +7,6 @@ use crate::{ schedule::{ executor::is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule, }, - warn_system_skipped, world::World, }; @@ -83,9 +82,6 @@ impl SystemExecutor for SimpleExecutor { let system = &mut schedule.systems[system_index]; if should_run { let valid_params = system.validate_param(world); - if !valid_params { - warn_system_skipped!("System", system.name()); - } should_run &= valid_params; } @@ -139,7 +135,6 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W .iter_mut() .map(|condition| { if !condition.validate_param(world) { - warn_system_skipped!("Condition", condition.name()); return false; } __rust_begin_short_backtrace::readonly_run(&mut **condition, world) diff --git a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs index 93d814d3b2f83e..9cc6199ce6a4ab 100644 --- a/crates/bevy_ecs/src/schedule/executor/single_threaded.rs +++ b/crates/bevy_ecs/src/schedule/executor/single_threaded.rs @@ -5,7 +5,6 @@ use fixedbitset::FixedBitSet; use crate::{ schedule::{is_apply_deferred, BoxedCondition, ExecutorKind, SystemExecutor, SystemSchedule}, - warn_system_skipped, world::World, }; @@ -89,9 +88,6 @@ impl SystemExecutor for SingleThreadedExecutor { let system = &mut schedule.systems[system_index]; if should_run { let valid_params = system.validate_param(world); - if !valid_params { - warn_system_skipped!("System", system.name()); - } should_run &= valid_params; } @@ -171,7 +167,6 @@ fn evaluate_and_fold_conditions(conditions: &mut [BoxedCondition], world: &mut W .iter_mut() .map(|condition| { if !condition.validate_param(world) { - warn_system_skipped!("Condition", condition.name()); return false; } __rust_begin_short_backtrace::readonly_run(&mut **condition, world) diff --git a/crates/bevy_ecs/src/system/adapter_system.rs b/crates/bevy_ecs/src/system/adapter_system.rs index 8c25e57bbd91ee..ef5e51c08f8e21 100644 --- a/crates/bevy_ecs/src/system/adapter_system.rs +++ b/crates/bevy_ecs/src/system/adapter_system.rs @@ -179,8 +179,9 @@ where } #[inline] - unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool { - self.system.validate_param_unsafe(world) + unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { + // SAFETY: Delegate to other `System` implementations. + unsafe { self.system.validate_param_unsafe(world) } } fn initialize(&mut self, world: &mut crate::prelude::World) { diff --git a/crates/bevy_ecs/src/system/combinator.rs b/crates/bevy_ecs/src/system/combinator.rs index ce4c8785feb05b..e21f35eee4e82f 100644 --- a/crates/bevy_ecs/src/system/combinator.rs +++ b/crates/bevy_ecs/src/system/combinator.rs @@ -212,8 +212,9 @@ where } #[inline] - unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool { - self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) + unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { + // SAFETY: Delegate to other `System` implementations. + unsafe { self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) } } fn initialize(&mut self, world: &mut World) { @@ -430,8 +431,9 @@ where self.b.queue_deferred(world); } - unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool { - self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) + unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { + // SAFETY: Delegate to other `System` implementations. + unsafe { self.a.validate_param_unsafe(world) && self.b.validate_param_unsafe(world) } } fn validate_param(&mut self, world: &World) -> bool { diff --git a/crates/bevy_ecs/src/system/exclusive_function_system.rs b/crates/bevy_ecs/src/system/exclusive_function_system.rs index 3075f3c55772ee..8b1a06fb3a60ce 100644 --- a/crates/bevy_ecs/src/system/exclusive_function_system.rs +++ b/crates/bevy_ecs/src/system/exclusive_function_system.rs @@ -150,7 +150,8 @@ where } #[inline] - unsafe fn validate_param_unsafe(&self, _world: UnsafeWorldCell) -> bool { + unsafe fn validate_param_unsafe(&mut self, _world: UnsafeWorldCell) -> bool { + // All exclusive system params are always available. true } diff --git a/crates/bevy_ecs/src/system/function_system.rs b/crates/bevy_ecs/src/system/function_system.rs index 0614d7339ca4bd..fcd6a829e8afd6 100644 --- a/crates/bevy_ecs/src/system/function_system.rs +++ b/crates/bevy_ecs/src/system/function_system.rs @@ -43,6 +43,7 @@ pub struct SystemMeta { is_send: bool, has_deferred: bool, pub(crate) last_run: Tick, + param_warn_policy: ParamWarnPolicy, #[cfg(feature = "trace")] pub(crate) system_span: Span, #[cfg(feature = "trace")] @@ -59,6 +60,7 @@ impl SystemMeta { is_send: true, has_deferred: false, last_run: Tick::new(0), + param_warn_policy: ParamWarnPolicy::Once, #[cfg(feature = "trace")] system_span: info_span!("system", name = name), #[cfg(feature = "trace")] @@ -75,6 +77,7 @@ impl SystemMeta { /// Sets the name of of this system. /// /// Useful to give closure systems more readable and unique names for debugging and tracing. + #[inline] pub fn set_name(&mut self, new_name: impl Into>) { let new_name: Cow<'static, str> = new_name.into(); #[cfg(feature = "trace")] @@ -108,9 +111,94 @@ impl SystemMeta { /// Marks the system as having deferred buffers like [`Commands`](`super::Commands`) /// This lets the scheduler insert [`apply_deferred`](`crate::prelude::apply_deferred`) systems automatically. + #[inline] pub fn set_has_deferred(&mut self) { self.has_deferred = true; } + + /// Changes the warn policy. + #[inline] + pub(crate) fn set_param_warn_policy(&mut self, warn_policy: ParamWarnPolicy) { + self.param_warn_policy = warn_policy; + } + + /// Advances the warn policy after validation failed. + #[inline] + pub(crate) fn advance_param_warn_policy(&mut self) { + self.param_warn_policy.advance(); + } + + /// Emits a warning about inaccessible system param if policy allows it. + #[inline] + pub fn try_warn_param

(&self) + where + P: SystemParam, + { + self.param_warn_policy.try_warn::

(&self.name); + } +} + +/// State machine for emitting warnings when [system params are invalid](System::validate_param). +#[derive(Clone, Copy)] +pub enum ParamWarnPolicy { + /// No warning should ever be emitted. + Never, + /// The warning will be emitted once and status will update to [`Self::Never`]. + Once, +} + +impl ParamWarnPolicy { + /// Advances the warn policy after validation failed. + #[inline] + fn advance(&mut self) { + *self = Self::Never; + } + + /// Emits a warning about inaccessible system param if policy allows it. + #[inline] + fn try_warn

(&self, name: &str) + where + P: SystemParam, + { + if matches!(self, Self::Never) { + return; + } + + bevy_utils::tracing::warn!( + "{0} did not run because it requested inaccessible system parameter {1}", + name, + disqualified::ShortName::of::

() + ); + } +} + +/// Trait for manipulating warn policy of systems. +#[doc(hidden)] +pub trait WithParamWarnPolicy +where + M: 'static, + F: SystemParamFunction, + Self: Sized, +{ + /// Set warn policy. + fn with_param_warn_policy(self, warn_policy: ParamWarnPolicy) -> FunctionSystem; + + /// Disable all param warnings. + fn never_param_warn(self) -> FunctionSystem { + self.with_param_warn_policy(ParamWarnPolicy::Never) + } +} + +impl WithParamWarnPolicy for F +where + M: 'static, + F: SystemParamFunction, +{ + fn with_param_warn_policy(self, param_warn_policy: ParamWarnPolicy) -> FunctionSystem { + let mut system = IntoSystem::into_system(self); + system.system_meta.set_param_warn_policy(param_warn_policy); + system + } } // TODO: Actually use this in FunctionSystem. We should probably only do this once Systems are constructed using a World reference @@ -657,9 +745,18 @@ where } #[inline] - unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool { + unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool { let param_state = self.param_state.as_ref().expect(Self::PARAM_MESSAGE); - F::Param::validate_param(param_state, &self.system_meta, world) + // SAFETY: + // - The caller has invoked `update_archetype_component_access`, which will panic + // if the world does not match. + // - All world accesses used by `F::Param` have been registered, so the caller + // will ensure that there are no data access conflicts. + let is_valid = unsafe { F::Param::validate_param(param_state, &self.system_meta, world) }; + if !is_valid { + self.system_meta.advance_param_warn_policy(); + } + is_valid } #[inline] diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index 60d3c58f62d478..ded89235e72ef4 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -117,7 +117,7 @@ pub trait System: Send + Sync + 'static { /// - The method [`System::update_archetype_component_access`] must be called at some /// point before this one, with the same exact [`World`]. If [`System::update_archetype_component_access`] /// panics (or otherwise does not return for any reason), this method must not be called. - unsafe fn validate_param_unsafe(&self, world: UnsafeWorldCell) -> bool; + unsafe fn validate_param_unsafe(&mut self, world: UnsafeWorldCell) -> bool; /// Safe version of [`System::validate_param_unsafe`]. /// that runs on exclusive, single-threaded `world` pointer. diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index 013d75265a900c..bfa2fe108d70d6 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -421,7 +421,11 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam fo world.change_tick(), ) }; - result.is_ok() + let is_valid = result.is_ok(); + if !is_valid { + system_meta.try_warn_param::(); + } + is_valid } } @@ -483,7 +487,11 @@ unsafe impl<'a, D: QueryData + 'static, F: QueryFilter + 'static> SystemParam world.change_tick(), ) }; - !matches!(result, Err(QuerySingleError::MultipleEntities(_))) + let is_valid = !matches!(result, Err(QuerySingleError::MultipleEntities(_))); + if !is_valid { + system_meta.try_warn_param::(); + } + is_valid } } @@ -773,14 +781,18 @@ unsafe impl<'a, T: Resource> SystemParam for Res<'a, T> { #[inline] unsafe fn validate_param( &component_id: &Self::State, - _system_meta: &SystemMeta, + system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> bool { // SAFETY: Read-only access to resource metadata. - unsafe { world.storages() } + let is_valid = unsafe { world.storages() } .resources .get(component_id) - .is_some_and(ResourceData::is_present) + .is_some_and(ResourceData::is_present); + if !is_valid { + system_meta.try_warn_param::(); + } + is_valid } #[inline] @@ -883,14 +895,18 @@ unsafe impl<'a, T: Resource> SystemParam for ResMut<'a, T> { #[inline] unsafe fn validate_param( &component_id: &Self::State, - _system_meta: &SystemMeta, + system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> bool { // SAFETY: Read-only access to resource metadata. - unsafe { world.storages() } + let is_valid = unsafe { world.storages() } .resources .get(component_id) - .is_some_and(ResourceData::is_present) + .is_some_and(ResourceData::is_present); + if !is_valid { + system_meta.try_warn_param::(); + } + is_valid } #[inline] @@ -1429,14 +1445,18 @@ unsafe impl<'a, T: 'static> SystemParam for NonSend<'a, T> { #[inline] unsafe fn validate_param( &component_id: &Self::State, - _system_meta: &SystemMeta, + system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> bool { // SAFETY: Read-only access to resource metadata. - unsafe { world.storages() } + let is_valid = unsafe { world.storages() } .non_send_resources .get(component_id) - .is_some_and(ResourceData::is_present) + .is_some_and(ResourceData::is_present); + if !is_valid { + system_meta.try_warn_param::(); + } + is_valid } #[inline] @@ -1536,14 +1556,18 @@ unsafe impl<'a, T: 'static> SystemParam for NonSendMut<'a, T> { #[inline] unsafe fn validate_param( &component_id: &Self::State, - _system_meta: &SystemMeta, + system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> bool { // SAFETY: Read-only access to resource metadata. - unsafe { world.storages() } + let is_valid = unsafe { world.storages() } .non_send_resources .get(component_id) - .is_some_and(ResourceData::is_present) + .is_some_and(ResourceData::is_present); + if !is_valid { + system_meta.try_warn_param::(); + } + is_valid } #[inline] diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index 914fe32dfb6e46..89b6edef1903df 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -77,12 +77,13 @@ where #[inline] unsafe fn validate_param( state: &Self::State, - _system_meta: &SystemMeta, + system_meta: &SystemMeta, world: UnsafeWorldCell, ) -> bool { // SAFETY: Read-only access to world data registered in `init_state`. let result = unsafe { world.get_resource_by_id(state.main_world_state) }; let Some(main_world) = result else { + system_meta.try_warn_param::<&World>(); return false; }; // SAFETY: Type is guaranteed by `SystemState`. diff --git a/examples/ecs/fallible_params.rs b/examples/ecs/fallible_params.rs index 397e883f9b2b91..a3ca04b9a00944 100644 --- a/examples/ecs/fallible_params.rs +++ b/examples/ecs/fallible_params.rs @@ -20,9 +20,22 @@ fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - // We add all the systems one after another. - // We don't need to use run conditions here. - .add_systems(Update, (user_input, move_targets, move_pointer).chain()) + // Systems that fail parameter validation will emit warnings. + // The default policy is to emit a warning once per system. + // This is good for catching unexpected behavior, but can + // lead to spam. You can disable invalid param warnings + // per system using the `.never_param_warn()` method. + .add_systems( + Update, + ( + user_input, + move_targets.never_param_warn(), + move_pointer.never_param_warn(), + ) + .chain(), + ) + // We will leave this systems with default warning policy. + .add_systems(Update, do_nothing_fail_validation) .run(); } @@ -67,9 +80,9 @@ fn setup(mut commands: Commands, asset_server: Res) { )); } -// System that reads user input. -// If user presses 'A' we spawn a new random enemy. -// If user presses 'R' we remove a random enemy (if any exist). +/// System that reads user input. +/// If user presses 'A' we spawn a new random enemy. +/// If user presses 'R' we remove a random enemy (if any exist). fn user_input( mut commands: Commands, enemies: Query>, @@ -146,3 +159,7 @@ fn move_pointer( player_transform.rotate_axis(Dir3::Z, player.rotation_speed * time.delta_seconds()); } } + +/// This system always fails param validation, because we never +/// create an entity with both [`Player`] and [`Enemy`] components. +fn do_nothing_fail_validation(_: Single<(), (With, With)>) {} From 5e81154e9c67aa88d78db299a4d3a970a8b7cfa8 Mon Sep 17 00:00:00 2001 From: rudderbucky Date: Thu, 3 Oct 2024 07:59:37 -0700 Subject: [PATCH 003/546] Despawn and despawn_recursive benchmarks (#15610) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Add despawn and despawn_recursive benchmarks in a similar vein to the spawn benchmark. ## Testing Ran `cargo bench` from `benches` and it compiled fine. On my machine: ``` despawn_world/1_entities time: [3.1495 ns 3.1574 ns 3.1652 ns] Found 4 outliers among 100 measurements (4.00%) 3 (3.00%) high mild 1 (1.00%) high severe despawn_world/10_entities time: [28.629 ns 28.674 ns 28.720 ns] Found 3 outliers among 100 measurements (3.00%) 2 (2.00%) high mild 1 (1.00%) high severe despawn_world/100_entities time: [286.95 ns 287.41 ns 287.90 ns] Found 5 outliers among 100 measurements (5.00%) 5 (5.00%) high mild despawn_world/1000_entities time: [2.8739 µs 2.9001 µs 2.9355 µs] Found 7 outliers among 100 measurements (7.00%) 1 (1.00%) high mild 6 (6.00%) high severe despawn_world/10000_entities time: [28.535 µs 28.617 µs 28.698 µs] Found 2 outliers among 100 measurements (2.00%) 1 (1.00%) high mild 1 (1.00%) high severe despawn_world_recursive/1_entities time: [5.2270 ns 5.2507 ns 5.2907 ns] Found 11 outliers among 100 measurements (11.00%) 1 (1.00%) low mild 6 (6.00%) high mild 4 (4.00%) high severe despawn_world_recursive/10_entities time: [57.495 ns 57.590 ns 57.691 ns] Found 2 outliers among 100 measurements (2.00%) 1 (1.00%) low mild 1 (1.00%) high mild despawn_world_recursive/100_entities time: [514.43 ns 518.91 ns 526.88 ns] Found 4 outliers among 100 measurements (4.00%) 1 (1.00%) high mild 3 (3.00%) high severe despawn_world_recursive/1000_entities time: [5.0362 µs 5.0463 µs 5.0578 µs] Found 7 outliers among 100 measurements (7.00%) 2 (2.00%) high mild 5 (5.00%) high severe despawn_world_recursive/10000_entities time: [51.159 µs 51.603 µs 52.215 µs] Found 9 outliers among 100 measurements (9.00%) 3 (3.00%) high mild 6 (6.00%) high severe ``` --- benches/benches/bevy_ecs/world/despawn.rs | 32 +++++++++++++++ .../bevy_ecs/world/despawn_recursive.rs | 39 +++++++++++++++++++ benches/benches/bevy_ecs/world/mod.rs | 8 ++++ 3 files changed, 79 insertions(+) create mode 100644 benches/benches/bevy_ecs/world/despawn.rs create mode 100644 benches/benches/bevy_ecs/world/despawn_recursive.rs diff --git a/benches/benches/bevy_ecs/world/despawn.rs b/benches/benches/bevy_ecs/world/despawn.rs new file mode 100644 index 00000000000000..ace88e744a482a --- /dev/null +++ b/benches/benches/bevy_ecs/world/despawn.rs @@ -0,0 +1,32 @@ +use bevy_ecs::prelude::*; +use criterion::Criterion; +use glam::*; + +#[derive(Component)] +struct A(Mat4); +#[derive(Component)] +struct B(Vec4); + +pub fn world_despawn(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("despawn_world"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + for entity_count in (0..5).map(|i| 10_u32.pow(i)) { + let mut world = World::default(); + for _ in 0..entity_count { + world.spawn((A(Mat4::default()), B(Vec4::default()))); + } + + let ents = world.iter_entities().map(|e| e.id()).collect::>(); + group.bench_function(format!("{}_entities", entity_count), |bencher| { + bencher.iter(|| { + ents.iter().for_each(|e| { + world.despawn(*e); + }); + }); + }); + } + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/world/despawn_recursive.rs b/benches/benches/bevy_ecs/world/despawn_recursive.rs new file mode 100644 index 00000000000000..3c2b523b5faa3b --- /dev/null +++ b/benches/benches/bevy_ecs/world/despawn_recursive.rs @@ -0,0 +1,39 @@ +use bevy_ecs::prelude::*; +use bevy_hierarchy::despawn_with_children_recursive; +use bevy_hierarchy::BuildChildren; +use bevy_hierarchy::ChildBuild; +use criterion::Criterion; +use glam::*; + +#[derive(Component)] +struct A(Mat4); +#[derive(Component)] +struct B(Vec4); + +pub fn world_despawn_recursive(criterion: &mut Criterion) { + let mut group = criterion.benchmark_group("despawn_world_recursive"); + group.warm_up_time(core::time::Duration::from_millis(500)); + group.measurement_time(core::time::Duration::from_secs(4)); + + for entity_count in (0..5).map(|i| 10_u32.pow(i)) { + let mut world = World::default(); + for _ in 0..entity_count { + world + .spawn((A(Mat4::default()), B(Vec4::default()))) + .with_children(|parent| { + parent.spawn((A(Mat4::default()), B(Vec4::default()))); + }); + } + + let ents = world.iter_entities().map(|e| e.id()).collect::>(); + group.bench_function(format!("{}_entities", entity_count), |bencher| { + bencher.iter(|| { + ents.iter().for_each(|e| { + despawn_with_children_recursive(&mut world, *e); + }); + }); + }); + } + + group.finish(); +} diff --git a/benches/benches/bevy_ecs/world/mod.rs b/benches/benches/bevy_ecs/world/mod.rs index 8b12a08fcd783d..8af5e399018a18 100644 --- a/benches/benches/bevy_ecs/world/mod.rs +++ b/benches/benches/bevy_ecs/world/mod.rs @@ -3,6 +3,12 @@ use criterion::criterion_group; mod commands; use commands::*; +mod despawn; +use despawn::*; + +mod despawn_recursive; +use despawn_recursive::*; + mod spawn; use spawn::*; @@ -28,6 +34,8 @@ criterion_group!( world_query_iter, world_query_for_each, world_spawn, + world_despawn, + world_despawn_recursive, query_get, query_get_many::<2>, query_get_many::<5>, From 2da8d17a4434246a83686f42ba2b85c5745865a5 Mon Sep 17 00:00:00 2001 From: rudderbucky Date: Thu, 3 Oct 2024 09:21:05 -0700 Subject: [PATCH 004/546] Add try_despawn methods to World/Commands (#15480) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fixes #14511. `despawn` allows you to remove entities from the world. However, if the entity does not exist, it emits a warning. This may not be intended behavior for many users who have use cases where they need to call `despawn` regardless of if the entity actually exists (see the issue), or don't care in general if the entity already doesn't exist. (Also trying to gauge interest on if this feature makes sense, I'd personally love to have it, but I could see arguments that this might be a footgun. Just trying to help here 😄 If there's no contention I could also implement this for `despawn_recursive` and `despawn_descendants` in the same PR) ## Solution Add `try_despawn`, `try_despawn_recursive` and `try_despawn_descendants`. Modify `World::despawn_with_caller` to also take in a `warn` boolean argument, which is then considered when logging the warning. Set `log_warning` to `true` in the case of `despawn`, and `false` in the case of `try_despawn`. ## Testing Ran `cargo run -p ci` on macOS, it seemed fine. --- .../bevy_ecs/world/despawn_recursive.rs | 2 +- crates/bevy_ecs/src/system/commands/mod.rs | 25 +++- crates/bevy_ecs/src/world/mod.rs | 15 +- crates/bevy_hierarchy/src/hierarchy.rs | 131 +++++++++++++----- crates/bevy_ui/src/layout/mod.rs | 2 +- 5 files changed, 135 insertions(+), 40 deletions(-) diff --git a/benches/benches/bevy_ecs/world/despawn_recursive.rs b/benches/benches/bevy_ecs/world/despawn_recursive.rs index 3c2b523b5faa3b..482086ab174449 100644 --- a/benches/benches/bevy_ecs/world/despawn_recursive.rs +++ b/benches/benches/bevy_ecs/world/despawn_recursive.rs @@ -29,7 +29,7 @@ pub fn world_despawn_recursive(criterion: &mut Criterion) { group.bench_function(format!("{}_entities", entity_count), |bencher| { bencher.iter(|| { ents.iter().for_each(|e| { - despawn_with_children_recursive(&mut world, *e); + despawn_with_children_recursive(&mut world, *e, true); }); }); }); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 4f941c07a6721a..3d6a6cf536c663 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1411,6 +1411,14 @@ impl EntityCommands<'_> { self.queue(despawn()); } + /// Despawns the entity. + /// This will not emit a warning if the entity does not exist, essentially performing + /// the same function as [`Self::despawn`] without emitting warnings. + #[track_caller] + pub fn try_despawn(&mut self) { + self.queue(try_despawn()); + } + /// Pushes an [`EntityCommand`] to the queue, which will get executed for the current [`Entity`]. /// /// # Examples @@ -1697,7 +1705,22 @@ where fn despawn() -> impl EntityCommand { let caller = Location::caller(); move |entity: Entity, world: &mut World| { - world.despawn_with_caller(entity, caller); + world.despawn_with_caller(entity, caller, true); + } +} + +/// A [`Command`] that despawns a specific entity. +/// This will not emit a warning if the entity does not exist. +/// +/// # Note +/// +/// This won't clean up external references to the entity (such as parent-child relationships +/// if you're using `bevy_hierarchy`), which may leave the world in an invalid state. +#[track_caller] +fn try_despawn() -> impl EntityCommand { + let caller = Location::caller(); + move |entity: Entity, world: &mut World| { + world.despawn_with_caller(entity, caller, false); } } diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 6f1774e2648332..8a665e51ece6b0 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -1385,7 +1385,15 @@ impl World { #[track_caller] #[inline] pub fn despawn(&mut self, entity: Entity) -> bool { - self.despawn_with_caller(entity, Location::caller()) + self.despawn_with_caller(entity, Location::caller(), true) + } + + /// Performs the same function as [`Self::despawn`] but does not emit a warning if + /// the entity does not exist. + #[track_caller] + #[inline] + pub fn try_despawn(&mut self, entity: Entity) -> bool { + self.despawn_with_caller(entity, Location::caller(), false) } #[inline] @@ -1393,13 +1401,16 @@ impl World { &mut self, entity: Entity, caller: &'static Location, + log_warning: bool, ) -> bool { self.flush(); if let Some(entity) = self.get_entity_mut(entity) { entity.despawn(); true } else { - warn!("error[B0003]: {caller}: Could not despawn entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", entity); + if log_warning { + warn!("error[B0003]: {caller}: Could not despawn entity {:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003", entity); + } false } } diff --git a/crates/bevy_hierarchy/src/hierarchy.rs b/crates/bevy_hierarchy/src/hierarchy.rs index 9f090d8ce4b91f..351f36ae7d747a 100644 --- a/crates/bevy_hierarchy/src/hierarchy.rs +++ b/crates/bevy_hierarchy/src/hierarchy.rs @@ -11,6 +11,8 @@ use bevy_utils::tracing::debug; pub struct DespawnRecursive { /// Target entity pub entity: Entity, + /// Whether or not this command should output a warning if the entity does not exist + pub warn: bool, } /// Despawns the given entity's children recursively @@ -18,10 +20,12 @@ pub struct DespawnRecursive { pub struct DespawnChildrenRecursive { /// Target entity pub entity: Entity, + /// Whether or not this command should output a warning if the entity does not exist + pub warn: bool, } /// Function for despawning an entity and all its children -pub fn despawn_with_children_recursive(world: &mut World, entity: Entity) { +pub fn despawn_with_children_recursive(world: &mut World, entity: Entity, warn: bool) { // first, make the entity's own parent forget about it if let Some(parent) = world.get::(entity).map(|parent| parent.0) { if let Some(mut children) = world.get_mut::(parent) { @@ -30,26 +34,30 @@ pub fn despawn_with_children_recursive(world: &mut World, entity: Entity) { } // then despawn the entity and all of its children - despawn_with_children_recursive_inner(world, entity); + despawn_with_children_recursive_inner(world, entity, warn); } -// Should only be called by `despawn_with_children_recursive`! -fn despawn_with_children_recursive_inner(world: &mut World, entity: Entity) { +// Should only be called by `despawn_with_children_recursive` and `try_despawn_with_children_recursive`! +fn despawn_with_children_recursive_inner(world: &mut World, entity: Entity, warn: bool) { if let Some(mut children) = world.get_mut::(entity) { for e in core::mem::take(&mut children.0) { - despawn_with_children_recursive_inner(world, e); + despawn_with_children_recursive_inner(world, e, warn); } } - if !world.despawn(entity) { + if warn { + if !world.despawn(entity) { + debug!("Failed to despawn entity {:?}", entity); + } + } else if !world.try_despawn(entity) { debug!("Failed to despawn entity {:?}", entity); } } -fn despawn_children_recursive(world: &mut World, entity: Entity) { +fn despawn_children_recursive(world: &mut World, entity: Entity, warn: bool) { if let Some(children) = world.entity_mut(entity).take::() { for e in children.0 { - despawn_with_children_recursive_inner(world, e); + despawn_with_children_recursive_inner(world, e, warn); } } } @@ -60,10 +68,11 @@ impl Command for DespawnRecursive { let _span = bevy_utils::tracing::info_span!( "command", name = "DespawnRecursive", - entity = bevy_utils::tracing::field::debug(self.entity) + entity = bevy_utils::tracing::field::debug(self.entity), + warn = bevy_utils::tracing::field::debug(self.warn) ) .entered(); - despawn_with_children_recursive(world, self.entity); + despawn_with_children_recursive(world, self.entity, self.warn); } } @@ -73,10 +82,12 @@ impl Command for DespawnChildrenRecursive { let _span = bevy_utils::tracing::info_span!( "command", name = "DespawnChildrenRecursive", - entity = bevy_utils::tracing::field::debug(self.entity) + entity = bevy_utils::tracing::field::debug(self.entity), + warn = bevy_utils::tracing::field::debug(self.warn) ) .entered(); - despawn_children_recursive(world, self.entity); + + despawn_children_recursive(world, self.entity, self.warn); } } @@ -87,6 +98,12 @@ pub trait DespawnRecursiveExt { /// Despawns all descendants of the given entity. fn despawn_descendants(&mut self) -> &mut Self; + + /// Similar to [`Self::despawn_recursive`] but does not emit warnings + fn try_despawn_recursive(self); + + /// Similar to [`Self::despawn_descendants`] but does not emit warnings + fn try_despawn_descendants(&mut self) -> &mut Self; } impl DespawnRecursiveExt for EntityCommands<'_> { @@ -94,46 +111,90 @@ impl DespawnRecursiveExt for EntityCommands<'_> { /// This will emit warnings for any entity that does not exist. fn despawn_recursive(mut self) { let entity = self.id(); - self.commands().queue(DespawnRecursive { entity }); + self.commands() + .queue(DespawnRecursive { entity, warn: true }); } fn despawn_descendants(&mut self) -> &mut Self { let entity = self.id(); - self.commands().queue(DespawnChildrenRecursive { entity }); + self.commands() + .queue(DespawnChildrenRecursive { entity, warn: true }); + self + } + + /// Despawns the provided entity and its children. + /// This will never emit warnings. + fn try_despawn_recursive(mut self) { + let entity = self.id(); + self.commands().queue(DespawnRecursive { + entity, + warn: false, + }); + } + + fn try_despawn_descendants(&mut self) -> &mut Self { + let entity = self.id(); + self.commands().queue(DespawnChildrenRecursive { + entity, + warn: false, + }); self } } +fn despawn_recursive_inner(world: EntityWorldMut, warn: bool) { + let entity = world.id(); + + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!( + "despawn_recursive", + entity = bevy_utils::tracing::field::debug(entity), + warn = bevy_utils::tracing::field::debug(warn) + ) + .entered(); + + despawn_with_children_recursive(world.into_world_mut(), entity, warn); +} + +fn despawn_descendants_inner<'v, 'w>( + world: &'v mut EntityWorldMut<'w>, + warn: bool, +) -> &'v mut EntityWorldMut<'w> { + let entity = world.id(); + + #[cfg(feature = "trace")] + let _span = bevy_utils::tracing::info_span!( + "despawn_descendants", + entity = bevy_utils::tracing::field::debug(entity), + warn = bevy_utils::tracing::field::debug(warn) + ) + .entered(); + + world.world_scope(|world| { + despawn_children_recursive(world, entity, warn); + }); + world +} + impl<'w> DespawnRecursiveExt for EntityWorldMut<'w> { /// Despawns the provided entity and its children. /// This will emit warnings for any entity that does not exist. fn despawn_recursive(self) { - let entity = self.id(); - - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!( - "despawn_recursive", - entity = bevy_utils::tracing::field::debug(entity) - ) - .entered(); - - despawn_with_children_recursive(self.into_world_mut(), entity); + despawn_recursive_inner(self, true); } fn despawn_descendants(&mut self) -> &mut Self { - let entity = self.id(); + despawn_descendants_inner(self, true) + } - #[cfg(feature = "trace")] - let _span = bevy_utils::tracing::info_span!( - "despawn_descendants", - entity = bevy_utils::tracing::field::debug(entity) - ) - .entered(); + /// Despawns the provided entity and its children. + /// This will not emit warnings. + fn try_despawn_recursive(self) { + despawn_recursive_inner(self, false); + } - self.world_scope(|world| { - despawn_children_recursive(world, entity); - }); - self + fn try_despawn_descendants(&mut self) -> &mut Self { + despawn_descendants_inner(self, false) } } diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index fe9ccca5b43b90..1965b24096d894 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -793,7 +793,7 @@ mod tests { } // despawn the parent entity and its descendants - despawn_with_children_recursive(&mut world, ui_parent_entity); + despawn_with_children_recursive(&mut world, ui_parent_entity, true); ui_schedule.run(&mut world); From 336c23c1aa865c7f329f699e99bd0560ef29cb79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B8holm?= Date: Thu, 3 Oct 2024 19:05:26 +0200 Subject: [PATCH 005/546] Rename observe to observe_entity on EntityWorldMut (#15616) # Objective The current observers have some unfortunate footguns where you can end up confused about what is actually being observed. For apps you can chain observe like `app.observe(..).observe(..)` which works like you would expect, but if you try the same with world the first `observe()` will return the `EntityWorldMut` for the created observer, and the second `observe()` will only observe on the observer entity. It took several hours for multiple people on discord to figure this out, which is not a great experience. ## Solution Rename `observe` on entities to `observe_entity`. It's slightly more verbose when you know you have an entity, but it feels right to me that observers for specific things have more specific naming, and it prevents this issue completely. Another possible solution would be to unify `observe` on `App` and `World` to have the same kind of return type, but I'm not sure exactly what that would look like. ## Testing Simple name change, so only concern is docs really. --- ## Migration Guide The `observe()` method on entities has been renamed to `observe_entity()` to prevent confusion about what is being observed in some cases. --- .../benches/bevy_ecs/observers/propagation.rs | 12 ++-- benches/benches/bevy_ecs/observers/simple.rs | 2 +- .../bevy_dev_tools/src/ci_testing/systems.rs | 2 +- crates/bevy_ecs/src/observer/mod.rs | 58 +++++++++++++------ crates/bevy_ecs/src/observer/runner.rs | 6 +- crates/bevy_ecs/src/system/commands/mod.rs | 2 +- crates/bevy_ecs/src/world/entity_ref.rs | 2 +- crates/bevy_picking/src/events.rs | 2 +- crates/bevy_picking/src/lib.rs | 2 +- 9 files changed, 52 insertions(+), 36 deletions(-) diff --git a/benches/benches/bevy_ecs/observers/propagation.rs b/benches/benches/bevy_ecs/observers/propagation.rs index b702662e7dd8cb..75fb1f9347e8ff 100644 --- a/benches/benches/bevy_ecs/observers/propagation.rs +++ b/benches/benches/bevy_ecs/observers/propagation.rs @@ -1,9 +1,5 @@ use bevy_ecs::{ - component::Component, - entity::Entity, - event::Event, - observer::Trigger, - world::World, + component::Component, entity::Entity, event::Event, observer::Trigger, world::World, }; use bevy_hierarchy::{BuildChildren, Parent}; @@ -110,15 +106,15 @@ fn add_listeners_to_hierarchy( world: &mut World, ) { for e in roots.iter() { - world.entity_mut(*e).observe(empty_listener::); + world.entity_mut(*e).observe_entity(empty_listener::); } for e in leaves.iter() { - world.entity_mut(*e).observe(empty_listener::); + world.entity_mut(*e).observe_entity(empty_listener::); } let mut rng = deterministic_rand(); for e in nodes.iter() { if rng.gen_bool(DENSITY as f64 / 100.0) { - world.entity_mut(*e).observe(empty_listener::); + world.entity_mut(*e).observe_entity(empty_listener::); } } } diff --git a/benches/benches/bevy_ecs/observers/simple.rs b/benches/benches/bevy_ecs/observers/simple.rs index 4d4d5bc2aa852a..7b7acc5564a55c 100644 --- a/benches/benches/bevy_ecs/observers/simple.rs +++ b/benches/benches/bevy_ecs/observers/simple.rs @@ -29,7 +29,7 @@ pub fn observe_simple(criterion: &mut Criterion) { let mut world = World::new(); let mut entities = vec![]; for _ in 0..10000 { - entities.push(world.spawn_empty().observe(empty_listener_base).id()); + entities.push(world.spawn_empty().observe_entity(empty_listener_base).id()); } entities.shuffle(&mut deterministic_rand()); bencher.iter(|| { diff --git a/crates/bevy_dev_tools/src/ci_testing/systems.rs b/crates/bevy_dev_tools/src/ci_testing/systems.rs index 20c758a91cdfc4..c3d83ec01ff044 100644 --- a/crates/bevy_dev_tools/src/ci_testing/systems.rs +++ b/crates/bevy_dev_tools/src/ci_testing/systems.rs @@ -25,7 +25,7 @@ pub(crate) fn send_events(world: &mut World, mut current_frame: Local) { let path = format!("./screenshot-{}.png", *current_frame); world .spawn(Screenshot::primary_window()) - .observe(save_to_disk(path)); + .observe_entity(save_to_disk(path)); info!("Took a screenshot at frame {}.", *current_frame); } // Custom events are forwarded to the world. diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index c74d78ae0318ff..5dcc7f70b65cb6 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -839,7 +839,7 @@ mod tests { world .spawn_empty() - .observe(|_: Trigger| panic!("Trigger routed to non-targeted entity.")); + .observe_entity(|_: Trigger| panic!("Trigger routed to non-targeted entity.")); world.observe(move |obs: Trigger, mut res: ResMut| { assert_eq!(obs.entity(), Entity::PLACEHOLDER); res.observed("event_a"); @@ -860,10 +860,10 @@ mod tests { world .spawn_empty() - .observe(|_: Trigger| panic!("Trigger routed to non-targeted entity.")); + .observe_entity(|_: Trigger| panic!("Trigger routed to non-targeted entity.")); let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("a_1")) + .observe_entity(|_: Trigger, mut res: ResMut| res.observed("a_1")) .id(); world.observe(move |obs: Trigger, mut res: ResMut| { assert_eq!(obs.entity(), entity); @@ -931,12 +931,16 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("parent")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("parent"); + }) .id(); let child = world .spawn(Parent(parent)) - .observe(|_: Trigger, mut res: ResMut| res.observed("child")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("child"); + }) .id(); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut @@ -954,12 +958,16 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("parent")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("parent"); + }) .id(); let child = world .spawn(Parent(parent)) - .observe(|_: Trigger, mut res: ResMut| res.observed("child")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("child"); + }) .id(); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut @@ -980,12 +988,16 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("parent")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("parent"); + }) .id(); let child = world .spawn(Parent(parent)) - .observe(|_: Trigger, mut res: ResMut| res.observed("child")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("child"); + }) .id(); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut @@ -1006,12 +1018,14 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("parent")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("parent"); + }) .id(); let child = world .spawn(Parent(parent)) - .observe( + .observe_entity( |mut trigger: Trigger, mut res: ResMut| { res.observed("child"); trigger.propagate(false); @@ -1034,19 +1048,21 @@ mod tests { let parent = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("parent")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("parent"); + }) .id(); let child_a = world .spawn(Parent(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe_entity(|_: Trigger, mut res: ResMut| { res.observed("child_a"); }) .id(); let child_b = world .spawn(Parent(parent)) - .observe(|_: Trigger, mut res: ResMut| { + .observe_entity(|_: Trigger, mut res: ResMut| { res.observed("child_b"); }) .id(); @@ -1069,7 +1085,9 @@ mod tests { let entity = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| res.observed("event")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("event"); + }) .id(); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut @@ -1087,14 +1105,14 @@ mod tests { let parent_a = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe_entity(|_: Trigger, mut res: ResMut| { res.observed("parent_a"); }) .id(); let child_a = world .spawn(Parent(parent_a)) - .observe( + .observe_entity( |mut trigger: Trigger, mut res: ResMut| { res.observed("child_a"); trigger.propagate(false); @@ -1104,14 +1122,16 @@ mod tests { let parent_b = world .spawn_empty() - .observe(|_: Trigger, mut res: ResMut| { + .observe_entity(|_: Trigger, mut res: ResMut| { res.observed("parent_b"); }) .id(); let child_b = world .spawn(Parent(parent_b)) - .observe(|_: Trigger, mut res: ResMut| res.observed("child_b")) + .observe_entity(|_: Trigger, mut res: ResMut| { + res.observed("child_b"); + }) .id(); // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut diff --git a/crates/bevy_ecs/src/observer/runner.rs b/crates/bevy_ecs/src/observer/runner.rs index f85725d3925250..02340b6128289f 100644 --- a/crates/bevy_ecs/src/observer/runner.rs +++ b/crates/bevy_ecs/src/observer/runner.rs @@ -228,12 +228,12 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// # let e2 = world.spawn_empty().id(); /// # #[derive(Event)] /// # struct Explode; -/// world.entity_mut(e1).observe(|trigger: Trigger, mut commands: Commands| { +/// world.entity_mut(e1).observe_entity(|trigger: Trigger, mut commands: Commands| { /// println!("Boom!"); /// commands.entity(trigger.entity()).despawn(); /// }); /// -/// world.entity_mut(e2).observe(|trigger: Trigger, mut commands: Commands| { +/// world.entity_mut(e2).observe_entity(|trigger: Trigger, mut commands: Commands| { /// println!("The explosion fizzles! This entity is immune!"); /// }); /// ``` @@ -241,7 +241,7 @@ pub type ObserverRunner = fn(DeferredWorld, ObserverTrigger, PtrMut, propagate: /// If all entities watched by a given [`Observer`] are despawned, the [`Observer`] entity will also be despawned. /// This protects against observer "garbage" building up over time. /// -/// The examples above calling [`EntityWorldMut::observe`] to add entity-specific observer logic are (once again) +/// The examples above calling [`EntityWorldMut::observe_entity`] to add entity-specific observer logic are (once again) /// just shorthand for spawning an [`Observer`] directly: /// /// ``` diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 3d6a6cf536c663..bec88f51b5afad 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1886,7 +1886,7 @@ fn observe( ) -> impl EntityCommand { move |entity, world: &mut World| { if let Some(mut entity) = world.get_entity_mut(entity) { - entity.observe(observer); + entity.observe_entity(observer); } } } diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 023db78f5dbdd2..fbc58a91048f4c 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1840,7 +1840,7 @@ impl<'w> EntityWorldMut<'w> { /// Creates an [`Observer`] listening for events of type `E` targeting this entity. /// In order to trigger the callback the entity must also match the query when the event is fired. - pub fn observe( + pub fn observe_entity( &mut self, observer: impl IntoObserverSystem, ) -> &mut Self { diff --git a/crates/bevy_picking/src/events.rs b/crates/bevy_picking/src/events.rs index c61ca42aea4c93..9cbec38592e14d 100644 --- a/crates/bevy_picking/src/events.rs +++ b/crates/bevy_picking/src/events.rs @@ -11,7 +11,7 @@ //! # use bevy_picking::prelude::*; //! # let mut world = World::default(); //! world.spawn_empty() -//! .observe(|trigger: Trigger>| { +//! .observe_entity(|trigger: Trigger>| { //! println!("I am being hovered over"); //! }); //! ``` diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index 68441496707aac..fdc710539c468e 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -15,7 +15,7 @@ //! # struct MyComponent; //! # let mut world = World::new(); //! world.spawn(MyComponent) -//! .observe(|mut trigger: Trigger>| { +//! .observe_entity(|mut trigger: Trigger>| { //! // Get the underlying event type //! let click_event: &Pointer = trigger.event(); //! // Stop the event from bubbling up the entity hierarchjy From 9bb27e97c59e0d1c1c2f26ce7f53ccdb2a3efbe3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 3 Oct 2024 19:15:32 +0100 Subject: [PATCH 006/546] Fix entity leak in `extract_uinode_borders` (#15626) # Objective Fix for another leak, this time when extracting outlines. --- crates/bevy_ui/src/render/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 6b003cd10d321d..d8c9948d22a7b6 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -409,7 +409,7 @@ pub fn extract_uinode_borders( if let Some(outline) = maybe_outline { let outline_size = uinode.outlined_node_size(); extracted_uinodes.uinodes.insert( - commands.spawn_empty().id(), + commands.spawn(TemporaryRenderEntity).id(), ExtractedUiNode { stack_index: uinode.stack_index, transform: global_transform.compute_matrix(), From 1e61092604388c392717d35d47403dc297e96532 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Thu, 3 Oct 2024 19:15:36 +0100 Subject: [PATCH 007/546] Fix `extract_text2d_sprite` entity leak (#15625) # Objective `extract_2d_sprite` still uses `spawn_empty()`, replace with `spawn(TemporaryRenderEntity)` . --- crates/bevy_text/src/text2d.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index ed744c54ff1774..0e0fa98468ac59 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -15,6 +15,7 @@ use bevy_ecs::{ system::{Commands, Local, Query, Res, ResMut}, }; use bevy_math::Vec2; +use bevy_render::world_sync::TemporaryRenderEntity; use bevy_render::{ primitives::Aabb, texture::Image, @@ -115,9 +116,8 @@ pub fn extract_text2d_sprite( } let atlas = texture_atlases.get(&atlas_info.texture_atlas).unwrap(); - let entity = commands.spawn_empty().id(); extracted_sprites.sprites.insert( - entity, + commands.spawn(TemporaryRenderEntity).id(), ExtractedSprite { transform: transform * GlobalTransform::from_translation(position.extend(0.)), color, From 46180a75f88e34982ac046447a7982dc653042ea Mon Sep 17 00:00:00 2001 From: Chris Russell <8494645+chescock@users.noreply.github.com> Date: Thu, 3 Oct 2024 14:20:34 -0400 Subject: [PATCH 008/546] System param for dynamic resources (#15189) # Objective Support accessing dynamic resources in a dynamic system, including accessing them by component id. This is similar to how dynamic components can be queried using `Query`. ## Solution Create `FilteredResources` and `FilteredResourcesMut` types that act similar to `FilteredEntityRef` and `FilteredEntityMut` and that can be used as system parameters. ## Example ```rust // Use `FilteredResourcesParamBuilder` to declare access to resources. let system = (FilteredResourcesParamBuilder::new(|builder| { builder.add_read::().add_read::(); }),) .build_state(&mut world) .build_system(resource_system); world.init_resource::(); world.init_resource::(); fn resource_system(res: FilteredResources) { // The resource exists, but we have no access, so we can't read it. assert!(res.get::().is_none()); // The resource doesn't exist, so we can't read it. assert!(res.get::().is_none()); // The resource exists and we have access, so we can read it. let c = res.get::().unwrap(); // The type parameter can be left out if it can be determined from use. let c: Res = res.get().unwrap(); } ``` ## Future Work As a follow-up PR, `ReflectResource` can be modified to take `impl Into`, similar to how `ReflectComponent` takes `impl Into`. That will allow dynamic resources to be accessed using reflection. --- crates/bevy_ecs/src/lib.rs | 4 +- crates/bevy_ecs/src/query/access.rs | 53 ++ crates/bevy_ecs/src/system/builder.rs | 263 +++++++- crates/bevy_ecs/src/system/mod.rs | 2 + crates/bevy_ecs/src/system/system_param.rs | 78 ++- .../bevy_ecs/src/world/filtered_resource.rs | 625 ++++++++++++++++++ crates/bevy_ecs/src/world/mod.rs | 2 + 7 files changed, 1005 insertions(+), 22 deletions(-) create mode 100644 crates/bevy_ecs/src/world/filtered_resource.rs diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index c481da091a77ef..d33eafb604e72d 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -63,8 +63,8 @@ pub mod prelude { SystemParamFunction, WithParamWarnPolicy, }, world::{ - Command, EntityMut, EntityRef, EntityWorldMut, FromWorld, OnAdd, OnInsert, OnRemove, - OnReplace, World, + Command, EntityMut, EntityRef, EntityWorldMut, FilteredResources, FilteredResourcesMut, + FromWorld, OnAdd, OnInsert, OnRemove, OnReplace, World, }, }; diff --git a/crates/bevy_ecs/src/query/access.rs b/crates/bevy_ecs/src/query/access.rs index cf504c2606635b..927e8ebcab72d6 100644 --- a/crates/bevy_ecs/src/query/access.rs +++ b/crates/bevy_ecs/src/query/access.rs @@ -1,4 +1,6 @@ +use crate::component::ComponentId; use crate::storage::SparseSetIndex; +use crate::world::World; use core::{fmt, fmt::Debug, marker::PhantomData}; use fixedbitset::FixedBitSet; @@ -727,6 +729,25 @@ impl Access { AccessConflicts::Individual(conflicts) } + /// Returns the indices of the resources this has access to. + pub fn resource_reads_and_writes(&self) -> impl Iterator + '_ { + self.resource_read_and_writes + .ones() + .map(T::get_sparse_set_index) + } + + /// Returns the indices of the resources this has non-exclusive access to. + pub fn resource_reads(&self) -> impl Iterator + '_ { + self.resource_read_and_writes + .difference(&self.resource_writes) + .map(T::get_sparse_set_index) + } + + /// Returns the indices of the resources this has exclusive access to. + pub fn resource_writes(&self) -> impl Iterator + '_ { + self.resource_writes.ones().map(T::get_sparse_set_index) + } + /// Returns the indices of the components that this has an archetypal access to. /// /// These are components whose values are not accessed (and thus will never cause conflicts), @@ -863,6 +884,24 @@ impl AccessConflicts { } } + pub(crate) fn format_conflict_list(&self, world: &World) -> String { + match self { + AccessConflicts::All => String::new(), + AccessConflicts::Individual(indices) => format!( + " {}", + indices + .ones() + .map(|index| world + .components + .get_info(ComponentId::get_sparse_set_index(index)) + .unwrap() + .name()) + .collect::>() + .join(", ") + ), + } + } + /// An [`AccessConflicts`] which represents the absence of any conflict pub(crate) fn empty() -> Self { Self::Individual(FixedBitSet::new()) @@ -1239,6 +1278,20 @@ impl FilteredAccessSet { self.add(filter); } + /// Adds read access to all resources to the set. + pub(crate) fn add_unfiltered_read_all_resources(&mut self) { + let mut filter = FilteredAccess::default(); + filter.access.read_all_resources(); + self.add(filter); + } + + /// Adds write access to all resources to the set. + pub(crate) fn add_unfiltered_write_all_resources(&mut self) { + let mut filter = FilteredAccess::default(); + filter.access.write_all_resources(); + self.add(filter); + } + /// Adds all of the accesses from the passed set to `self`. pub fn extend(&mut self, filtered_access_set: FilteredAccessSet) { self.combined_access diff --git a/crates/bevy_ecs/src/system/builder.rs b/crates/bevy_ecs/src/system/builder.rs index a9411dd176a023..91aa67f3ee5d58 100644 --- a/crates/bevy_ecs/src/system/builder.rs +++ b/crates/bevy_ecs/src/system/builder.rs @@ -6,7 +6,10 @@ use crate::{ system::{ DynSystemParam, DynSystemParamState, Local, ParamSet, Query, SystemMeta, SystemParam, }, - world::{FromWorld, World}, + world::{ + FilteredResources, FilteredResourcesBuilder, FilteredResourcesMut, + FilteredResourcesMutBuilder, FromWorld, World, + }, }; use core::fmt::Debug; @@ -77,6 +80,10 @@ use super::{init_query_param, Res, ResMut, Resource, SystemState}; /// /// [`LocalBuilder`] can build a [`Local`] to supply the initial value for the `Local`. /// +/// [`FilteredResourcesParamBuilder`] can build a [`FilteredResources`], +/// and [`FilteredResourcesMutParamBuilder`] can build a [`FilteredResourcesMut`], +/// to configure the resources that can be accessed. +/// /// [`DynParamBuilder`] can build a [`DynSystemParam`] to determine the type of the inner parameter, /// and to supply any `SystemParamBuilder` it needs. /// @@ -526,6 +533,147 @@ unsafe impl<'s, T: FromWorld + Send + 'static> SystemParamBuilder> } } +/// A [`SystemParamBuilder`] for a [`FilteredResources`]. +/// See the [`FilteredResources`] docs for examples. +pub struct FilteredResourcesParamBuilder(T); + +impl FilteredResourcesParamBuilder { + /// Creates a [`SystemParamBuilder`] for a [`FilteredResources`] that accepts a callback to configure the [`FilteredResourcesBuilder`]. + pub fn new(f: T) -> Self + where + T: FnOnce(&mut FilteredResourcesBuilder), + { + Self(f) + } +} + +impl<'a> FilteredResourcesParamBuilder> { + /// Creates a [`SystemParamBuilder`] for a [`FilteredResources`] that accepts a callback to configure the [`FilteredResourcesBuilder`]. + /// This boxes the callback so that it has a common type. + pub fn new_box(f: impl FnOnce(&mut FilteredResourcesBuilder) + 'a) -> Self { + Self(Box::new(f)) + } +} + +// SAFETY: Resource ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this FilteredResources +// conflicts with any prior access, a panic will occur. +unsafe impl<'w, 's, T: FnOnce(&mut FilteredResourcesBuilder)> + SystemParamBuilder> for FilteredResourcesParamBuilder +{ + fn build( + self, + world: &mut World, + meta: &mut SystemMeta, + ) -> as SystemParam>::State { + let mut builder = FilteredResourcesBuilder::new(world); + (self.0)(&mut builder); + let access = builder.build(); + + let combined_access = meta.component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(&access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &meta.name; + panic!("error[B0002]: FilteredResources in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/#b0002"); + } + + if access.has_read_all_resources() { + meta.component_access_set + .add_unfiltered_read_all_resources(); + meta.archetype_component_access.read_all_resources(); + } else { + for component_id in access.resource_reads_and_writes() { + meta.component_access_set + .add_unfiltered_resource_read(component_id); + + let archetype_component_id = world.initialize_resource_internal(component_id).id(); + meta.archetype_component_access + .add_resource_read(archetype_component_id); + } + } + + access + } +} + +/// A [`SystemParamBuilder`] for a [`FilteredResourcesMut`]. +/// See the [`FilteredResourcesMut`] docs for examples. +pub struct FilteredResourcesMutParamBuilder(T); + +impl FilteredResourcesMutParamBuilder { + /// Creates a [`SystemParamBuilder`] for a [`FilteredResourcesMut`] that accepts a callback to configure the [`FilteredResourcesMutBuilder`]. + pub fn new(f: T) -> Self + where + T: FnOnce(&mut FilteredResourcesMutBuilder), + { + Self(f) + } +} + +impl<'a> FilteredResourcesMutParamBuilder> { + /// Creates a [`SystemParamBuilder`] for a [`FilteredResourcesMut`] that accepts a callback to configure the [`FilteredResourcesMutBuilder`]. + /// This boxes the callback so that it has a common type. + pub fn new_box(f: impl FnOnce(&mut FilteredResourcesMutBuilder) + 'a) -> Self { + Self(Box::new(f)) + } +} + +// SAFETY: Resource ComponentId and ArchetypeComponentId access is applied to SystemMeta. If this FilteredResources +// conflicts with any prior access, a panic will occur. +unsafe impl<'w, 's, T: FnOnce(&mut FilteredResourcesMutBuilder)> + SystemParamBuilder> for FilteredResourcesMutParamBuilder +{ + fn build( + self, + world: &mut World, + meta: &mut SystemMeta, + ) -> as SystemParam>::State { + let mut builder = FilteredResourcesMutBuilder::new(world); + (self.0)(&mut builder); + let access = builder.build(); + + let combined_access = meta.component_access_set.combined_access(); + let conflicts = combined_access.get_conflicts(&access); + if !conflicts.is_empty() { + let accesses = conflicts.format_conflict_list(world); + let system_name = &meta.name; + panic!("error[B0002]: FilteredResourcesMut in system {system_name} accesses resources(s){accesses} in a way that conflicts with a previous system parameter. Consider removing the duplicate access. See: https://bevyengine.org/learn/errors/#b0002"); + } + + if access.has_read_all_resources() { + meta.component_access_set + .add_unfiltered_read_all_resources(); + meta.archetype_component_access.read_all_resources(); + } else { + for component_id in access.resource_reads() { + meta.component_access_set + .add_unfiltered_resource_read(component_id); + + let archetype_component_id = world.initialize_resource_internal(component_id).id(); + meta.archetype_component_access + .add_resource_read(archetype_component_id); + } + } + + if access.has_write_all_resources() { + meta.component_access_set + .add_unfiltered_write_all_resources(); + meta.archetype_component_access.write_all_resources(); + } else { + for component_id in access.resource_writes() { + meta.component_access_set + .add_unfiltered_resource_write(component_id); + + let archetype_component_id = world.initialize_resource_internal(component_id).id(); + meta.archetype_component_access + .add_resource_write(archetype_component_id); + } + } + + access + } +} + #[cfg(test)] mod tests { use crate as bevy_ecs; @@ -546,6 +694,9 @@ mod tests { #[derive(Component)] struct C; + #[derive(Resource, Default)] + struct R; + fn local_system(local: Local) -> u64 { *local } @@ -774,4 +925,114 @@ mod tests { let output = world.run_system_once(system).unwrap(); assert_eq!(output, 101); } + + #[test] + fn filtered_resource_conflicts_read_with_res() { + let mut world = World::new(); + ( + ParamBuilder::resource(), + FilteredResourcesParamBuilder::new(|builder| { + builder.add_read::(); + }), + ) + .build_state(&mut world) + .build_system(|_r: Res, _fr: FilteredResources| {}); + } + + #[test] + #[should_panic] + fn filtered_resource_conflicts_read_with_resmut() { + let mut world = World::new(); + ( + ParamBuilder::resource_mut(), + FilteredResourcesParamBuilder::new(|builder| { + builder.add_read::(); + }), + ) + .build_state(&mut world) + .build_system(|_r: ResMut, _fr: FilteredResources| {}); + } + + #[test] + #[should_panic] + fn filtered_resource_conflicts_read_all_with_resmut() { + let mut world = World::new(); + ( + ParamBuilder::resource_mut(), + FilteredResourcesParamBuilder::new(|builder| { + builder.add_read_all(); + }), + ) + .build_state(&mut world) + .build_system(|_r: ResMut, _fr: FilteredResources| {}); + } + + #[test] + fn filtered_resource_mut_conflicts_read_with_res() { + let mut world = World::new(); + ( + ParamBuilder::resource(), + FilteredResourcesMutParamBuilder::new(|builder| { + builder.add_read::(); + }), + ) + .build_state(&mut world) + .build_system(|_r: Res, _fr: FilteredResourcesMut| {}); + } + + #[test] + #[should_panic] + fn filtered_resource_mut_conflicts_read_with_resmut() { + let mut world = World::new(); + ( + ParamBuilder::resource_mut(), + FilteredResourcesMutParamBuilder::new(|builder| { + builder.add_read::(); + }), + ) + .build_state(&mut world) + .build_system(|_r: ResMut, _fr: FilteredResourcesMut| {}); + } + + #[test] + #[should_panic] + fn filtered_resource_mut_conflicts_write_with_res() { + let mut world = World::new(); + ( + ParamBuilder::resource(), + FilteredResourcesMutParamBuilder::new(|builder| { + builder.add_write::(); + }), + ) + .build_state(&mut world) + .build_system(|_r: Res, _fr: FilteredResourcesMut| {}); + } + + #[test] + #[should_panic] + fn filtered_resource_mut_conflicts_write_all_with_res() { + let mut world = World::new(); + ( + ParamBuilder::resource(), + FilteredResourcesMutParamBuilder::new(|builder| { + builder.add_write_all(); + }), + ) + .build_state(&mut world) + .build_system(|_r: Res, _fr: FilteredResourcesMut| {}); + } + + #[test] + #[should_panic] + fn filtered_resource_mut_conflicts_write_with_resmut() { + let mut world = World::new(); + ( + ParamBuilder::resource_mut(), + FilteredResourcesMutParamBuilder::new(|builder| { + builder.add_write::(); + }), + ) + .build_state(&mut world) + .build_system(|_r: ResMut, _fr: FilteredResourcesMut| {}); + } } diff --git a/crates/bevy_ecs/src/system/mod.rs b/crates/bevy_ecs/src/system/mod.rs index acf28fac37905a..dd6a950f72a8e3 100644 --- a/crates/bevy_ecs/src/system/mod.rs +++ b/crates/bevy_ecs/src/system/mod.rs @@ -105,6 +105,8 @@ //! In addition, the following parameters can be used when constructing a dynamic system with [`SystemParamBuilder`], //! but will only provide an empty value when used with an ordinary system: //! +//! - [`FilteredResources`](crate::world::FilteredResources) +//! - [`FilteredResourcesMut`](crate::world::FilteredResourcesMut) //! - [`DynSystemParam`] //! - [`Vec

`] where `P: SystemParam` //! - [`ParamSet>`] where `P: SystemParam` diff --git a/crates/bevy_ecs/src/system/system_param.rs b/crates/bevy_ecs/src/system/system_param.rs index bfa2fe108d70d6..9d06aec04d9ef4 100644 --- a/crates/bevy_ecs/src/system/system_param.rs +++ b/crates/bevy_ecs/src/system/system_param.rs @@ -6,12 +6,15 @@ use crate::{ component::{ComponentId, ComponentTicks, Components, Tick}, entity::Entities, query::{ - Access, AccessConflicts, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, - QuerySingleError, QueryState, ReadOnlyQueryData, + Access, FilteredAccess, FilteredAccessSet, QueryData, QueryFilter, QuerySingleError, + QueryState, ReadOnlyQueryData, }, - storage::{ResourceData, SparseSetIndex}, + storage::ResourceData, system::{Query, Single, SystemMeta}, - world::{unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FromWorld, World}, + world::{ + unsafe_world_cell::UnsafeWorldCell, DeferredWorld, FilteredResources, FilteredResourcesMut, + FromWorld, World, + }, }; use bevy_ecs_macros::impl_param_set; pub use bevy_ecs_macros::{Resource, SystemParam}; @@ -349,21 +352,7 @@ fn assert_component_access_compatibility( if conflicts.is_empty() { return; } - let accesses = match conflicts { - AccessConflicts::All => "", - AccessConflicts::Individual(indices) => &format!( - " {}", - indices - .ones() - .map(|index| world - .components - .get_info(ComponentId::get_sparse_set_index(index)) - .unwrap() - .name()) - .collect::>() - .join(", ") - ), - }; + let accesses = conflicts.format_conflict_list(world); panic!("error[B0001]: Query<{query_type}, {filter_type}> in system {system_name} accesses component(s){accesses} in a way that conflicts with a previous system parameter. Consider using `Without` to create disjoint Queries or merging conflicting Queries into a `ParamSet`. See: https://bevyengine.org/learn/errors/b0001"); } @@ -2447,6 +2436,57 @@ unsafe impl SystemParam for DynSystemParam<'_, '_> { } } +// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResources` with no access. +// Therefore, `init_state` trivially registers all access, and no accesses can conflict. +// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +unsafe impl SystemParam for FilteredResources<'_, '_> { + type State = Access; + + type Item<'world, 'state> = FilteredResources<'world, 'state>; + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + Access::new() + } + + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + change_tick: Tick, + ) -> Self::Item<'world, 'state> { + // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, + // and the builder registers `access` in `build`. + unsafe { FilteredResources::new(world, state, system_meta.last_run, change_tick) } + } +} + +// SAFETY: FilteredResources only reads resources. +unsafe impl ReadOnlySystemParam for FilteredResources<'_, '_> {} + +// SAFETY: When initialized with `init_state`, `get_param` returns a `FilteredResourcesMut` with no access. +// Therefore, `init_state` trivially registers all access, and no accesses can conflict. +// Note that the safety requirements for non-empty access are handled by the `SystemParamBuilder` impl that builds them. +unsafe impl SystemParam for FilteredResourcesMut<'_, '_> { + type State = Access; + + type Item<'world, 'state> = FilteredResourcesMut<'world, 'state>; + + fn init_state(_world: &mut World, _system_meta: &mut SystemMeta) -> Self::State { + Access::new() + } + + unsafe fn get_param<'world, 'state>( + state: &'state mut Self::State, + system_meta: &SystemMeta, + world: UnsafeWorldCell<'world>, + change_tick: Tick, + ) -> Self::Item<'world, 'state> { + // SAFETY: The caller ensures that `world` has access to anything registered in `init_state` or `build`, + // and the builder registers `access` in `build`. + unsafe { FilteredResourcesMut::new(world, state, system_meta.last_run, change_tick) } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/bevy_ecs/src/world/filtered_resource.rs b/crates/bevy_ecs/src/world/filtered_resource.rs new file mode 100644 index 00000000000000..527957d87c89db --- /dev/null +++ b/crates/bevy_ecs/src/world/filtered_resource.rs @@ -0,0 +1,625 @@ +use std::sync::OnceLock; + +use crate::{ + change_detection::{Mut, MutUntyped, Ref, Ticks, TicksMut}, + component::{ComponentId, Tick}, + query::Access, + system::Resource, + world::{unsafe_world_cell::UnsafeWorldCell, World}, +}; +use bevy_ptr::Ptr; +#[cfg(feature = "track_change_detection")] +use bevy_ptr::UnsafeCellDeref; + +/// Provides read-only access to a set of [`Resource`]s defined by the contained [`Access`]. +/// +/// Use [`FilteredResourcesMut`] if you need mutable access to some resources. +/// +/// To be useful as a [`SystemParam`](crate::system::SystemParam), +/// this must be configured using a [`FilteredResourcesParamBuilder`](crate::system::FilteredResourcesParamBuilder) +/// to build the system using a [`SystemParamBuilder`](crate::prelude::SystemParamBuilder). +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Default, Resource)] +/// # struct A; +/// # +/// # #[derive(Default, Resource)] +/// # struct B; +/// # +/// # #[derive(Default, Resource)] +/// # struct C; +/// # +/// # let mut world = World::new(); +/// // Use `FilteredResourcesParamBuilder` to declare access to resources. +/// let system = (FilteredResourcesParamBuilder::new(|builder| { +/// builder.add_read::().add_read::(); +/// }),) +/// .build_state(&mut world) +/// .build_system(resource_system); +/// +/// world.init_resource::(); +/// world.init_resource::(); +/// +/// fn resource_system(res: FilteredResources) { +/// // The resource exists, but we have no access, so we can't read it. +/// assert!(res.get::().is_none()); +/// // The resource doesn't exist, so we can't read it. +/// assert!(res.get::().is_none()); +/// // The resource exists and we have access, so we can read it. +/// let c = res.get::().unwrap(); +/// // The type parameter can be left out if it can be determined from use. +/// let c: Ref = res.get().unwrap(); +/// } +/// # +/// # world.run_system_once(system); +/// ``` +/// +/// This can be used alongside ordinary [`Res`](crate::system::Res) and [`ResMut`](crate::system::ResMut) parameters if they do not conflict. +/// +/// ``` +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Default, Resource)] +/// # struct A; +/// # +/// # #[derive(Default, Resource)] +/// # struct B; +/// # +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// # world.init_resource::(); +/// # +/// let system = ( +/// FilteredResourcesParamBuilder::new(|builder| { +/// builder.add_read::(); +/// }), +/// ParamBuilder, +/// ParamBuilder, +/// ) +/// .build_state(&mut world) +/// .build_system(resource_system); +/// +/// // Read access to A does not conflict with read access to A or write access to B. +/// fn resource_system(filtered: FilteredResources, res_a: Res, res_mut_b: ResMut) { +/// let res_a_2: Ref = filtered.get::().unwrap(); +/// } +/// # +/// # world.run_system_once(system); +/// ``` +/// +/// But it will conflict if it tries to read the same resource that another parameter writes. +/// +/// ```should_panic +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Default, Resource)] +/// # struct A; +/// # +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// # +/// let system = ( +/// FilteredResourcesParamBuilder::new(|builder| { +/// builder.add_read::(); +/// }), +/// ParamBuilder, +/// ) +/// .build_state(&mut world) +/// .build_system(invalid_resource_system); +/// +/// // Read access to A conflicts with write access to A. +/// fn invalid_resource_system(filtered: FilteredResources, res_mut_a: ResMut) { } +/// # +/// # world.run_system_once(system); +/// ``` +#[derive(Clone, Copy)] +pub struct FilteredResources<'w, 's> { + world: UnsafeWorldCell<'w>, + access: &'s Access, + last_run: Tick, + this_run: Tick, +} + +impl<'w, 's> FilteredResources<'w, 's> { + /// Creates a new [`FilteredResources`]. + /// # Safety + /// It is the callers responsibility to ensure that nothing else may access the any resources in the `world` in a way that conflicts with `access`. + pub(crate) unsafe fn new( + world: UnsafeWorldCell<'w>, + access: &'s Access, + last_run: Tick, + this_run: Tick, + ) -> Self { + Self { + world, + access, + last_run, + this_run, + } + } + + /// Returns a reference to the underlying [`Access`]. + pub fn access(&self) -> &Access { + self.access + } + + /// Returns `true` if the `FilteredResources` has access to the given resource. + /// Note that [`Self::get()`] may still return `None` if the resource does not exist. + pub fn has_read(&self) -> bool { + let component_id = self.world.components().resource_id::(); + component_id.is_some_and(|component_id| self.access.has_resource_read(component_id)) + } + + /// Gets a reference to the resource of the given type if it exists and the `FilteredResources` has access to it. + pub fn get(&self) -> Option> { + let component_id = self.world.components().resource_id::()?; + if !self.access.has_resource_read(component_id) { + return None; + } + // SAFETY: We have read access to this resource + unsafe { self.world.get_resource_with_ticks(component_id) }.map( + |(value, ticks, _caller)| Ref { + // SAFETY: `component_id` was obtained from the type ID of `R`. + value: unsafe { value.deref() }, + // SAFETY: We have read access to the resource, so no mutable reference can exist. + ticks: unsafe { Ticks::from_tick_cells(ticks, self.last_run, self.this_run) }, + #[cfg(feature = "track_change_detection")] + // SAFETY: We have read access to the resource, so no mutable reference can exist. + changed_by: unsafe { _caller.deref() }, + }, + ) + } + + /// Gets a pointer to the resource with the given [`ComponentId`] if it exists and the `FilteredResources` has access to it. + pub fn get_by_id(&self, component_id: ComponentId) -> Option> { + if !self.access.has_resource_read(component_id) { + return None; + } + // SAFETY: We have read access to this resource + unsafe { self.world.get_resource_by_id(component_id) } + } +} + +impl<'w, 's> From> for FilteredResources<'w, 's> { + fn from(resources: FilteredResourcesMut<'w, 's>) -> Self { + // SAFETY: + // - `FilteredResourcesMut` guarantees exclusive access to all resources in the new `FilteredResources`. + unsafe { + FilteredResources::new( + resources.world, + resources.access, + resources.last_run, + resources.this_run, + ) + } + } +} + +impl<'w, 's> From<&'w FilteredResourcesMut<'_, 's>> for FilteredResources<'w, 's> { + fn from(resources: &'w FilteredResourcesMut<'_, 's>) -> Self { + // SAFETY: + // - `FilteredResourcesMut` guarantees exclusive access to all components in the new `FilteredResources`. + unsafe { + FilteredResources::new( + resources.world, + resources.access, + resources.last_run, + resources.this_run, + ) + } + } +} + +impl<'w> From<&'w World> for FilteredResources<'w, 'static> { + fn from(value: &'w World) -> Self { + static READ_ALL_RESOURCES: OnceLock> = OnceLock::new(); + let access = READ_ALL_RESOURCES.get_or_init(|| { + let mut access = Access::new(); + access.read_all_resources(); + access + }); + + let last_run = value.last_change_tick(); + let this_run = value.read_change_tick(); + // SAFETY: We have a reference to the entire world, so nothing else can alias with read access to all resources. + unsafe { + Self::new( + value.as_unsafe_world_cell_readonly(), + access, + last_run, + this_run, + ) + } + } +} + +impl<'w> From<&'w mut World> for FilteredResources<'w, 'static> { + fn from(value: &'w mut World) -> Self { + Self::from(&*value) + } +} + +/// Provides mutable access to a set of [`Resource`]s defined by the contained [`Access`]. +/// +/// Use [`FilteredResources`] if you only need read-only access to resources. +/// +/// To be useful as a [`SystemParam`](crate::system::SystemParam), +/// this must be configured using a [`FilteredResourcesMutParamBuilder`](crate::system::FilteredResourcesMutParamBuilder) +/// to build the system using a [`SystemParamBuilder`](crate::prelude::SystemParamBuilder). +/// +/// # Examples +/// +/// ``` +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Default, Resource)] +/// # struct A; +/// # +/// # #[derive(Default, Resource)] +/// # struct B; +/// # +/// # #[derive(Default, Resource)] +/// # struct C; +/// # +/// # #[derive(Default, Resource)] +/// # struct D; +/// # +/// # let mut world = World::new(); +/// // Use `FilteredResourcesMutParamBuilder` to declare access to resources. +/// let system = (FilteredResourcesMutParamBuilder::new(|builder| { +/// builder.add_write::().add_read::().add_write::(); +/// }),) +/// .build_state(&mut world) +/// .build_system(resource_system); +/// +/// world.init_resource::(); +/// world.init_resource::(); +/// world.init_resource::(); +/// +/// fn resource_system(mut res: FilteredResourcesMut) { +/// // The resource exists, but we have no access, so we can't read it or write it. +/// assert!(res.get::().is_none()); +/// assert!(res.get_mut::().is_none()); +/// // The resource doesn't exist, so we can't read it or write it. +/// assert!(res.get::().is_none()); +/// assert!(res.get_mut::().is_none()); +/// // The resource exists and we have read access, so we can read it but not write it. +/// let c = res.get::().unwrap(); +/// assert!(res.get_mut::().is_none()); +/// // The resource exists and we have write access, so we can read it or write it. +/// let d = res.get::().unwrap(); +/// let d = res.get_mut::().unwrap(); +/// // The type parameter can be left out if it can be determined from use. +/// let c: Ref = res.get().unwrap(); +/// } +/// # +/// # world.run_system_once(system); +/// ``` +/// +/// This can be used alongside ordinary [`Res`](crate::system::ResMut) and [`ResMut`](crate::system::ResMut) parameters if they do not conflict. +/// +/// ``` +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Default, Resource)] +/// # struct A; +/// # +/// # #[derive(Default, Resource)] +/// # struct B; +/// # +/// # #[derive(Default, Resource)] +/// # struct C; +/// # +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// # world.init_resource::(); +/// # world.init_resource::(); +/// # +/// let system = ( +/// FilteredResourcesMutParamBuilder::new(|builder| { +/// builder.add_read::().add_write::(); +/// }), +/// ParamBuilder, +/// ParamBuilder, +/// ) +/// .build_state(&mut world) +/// .build_system(resource_system); +/// +/// // Read access to A does not conflict with read access to A or write access to C. +/// // Write access to B does not conflict with access to A or C. +/// fn resource_system(mut filtered: FilteredResourcesMut, res_a: Res, res_mut_c: ResMut) { +/// let res_a_2: Ref = filtered.get::().unwrap(); +/// let res_mut_b: Mut = filtered.get_mut::().unwrap(); +/// } +/// # +/// # world.run_system_once(system); +/// ``` +/// +/// But it will conflict if it tries to read the same resource that another parameter writes, +/// or write the same resource that another parameter reads. +/// +/// ```should_panic +/// # use bevy_ecs::{prelude::*, system::*}; +/// # +/// # #[derive(Default, Resource)] +/// # struct A; +/// # +/// # let mut world = World::new(); +/// # world.init_resource::(); +/// # +/// let system = ( +/// FilteredResourcesMutParamBuilder::new(|builder| { +/// builder.add_write::(); +/// }), +/// ParamBuilder, +/// ) +/// .build_state(&mut world) +/// .build_system(invalid_resource_system); +/// +/// // Read access to A conflicts with write access to A. +/// fn invalid_resource_system(filtered: FilteredResourcesMut, res_a: Res) { } +/// # +/// # world.run_system_once(system); +/// ``` +pub struct FilteredResourcesMut<'w, 's> { + world: UnsafeWorldCell<'w>, + access: &'s Access, + last_run: Tick, + this_run: Tick, +} + +impl<'w, 's> FilteredResourcesMut<'w, 's> { + /// Creates a new [`FilteredResources`]. + /// # Safety + /// It is the callers responsibility to ensure that nothing else may access the any resources in the `world` in a way that conflicts with `access`. + pub(crate) unsafe fn new( + world: UnsafeWorldCell<'w>, + access: &'s Access, + last_run: Tick, + this_run: Tick, + ) -> Self { + Self { + world, + access, + last_run, + this_run, + } + } + + /// Gets read-only access to all of the resources this `FilteredResourcesMut` can access. + pub fn as_readonly(&self) -> FilteredResources<'_, 's> { + FilteredResources::from(self) + } + + /// Returns a new instance with a shorter lifetime. + /// This is useful if you have `&mut FilteredResourcesMut`, but you need `FilteredResourcesMut`. + pub fn reborrow(&mut self) -> FilteredResourcesMut<'_, 's> { + // SAFETY: We have exclusive access to this access for the duration of `'_`, so there cannot be anything else that conflicts. + unsafe { Self::new(self.world, self.access, self.last_run, self.this_run) } + } + + /// Returns a reference to the underlying [`Access`]. + pub fn access(&self) -> &Access { + self.access + } + + /// Returns `true` if the `FilteredResources` has read access to the given resource. + /// Note that [`Self::get()`] may still return `None` if the resource does not exist. + pub fn has_read(&self) -> bool { + let component_id = self.world.components().resource_id::(); + component_id.is_some_and(|component_id| self.access.has_resource_read(component_id)) + } + + /// Returns `true` if the `FilteredResources` has write access to the given resource. + /// Note that [`Self::get_mut()`] may still return `None` if the resource does not exist. + pub fn has_write(&self) -> bool { + let component_id = self.world.components().resource_id::(); + component_id.is_some_and(|component_id| self.access.has_resource_write(component_id)) + } + + /// Gets a reference to the resource of the given type if it exists and the `FilteredResources` has access to it. + pub fn get(&self) -> Option> { + self.as_readonly().get() + } + + /// Gets a pointer to the resource with the given [`ComponentId`] if it exists and the `FilteredResources` has access to it. + pub fn get_by_id(&self, component_id: ComponentId) -> Option> { + self.as_readonly().get_by_id(component_id) + } + + /// Gets a mutable reference to the resource of the given type if it exists and the `FilteredResources` has access to it. + pub fn get_mut(&mut self) -> Option> { + // SAFETY: We have exclusive access to the resources in `access` for `'_`, and we shorten the returned lifetime to that. + unsafe { self.get_mut_unchecked() } + } + + /// Gets a mutable pointer to the resource with the given [`ComponentId`] if it exists and the `FilteredResources` has access to it. + pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + // SAFETY: We have exclusive access to the resources in `access` for `'_`, and we shorten the returned lifetime to that. + unsafe { self.get_mut_by_id_unchecked(component_id) } + } + + /// Consumes self and gets mutable access to resource of the given type with the world `'w` lifetime if it exists and the `FilteredResources` has access to it. + pub fn into_mut(mut self) -> Option> { + // SAFETY: This consumes self, so we have exclusive access to the resources in `access` for the entirety of `'w`. + unsafe { self.get_mut_unchecked() } + } + + /// Consumes self and gets mutable access to resource with the given [`ComponentId`] with the world `'w` lifetime if it exists and the `FilteredResources` has access to it. + pub fn into_mut_by_id(mut self, component_id: ComponentId) -> Option> { + // SAFETY: This consumes self, so we have exclusive access to the resources in `access` for the entirety of `'w`. + unsafe { self.get_mut_by_id_unchecked(component_id) } + } + + /// Gets a mutable pointer to the resource of the given type if it exists and the `FilteredResources` has access to it. + /// # Safety + /// It is the callers responsibility to ensure that there are no conflicting borrows of anything in `access` for the duration of the returned value. + unsafe fn get_mut_unchecked(&mut self) -> Option> { + let component_id = self.world.components().resource_id::()?; + // SAFETY: THe caller ensures that there are no conflicting borrows. + unsafe { self.get_mut_by_id_unchecked(component_id) } + // SAFETY: The underlying type of the resource is `R`. + .map(|ptr| unsafe { ptr.with_type::() }) + } + + /// Gets a mutable pointer to the resource with the given [`ComponentId`] if it exists and the `FilteredResources` has access to it. + /// # Safety + /// It is the callers responsibility to ensure that there are no conflicting borrows of anything in `access` for the duration of the returned value. + unsafe fn get_mut_by_id_unchecked( + &mut self, + component_id: ComponentId, + ) -> Option> { + if !self.access.has_resource_write(component_id) { + return None; + } + // SAFETY: We have access to this resource in `access`, and the caller ensures that there are no conflicting borrows for the duration of the returned value. + unsafe { self.world.get_resource_with_ticks(component_id) }.map( + |(value, ticks, _caller)| MutUntyped { + // SAFETY: We have exclusive access to the underlying storage. + value: unsafe { value.assert_unique() }, + // SAFETY: We have exclusive access to the underlying storage. + ticks: unsafe { TicksMut::from_tick_cells(ticks, self.last_run, self.this_run) }, + #[cfg(feature = "track_change_detection")] + // SAFETY: We have exclusive access to the underlying storage. + changed_by: unsafe { _caller.deref_mut() }, + }, + ) + } +} + +impl<'w> From<&'w mut World> for FilteredResourcesMut<'w, 'static> { + fn from(value: &'w mut World) -> Self { + static WRITE_ALL_RESOURCES: OnceLock> = OnceLock::new(); + let access = WRITE_ALL_RESOURCES.get_or_init(|| { + let mut access = Access::new(); + access.write_all_resources(); + access + }); + + let last_run = value.last_change_tick(); + let this_run = value.change_tick(); + // SAFETY: We have a mutable reference to the entire world, so nothing else can alias with mutable access to all resources. + unsafe { + Self::new( + value.as_unsafe_world_cell_readonly(), + access, + last_run, + this_run, + ) + } + } +} + +/// Builder struct to define the access for a [`FilteredResources`]. +/// +/// This is passed to a callback in [`FilteredResourcesParamBuilder`](crate::system::FilteredResourcesParamBuilder). +pub struct FilteredResourcesBuilder<'w> { + world: &'w mut World, + access: Access, +} + +impl<'w> FilteredResourcesBuilder<'w> { + /// Creates a new builder with no access. + pub fn new(world: &'w mut World) -> Self { + Self { + world, + access: Access::new(), + } + } + + /// Returns a reference to the underlying [`Access`]. + pub fn access(&self) -> &Access { + &self.access + } + + /// Add accesses required to read all resources. + pub fn add_read_all(&mut self) -> &mut Self { + self.access.read_all_resources(); + self + } + + /// Add accesses required to read the resource of the given type. + pub fn add_read(&mut self) -> &mut Self { + let component_id = self.world.components.register_resource::(); + self.add_read_by_id(component_id) + } + + /// Add accesses required to read the resource with the given [`ComponentId`]. + pub fn add_read_by_id(&mut self, component_id: ComponentId) -> &mut Self { + self.access.add_resource_read(component_id); + self + } + + /// Create an [`Access`] that represents the accesses of the builder. + pub fn build(self) -> Access { + self.access + } +} + +/// Builder struct to define the access for a [`FilteredResourcesMut`]. +/// +/// This is passed to a callback in [`FilteredResourcesMutParamBuilder`](crate::system::FilteredResourcesMutParamBuilder). +pub struct FilteredResourcesMutBuilder<'w> { + world: &'w mut World, + access: Access, +} + +impl<'w> FilteredResourcesMutBuilder<'w> { + /// Creates a new builder with no access. + pub fn new(world: &'w mut World) -> Self { + Self { + world, + access: Access::new(), + } + } + + /// Returns a reference to the underlying [`Access`]. + pub fn access(&self) -> &Access { + &self.access + } + + /// Add accesses required to read all resources. + pub fn add_read_all(&mut self) -> &mut Self { + self.access.read_all_resources(); + self + } + + /// Add accesses required to read the resource of the given type. + pub fn add_read(&mut self) -> &mut Self { + let component_id = self.world.components.register_resource::(); + self.add_read_by_id(component_id) + } + + /// Add accesses required to read the resource with the given [`ComponentId`]. + pub fn add_read_by_id(&mut self, component_id: ComponentId) -> &mut Self { + self.access.add_resource_read(component_id); + self + } + + /// Add accesses required to get mutable access to all resources. + pub fn add_write_all(&mut self) -> &mut Self { + self.access.write_all_resources(); + self + } + + /// Add accesses required to get mutable access to the resource of the given type. + pub fn add_write(&mut self) -> &mut Self { + let component_id = self.world.components.register_resource::(); + self.add_write_by_id(component_id) + } + + /// Add accesses required to get mutable access to the resource with the given [`ComponentId`]. + pub fn add_write_by_id(&mut self, component_id: ComponentId) -> &mut Self { + self.access.add_resource_write(component_id); + self + } + + /// Create an [`Access`] that represents the accesses of the builder. + pub fn build(self) -> Access { + self.access + } +} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 8a665e51ece6b0..7dae46a97a8570 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -5,6 +5,7 @@ mod component_constants; mod deferred_world; mod entity_ref; pub mod error; +mod filtered_resource; mod identifier; mod spawn_batch; pub mod unsafe_world_cell; @@ -22,6 +23,7 @@ pub use entity_ref::{ EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, VacantEntry, }; +pub use filtered_resource::*; pub use identifier::WorldId; pub use spawn_batch::*; From 528ca4f95ee80b790c1ba0e73fdc5836a013eabb Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 3 Oct 2024 14:26:41 -0400 Subject: [PATCH 009/546] Eliminate redundant clamping from sample-interpolated curves (#15620) # Objective Currently, sample-interpolated curves (such as those used by the glTF loader for animations) do unnecessary extra work when `sample_clamped` is called, since their implementations of `sample_unchecked` are already clamped. Eliminating this redundant sampling is a small, easy performance win which doesn't compromise on the animation system's internal usage of `sample_clamped`, which guarantees that it never samples curves out-of-bounds. ## Solution For sample-interpolated curves, define `sample_clamped` in the way `sample_unchecked` is currently defined, and then redirect `sample_unchecked` to `sample_clamped`. This is arguably a more idiomatic way of using the `cores` as well, which is nice. ## Testing Ran `many_foxes` to make sure I didn't break anything. --- crates/bevy_animation/src/animation_curves.rs | 8 ++-- crates/bevy_animation/src/gltf_curves.rs | 42 ++++++++++++++++--- crates/bevy_color/src/color_gradient.rs | 10 ++++- crates/bevy_math/src/curve/sample_curves.rs | 32 ++++++++++++-- 4 files changed, 77 insertions(+), 15 deletions(-) diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 26589b8e6e56f9..82d84ee42ffefd 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -1021,14 +1021,14 @@ where } #[inline] - fn sample_unchecked(&self, t: f32) -> T { + fn sample_clamped(&self, t: f32) -> T { + // `UnevenCore::sample_with` is implicitly clamped. 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) + fn sample_unchecked(&self, t: f32) -> T { + self.sample_clamped(t) } } diff --git a/crates/bevy_animation/src/gltf_curves.rs b/crates/bevy_animation/src/gltf_curves.rs index f32ddc4ab01760..60367967a2d5a4 100644 --- a/crates/bevy_animation/src/gltf_curves.rs +++ b/crates/bevy_animation/src/gltf_curves.rs @@ -23,10 +23,15 @@ where } #[inline] - fn sample_unchecked(&self, t: f32) -> T { + fn sample_clamped(&self, t: f32) -> T { self.core .sample_with(t, |x, y, t| if t >= 1.0 { y.clone() } else { x.clone() }) } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.sample_clamped(t) + } } impl SteppedKeyframeCurve { @@ -57,7 +62,7 @@ where } #[inline] - fn sample_unchecked(&self, t: f32) -> V { + fn sample_clamped(&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)) @@ -69,6 +74,11 @@ where } } } + + #[inline] + fn sample_unchecked(&self, t: f32) -> V { + self.sample_clamped(t) + } } impl CubicKeyframeCurve { @@ -112,7 +122,7 @@ impl Curve for CubicRotationCurve { } #[inline] - fn sample_unchecked(&self, t: f32) -> Quat { + fn sample_clamped(&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)) @@ -125,6 +135,11 @@ impl Curve for CubicRotationCurve { }; Quat::from_vec4(vec.normalize()) } + + #[inline] + fn sample_unchecked(&self, t: f32) -> Quat { + self.sample_clamped(t) + } } impl CubicRotationCurve { @@ -170,7 +185,7 @@ where } #[inline] - fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + fn sample_iter_clamped(&self, t: f32) -> impl Iterator { match self.core.sample_interp(t) { InterpolationDatum::Exact(v) | InterpolationDatum::LeftTail(v) @@ -182,6 +197,11 @@ where } } } + + #[inline] + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + self.sample_iter_clamped(t) + } } impl WideLinearKeyframeCurve { @@ -219,7 +239,7 @@ where } #[inline] - fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + fn sample_iter_clamped(&self, t: f32) -> impl Iterator { match self.core.sample_interp(t) { InterpolationDatum::Exact(v) | InterpolationDatum::LeftTail(v) @@ -234,6 +254,11 @@ where } } } + + #[inline] + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + self.sample_iter_clamped(t) + } } impl WideSteppedKeyframeCurve { @@ -269,7 +294,7 @@ where self.core.domain() } - fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + fn sample_iter_clamped(&self, t: f32) -> impl Iterator { match self.core.sample_interp_timed(t) { InterpolationDatum::Exact((_, v)) | InterpolationDatum::LeftTail((_, v)) @@ -285,6 +310,11 @@ where ), } } + + #[inline] + fn sample_iter_unchecked(&self, t: f32) -> impl Iterator { + self.sample_iter_clamped(t) + } } /// An error indicating that a multisampling keyframe curve could not be constructed. diff --git a/crates/bevy_color/src/color_gradient.rs b/crates/bevy_color/src/color_gradient.rs index bb6457e6fdc684..979f4a2cd67030 100644 --- a/crates/bevy_color/src/color_gradient.rs +++ b/crates/bevy_color/src/color_gradient.rs @@ -54,13 +54,21 @@ impl Curve for ColorCurve where T: Mix + Clone, { + #[inline] fn domain(&self) -> Interval { self.core.domain() } - fn sample_unchecked(&self, t: f32) -> T { + #[inline] + fn sample_clamped(&self, t: f32) -> T { + // `EvenCore::sample_with` clamps the input implicitly. self.core.sample_with(t, T::mix) } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.sample_clamped(t) + } } #[cfg(test)] diff --git a/crates/bevy_math/src/curve/sample_curves.rs b/crates/bevy_math/src/curve/sample_curves.rs index 255fa84d10275e..d842fb66be858f 100644 --- a/crates/bevy_math/src/curve/sample_curves.rs +++ b/crates/bevy_math/src/curve/sample_curves.rs @@ -94,9 +94,15 @@ where } #[inline] - fn sample_unchecked(&self, t: f32) -> T { + fn sample_clamped(&self, t: f32) -> T { + // `EvenCore::sample_with` is implicitly clamped. self.core.sample_with(t, &self.interpolation) } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.sample_clamped(t) + } } impl SampleCurve { @@ -143,10 +149,16 @@ where } #[inline] - fn sample_unchecked(&self, t: f32) -> T { + fn sample_clamped(&self, t: f32) -> T { + // `EvenCore::sample_with` is implicitly clamped. self.core .sample_with(t, ::interpolate_stable) } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.sample_clamped(t) + } } impl SampleAutoCurve { @@ -242,9 +254,15 @@ where } #[inline] - fn sample_unchecked(&self, t: f32) -> T { + fn sample_clamped(&self, t: f32) -> T { + // `UnevenCore::sample_with` is implicitly clamped. self.core.sample_with(t, &self.interpolation) } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.sample_clamped(t) + } } impl UnevenSampleCurve { @@ -301,10 +319,16 @@ where } #[inline] - fn sample_unchecked(&self, t: f32) -> T { + fn sample_clamped(&self, t: f32) -> T { + // `UnevenCore::sample_with` is implicitly clamped. self.core .sample_with(t, ::interpolate_stable) } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.sample_clamped(t) + } } impl UnevenSampleAutoCurve { From 0628255c4513c38239b297afe0286a624ac6c7b4 Mon Sep 17 00:00:00 2001 From: IceSentry Date: Thu, 3 Oct 2024 16:02:52 -0400 Subject: [PATCH 010/546] send_events is ambiguous_with_all (#15629) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective > Alice 🌹 — Today at 3:43 PM bevy_dev_tools::ci_testing::systems::send_events This system should be marked as ambiguous with everything I think ## Solution - Mark it as `ambiguous_with_all` --- crates/bevy_dev_tools/src/ci_testing/mod.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/bevy_dev_tools/src/ci_testing/mod.rs b/crates/bevy_dev_tools/src/ci_testing/mod.rs index 5c85aeff483403..18eeb6ba611b09 100644 --- a/crates/bevy_dev_tools/src/ci_testing/mod.rs +++ b/crates/bevy_dev_tools/src/ci_testing/mod.rs @@ -56,7 +56,8 @@ impl Plugin for CiTestingPlugin { systems::send_events .before(trigger_screenshots) .before(bevy_window::close_when_requested) - .in_set(SendEvents), + .in_set(SendEvents) + .ambiguous_with_all(), ); // The offending system does not exist in the wasm32 target. From 8bf5d99d8668745bb6c3fccbd64b1d1776d71fb8 Mon Sep 17 00:00:00 2001 From: rewin Date: Thu, 3 Oct 2024 23:35:08 +0300 Subject: [PATCH 011/546] Add method to remove component and all required components for removed component (#15026) ## Objective The new Required Components feature (#14791) in Bevy allows spawning a fixed set of components with a single method with cool require macro. However, there's currently no corresponding method to remove all those components together. This makes it challenging to keep insertion and removal code in sync, especially for simple using cases. ```rust #[derive(Component)] #[require(Y)] struct X; #[derive(Component, Default)] struct Y; world.entity_mut(e).insert(X); // Spawns both X and Y world.entity_mut(e).remove::(); world.entity_mut(e).remove::(); // We need to manually remove dependencies without any sync with the `require` macro ``` ## Solution Simplifies component management by providing operations for removal required components. This PR introduces simple 'footgun' methods to removes all components of this bundle and its required components. Two new methods are introduced: For Commands: ```rust commands.entity(e).remove_with_requires::(); ``` For World: ```rust world.entity_mut(e).remove_with_requires::(); ``` For performance I created new field in Bundels struct. This new field "contributed_bundle_ids" contains cached ids for dynamic bundles constructed from bundle_info.cintributed_components() ## Testing The PR includes three test cases: 1. Removing a single component with requirements using World. 2. Removing a bundle with requirements using World. 3. Removing a single component with requirements using Commands. 4. Removing a single component with **runtime** requirements using Commands These tests ensure the feature works as expected across different scenarios. ## Showcase Example: ```rust use bevy_ecs::prelude::*; #[derive(Component)] #[require(Y)] struct X; #[derive(Component, Default)] #[require(Z)] struct Y; #[derive(Component, Default)] struct Z; #[derive(Component)] struct W; let mut world = World::new(); // Spawn an entity with X, Y, Z, and W components let entity = world.spawn((X, W)).id(); assert!(world.entity(entity).contains::()); assert!(world.entity(entity).contains::()); assert!(world.entity(entity).contains::()); assert!(world.entity(entity).contains::()); // Remove X and required components Y, Z world.entity_mut(entity).remove_with_requires::(); assert!(!world.entity(entity).contains::()); assert!(!world.entity(entity).contains::()); assert!(!world.entity(entity).contains::()); assert!(world.entity(entity).contains::()); ``` ## Motivation for PR #15580 ## Performance I made simple benchmark ```rust let mut world = World::default(); let entity = world.spawn_empty().id(); let steps = 100_000_000; let start = std::time::Instant::now(); for _ in 0..steps { world.entity_mut(entity).insert(X); world.entity_mut(entity).remove::<(X, Y, Z, W)>(); } let end = std::time::Instant::now(); println!("normal remove: {:?} ", (end - start).as_secs_f32()); println!("one remove: {:?} micros", (end - start).as_secs_f64() / steps as f64 * 1_000_000.0); let start = std::time::Instant::now(); for _ in 0..steps { world.entity_mut(entity).insert(X); world.entity_mut(entity).remove_with_requires::(); } let end = std::time::Instant::now(); println!("remove_with_requires: {:?} ", (end - start).as_secs_f32()); println!("one remove_with_requires: {:?} micros", (end - start).as_secs_f64() / steps as f64 * 1_000_000.0); ``` Output: CPU: Amd Ryzen 7 2700x ```bash normal remove: 17.36135 one remove: 0.17361348299999999 micros remove_with_requires: 17.534006 one remove_with_requires: 0.17534005400000002 micros ``` NOTE: I didn't find any tests or mechanism in the repository to update BundleInfo after creating new runtime requirements with an existing BundleInfo. So this PR also does not contain such logic. ## Future work (outside this PR) Create cache system for fast removing components in "safe" mode, where "safe" mode is remove only required components that will be no longer required after removing root component. --------- Co-authored-by: a.yamaev Co-authored-by: Carter Anderson --- crates/bevy_ecs/src/bundle.rs | 32 +++++ crates/bevy_ecs/src/lib.rs | 132 +++++++++++++++++++++ crates/bevy_ecs/src/system/commands/mod.rs | 71 +++++++++++ crates/bevy_ecs/src/world/entity_ref.rs | 14 +++ 4 files changed, 249 insertions(+) diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 446eb30921225b..56e11e854e13e9 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -1302,6 +1302,8 @@ pub struct Bundles { bundle_infos: Vec, /// Cache static [`BundleId`] bundle_ids: TypeIdMap, + /// Cache bundles, which contains both explicit and required components of [`Bundle`] + contributed_bundle_ids: TypeIdMap, /// Cache dynamic [`BundleId`] with multiple components dynamic_bundle_ids: HashMap, BundleId>, dynamic_bundle_storages: HashMap>, @@ -1351,6 +1353,36 @@ impl Bundles { id } + /// Registers a new [`BundleInfo`], which contains both explicit and required components for a statically known type. + /// + /// Also registers all the components in the bundle. + pub(crate) fn register_contributed_bundle_info( + &mut self, + components: &mut Components, + storages: &mut Storages, + ) -> BundleId { + if let Some(id) = self.contributed_bundle_ids.get(&TypeId::of::()).cloned() { + id + } else { + let explicit_bundle_id = self.register_info::(components, storages); + // SAFETY: reading from `explicit_bundle_id` and creating new bundle in same time. Its valid because bundle hashmap allow this + let id = unsafe { + let (ptr, len) = { + // SAFETY: `explicit_bundle_id` is valid and defined above + let contributed = self + .get_unchecked(explicit_bundle_id) + .contributed_components(); + (contributed.as_ptr(), contributed.len()) + }; + // SAFETY: this is sound because the contributed_components Vec for explicit_bundle_id will not be accessed mutably as + // part of init_dynamic_info. No mutable references will be created and the allocation will remain valid. + self.init_dynamic_info(components, core::slice::from_raw_parts(ptr, len)) + }; + self.contributed_bundle_ids.insert(TypeId::of::(), id); + id + } + } + /// # Safety /// A [`BundleInfo`] with the given [`BundleId`] must have been initialized for this instance of `Bundles`. pub(crate) unsafe fn get_unchecked(&self, id: BundleId) -> &BundleInfo { diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index d33eafb604e72d..4c7737328a4065 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -2029,6 +2029,138 @@ mod tests { assert!(e.contains::()); } + #[test] + fn remove_component_and_his_runtime_required_components() { + #[derive(Component)] + struct X; + + #[derive(Component, Default)] + struct Y; + + #[derive(Component, Default)] + struct Z; + + #[derive(Component)] + struct V; + + let mut world = World::new(); + world.register_required_components::(); + world.register_required_components::(); + + let e = world.spawn((X, V)).id(); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + + //check that `remove` works as expected + world.entity_mut(e).remove::(); + assert!(!world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + + world.entity_mut(e).insert(X); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + + //remove `X` again and ensure that `Y` and `Z` was removed too + world.entity_mut(e).remove_with_requires::(); + assert!(!world.entity(e).contains::()); + assert!(!world.entity(e).contains::()); + assert!(!world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + } + + #[test] + fn remove_component_and_his_required_components() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + #[require(Z)] + struct Y; + + #[derive(Component, Default)] + struct Z; + + #[derive(Component)] + struct V; + + let mut world = World::new(); + + let e = world.spawn((X, V)).id(); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + + //check that `remove` works as expected + world.entity_mut(e).remove::(); + assert!(!world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + + world.entity_mut(e).insert(X); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + + //remove `X` again and ensure that `Y` and `Z` was removed too + world.entity_mut(e).remove_with_requires::(); + assert!(!world.entity(e).contains::()); + assert!(!world.entity(e).contains::()); + assert!(!world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + } + + #[test] + fn remove_bundle_and_his_required_components() { + #[derive(Component, Default)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y; + + #[derive(Component, Default)] + #[require(W)] + struct Z; + + #[derive(Component, Default)] + struct W; + + #[derive(Component)] + struct V; + + #[derive(Bundle, Default)] + struct TestBundle { + x: X, + z: Z, + } + + let mut world = World::new(); + let e = world.spawn((TestBundle::default(), V)).id(); + + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + + world.entity_mut(e).remove_with_requires::(); + assert!(!world.entity(e).contains::()); + assert!(!world.entity(e).contains::()); + assert!(!world.entity(e).contains::()); + assert!(!world.entity(e).contains::()); + assert!(world.entity(e).contains::()); + } + #[test] fn runtime_required_components() { // Same as `required_components` test but with runtime registration diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index bec88f51b5afad..bb40dd48a1c4c0 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1369,6 +1369,34 @@ impl EntityCommands<'_> { self.queue(remove::) } + /// Removes all components in the [`Bundle`] components and remove all required components for each component in the [`Bundle`] from entity. + /// + /// # Example + /// + /// ``` + /// use bevy_ecs::prelude::*; + /// + /// #[derive(Component)] + /// #[require(B)] + /// struct A; + /// #[derive(Component, Default)] + /// struct B; + /// + /// #[derive(Resource)] + /// struct PlayerEntity { entity: Entity } + /// + /// fn remove_with_requires_system(mut commands: Commands, player: Res) { + /// commands + /// .entity(player.entity) + /// // Remove both A and B components from the entity, because B is required by A + /// .remove_with_requires::(); + /// } + /// # bevy_ecs::system::assert_is_system(remove_with_requires_system); + /// ``` + pub fn remove_with_requires(&mut self) -> &mut Self { + self.queue(remove_with_requires::) + } + /// Removes a component from the entity. pub fn remove_by_id(&mut self, component_id: ComponentId) -> &mut Self { self.queue(remove_by_id(component_id)) @@ -1826,6 +1854,13 @@ fn remove_by_id(component_id: ComponentId) -> impl EntityCommand { } } +/// An [`EntityCommand`] that remove all components in the bundle and remove all required components for each component in the bundle. +fn remove_with_requires(entity: Entity, world: &mut World) { + if let Some(mut entity) = world.get_entity_mut(entity) { + entity.remove_with_requires::(); + } +} + /// An [`EntityCommand`] that removes all components associated with a provided entity. fn clear() -> impl EntityCommand { move |entity: Entity, world: &mut World| { @@ -2190,6 +2225,42 @@ mod tests { assert!(world.contains_resource::>()); } + #[test] + fn remove_component_with_required_components() { + #[derive(Component)] + #[require(Y)] + struct X; + + #[derive(Component, Default)] + struct Y; + + #[derive(Component)] + struct Z; + + let mut world = World::default(); + let mut queue = CommandQueue::default(); + let e = { + let mut commands = Commands::new(&mut queue, &world); + commands.spawn((X, Z)).id() + }; + queue.apply(&mut world); + + assert!(world.get::(e).is_some()); + assert!(world.get::(e).is_some()); + assert!(world.get::(e).is_some()); + + { + let mut commands = Commands::new(&mut queue, &world); + commands.entity(e).remove_with_requires::(); + } + queue.apply(&mut world); + + assert!(world.get::(e).is_none()); + assert!(world.get::(e).is_none()); + + assert!(world.get::(e).is_some()); + } + fn is_send() {} fn is_sync() {} diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index fbc58a91048f4c..57d45366c6940f 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -1558,6 +1558,20 @@ impl<'w> EntityWorldMut<'w> { self } + /// Removes all components in the [`Bundle`] and remove all required components for each component in the bundle + pub fn remove_with_requires(&mut self) -> &mut Self { + let storages = &mut self.world.storages; + let components = &mut self.world.components; + let bundles = &mut self.world.bundles; + + let bundle_id = bundles.register_contributed_bundle_info::(components, storages); + + // SAFETY: the dynamic `BundleInfo` is initialized above + self.location = unsafe { self.remove_bundle(bundle_id) }; + + self + } + /// Removes any components except those in the [`Bundle`] (and its Required Components) from the entity. /// /// See [`EntityCommands::retain`](crate::system::EntityCommands::retain) for more details. From 20dbf790a683263fceeb0681e988dcd65416034c Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 3 Oct 2024 21:30:52 +0000 Subject: [PATCH 012/546] Get rid of unnecessary mutable access in ui picking backend (#15630) ## Solution Yeet ## Testing Tested the `simple_picking` example --- crates/bevy_ui/src/picking_backend.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index d66c3f995c15e4..da708511bbe9f5 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -67,7 +67,7 @@ pub fn ui_picking( primary_window: Query>, ui_scale: Res, ui_stack: Res, - mut node_query: Query, + node_query: Query, mut output: EventWriter, ) { // For each camera, the pointer and its position @@ -119,7 +119,7 @@ pub fn ui_picking( // reverse the iterator to traverse the tree from closest nodes to furthest .rev() { - let Ok(node) = node_query.get_mut(*node_entity) else { + let Ok(node) = node_query.get(*node_entity) else { continue; }; @@ -183,11 +183,10 @@ pub fn ui_picking( for ((camera, pointer), hovered_nodes) in hit_nodes.iter() { // As soon as a node with a `Block` focus policy is detected, the iteration will stop on it // because it "captures" the interaction. - let mut iter = node_query.iter_many_mut(hovered_nodes.iter()); let mut picks = Vec::new(); let mut depth = 0.0; - while let Some(node) = iter.fetch_next() { + for node in node_query.iter_many(hovered_nodes) { let Some(camera_entity) = node .target_camera .map(TargetCamera::entity) From f0704cffa440f6270281ca68f702cf387f8bd74e Mon Sep 17 00:00:00 2001 From: fluffiac Date: Thu, 3 Oct 2024 18:34:39 -0600 Subject: [PATCH 013/546] Allow a closure to be used as a required component default (#15269) # Objective Allow required component default values to be provided in-line. ```rust #[derive(Component)] #[require( FocusPolicy(block_focus_policy) )] struct SomeComponent; fn block_focus_policy() -> FocusPolicy { FocusPolicy::Block } ``` May now be expressed as: ```rust #[derive(Component)] #[require( FocusPolicy(|| FocusPolicy::Block) )] struct SomeComponent; ``` ## Solution Modified the #[require] proc macro to accept a closure. ## Testing Tested using my branch as a dependency, and switching between the inline closure syntax and function syntax for a bunch of different components. --- crates/bevy_ecs/macros/src/component.rs | 66 ++++++++++++++++--------- crates/bevy_ecs/src/component.rs | 34 ++++++------- 2 files changed, 61 insertions(+), 39 deletions(-) diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index de41596f97a8ed..08b4e73056635b 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -9,7 +9,7 @@ use syn::{ punctuated::Punctuated, spanned::Spanned, token::{Comma, Paren}, - DeriveInput, ExprPath, Ident, LitStr, Path, Result, + DeriveInput, ExprClosure, ExprPath, Ident, LitStr, Path, Result, }; pub fn derive_event(input: TokenStream) -> TokenStream { @@ -90,24 +90,37 @@ pub fn derive_component(input: TokenStream) -> TokenStream { inheritance_depth + 1 ); }); - if let Some(func) = &require.func { - register_required.push(quote! { - components.register_required_components_manual::( - storages, - required_components, - || { let x: #ident = #func().into(); x }, - inheritance_depth - ); - }); - } else { - register_required.push(quote! { - components.register_required_components_manual::( - storages, - required_components, - <#ident as Default>::default, - inheritance_depth - ); - }); + match &require.func { + Some(RequireFunc::Path(func)) => { + register_required.push(quote! { + components.register_required_components_manual::( + storages, + required_components, + || { let x: #ident = #func().into(); x }, + inheritance_depth + ); + }); + } + Some(RequireFunc::Closure(func)) => { + register_required.push(quote! { + components.register_required_components_manual::( + storages, + required_components, + || { let x: #ident = (#func)().into(); x }, + inheritance_depth + ); + }); + } + None => { + register_required.push(quote! { + components.register_required_components_manual::( + storages, + required_components, + <#ident as Default>::default, + inheritance_depth + ); + }); + } } } } @@ -180,7 +193,12 @@ enum StorageTy { struct Require { path: Path, - func: Option, + func: Option, +} + +enum RequireFunc { + Path(Path), + Closure(ExprClosure), } // values for `storage` attribute @@ -256,8 +274,12 @@ impl Parse for Require { let func = if input.peek(Paren) { let content; parenthesized!(content in input); - let func = content.parse::()?; - Some(func) + if let Ok(func) = content.parse::() { + Some(RequireFunc::Closure(func)) + } else { + let func = content.parse::()?; + Some(RequireFunc::Path(func)) + } } else { None }; diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 121d49a2919857..5c443e8bcc1c54 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -146,25 +146,33 @@ use thiserror::Error; /// assert_eq!(&C(0), world.entity(id).get::().unwrap()); /// ``` /// -/// You can also define a custom constructor: +/// You can also define a custom constructor function or closure: /// /// ``` /// # use bevy_ecs::prelude::*; /// #[derive(Component)] -/// #[require(B(init_b))] +/// #[require(C(init_c))] /// struct A; /// /// #[derive(Component, PartialEq, Eq, Debug)] -/// struct B(usize); +/// #[require(C(|| C(20)))] +/// struct B; +/// +/// #[derive(Component, PartialEq, Eq, Debug)] +/// struct C(usize); /// -/// fn init_b() -> B { -/// B(10) +/// fn init_c() -> C { +/// C(10) /// } /// /// # let mut world = World::default(); -/// // This will implicitly also insert B with the init_b() constructor +/// // This will implicitly also insert C with the init_c() constructor /// let id = world.spawn(A).id(); -/// assert_eq!(&B(10), world.entity(id).get::().unwrap()); +/// assert_eq!(&C(10), world.entity(id).get::().unwrap()); +/// +/// // This will implicitly also insert C with the `|| C(20)` constructor closure +/// let id = world.spawn(B).id(); +/// assert_eq!(&C(20), world.entity(id).get::().unwrap()); /// ``` /// /// Required components are _recursive_. This means, if a Required Component has required components, @@ -202,24 +210,16 @@ use thiserror::Error; /// struct X(usize); /// /// #[derive(Component, Default)] -/// #[require(X(x1))] +/// #[require(X(|| X(1)))] /// struct Y; /// -/// fn x1() -> X { -/// X(1) -/// } -/// /// #[derive(Component)] /// #[require( /// Y, -/// X(x2), +/// X(|| X(2)), /// )] /// struct Z; /// -/// fn x2() -> X { -/// X(2) -/// } -/// /// # let mut world = World::default(); /// // In this case, the x2 constructor is used for X /// let id = world.spawn(Z).id(); From 26808745cf5957797693fdef4c64436173865083 Mon Sep 17 00:00:00 2001 From: Liam Gallagher Date: Fri, 4 Oct 2024 13:38:49 +1300 Subject: [PATCH 014/546] Fix `bevy_window` and `bevy_winit` readme badges (#15637) ## Objective Fix the badges for `bevy_window` and `bevy_winit`. ## Solution Replace the placeholder `bevy_name` wit the correct crate name. --- crates/bevy_window/README.md | 6 +++--- crates/bevy_winit/README.md | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_window/README.md b/crates/bevy_window/README.md index f450fc55d3b0da..e8a5c09b0277d0 100644 --- a/crates/bevy_window/README.md +++ b/crates/bevy_window/README.md @@ -1,7 +1,7 @@ # Bevy Window [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) -[![Crates.io](https://img.shields.io/crates/v/bevy_name.svg)](https://crates.io/crates/bevy_name) -[![Downloads](https://img.shields.io/crates/d/bevy_name.svg)](https://crates.io/crates/bevy_name) -[![Docs](https://docs.rs/bevy_name/badge.svg)](https://docs.rs/bevy_name/latest/bevy_name/) +[![Crates.io](https://img.shields.io/crates/v/bevy_window.svg)](https://crates.io/crates/bevy_window) +[![Downloads](https://img.shields.io/crates/d/bevy_window.svg)](https://crates.io/crates/bevy_window) +[![Docs](https://docs.rs/bevy_window/badge.svg)](https://docs.rs/bevy_window/latest/bevy_window/) [![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) diff --git a/crates/bevy_winit/README.md b/crates/bevy_winit/README.md index 2e7c9b77a7ef6a..5d94f0207f5b37 100644 --- a/crates/bevy_winit/README.md +++ b/crates/bevy_winit/README.md @@ -1,7 +1,7 @@ # Bevy Winit [![License](https://img.shields.io/badge/license-MIT%2FApache-blue.svg)](https://github.com/bevyengine/bevy#license) -[![Crates.io](https://img.shields.io/crates/v/bevy_name.svg)](https://crates.io/crates/bevy_name) -[![Downloads](https://img.shields.io/crates/d/bevy_name.svg)](https://crates.io/crates/bevy_name) -[![Docs](https://docs.rs/bevy_name/badge.svg)](https://docs.rs/bevy_name/latest/bevy_name/) +[![Crates.io](https://img.shields.io/crates/v/bevy_winit.svg)](https://crates.io/crates/bevy_winit) +[![Downloads](https://img.shields.io/crates/d/bevy_winit.svg)](https://crates.io/crates/bevy_winit) +[![Docs](https://docs.rs/bevy_winit/badge.svg)](https://docs.rs/bevy_winit/latest/bevy_winit/) [![Discord](https://img.shields.io/discord/691052431525675048.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/bevy) From 61e11ea44063138b993a33711d0c3ae84fa28051 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Fri, 4 Oct 2024 04:07:09 +0300 Subject: [PATCH 015/546] Fix audio not playing (#15638) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Someone (let's not name names here) might've been a bit of a goofball, and happened to forget that "playing audio" should cause this thing called "sound" to be emitted! That someone might not have realized that queries should be updated to account for audio using wrapper components instead of raw asset handles after #15573. ## Solution Update systems, and listen to the relaxing soundscapes of `Windless Slopes.ogg` 🎵 --- crates/bevy_audio/src/audio_output.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index c595be70ca6090..bbb9c5b6821336 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -2,7 +2,7 @@ use crate::{ AudioPlayer, Decodable, DefaultSpatialScale, GlobalVolume, PlaybackMode, PlaybackSettings, SpatialAudioSink, SpatialListener, }; -use bevy_asset::{Asset, Assets, Handle}; +use bevy_asset::{Asset, Assets}; use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_hierarchy::DespawnRecursiveExt; use bevy_math::Vec3; @@ -101,7 +101,7 @@ pub(crate) fn play_queued_audio_system( query_nonplaying: Query< ( Entity, - &Handle, + &AudioPlayer, &PlaybackSettings, Option<&GlobalTransform>, ), @@ -119,7 +119,7 @@ pub(crate) fn play_queued_audio_system( }; for (entity, source_handle, settings, maybe_emitter_transform) in &query_nonplaying { - let Some(audio_source) = audio_sources.get(source_handle) else { + let Some(audio_source) = audio_sources.get(&source_handle.0) else { continue; }; // audio data is available (has loaded), begin playback and insert sink component @@ -236,19 +236,19 @@ pub(crate) fn cleanup_finished_audio( mut commands: Commands, query_nonspatial_despawn: Query< (Entity, &AudioSink), - (With, With>), + (With, With>), >, query_spatial_despawn: Query< (Entity, &SpatialAudioSink), - (With, With>), + (With, With>), >, query_nonspatial_remove: Query< (Entity, &AudioSink), - (With, With>), + (With, With>), >, query_spatial_remove: Query< (Entity, &SpatialAudioSink), - (With, With>), + (With, With>), >, ) { for (entity, sink) in &query_nonspatial_despawn { From 0b9a461d5d3e5e478feccffac385375ef37a3806 Mon Sep 17 00:00:00 2001 From: vero Date: Thu, 3 Oct 2024 21:27:20 -0400 Subject: [PATCH 016/546] Invert the dependency between bevy_animation and bevy_ui (#15634) # Objective - Improve crate dependency graph ## Solution - Invert a dependency ## Testing - Tested ui and animation examples --- crates/bevy_animation/Cargo.toml | 3 --- crates/bevy_animation/src/lib.rs | 9 ++++++--- crates/bevy_ui/Cargo.toml | 1 + crates/bevy_ui/src/lib.rs | 4 +++- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index 66d20c49fe8728..d507f03342ca1d 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -27,9 +27,6 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev", features = [ - "bevy_text", -] } bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } # other diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 23585432041e6a..529377cab70978 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -44,7 +44,6 @@ use bevy_reflect::{ }; use bevy_time::Time; use bevy_transform::{prelude::Transform, TransformSystem}; -use bevy_ui::UiSystem; use bevy_utils::{ hashbrown::HashMap, tracing::{trace, warn}, @@ -1043,6 +1042,10 @@ pub fn animate_targets( }); } +/// Animation system set +#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone)] +pub struct Animation; + /// Adds animation support to an app #[derive(Default)] pub struct AnimationPlugin; @@ -1078,8 +1081,8 @@ impl Plugin for AnimationPlugin { expire_completed_transitions, ) .chain() - .before(TransformSystem::TransformPropagate) - .before(UiSystem::Prepare), + .in_set(Animation) + .before(TransformSystem::TransformPropagate), ); } } diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index c0d95b85d6b1c2..2ee274d4ba1529 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -24,6 +24,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "bevy", ] } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } +bevy_animation = { path = "../bevy_animation", version = "0.15.0-dev" } bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" } bevy_text = { path = "../bevy_text", version = "0.15.0-dev", optional = true } bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index c2a773ba5a3a0f..7120dd9f056ad0 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -152,7 +152,9 @@ impl Plugin for UiPlugin { PostUpdate, ( CameraUpdateSystem, - UiSystem::Prepare.before(UiSystem::Stack), + UiSystem::Prepare + .before(UiSystem::Stack) + .after(bevy_animation::Animation), UiSystem::Layout, UiSystem::PostLayout, ) From 252641009650b694c00a5c97a5b4cacf4f809504 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 4 Oct 2024 01:31:21 +0000 Subject: [PATCH 017/546] Clean up the `simple_picking` example (#15633) ## Solution - Removed superfluous `Pickable` components - Slightly simplified the code for updating the text color - Removed the `Pointer` observer from the mesh entirely since that doesn't support picking yet --- examples/picking/simple_picking.rs | 43 +++++++++++------------------- 1 file changed, 16 insertions(+), 27 deletions(-) diff --git a/examples/picking/simple_picking.rs b/examples/picking/simple_picking.rs index 1ff0a78a1c748f..a2cf63c611b13f 100644 --- a/examples/picking/simple_picking.rs +++ b/examples/picking/simple_picking.rs @@ -18,19 +18,16 @@ fn setup( mut materials: ResMut>, ) { commands - .spawn(( - TextBundle { - text: Text::from_section("Click Me to get a box", TextStyle::default()), - style: Style { - position_type: PositionType::Absolute, - top: Val::Percent(12.0), - left: Val::Percent(12.0), - ..default() - }, - ..Default::default() + .spawn(TextBundle { + text: Text::from_section("Click Me to get a box", TextStyle::default()), + style: Style { + position_type: PositionType::Absolute, + top: Val::Percent(12.0), + left: Val::Percent(12.0), + ..default() }, - Pickable::default(), - )) + ..Default::default() + }) .observe( |_click: Trigger>, mut commands: Commands, @@ -47,26 +44,18 @@ fn setup( ) .observe(|evt: Trigger>, mut texts: Query<&mut Text>| { let mut text = texts.get_mut(evt.entity()).unwrap(); - let first = text.sections.first_mut().unwrap(); - first.style.color = WHITE.into(); + text.sections[0].style.color = WHITE.into(); }) .observe(|evt: Trigger>, mut texts: Query<&mut Text>| { let mut text = texts.get_mut(evt.entity()).unwrap(); - let first = text.sections.first_mut().unwrap(); - first.style.color = BLUE.into(); + text.sections[0].style.color = BLUE.into(); }); // circular base - commands - .spawn(( - Mesh3d(meshes.add(Circle::new(4.0))), - MeshMaterial3d(materials.add(Color::WHITE)), - Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), - Pickable::default(), - )) - .observe(|click: Trigger>| { - let click = click.event(); - println!("{click:?}"); - }); + commands.spawn(( + Mesh3d(meshes.add(Circle::new(4.0))), + MeshMaterial3d(materials.add(Color::WHITE)), + Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)), + )); // light commands.spawn(( PointLight { From 2530f262f5b11b6a20f38b6f103ab08b29c4b4d1 Mon Sep 17 00:00:00 2001 From: vero Date: Fri, 4 Oct 2024 08:22:15 -0400 Subject: [PATCH 018/546] Remove bevy_animation dependency on bevy_text (#15642) # Objective - Fixes #15640 ## Solution - Do it ## Testing - ran many_foxes --- crates/bevy_animation/Cargo.toml | 1 - crates/bevy_animation/src/animation_curves.rs | 34 +++++++++---------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index d507f03342ca1d..33cec877589bfb 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -27,7 +27,6 @@ bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } -bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } # other petgraph = { version = "0.6", features = ["serde-1"] } diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index 82d84ee42ffefd..c0101ad5b8f773 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -104,20 +104,20 @@ use crate::{ /// 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: +/// to define the animation itself). +/// For example, in order to animate field of view, you might use: /// /// # use bevy_animation::prelude::AnimatableProperty; /// # use bevy_reflect::Reflect; -/// # use bevy_text::Text; +/// # use bevy_render::camera::PerspectiveProjection; /// #[derive(Reflect)] -/// struct FontSizeProperty; +/// struct FieldOfViewProperty; /// -/// impl AnimatableProperty for FontSizeProperty { -/// type Component = Text; +/// impl AnimatableProperty for FieldOfViewProperty { +/// type Component = PerspectiveProjection; /// type Property = f32; /// fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// Some(&mut component.sections.get_mut(0)?.style.font_size) +/// Some(&mut component.fov) /// } /// } /// @@ -127,15 +127,15 @@ use crate::{ /// # use bevy_animation::prelude::{AnimatableProperty, AnimatableKeyframeCurve, AnimatableCurve}; /// # use bevy_core::Name; /// # use bevy_reflect::Reflect; -/// # use bevy_text::Text; +/// # use bevy_render::camera::PerspectiveProjection; /// # let animation_target_id = AnimationTargetId::from(&Name::new("Test")); /// # #[derive(Reflect)] -/// # struct FontSizeProperty; -/// # impl AnimatableProperty for FontSizeProperty { -/// # type Component = Text; +/// # struct FieldOfViewProperty; +/// # impl AnimatableProperty for FieldOfViewProperty { +/// # type Component = PerspectiveProjection; /// # type Property = f32; /// # fn get_mut(component: &mut Self::Component) -> Option<&mut Self::Property> { -/// # Some(&mut component.sections.get_mut(0)?.style.font_size) +/// # Some(&mut component.fov) /// # } /// # } /// let mut animation_clip = AnimationClip::default(); @@ -143,18 +143,18 @@ use crate::{ /// animation_target_id, /// AnimatableKeyframeCurve::new( /// [ -/// (0.0, 24.0), -/// (1.0, 80.0), +/// (0.0, core::f32::consts::PI / 4.0), +/// (1.0, core::f32::consts::PI / 3.0), /// ] /// ) -/// .map(AnimatableCurve::::from_curve) +/// .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 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 +/// invocation of [`AnimatableCurve::from_curve`] with `FieldOfViewProperty` indicates that the `f32` +/// output from that curve is to be used to animate the font size of a `PerspectiveProjection` component (as /// configured above). /// /// [`AnimationClip`]: crate::AnimationClip From e72b9625d7a2a55f940deeb31d71b41122371a83 Mon Sep 17 00:00:00 2001 From: robtfm <50659922+robtfm@users.noreply.github.com> Date: Fri, 4 Oct 2024 13:28:57 +0100 Subject: [PATCH 019/546] drop info locks in single threaded (#15522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective addresses half of issue #15508 avoid asset server deadlock when `multi_threaded` feature is not enabled. ## Solution drop the locks in the single-threaded case. the lock is still held with the `multi-threaded` feature enabled to avoid re-locking to insert the load task. i guess this might possibly cause issues on single-core machines ... is that something we should worry about? --------- Co-authored-by: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> Co-authored-by: james7132 Co-authored-by: Chris Russell <8494645+chescock@users.noreply.github.com> Co-authored-by: Emerson Coskey <56370779+ecoskey@users.noreply.github.com> Co-authored-by: Alice Cecile Co-authored-by: Tim Co-authored-by: Joona Aalto Co-authored-by: s-puig <39652109+s-puig@users.noreply.github.com> Co-authored-by: Carter Anderson Co-authored-by: Liam Gallagher Co-authored-by: Matty Co-authored-by: Zachary Harrold Co-authored-by: Benjamin Brienen Co-authored-by: charlotte Co-authored-by: akimakinai <105044389+akimakinai@users.noreply.github.com> Co-authored-by: Antony Co-authored-by: JohnTheCoolingFan <43478602+JohnTheCoolingFan@users.noreply.github.com> Co-authored-by: hshrimp <182684536+hooded-shrimp@users.noreply.github.com> Co-authored-by: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Co-authored-by: Dokkae <90514461+Dokkae6949@users.noreply.github.com> Co-authored-by: François Mockers Co-authored-by: MiniaczQ Co-authored-by: Pablo Reinhardt <126117294+pablo-lua@users.noreply.github.com> Co-authored-by: JMS55 <47158642+JMS55@users.noreply.github.com> Co-authored-by: Sou1gh0st Co-authored-by: Robert Walter <26892280+RobWalt@users.noreply.github.com> Co-authored-by: eckz <567737+eckz@users.noreply.github.com> Co-authored-by: Matty <2975848+mweatherley@users.noreply.github.com> Co-authored-by: IQuick 143 Co-authored-by: Giacomo Stevanato Co-authored-by: Clar Fon <15850505+clarfonthey@users.noreply.github.com> Co-authored-by: andriyDev Co-authored-by: TheBigCheese <32036861+13ros27@users.noreply.github.com> Co-authored-by: Kristoffer Søholm Co-authored-by: IceSentry Co-authored-by: Josh Robson Chase Co-authored-by: Erik Živković Co-authored-by: ChosenName <69129796+ChosenName@users.noreply.github.com> Co-authored-by: mgi388 <135186256+mgi388@users.noreply.github.com> Co-authored-by: SpecificProtagonist Co-authored-by: ickshonpe Co-authored-by: Gabriel Bourgeois Co-authored-by: UkoeHB <37489173+UkoeHB@users.noreply.github.com> Co-authored-by: Trashtalk217 Co-authored-by: re0312 Co-authored-by: re0312 <45868716+re0312@users.noreply.github.com> Co-authored-by: Periwink Co-authored-by: Anselmo Sampietro Co-authored-by: rudderbucky Co-authored-by: aecsocket <43144841+aecsocket@users.noreply.github.com> Co-authored-by: Andreas <34456840+nilsiker@users.noreply.github.com> Co-authored-by: Ludwig DUBOS Co-authored-by: Ensar Sarajčić Co-authored-by: Kanabenki Co-authored-by: François Mockers Co-authored-by: m-edlund Co-authored-by: vero Co-authored-by: Hennadii Chernyshchyk Co-authored-by: Litttle_fish <38809254+Litttlefish@users.noreply.github.com> Co-authored-by: BD103 <59022059+BD103@users.noreply.github.com> Co-authored-by: Rich Churcher Co-authored-by: Viktor Gustavsson Co-authored-by: Dragoș Tiselice Co-authored-by: Miles Silberling-Cook Co-authored-by: notmd <33456881+notmd@users.noreply.github.com> Co-authored-by: Matt Tracy Co-authored-by: Patrick Walton Co-authored-by: SpecificProtagonist Co-authored-by: rewin Co-authored-by: a.yamaev Co-authored-by: fluffiac --- crates/bevy_asset/src/server/mod.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 316642a45a260f..a56e653818e65d 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -420,6 +420,10 @@ impl AssetServer { infos: &mut AssetInfos, guard: G, ) { + // drop the lock on `AssetInfos` before spawning a task that may block on it in single-threaded + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + drop(infos); + let owned_handle = handle.clone(); let server = self.clone(); let task = IoTaskPool::get().spawn(async move { @@ -469,6 +473,11 @@ impl AssetServer { HandleLoadingMode::Request, meta_transform, ); + + // drop the lock on `AssetInfos` before spawning a task that may block on it in single-threaded + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + drop(infos); + if !should_load { return handle; } @@ -778,6 +787,11 @@ impl AssetServer { let mut infos = self.data.infos.write(); let handle = infos.create_loading_handle_untyped(TypeId::of::(), core::any::type_name::()); + + // drop the lock on `AssetInfos` before spawning a task that may block on it in single-threaded + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + drop(infos); + let id = handle.id(); let event_sender = self.data.asset_event_sender.clone(); From d0edbdac78e28858b52f8d5d4691492e3f98bc9e Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Fri, 4 Oct 2024 22:20:25 +0300 Subject: [PATCH 020/546] Fix cargo-ndk build command (#15648) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Fix cargo-ndk build command documentation in readme. ```sh ❯ cargo ndk -t arm64-v8a build -o android_example/app/src/main/jniLibs Building arm64-v8a (aarch64-linux-android) error: unexpected argument '-o' found ``` ## Solution - Move "build" to the end of the command. ## Testing - With the new command order building works. ```sh ❯ cargo ndk -t arm64-v8a -o android_example/app/src/main/jniLibs build Building arm64-v8a (aarch64-linux-android) Compiling bevy_ptr v0.15.0-dev (/home/eero/repos/bevy/crates/bevy_ptr) Compiling bevy_macro_utils v0.15.0-dev (/home/eero/repos/bevy/crates/bevy_macro_utils) Compiling event-listener v5.3.1 ... rest of compilation ... ``` --- docs-template/EXAMPLE_README.md.tpl | 4 ++-- examples/README.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs-template/EXAMPLE_README.md.tpl b/docs-template/EXAMPLE_README.md.tpl index 7d8fa20f392e21..c1fbfc08eb153d 100644 --- a/docs-template/EXAMPLE_README.md.tpl +++ b/docs-template/EXAMPLE_README.md.tpl @@ -106,13 +106,13 @@ Alternatively, you can install Android Studio. To build an Android app, you first need to build shared object files for the target architecture with `cargo-ndk`: ```sh -cargo ndk -t build -o /app/src/main/jniLibs +cargo ndk -t -o /app/src/main/jniLibs build ``` For example, to compile to a 64-bit ARM platform: ```sh -cargo ndk -t arm64-v8a build -o android_example/app/src/main/jniLibs +cargo ndk -t arm64-v8a -o android_example/app/src/main/jniLibs build ``` Setting the output path ensures the shared object files can be found in target-specific directories under `jniLibs` where the JNI can find them. diff --git a/examples/README.md b/examples/README.md index e8bbb36e455361..9847a69508a5da 100644 --- a/examples/README.md +++ b/examples/README.md @@ -561,13 +561,13 @@ Alternatively, you can install Android Studio. To build an Android app, you first need to build shared object files for the target architecture with `cargo-ndk`: ```sh -cargo ndk -t build -o /app/src/main/jniLibs +cargo ndk -t -o /app/src/main/jniLibs build ``` For example, to compile to a 64-bit ARM platform: ```sh -cargo ndk -t arm64-v8a build -o android_example/app/src/main/jniLibs +cargo ndk -t arm64-v8a -o android_example/app/src/main/jniLibs build ``` Setting the output path ensures the shared object files can be found in target-specific directories under `jniLibs` where the JNI can find them. From 53adcd766799622fcdc1126442f098a4632defa9 Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Sat, 5 Oct 2024 05:25:49 +1000 Subject: [PATCH 021/546] Minor fixes for `bevy_utils` in `no_std` (#15463) # Objective - Contributes to #15460 ## Solution - Made `web-time` a `wasm32`-only dependency. - Moved time-related exports to its own module for clarity. - Feature-gated allocator requirements for `hashbrown` behind `alloc`. - Enabled compile-time RNG for `ahash` (runtime RNG will preferentially used in `std` environments) - Made `thread_local` optional by feature-gating the `Parallel` type. ## Testing - Ran CI locally. - `cargo build -p bevy_utils --target "x86_64-unknown-none" --no-default-features` --- crates/bevy_utils/Cargo.toml | 21 ++++++++++++++------- crates/bevy_utils/src/lib.rs | 5 ++++- crates/bevy_utils/src/time.rs | 11 +++++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) create mode 100644 crates/bevy_utils/src/time.rs diff --git a/crates/bevy_utils/Cargo.toml b/crates/bevy_utils/Cargo.toml index 7f83ec17c9a7bc..c1555d8ba94bb0 100644 --- a/crates/bevy_utils/Cargo.toml +++ b/crates/bevy_utils/Cargo.toml @@ -9,26 +9,33 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -default = ["std"] -std = ["alloc", "tracing/std", "ahash/std"] -alloc = [] +default = ["std", "serde"] +std = [ + "alloc", + "tracing/std", + "ahash/std", + "dep:thread_local", + "ahash/runtime-rng", +] +alloc = ["hashbrown/default"] detailed_trace = [] +serde = ["hashbrown/serde"] [dependencies] ahash = { version = "0.8.7", default-features = false, features = [ - "runtime-rng", + "compile-time-rng", ] } tracing = { version = "0.1", default-features = false } -web-time = { version = "1.1" } -hashbrown = { version = "0.14.2", features = ["serde"] } +hashbrown = { version = "0.14.2", default-features = false } bevy_utils_proc_macros = { version = "0.15.0-dev", path = "macros" } -thread_local = "1.0" +thread_local = { version = "1.0", optional = true } [dev-dependencies] static_assertions = "1.1.0" [target.'cfg(target_arch = "wasm32")'.dependencies] getrandom = { version = "0.2.0", features = ["js"] } +web-time = { version = "1.1" } [lints] workspace = true diff --git a/crates/bevy_utils/src/lib.rs b/crates/bevy_utils/src/lib.rs index 9cd1782345e886..1f7e1f7e5c32b1 100644 --- a/crates/bevy_utils/src/lib.rs +++ b/crates/bevy_utils/src/lib.rs @@ -31,15 +31,18 @@ mod default; mod object_safe; pub use object_safe::assert_object_safe; mod once; +#[cfg(feature = "std")] mod parallel_queue; +mod time; pub use ahash::{AHasher, RandomState}; pub use bevy_utils_proc_macros::*; pub use default::default; pub use hashbrown; +#[cfg(feature = "std")] pub use parallel_queue::*; +pub use time::*; pub use tracing; -pub use web_time::{Duration, Instant, SystemTime, SystemTimeError, TryFromFloatSecsError}; #[cfg(feature = "alloc")] use alloc::boxed::Box; diff --git a/crates/bevy_utils/src/time.rs b/crates/bevy_utils/src/time.rs new file mode 100644 index 00000000000000..fb5136eb03d2ac --- /dev/null +++ b/crates/bevy_utils/src/time.rs @@ -0,0 +1,11 @@ +#[cfg(target_arch = "wasm32")] +pub use web_time::{Duration, Instant, SystemTime, SystemTimeError, TryFromFloatSecsError}; + +#[cfg(all(not(target_arch = "wasm32"), feature = "std"))] +pub use { + core::time::{Duration, TryFromFloatSecsError}, + std::time::{Instant, SystemTime, SystemTimeError}, +}; + +#[cfg(all(not(target_arch = "wasm32"), not(feature = "std")))] +pub use core::time::{Duration, TryFromFloatSecsError}; From 8b0388c74a751773cdc21356e6976ebd63751cb9 Mon Sep 17 00:00:00 2001 From: vero Date: Fri, 4 Oct 2024 16:16:47 -0400 Subject: [PATCH 022/546] Split off bevy_image from bevy_render (#15650) # Objective - bevy_render is gargantuan ## Solution - Split off bevy_image ## Testing - Ran some examples --- crates/bevy_asset/Cargo.toml | 1 + crates/bevy_asset/src/lib.rs | 2 + crates/bevy_asset/src/render_asset.rs | 49 ++++++++ crates/bevy_image/Cargo.toml | 61 ++++++++++ .../src/texture => bevy_image/src}/basis.rs | 0 .../src/texture => bevy_image/src}/dds.rs | 2 +- .../src}/exr_texture_loader.rs | 7 +- .../src}/hdr_texture_loader.rs | 6 +- .../src/texture => bevy_image/src}/image.rs | 110 +++--------------- .../src}/image_texture_conversion.rs | 6 +- .../src/texture => bevy_image/src}/ktx2.rs | 2 +- crates/bevy_image/src/lib.rs | 28 +++++ crates/bevy_render/Cargo.toml | 30 +++-- crates/bevy_render/src/lib.rs | 2 +- crates/bevy_render/src/render_asset.rs | 50 +------- .../src/render_resource/texture.rs | 10 ++ .../src/texture/compressed_image_saver.rs | 2 +- .../bevy_render/src/texture/fallback_image.rs | 14 +-- crates/bevy_render/src/texture/gpu_image.rs | 78 +++++++++++++ crates/bevy_render/src/texture/mod.rs | 42 ++----- 20 files changed, 293 insertions(+), 209 deletions(-) create mode 100644 crates/bevy_asset/src/render_asset.rs create mode 100644 crates/bevy_image/Cargo.toml rename crates/{bevy_render/src/texture => bevy_image/src}/basis.rs (100%) rename crates/{bevy_render/src/texture => bevy_image/src}/dds.rs (99%) rename crates/{bevy_render/src/texture => bevy_image/src}/exr_texture_loader.rs (92%) rename crates/{bevy_render/src/texture => bevy_image/src}/hdr_texture_loader.rs (96%) rename crates/{bevy_render/src/texture => bevy_image/src}/image.rs (91%) rename crates/{bevy_render/src/texture => bevy_image/src}/image_texture_conversion.rs (98%) rename crates/{bevy_render/src/texture => bevy_image/src}/ktx2.rs (99%) create mode 100644 crates/bevy_image/src/lib.rs create mode 100644 crates/bevy_render/src/texture/gpu_image.rs diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 4bee550a4879c4..60cbdf42100eb8 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -33,6 +33,7 @@ atomicow = "1.0" async-broadcast = "0.5" async-fs = "2.0" async-lock = "3.0" +bitflags = { version = "2.3", features = ["serde"] } crossbeam-channel = "0.5" downcast-rs = "1.2" disqualified = "1.0" diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 08d08c4b1c2dfa..c1ffb6b2930ecf 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -176,6 +176,7 @@ mod loader; mod loader_builders; mod path; mod reflect; +mod render_asset; mod server; pub use assets::*; @@ -192,6 +193,7 @@ pub use loader_builders::{ }; pub use path::*; pub use reflect::*; +pub use render_asset::*; pub use server::*; /// Rusty Object Notation, a crate used to serialize and deserialize bevy assets. diff --git a/crates/bevy_asset/src/render_asset.rs b/crates/bevy_asset/src/render_asset.rs new file mode 100644 index 00000000000000..3bbc3dfd484586 --- /dev/null +++ b/crates/bevy_asset/src/render_asset.rs @@ -0,0 +1,49 @@ +use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; +use serde::{Deserialize, Serialize}; + +bitflags::bitflags! { + /// Defines where the asset will be used. + /// + /// If an asset is set to the `RENDER_WORLD` but not the `MAIN_WORLD`, the asset will be + /// unloaded from the asset server once it's been extracted and prepared in the render world. + /// + /// Unloading the asset saves on memory, as for most cases it is no longer necessary to keep + /// it in RAM once it's been uploaded to the GPU's VRAM. However, this means you can no longer + /// access the asset from the CPU (via the `Assets` resource) once unloaded (without re-loading it). + /// + /// If you never need access to the asset from the CPU past the first frame it's loaded on, + /// or only need very infrequent access, then set this to `RENDER_WORLD`. Otherwise, set this to + /// `RENDER_WORLD | MAIN_WORLD`. + /// + /// If you have an asset that doesn't actually need to end up in the render world, like an Image + /// that will be decoded into another Image asset, use `MAIN_WORLD` only. + /// + /// ## Platform-specific + /// + /// On Wasm, it is not possible for now to free reserved memory. To control memory usage, load assets + /// in sequence and unload one before loading the next. See this + /// [discussion about memory management](https://github.com/WebAssembly/design/issues/1397) for more + /// details. + #[repr(transparent)] + #[derive(Serialize, Deserialize, Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] + #[reflect(opaque)] + #[reflect(Serialize, Deserialize, Hash, PartialEq, Debug)] + pub struct RenderAssetUsages: u8 { + const MAIN_WORLD = 1 << 0; + const RENDER_WORLD = 1 << 1; + } +} + +impl Default for RenderAssetUsages { + /// Returns the default render asset usage flags: + /// `RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD` + /// + /// This default configuration ensures the asset persists in the main world, even after being prepared for rendering. + /// + /// If your asset does not change, consider using `RenderAssetUsages::RENDER_WORLD` exclusively. This will cause + /// the asset to be unloaded from the main world once it has been prepared for rendering. If the asset does not need + /// to reach the render world at all, use `RenderAssetUsages::MAIN_WORLD` exclusively. + fn default() -> Self { + RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD + } +} diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml new file mode 100644 index 00000000000000..bc3e187ff893e6 --- /dev/null +++ b/crates/bevy_image/Cargo.toml @@ -0,0 +1,61 @@ +[package] +name = "bevy_image" +version = "0.15.0-dev" +edition = "2021" +description = "Provides image types for Bevy Engine" +homepage = "https://bevyengine.org" +repository = "https://github.com/bevyengine/bevy" +license = "MIT OR Apache-2.0" +keywords = ["bevy"] + +[features] +png = ["image/png"] +exr = ["image/exr"] +hdr = ["image/hdr"] +tga = ["image/tga"] +jpeg = ["image/jpeg"] +bmp = ["image/bmp"] +webp = ["image/webp"] +dds = ["ddsfile"] +pnm = ["image/pnm"] + +# For ktx2 supercompression +zlib = ["flate2"] +zstd = ["ruzstd"] + +[dependencies] +bevy_asset = { path = "../bevy_asset", version = "0.15.0-dev" } +bevy_color = { path = "../bevy_color", version = "0.15.0-dev", features = [ + "serialize", + "wgpu-types", +] } +bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } +bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ + "bevy", +] } +bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } + +# rendering +image = { version = "0.25.2", default-features = false } + +# misc +bitflags = { version = "2.3", features = ["serde"] } +bytemuck = { version = "1.5" } +wgpu = { version = "22", default-features = false } +serde = { version = "1", features = ["derive"] } +thiserror = "1.0" +futures-lite = "2.0.1" +ddsfile = { version = "0.5.2", optional = true } +ktx2 = { version = "0.3.0", optional = true } +# For ktx2 supercompression +flate2 = { version = "1.0.22", optional = true } +ruzstd = { version = "0.7.0", optional = true } +# For transcoding of UASTC/ETC1S universal formats, and for .basis file support +basis-universal = { version = "0.3.0", optional = true } + +[lints] +workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["-Zunstable-options", "--generate-link-to-definition"] +all-features = true diff --git a/crates/bevy_render/src/texture/basis.rs b/crates/bevy_image/src/basis.rs similarity index 100% rename from crates/bevy_render/src/texture/basis.rs rename to crates/bevy_image/src/basis.rs diff --git a/crates/bevy_render/src/texture/dds.rs b/crates/bevy_image/src/dds.rs similarity index 99% rename from crates/bevy_render/src/texture/dds.rs rename to crates/bevy_image/src/dds.rs index 29454278d39783..aa7ba06efeaf32 100644 --- a/crates/bevy_render/src/texture/dds.rs +++ b/crates/bevy_image/src/dds.rs @@ -283,7 +283,7 @@ pub fn dds_format_to_texture_format( mod test { use wgpu::{util::TextureDataOrder, TextureDescriptor, TextureDimension}; - use crate::texture::CompressedImageFormats; + use crate::CompressedImageFormats; use super::dds_buffer_to_image; diff --git a/crates/bevy_render/src/texture/exr_texture_loader.rs b/crates/bevy_image/src/exr_texture_loader.rs similarity index 92% rename from crates/bevy_render/src/texture/exr_texture_loader.rs rename to crates/bevy_image/src/exr_texture_loader.rs index ddab1b58e9cdf5..bb84ab61f646ce 100644 --- a/crates/bevy_render/src/texture/exr_texture_loader.rs +++ b/crates/bevy_image/src/exr_texture_loader.rs @@ -1,8 +1,5 @@ -use crate::{ - render_asset::RenderAssetUsages, - texture::{Image, TextureFormatPixelInfo}, -}; -use bevy_asset::{io::Reader, AssetLoader, LoadContext}; +use crate::{Image, TextureFormatPixelInfo}; +use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; use image::ImageDecoder; use serde::{Deserialize, Serialize}; use thiserror::Error; diff --git a/crates/bevy_render/src/texture/hdr_texture_loader.rs b/crates/bevy_image/src/hdr_texture_loader.rs similarity index 96% rename from crates/bevy_render/src/texture/hdr_texture_loader.rs rename to crates/bevy_image/src/hdr_texture_loader.rs index 08d907981f0592..24bad7bf5b458b 100644 --- a/crates/bevy_render/src/texture/hdr_texture_loader.rs +++ b/crates/bevy_image/src/hdr_texture_loader.rs @@ -1,7 +1,5 @@ -use crate::{ - render_asset::RenderAssetUsages, - texture::{Image, TextureFormatPixelInfo}, -}; +use crate::{Image, TextureFormatPixelInfo}; +use bevy_asset::RenderAssetUsages; use bevy_asset::{io::Reader, AssetLoader, LoadContext}; use image::DynamicImage; use serde::{Deserialize, Serialize}; diff --git a/crates/bevy_render/src/texture/image.rs b/crates/bevy_image/src/image.rs similarity index 91% rename from crates/bevy_render/src/texture/image.rs rename to crates/bevy_image/src/image.rs index 9204cc7d0a9d57..ad0b046e28cb4a 100644 --- a/crates/bevy_render/src/texture/image.rs +++ b/crates/bevy_image/src/image.rs @@ -5,21 +5,22 @@ use super::dds::*; #[cfg(feature = "ktx2")] use super::ktx2::*; -use crate::{ - render_asset::{PrepareAssetError, RenderAsset, RenderAssetUsages}, - render_resource::{Sampler, Texture, TextureView}, - renderer::{RenderDevice, RenderQueue}, - texture::BevyDefault, -}; -use bevy_asset::Asset; -use bevy_derive::{Deref, DerefMut}; -use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem}; +use bevy_asset::{Asset, RenderAssetUsages}; use bevy_math::{AspectRatio, UVec2, Vec2}; use bevy_reflect::prelude::*; -use core::hash::Hash; +use bevy_reflect::Reflect; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor}; +pub trait BevyDefault { + fn bevy_default() -> Self; +} + +impl BevyDefault for TextureFormat { + fn bevy_default() -> Self { + TextureFormat::Rgba8UnormSrgb + } +} pub const TEXTURE_ASSET_INDEX: u64 = 0; pub const SAMPLER_ASSET_INDEX: u64 = 1; @@ -180,11 +181,11 @@ pub struct Image { } /// Used in [`Image`], this determines what image sampler to use when rendering. The default setting, -/// [`ImageSampler::Default`], will read the sampler from the [`ImagePlugin`](super::ImagePlugin) at setup. +/// [`ImageSampler::Default`], will read the sampler from the `ImagePlugin` at setup. /// Setting this to [`ImageSampler::Descriptor`] will override the global default descriptor for this [`Image`]. #[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum ImageSampler { - /// Default image sampler, derived from the [`ImagePlugin`](super::ImagePlugin) setup. + /// Default image sampler, derived from the `ImagePlugin` setup. #[default] Default, /// Custom sampler for this image which will override global default. @@ -222,14 +223,6 @@ impl ImageSampler { } } -/// A rendering resource for the default image sampler which is set during renderer -/// initialization. -/// -/// The [`ImagePlugin`](super::ImagePlugin) can be set during app initialization to change the default -/// image sampler. -#[derive(Resource, Debug, Clone, Deref, DerefMut)] -pub struct DefaultImageSampler(pub(crate) Sampler); - /// How edges should be handled in texture addressing. /// /// See [`ImageSamplerDescriptor`] for information how to configure this. @@ -323,9 +316,9 @@ pub enum ImageSamplerBorderColor { Zero, } -/// Indicates to an [`ImageLoader`](super::ImageLoader) how an [`Image`] should be sampled. +/// Indicates to an `ImageLoader` how an [`Image`] should be sampled. /// -/// As this type is part of the [`ImageLoaderSettings`](super::ImageLoaderSettings), +/// As this type is part of the `ImageLoaderSettings`, /// it will be serialized to an image asset `.meta` file which might require a migration in case of /// a breaking change. /// @@ -377,7 +370,7 @@ impl Default for ImageSamplerDescriptor { } impl ImageSamplerDescriptor { - /// Returns a sampler descriptor with [`Linear`](crate::render_resource::FilterMode::Linear) min and mag filters + /// Returns a sampler descriptor with [`Linear`](ImageFilterMode::Linear) min and mag filters #[inline] pub fn linear() -> ImageSamplerDescriptor { ImageSamplerDescriptor { @@ -388,7 +381,7 @@ impl ImageSamplerDescriptor { } } - /// Returns a sampler descriptor with [`Nearest`](crate::render_resource::FilterMode::Nearest) min and mag filters + /// Returns a sampler descriptor with [`Nearest`](ImageFilterMode::Nearest) min and mag filters #[inline] pub fn nearest() -> ImageSamplerDescriptor { ImageSamplerDescriptor { @@ -924,75 +917,6 @@ impl TextureFormatPixelInfo for TextureFormat { } } -/// The GPU-representation of an [`Image`]. -/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`], and the texture's size. -#[derive(Debug, Clone)] -pub struct GpuImage { - pub texture: Texture, - pub texture_view: TextureView, - pub texture_format: TextureFormat, - pub sampler: Sampler, - pub size: UVec2, - pub mip_level_count: u32, -} - -impl RenderAsset for GpuImage { - type SourceAsset = Image; - type Param = ( - SRes, - SRes, - SRes, - ); - - #[inline] - fn asset_usage(image: &Self::SourceAsset) -> RenderAssetUsages { - image.asset_usage - } - - #[inline] - fn byte_len(image: &Self::SourceAsset) -> Option { - Some(image.data.len()) - } - - /// Converts the extracted image into a [`GpuImage`]. - fn prepare_asset( - image: Self::SourceAsset, - (render_device, render_queue, default_sampler): &mut SystemParamItem, - ) -> Result> { - let texture = render_device.create_texture_with_data( - render_queue, - &image.texture_descriptor, - // TODO: Is this correct? Do we need to use `MipMajor` if it's a ktx2 file? - wgpu::util::TextureDataOrder::default(), - &image.data, - ); - - let size = image.size(); - let texture_view = texture.create_view( - image - .texture_view_descriptor - .or_else(|| Some(TextureViewDescriptor::default())) - .as_ref() - .unwrap(), - ); - let sampler = match image.sampler { - ImageSampler::Default => (***default_sampler).clone(), - ImageSampler::Descriptor(descriptor) => { - render_device.create_sampler(&descriptor.as_wgpu()) - } - }; - - Ok(GpuImage { - texture, - texture_view, - texture_format: image.texture_descriptor.format, - sampler, - size, - mip_level_count: image.texture_descriptor.mip_level_count, - }) - } -} - bitflags::bitflags! { #[derive(Default, Clone, Copy, Eq, PartialEq, Debug)] #[repr(transparent)] diff --git a/crates/bevy_render/src/texture/image_texture_conversion.rs b/crates/bevy_image/src/image_texture_conversion.rs similarity index 98% rename from crates/bevy_render/src/texture/image_texture_conversion.rs rename to crates/bevy_image/src/image_texture_conversion.rs index 5284c0adcca10b..f3a8a2029adfc4 100644 --- a/crates/bevy_render/src/texture/image_texture_conversion.rs +++ b/crates/bevy_image/src/image_texture_conversion.rs @@ -1,7 +1,5 @@ -use crate::{ - render_asset::RenderAssetUsages, - texture::{Image, TextureFormatPixelInfo}, -}; +use crate::{Image, TextureFormatPixelInfo}; +use bevy_asset::RenderAssetUsages; use image::{DynamicImage, ImageBuffer}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat}; diff --git a/crates/bevy_render/src/texture/ktx2.rs b/crates/bevy_image/src/ktx2.rs similarity index 99% rename from crates/bevy_render/src/texture/ktx2.rs rename to crates/bevy_image/src/ktx2.rs index 8924ea72c45338..3140418263a531 100644 --- a/crates/bevy_render/src/texture/ktx2.rs +++ b/crates/bevy_image/src/ktx2.rs @@ -1496,7 +1496,7 @@ pub fn ktx2_format_to_texture_format( #[cfg(test)] mod tests { - use crate::texture::CompressedImageFormats; + use crate::CompressedImageFormats; use super::ktx2_buffer_to_image; diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs new file mode 100644 index 00000000000000..31665b9645165d --- /dev/null +++ b/crates/bevy_image/src/lib.rs @@ -0,0 +1,28 @@ +// FIXME(15321): solve CI failures, then replace with `#![expect()]`. +#![allow(missing_docs, reason = "Not all docs are written yet, see #3492.")] +#![allow(unsafe_code)] + +mod image; +pub use self::image::*; +#[cfg(feature = "basis-universal")] +mod basis; +#[cfg(feature = "dds")] +mod dds; +#[cfg(feature = "exr")] +mod exr_texture_loader; +#[cfg(feature = "hdr")] +mod hdr_texture_loader; +#[cfg(feature = "ktx2")] +mod ktx2; + +#[cfg(feature = "ktx2")] +pub use self::ktx2::*; +#[cfg(feature = "dds")] +pub use dds::*; +#[cfg(feature = "exr")] +pub use exr_texture_loader::*; +#[cfg(feature = "hdr")] +pub use hdr_texture_loader::*; + +pub(crate) mod image_texture_conversion; +pub use image_texture_conversion::IntoDynamicImageError; diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 41071526800f14..c73c3da310a1e0 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -9,23 +9,30 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -png = ["image/png"] -exr = ["image/exr"] -hdr = ["image/hdr"] -tga = ["image/tga"] -jpeg = ["image/jpeg"] -bmp = ["image/bmp"] -webp = ["image/webp"] -dds = ["ddsfile"] -pnm = ["image/pnm"] +png = ["image/png", "bevy_image/png"] +exr = ["image/exr", "bevy_image/exr"] +hdr = ["image/hdr", "bevy_image/hdr"] +tga = ["image/tga", "bevy_image/tga"] +jpeg = ["image/jpeg", "bevy_image/jpeg"] +bmp = ["image/bmp", "bevy_image/bmp"] +webp = ["image/webp", "bevy_image/webp"] +dds = ["ddsfile", "bevy_image/dds"] +pnm = ["image/pnm", "bevy_image/pnm"] + +ddsfile = ["dep:ddsfile", "bevy_image/ddsfile"] +ktx2 = ["dep:ktx2", "bevy_image/ktx2"] +flate2 = ["dep:flate2", "bevy_image/flate2"] +ruzstd = ["dep:ruzstd", "bevy_image/ruzstd"] +basis-universal = ["dep:basis-universal", "bevy_image/basis-universal"] + multi_threaded = ["bevy_tasks/multi_threaded"] shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out", "naga_oil/glsl"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] # For ktx2 supercompression -zlib = ["flate2"] -zstd = ["ruzstd"] +zlib = ["flate2", "bevy_image/zlib"] +zstd = ["ruzstd", "bevy_image/zstd"] # Enable SPIR-V shader passthrough spirv_shader_passthrough = [] @@ -63,6 +70,7 @@ bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_winit = { path = "../bevy_winit", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } # rendering image = { version = "0.25.2", default-features = false } diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 40a8c1ebb0d5e0..f0ff52f32f2a36 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -60,7 +60,7 @@ pub mod prelude { }, render_resource::Shader, spatial_bundle::SpatialBundle, - texture::{image_texture_conversion::IntoDynamicImageError, Image, ImagePlugin}, + texture::{Image, ImagePlugin, IntoDynamicImageError}, view::{InheritedVisibility, Msaa, ViewVisibility, Visibility, VisibilityBundle}, ExtractSchedule, }; diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 46ee40fd20ef8e..73d23e106cf654 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -2,6 +2,7 @@ use crate::{ render_resource::AsBindGroupError, ExtractSchedule, MainWorld, Render, RenderApp, RenderSet, }; use bevy_app::{App, Plugin, SubApp}; +pub use bevy_asset::RenderAssetUsages; use bevy_asset::{Asset, AssetEvent, AssetId, Assets}; use bevy_ecs::{ prelude::{Commands, EventReader, IntoSystemConfigs, ResMut, Resource}, @@ -9,14 +10,12 @@ use bevy_ecs::{ system::{StaticSystemParam, SystemParam, SystemParamItem, SystemState}, world::{FromWorld, Mut}, }; -use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_render_macros::ExtractResource; use bevy_utils::{ tracing::{debug, error}, HashMap, HashSet, }; use core::marker::PhantomData; -use serde::{Deserialize, Serialize}; use thiserror::Error; #[derive(Debug, Error)] @@ -66,53 +65,6 @@ pub trait RenderAsset: Send + Sync + 'static + Sized { ) -> Result>; } -bitflags::bitflags! { - /// Defines where the asset will be used. - /// - /// If an asset is set to the `RENDER_WORLD` but not the `MAIN_WORLD`, the asset will be - /// unloaded from the asset server once it's been extracted and prepared in the render world. - /// - /// Unloading the asset saves on memory, as for most cases it is no longer necessary to keep - /// it in RAM once it's been uploaded to the GPU's VRAM. However, this means you can no longer - /// access the asset from the CPU (via the `Assets` resource) once unloaded (without re-loading it). - /// - /// If you never need access to the asset from the CPU past the first frame it's loaded on, - /// or only need very infrequent access, then set this to `RENDER_WORLD`. Otherwise, set this to - /// `RENDER_WORLD | MAIN_WORLD`. - /// - /// If you have an asset that doesn't actually need to end up in the render world, like an Image - /// that will be decoded into another Image asset, use `MAIN_WORLD` only. - /// - /// ## Platform-specific - /// - /// On Wasm, it is not possible for now to free reserved memory. To control memory usage, load assets - /// in sequence and unload one before loading the next. See this - /// [discussion about memory management](https://github.com/WebAssembly/design/issues/1397) for more - /// details. - #[repr(transparent)] - #[derive(Serialize, Deserialize, Hash, Clone, Copy, PartialEq, Eq, Debug, Reflect)] - #[reflect(opaque)] - #[reflect(Serialize, Deserialize, Hash, PartialEq, Debug)] - pub struct RenderAssetUsages: u8 { - const MAIN_WORLD = 1 << 0; - const RENDER_WORLD = 1 << 1; - } -} - -impl Default for RenderAssetUsages { - /// Returns the default render asset usage flags: - /// `RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD` - /// - /// This default configuration ensures the asset persists in the main world, even after being prepared for rendering. - /// - /// If your asset does not change, consider using `RenderAssetUsages::RENDER_WORLD` exclusively. This will cause - /// the asset to be unloaded from the main world once it has been prepared for rendering. If the asset does not need - /// to reach the render world at all, use `RenderAssetUsages::MAIN_WORLD` exclusively. - fn default() -> Self { - RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD - } -} - /// This plugin extracts the changed assets from the "app world" into the "render world" /// and prepares them for the GPU. They can then be accessed from the [`RenderAssets`] resource. /// diff --git a/crates/bevy_render/src/render_resource/texture.rs b/crates/bevy_render/src/render_resource/texture.rs index 58398ba8be2a99..ca8d26b085f437 100644 --- a/crates/bevy_render/src/render_resource/texture.rs +++ b/crates/bevy_render/src/render_resource/texture.rs @@ -1,6 +1,8 @@ use crate::define_atomic_id; use crate::renderer::WgpuWrapper; use alloc::sync::Arc; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::system::Resource; use core::ops::Deref; define_atomic_id!(TextureId); @@ -148,3 +150,11 @@ impl Deref for Sampler { &self.value } } + +/// A rendering resource for the default image sampler which is set during renderer +/// initialization. +/// +/// The [`ImagePlugin`](crate::texture::ImagePlugin) can be set during app initialization to change the default +/// image sampler. +#[derive(Resource, Debug, Clone, Deref, DerefMut)] +pub struct DefaultImageSampler(pub(crate) Sampler); diff --git a/crates/bevy_render/src/texture/compressed_image_saver.rs b/crates/bevy_render/src/texture/compressed_image_saver.rs index 3031e310afa603..2923d4b8d1c2d7 100644 --- a/crates/bevy_render/src/texture/compressed_image_saver.rs +++ b/crates/bevy_render/src/texture/compressed_image_saver.rs @@ -1,4 +1,4 @@ -use crate::texture::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; +use super::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; use bevy_asset::saver::{AssetSaver, SavedAsset}; use futures_lite::AsyncWriteExt; use thiserror::Error; diff --git a/crates/bevy_render/src/texture/fallback_image.rs b/crates/bevy_render/src/texture/fallback_image.rs index f8d3c7a6176d7f..ee359f1d3f3bbf 100644 --- a/crates/bevy_render/src/texture/fallback_image.rs +++ b/crates/bevy_render/src/texture/fallback_image.rs @@ -1,17 +1,17 @@ -use crate::{render_asset::RenderAssetUsages, render_resource::*, texture::DefaultImageSampler}; +use crate::{ + render_asset::RenderAssetUsages, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{DefaultImageSampler, GpuImage}, +}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ prelude::{FromWorld, Res, ResMut}, system::{Resource, SystemParam}, }; +use bevy_image::{BevyDefault, Image, ImageSampler, TextureFormatPixelInfo}; use bevy_utils::HashMap; -use crate::{ - prelude::Image, - renderer::{RenderDevice, RenderQueue}, - texture::{image::TextureFormatPixelInfo, BevyDefault, GpuImage, ImageSampler}, -}; - /// A [`RenderApp`](crate::RenderApp) resource that contains the default "fallback image", /// which can be used in situations where an image was not explicitly defined. The most common /// use case is [`AsBindGroup`] implementations (such as materials) that support optional textures. diff --git a/crates/bevy_render/src/texture/gpu_image.rs b/crates/bevy_render/src/texture/gpu_image.rs new file mode 100644 index 00000000000000..2e760054f3e20e --- /dev/null +++ b/crates/bevy_render/src/texture/gpu_image.rs @@ -0,0 +1,78 @@ +use crate::{ + render_asset::{PrepareAssetError, RenderAsset, RenderAssetUsages}, + render_resource::{DefaultImageSampler, Sampler, Texture, TextureView}, + renderer::{RenderDevice, RenderQueue}, +}; +use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; +use bevy_image::{Image, ImageSampler}; +use bevy_math::UVec2; +use wgpu::{TextureFormat, TextureViewDescriptor}; + +/// The GPU-representation of an [`Image`]. +/// Consists of the [`Texture`], its [`TextureView`] and the corresponding [`Sampler`], and the texture's size. +#[derive(Debug, Clone)] +pub struct GpuImage { + pub texture: Texture, + pub texture_view: TextureView, + pub texture_format: TextureFormat, + pub sampler: Sampler, + pub size: UVec2, + pub mip_level_count: u32, +} + +impl RenderAsset for GpuImage { + type SourceAsset = Image; + type Param = ( + SRes, + SRes, + SRes, + ); + + #[inline] + fn asset_usage(image: &Self::SourceAsset) -> RenderAssetUsages { + image.asset_usage + } + + #[inline] + fn byte_len(image: &Self::SourceAsset) -> Option { + Some(image.data.len()) + } + + /// Converts the extracted image into a [`GpuImage`]. + fn prepare_asset( + image: Self::SourceAsset, + (render_device, render_queue, default_sampler): &mut SystemParamItem, + ) -> Result> { + let texture = render_device.create_texture_with_data( + render_queue, + &image.texture_descriptor, + // TODO: Is this correct? Do we need to use `MipMajor` if it's a ktx2 file? + wgpu::util::TextureDataOrder::default(), + &image.data, + ); + + let size = image.size(); + let texture_view = texture.create_view( + image + .texture_view_descriptor + .or_else(|| Some(TextureViewDescriptor::default())) + .as_ref() + .unwrap(), + ); + let sampler = match image.sampler { + ImageSampler::Default => (***default_sampler).clone(), + ImageSampler::Descriptor(descriptor) => { + render_device.create_sampler(&descriptor.as_wgpu()) + } + }; + + Ok(GpuImage { + texture, + texture_view, + texture_format: image.texture_descriptor.format, + sampler, + size, + mip_level_count: image.texture_descriptor.mip_level_count, + }) + } +} diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 566579d7c86e36..1c64dde93f57b2 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -1,37 +1,25 @@ #[cfg(feature = "basis-universal")] -mod basis; -#[cfg(feature = "basis-universal")] mod compressed_image_saver; -#[cfg(feature = "dds")] -mod dds; -#[cfg(feature = "exr")] -mod exr_texture_loader; mod fallback_image; -#[cfg(feature = "hdr")] -mod hdr_texture_loader; -#[allow(clippy::module_inception)] -mod image; +mod gpu_image; mod image_loader; -#[cfg(feature = "ktx2")] -mod ktx2; mod texture_attachment; mod texture_cache; -pub(crate) mod image_texture_conversion; - -pub use self::image::*; -#[cfg(feature = "ktx2")] -pub use self::ktx2::*; -#[cfg(feature = "dds")] -pub use dds::*; +pub use crate::render_resource::DefaultImageSampler; #[cfg(feature = "exr")] -pub use exr_texture_loader::*; +pub use bevy_image::ExrTextureLoader; #[cfg(feature = "hdr")] -pub use hdr_texture_loader::*; - +pub use bevy_image::HdrTextureLoader; +pub use bevy_image::{ + BevyDefault, CompressedImageFormats, Image, ImageAddressMode, ImageFilterMode, ImageFormat, + ImageSampler, ImageSamplerDescriptor, ImageType, IntoDynamicImageError, TextureError, + TextureFormatPixelInfo, +}; #[cfg(feature = "basis-universal")] pub use compressed_image_saver::*; pub use fallback_image::*; +pub use gpu_image::*; pub use image_loader::*; pub use texture_attachment::*; pub use texture_cache::*; @@ -170,13 +158,3 @@ impl Plugin for ImagePlugin { } } } - -pub trait BevyDefault { - fn bevy_default() -> Self; -} - -impl BevyDefault for wgpu::TextureFormat { - fn bevy_default() -> Self { - wgpu::TextureFormat::Rgba8UnormSrgb - } -} From 7eadc1d46780c58ced9bf8ad6d1c99afe5589c34 Mon Sep 17 00:00:00 2001 From: vero Date: Fri, 4 Oct 2024 17:24:44 -0400 Subject: [PATCH 023/546] Zero Copy Mesh (#15569) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective - Another step towards #15558 ## Solution - Instead of allocating a Vec and then having wgpu copy it into a staging buffer, write directly into the staging buffer. - gets rid of another hidden copy, in `pad_to_alignment`. future work: - why is there a gcd implementation in here (and its subpar, use binary_gcd. its in the hot path, run twice for every mesh, every frame i think?) make it better and put it in bevy_math - zero-copy custom mesh api to avoid having to write out a Mesh from a custom rep ## Testing - lighting and many_cubes run fine (and slightly faster. havent benchmarked though) --- ## Showcase - look ma... no copies at least when RenderAssetUsage is GPU only :3 --------- Co-authored-by: Alice Cecile Co-authored-by: Kristoffer Søholm --- crates/bevy_render/src/mesh/allocator.rs | 79 ++++++++++++------------ crates/bevy_render/src/mesh/mesh/mod.rs | 28 +++++++-- 2 files changed, 61 insertions(+), 46 deletions(-) diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index c9d36c5855b513..3493f7dcf17226 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -1,9 +1,8 @@ //! Manages mesh vertex and index buffers. -use alloc::{borrow::Cow, vec::Vec}; +use alloc::vec::Vec; use core::{ fmt::{self, Display, Formatter}, - iter, ops::Range, }; @@ -21,8 +20,8 @@ use bevy_utils::{ }; use offset_allocator::{Allocation, Allocator}; use wgpu::{ - util::BufferInitDescriptor, BufferDescriptor, BufferUsages, CommandEncoderDescriptor, - DownlevelFlags, COPY_BUFFER_ALIGNMENT, + BufferDescriptor, BufferSize, BufferUsages, CommandEncoderDescriptor, DownlevelFlags, + COPY_BUFFER_ALIGNMENT, }; use crate::{ @@ -427,7 +426,7 @@ impl MeshAllocator { if self.general_vertex_slabs_supported { self.allocate( mesh_id, - mesh.get_vertex_size() * mesh.count_vertices() as u64, + mesh.get_vertex_buffer_size() as u64, vertex_element_layout, &mut slabs_to_grow, mesh_allocator_settings, @@ -474,12 +473,12 @@ impl MeshAllocator { let Some(&slab_id) = self.mesh_id_to_vertex_slab.get(mesh_id) else { return; }; - let vertex_data = mesh.create_packed_vertex_buffer_data(); // Call the generic function. self.copy_element_data( mesh_id, - &vertex_data, + mesh.get_vertex_buffer_size(), + |slice| mesh.write_packed_vertex_buffer_data(slice), BufferUsages::VERTEX, slab_id, render_device, @@ -506,7 +505,8 @@ impl MeshAllocator { // Call the generic function. self.copy_element_data( mesh_id, - index_data, + index_data.len(), + |slice| slice.copy_from_slice(index_data), BufferUsages::INDEX, slab_id, render_device, @@ -519,7 +519,8 @@ impl MeshAllocator { fn copy_element_data( &mut self, mesh_id: &AssetId, - data: &[u8], + len: usize, + fill_data: impl Fn(&mut [u8]), buffer_usages: BufferUsages, slab_id: SlabId, render_device: &RenderDevice, @@ -540,12 +541,18 @@ impl MeshAllocator { let slot_size = general_slab.element_layout.slot_size(); - // Write the data in. - render_queue.write_buffer( - buffer, - allocated_range.allocation.offset as u64 * slot_size, - &pad_to_alignment(data, slot_size as usize), - ); + // round up size to a multiple of the slot size to satisfy wgpu alignment requirements + if let Some(size) = BufferSize::new((len as u64).next_multiple_of(slot_size)) { + // Write the data in. + if let Some(mut buffer) = render_queue.write_buffer_with( + buffer, + allocated_range.allocation.offset as u64 * slot_size, + size, + ) { + let slice = &mut buffer.as_mut()[..len]; + fill_data(slice); + } + } // Mark the allocation as resident. general_slab @@ -557,17 +564,22 @@ impl MeshAllocator { debug_assert!(large_object_slab.buffer.is_none()); // Create the buffer and its data in one go. - large_object_slab.buffer = Some(render_device.create_buffer_with_data( - &BufferInitDescriptor { - label: Some(&format!( - "large mesh slab {} ({}buffer)", - slab_id, - buffer_usages_to_str(buffer_usages) - )), - contents: data, - usage: buffer_usages | BufferUsages::COPY_DST, - }, - )); + let buffer = render_device.create_buffer(&BufferDescriptor { + label: Some(&format!( + "large mesh slab {} ({}buffer)", + slab_id, + buffer_usages_to_str(buffer_usages) + )), + size: len as u64, + usage: buffer_usages | BufferUsages::COPY_DST, + mapped_at_creation: true, + }); + { + let slice = &mut buffer.slice(..).get_mapped_range_mut()[..len]; + fill_data(slice); + } + buffer.unmap(); + large_object_slab.buffer = Some(buffer); } } } @@ -1000,21 +1012,6 @@ fn gcd(mut a: u64, mut b: u64) -> u64 { a } -/// Ensures that the size of a buffer is a multiple of the given alignment by -/// padding it with zeroes if necessary. -/// -/// If the buffer already has the required size, then this function doesn't -/// allocate. Otherwise, it copies the buffer into a new one and writes the -/// appropriate number of zeroes to the end. -fn pad_to_alignment(buffer: &[u8], align: usize) -> Cow<[u8]> { - if buffer.len() % align == 0 { - return Cow::Borrowed(buffer); - } - let mut buffer = buffer.to_vec(); - buffer.extend(iter::repeat(0).take(align - buffer.len() % align)); - Cow::Owned(buffer) -} - /// Returns a string describing the given buffer usages. fn buffer_usages_to_str(buffer_usages: BufferUsages) -> &'static str { if buffer_usages.contains(BufferUsages::VERTEX) { diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index 1f40cb8b41a4c4..6e6fd3f072f0c5 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -385,6 +385,13 @@ impl Mesh { .sum() } + /// Returns the size required for the vertex buffer in bytes. + pub fn get_vertex_buffer_size(&self) -> usize { + let vertex_size = self.get_vertex_size() as usize; + let vertex_count = self.count_vertices(); + vertex_count * vertex_size + } + /// Computes and returns the index data of the mesh as bytes. /// This is used to transform the index data into a GPU friendly format. pub fn get_index_buffer_bytes(&self) -> Option<&[u8]> { @@ -458,10 +465,24 @@ impl Mesh { /// /// If the vertex attributes have different lengths, they are all truncated to /// the length of the smallest. + /// + /// This is a convenience method which allocates a Vec. + /// Prefer pre-allocating and using [`Mesh::write_packed_vertex_buffer_data`] when possible. pub fn create_packed_vertex_buffer_data(&self) -> Vec { + let mut attributes_interleaved_buffer = vec![0; self.get_vertex_buffer_size()]; + self.write_packed_vertex_buffer_data(&mut attributes_interleaved_buffer); + attributes_interleaved_buffer + } + + /// Computes and write the vertex data of the mesh into a mutable byte slice. + /// The attributes are located in the order of their [`MeshVertexAttribute::id`]. + /// This is used to transform the vertex data into a GPU friendly format. + /// + /// If the vertex attributes have different lengths, they are all truncated to + /// the length of the smallest. + pub fn write_packed_vertex_buffer_data(&self, slice: &mut [u8]) { let vertex_size = self.get_vertex_size() as usize; let vertex_count = self.count_vertices(); - let mut attributes_interleaved_buffer = vec![0; vertex_count * vertex_size]; // bundle into interleaved buffers let mut attribute_offset = 0; for attribute_data in self.attributes.values() { @@ -473,14 +494,11 @@ impl Mesh { .enumerate() { let offset = vertex_index * vertex_size + attribute_offset; - attributes_interleaved_buffer[offset..offset + attribute_size] - .copy_from_slice(attribute_bytes); + slice[offset..offset + attribute_size].copy_from_slice(attribute_bytes); } attribute_offset += attribute_size; } - - attributes_interleaved_buffer } /// Duplicates the vertex attributes so that no vertices are shared. From 0094bcbc0716b819a13eef0888e615a0f6fcdeeb Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Fri, 4 Oct 2024 15:13:22 -0700 Subject: [PATCH 024/546] Implement additive blending for animation graphs. (#15631) *Additive blending* is an ubiquitous feature in game engines that allows animations to be concatenated instead of blended. The canonical use case is to allow a character to hold a weapon while performing arbitrary poses. For example, if you had a character that needed to be able to walk or run while attacking with a weapon, the typical workflow is to have an additive blend node that combines walking and running animation clips with an animation clip of one of the limbs performing a weapon attack animation. This commit adds support for additive blending to Bevy. It builds on top of the flexible infrastructure in #15589 and introduces a new type of node, the *add node*. Like blend nodes, add nodes combine the animations of their children according to their weights. Unlike blend nodes, however, add nodes don't normalize the weights to 1.0. The `animation_masks` example has been overhauled to demonstrate the use of additive blending in combination with masks. There are now controls to choose an animation clip for every limb of the fox individually. This patch also fixes a bug whereby masks were incorrectly accumulated with `insert()` during the graph threading phase, which could cause corruption of computed masks in some cases. Note that the `clip` field has been replaced with an `AnimationNodeType` enum, which breaks `animgraph.ron` files. The `Fox.animgraph.ron` asset has been updated to the new format. Closes #14395. ## Showcase https://github.com/user-attachments/assets/52dfe05f-fdb3-477a-9462-ec150f93df33 ## Migration Guide * The `animgraph.ron` format has changed to accommodate the new *additive blending* feature. You'll need to change `clip` fields to instances of the new `AnimationNodeType` enum. --- assets/animation_graphs/Fox.animgraph.ron | 12 +- crates/bevy_animation/src/animation_curves.rs | 111 +++++- crates/bevy_animation/src/graph.rs | 169 +++++++-- crates/bevy_animation/src/lib.rs | 11 +- examples/animation/animation_masks.rs | 343 +++++++++++++----- 5 files changed, 488 insertions(+), 158 deletions(-) diff --git a/assets/animation_graphs/Fox.animgraph.ron b/assets/animation_graphs/Fox.animgraph.ron index cf87b1400e3b21..e9d6f4f9cf19c0 100644 --- a/assets/animation_graphs/Fox.animgraph.ron +++ b/assets/animation_graphs/Fox.animgraph.ron @@ -2,27 +2,27 @@ graph: ( nodes: [ ( - clip: None, + node_type: Blend, mask: 0, weight: 1.0, ), ( - clip: None, + node_type: Blend, mask: 0, - weight: 0.5, + weight: 1.0, ), ( - clip: Some(AssetPath("models/animated/Fox.glb#Animation0")), + node_type: Clip(AssetPath("models/animated/Fox.glb#Animation0")), mask: 0, weight: 1.0, ), ( - clip: Some(AssetPath("models/animated/Fox.glb#Animation1")), + node_type: Clip(AssetPath("models/animated/Fox.glb#Animation1")), mask: 0, weight: 1.0, ), ( - clip: Some(AssetPath("models/animated/Fox.glb#Animation2")), + node_type: Clip(AssetPath("models/animated/Fox.glb#Animation2")), mask: 0, weight: 1.0, ), diff --git a/crates/bevy_animation/src/animation_curves.rs b/crates/bevy_animation/src/animation_curves.rs index c0101ad5b8f773..e4a6e46734e147 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -96,7 +96,9 @@ use bevy_render::mesh::morph::MorphWeights; use bevy_transform::prelude::Transform; use crate::{ - graph::AnimationNodeIndex, prelude::Animatable, AnimationEntityMut, AnimationEvaluationError, + graph::AnimationNodeIndex, + prelude::{Animatable, BlendInput}, + AnimationEntityMut, AnimationEvaluationError, }; /// A value on a component that Bevy can animate. @@ -297,7 +299,11 @@ where P: AnimatableProperty, { fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.blend(graph_node) + self.evaluator.combine(graph_node, /*additive=*/ false) + } + + fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.combine(graph_node, /*additive=*/ true) } fn push_blend_register( @@ -393,7 +399,11 @@ where impl AnimationCurveEvaluator for TranslationCurveEvaluator { fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.blend(graph_node) + self.evaluator.combine(graph_node, /*additive=*/ false) + } + + fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.combine(graph_node, /*additive=*/ true) } fn push_blend_register( @@ -487,7 +497,11 @@ where impl AnimationCurveEvaluator for RotationCurveEvaluator { fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.blend(graph_node) + self.evaluator.combine(graph_node, /*additive=*/ false) + } + + fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.combine(graph_node, /*additive=*/ true) } fn push_blend_register( @@ -581,7 +595,11 @@ where impl AnimationCurveEvaluator for ScaleCurveEvaluator { fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { - self.evaluator.blend(graph_node) + self.evaluator.combine(graph_node, /*additive=*/ false) + } + + fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.combine(graph_node, /*additive=*/ true) } fn push_blend_register( @@ -708,8 +726,12 @@ where } } -impl AnimationCurveEvaluator for WeightsCurveEvaluator { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { +impl WeightsCurveEvaluator { + fn combine( + &mut self, + graph_node: AnimationNodeIndex, + additive: bool, + ) -> Result<(), AnimationEvaluationError> { let Some(&(_, top_graph_node)) = self.stack_blend_weights_and_graph_nodes.last() else { return Ok(()); }; @@ -736,13 +758,27 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { .iter_mut() .zip(stack_iter) { - *dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight); + if additive { + *dest += src * weight_to_blend; + } else { + *dest = f32::interpolate(dest, &src, weight_to_blend / *current_weight); + } } } } Ok(()) } +} + +impl AnimationCurveEvaluator for WeightsCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.combine(graph_node, /*additive=*/ false) + } + + fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.combine(graph_node, /*additive=*/ true) + } fn push_blend_register( &mut self, @@ -826,7 +862,11 @@ impl BasicAnimationCurveEvaluator where A: Animatable, { - fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + fn combine( + &mut self, + graph_node: AnimationNodeIndex, + additive: bool, + ) -> Result<(), AnimationEvaluationError> { let Some(top) = self.stack.last() else { return Ok(()); }; @@ -840,15 +880,36 @@ where graph_node: _, } = self.stack.pop().unwrap(); - match self.blend_register { + match self.blend_register.take() { None => self.blend_register = Some((value_to_blend, weight_to_blend)), - Some((ref mut current_value, ref mut current_weight)) => { - *current_weight += weight_to_blend; - *current_value = A::interpolate( - current_value, - &value_to_blend, - weight_to_blend / *current_weight, - ); + Some((mut current_value, mut current_weight)) => { + current_weight += weight_to_blend; + + if additive { + current_value = A::blend( + [ + BlendInput { + weight: 1.0, + value: current_value, + additive: true, + }, + BlendInput { + weight: weight_to_blend, + value: value_to_blend, + additive: true, + }, + ] + .into_iter(), + ); + } else { + current_value = A::interpolate( + ¤t_value, + &value_to_blend, + weight_to_blend / current_weight, + ); + } + + self.blend_register = Some((current_value, current_weight)); } } @@ -967,6 +1028,22 @@ pub trait AnimationCurveEvaluator: Reflect { /// 4. Return success. fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>; + /// Additively blends the top element of the stack with the blend register. + /// + /// The semantics of this method are as follows: + /// + /// 1. Pop the top element of the stack. Call its value vₘ and its weight + /// wₘ. If the stack was empty, return success. + /// + /// 2. If the blend register is empty, set the blend register value to vₘ + /// and the blend register weight to wₘ; then, return success. + /// + /// 3. If the blend register is nonempty, call its current value vₙ. + /// Then, set the value of the blend register to vₙ + vₘwₘ. + /// + /// 4. Return success. + fn add(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError>; + /// Pushes the current value of the blend register onto the stack. /// /// If the blend register is empty, this method does nothing successfully. diff --git a/crates/bevy_animation/src/graph.rs b/crates/bevy_animation/src/graph.rs index 22c0e1a60842c8..6121ecb2ab8a26 100644 --- a/crates/bevy_animation/src/graph.rs +++ b/crates/bevy_animation/src/graph.rs @@ -35,11 +35,12 @@ use crate::{AnimationClip, AnimationTargetId}; /// the root and blends the animations together in a bottom-up fashion to /// produce the final pose. /// -/// There are two types of nodes: *blend nodes* and *clip nodes*, both of which -/// can have an associated weight. Blend nodes have no associated animation clip -/// and simply affect the weights of all their descendant nodes. Clip nodes -/// specify an animation clip to play. When a graph is created, it starts with -/// only a single blend node, the root node. +/// There are three types of nodes: *blend nodes*, *add nodes*, and *clip +/// nodes*, all of which can have an associated weight. Blend nodes and add +/// nodes have no associated animation clip and combine the animations of their +/// children according to those children's weights. Clip nodes specify an +/// animation clip to play. When a graph is created, it starts with only a +/// single blend node, the root node. /// /// For example, consider the following graph: /// @@ -133,16 +134,19 @@ pub type AnimationNodeIndex = NodeIndex; /// An individual node within an animation graph. /// -/// If `clip` is present, this is a *clip node*. Otherwise, it's a *blend node*. -/// Both clip and blend nodes can have weights, and those weights are propagated -/// down to descendants. +/// The [`AnimationGraphNode::node_type`] field specifies the type of node: one +/// of a *clip node*, a *blend node*, or an *add node*. Clip nodes, the leaves +/// of the graph, contain animation clips to play. Blend and add nodes describe +/// how to combine their children to produce a final animation. The difference +/// between blend nodes and add nodes is that blend nodes normalize the weights +/// of their children to 1.0, while add nodes don't. #[derive(Clone, Reflect, Debug)] pub struct AnimationGraphNode { - /// The animation clip associated with this node, if any. + /// Animation node data specific to the type of node (clip, blend, or add). /// - /// If the clip is present, this node is an *animation clip node*. - /// Otherwise, this node is a *blend node*. - pub clip: Option>, + /// In the case of clip nodes, this contains the actual animation clip + /// associated with the node. + pub node_type: AnimationNodeType, /// A bitfield specifying the mask groups that this node and its descendants /// will not affect. @@ -155,11 +159,42 @@ pub struct AnimationGraphNode { /// The weight of this node. /// /// Weights are propagated down to descendants. Thus if an animation clip - /// has weight 0.3 and its parent blend node has weight 0.6, the computed - /// weight of the animation clip is 0.18. + /// has weight 0.3 and its parent blend node has effective weight 0.6, the + /// computed weight of the animation clip is 0.18. pub weight: f32, } +/// Animation node data specific to the type of node (clip, blend, or add). +/// +/// In the case of clip nodes, this contains the actual animation clip +/// associated with the node. +#[derive(Clone, Default, Reflect, Debug)] +pub enum AnimationNodeType { + /// A *clip node*, which plays an animation clip. + /// + /// These are always the leaves of the graph. + Clip(Handle), + + /// A *blend node*, which blends its children according to their weights. + /// + /// The weights of all the children of this node are normalized to 1.0. + #[default] + Blend, + + /// An *additive blend node*, which combines the animations of its children, + /// scaled by their weights. + /// + /// The weights of all the children of this node are *not* normalized to + /// 1.0. + /// + /// Add nodes are primarily useful for superimposing an animation for a + /// portion of a rig on top of the main animation. For example, an add node + /// could superimpose a weapon attack animation for a character's limb on + /// top of a running animation to produce an animation of a character + /// attacking while running. + Add, +} + /// An [`AssetLoader`] that can load [`AnimationGraph`]s as assets. /// /// The canonical extension for [`AnimationGraph`]s is `.animgraph.ron`. Plain @@ -300,14 +335,26 @@ pub struct SerializedAnimationGraph { /// See the comments in [`SerializedAnimationGraph`] for more information. #[derive(Serialize, Deserialize)] pub struct SerializedAnimationGraphNode { - /// Corresponds to the `clip` field on [`AnimationGraphNode`]. - pub clip: Option, + /// Corresponds to the `node_type` field on [`AnimationGraphNode`]. + pub node_type: SerializedAnimationNodeType, /// Corresponds to the `mask` field on [`AnimationGraphNode`]. pub mask: AnimationMask, /// Corresponds to the `weight` field on [`AnimationGraphNode`]. pub weight: f32, } +/// A version of [`AnimationNodeType`] suitable for serializing as part of a +/// [`SerializedAnimationGraphNode`] asset. +#[derive(Serialize, Deserialize)] +pub enum SerializedAnimationNodeType { + /// Corresponds to [`AnimationNodeType::Clip`]. + Clip(SerializedAnimationClip), + /// Corresponds to [`AnimationNodeType::Blend`]. + Blend, + /// Corresponds to [`AnimationNodeType::Add`]. + Add, +} + /// A version of `Handle` suitable for serializing as an asset. /// /// This replaces any handle that has a path with an [`AssetPath`]. Failing @@ -383,7 +430,7 @@ impl AnimationGraph { parent: AnimationNodeIndex, ) -> AnimationNodeIndex { let node_index = self.graph.add_node(AnimationGraphNode { - clip: Some(clip), + node_type: AnimationNodeType::Clip(clip), mask: 0, weight, }); @@ -403,7 +450,7 @@ impl AnimationGraph { parent: AnimationNodeIndex, ) -> AnimationNodeIndex { let node_index = self.graph.add_node(AnimationGraphNode { - clip: Some(clip), + node_type: AnimationNodeType::Clip(clip), mask, weight, }); @@ -442,7 +489,7 @@ impl AnimationGraph { /// no mask. pub fn add_blend(&mut self, weight: f32, parent: AnimationNodeIndex) -> AnimationNodeIndex { let node_index = self.graph.add_node(AnimationGraphNode { - clip: None, + node_type: AnimationNodeType::Blend, mask: 0, weight, }); @@ -465,7 +512,51 @@ impl AnimationGraph { parent: AnimationNodeIndex, ) -> AnimationNodeIndex { let node_index = self.graph.add_node(AnimationGraphNode { - clip: None, + node_type: AnimationNodeType::Blend, + mask, + weight, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds a blend node to the animation graph with the given weight and + /// returns its index. + /// + /// The blend node will be placed under the supplied `parent` node. During + /// animation evaluation, the descendants of this blend node will have their + /// weights multiplied by the weight of the blend. The blend node will have + /// no mask. + pub fn add_additive_blend( + &mut self, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + node_type: AnimationNodeType::Add, + mask: 0, + weight, + }); + self.graph.add_edge(parent, node_index, ()); + node_index + } + + /// Adds a blend node to the animation graph with the given weight and + /// returns its index. + /// + /// The blend node will be placed under the supplied `parent` node. During + /// animation evaluation, the descendants of this blend node will have their + /// weights multiplied by the weight of the blend. Neither this node nor its + /// descendants will affect animation targets that belong to mask groups not + /// in the given `mask`. + pub fn add_additive_blend_with_mask( + &mut self, + mask: AnimationMask, + weight: f32, + parent: AnimationNodeIndex, + ) -> AnimationNodeIndex { + let node_index = self.graph.add_node(AnimationGraphNode { + node_type: AnimationNodeType::Add, mask, weight, }); @@ -592,7 +683,7 @@ impl IndexMut for AnimationGraph { impl Default for AnimationGraphNode { fn default() -> Self { Self { - clip: None, + node_type: Default::default(), mask: 0, weight: 1.0, } @@ -632,12 +723,18 @@ impl AssetLoader for AnimationGraphAssetLoader { Ok(AnimationGraph { graph: serialized_animation_graph.graph.map( |_, serialized_node| AnimationGraphNode { - clip: serialized_node.clip.as_ref().map(|clip| match clip { - SerializedAnimationClip::AssetId(asset_id) => Handle::Weak(*asset_id), - SerializedAnimationClip::AssetPath(asset_path) => { - load_context.load(asset_path) - } - }), + node_type: match serialized_node.node_type { + SerializedAnimationNodeType::Clip(ref clip) => match clip { + SerializedAnimationClip::AssetId(asset_id) => { + AnimationNodeType::Clip(Handle::Weak(*asset_id)) + } + SerializedAnimationClip::AssetPath(asset_path) => { + AnimationNodeType::Clip(load_context.load(asset_path)) + } + }, + SerializedAnimationNodeType::Blend => AnimationNodeType::Blend, + SerializedAnimationNodeType::Add => AnimationNodeType::Add, + }, mask: serialized_node.mask, weight: serialized_node.weight, }, @@ -663,10 +760,18 @@ impl From for SerializedAnimationGraph { |_, node| SerializedAnimationGraphNode { weight: node.weight, mask: node.mask, - clip: node.clip.as_ref().map(|clip| match clip.path() { - Some(path) => SerializedAnimationClip::AssetPath(path.clone()), - None => SerializedAnimationClip::AssetId(clip.id()), - }), + node_type: match node.node_type { + AnimationNodeType::Clip(ref clip) => match clip.path() { + Some(path) => SerializedAnimationNodeType::Clip( + SerializedAnimationClip::AssetPath(path.clone()), + ), + None => SerializedAnimationNodeType::Clip( + SerializedAnimationClip::AssetId(clip.id()), + ), + }, + AnimationNodeType::Blend => SerializedAnimationNodeType::Blend, + AnimationNodeType::Add => SerializedAnimationNodeType::Add, + }, }, |_, _| (), ), @@ -762,7 +867,7 @@ impl ThreadedAnimationGraph { ) { // Accumulate the mask. mask |= graph.node_weight(node_index).unwrap().mask; - self.computed_masks.insert(node_index.index(), mask); + self.computed_masks[node_index.index()] = mask; // Gather up the indices of our children, and sort them. let mut kids: SmallVec<[AnimationNodeIndex; 8]> = graph diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 529377cab70978..f4b24807c1c9d4 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -23,6 +23,7 @@ use core::{ hash::{Hash, Hasher}, iter, }; +use graph::AnimationNodeType; use prelude::AnimationCurveEvaluator; use crate::graph::ThreadedAnimationGraphs; @@ -478,8 +479,6 @@ pub enum AnimationEvaluationError { pub struct ActiveAnimation { /// The factor by which the weight from the [`AnimationGraph`] is multiplied. weight: f32, - /// The mask groups that are masked out (i.e. won't be animated) this frame, - /// taking the `AnimationGraph` into account. repeat: RepeatAnimation, speed: f32, /// Total time the animation has been played. @@ -868,7 +867,7 @@ pub fn advance_animations( if let Some(active_animation) = active_animations.get_mut(&node_index) { // Tick the animation if necessary. if !active_animation.paused { - if let Some(ref clip_handle) = node.clip { + if let AnimationNodeType::Clip(ref clip_handle) = node.node_type { if let Some(clip) = animation_clips.get(clip_handle) { active_animation.update(delta_seconds, clip.duration); } @@ -951,8 +950,8 @@ pub fn animate_targets( continue; }; - match animation_graph_node.clip { - None => { + match animation_graph_node.node_type { + AnimationNodeType::Blend | AnimationNodeType::Add => { // This is a blend node. for edge_index in threaded_animation_graph.sorted_edge_ranges [animation_graph_node_index.index()] @@ -973,7 +972,7 @@ pub fn animate_targets( } } - Some(ref animation_clip_handle) => { + AnimationNodeType::Clip(ref animation_clip_handle) => { // This is a clip node. let Some(active_animation) = animation_player .active_animations diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs index 203fb8ce9c0ed4..4a9177074dd062 100644 --- a/examples/animation/animation_masks.rs +++ b/examples/animation/animation_masks.rs @@ -1,23 +1,26 @@ //! Demonstrates how to use masks to limit the scope of animations. -use bevy::{animation::AnimationTargetId, color::palettes::css::WHITE, prelude::*}; +use bevy::{ + animation::{AnimationTarget, AnimationTargetId}, + color::palettes::css::{LIGHT_GRAY, WHITE}, + prelude::*, + utils::hashbrown::HashSet, +}; // IDs of the mask groups we define for the running fox model. // // Each mask group defines a set of bones for which animations can be toggled on // and off. -const MASK_GROUP_LEFT_FRONT_LEG: u32 = 0; -const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 1; -const MASK_GROUP_LEFT_HIND_LEG: u32 = 2; -const MASK_GROUP_RIGHT_HIND_LEG: u32 = 3; -const MASK_GROUP_TAIL: u32 = 4; +const MASK_GROUP_HEAD: u32 = 0; +const MASK_GROUP_LEFT_FRONT_LEG: u32 = 1; +const MASK_GROUP_RIGHT_FRONT_LEG: u32 = 2; +const MASK_GROUP_LEFT_HIND_LEG: u32 = 3; +const MASK_GROUP_RIGHT_HIND_LEG: u32 = 4; +const MASK_GROUP_TAIL: u32 = 5; // The width in pixels of the small buttons that allow the user to toggle a mask // group on or off. -const MASK_GROUP_SMALL_BUTTON_WIDTH: f32 = 150.0; - -// The ID of the animation in the glTF file that we're going to play. -const FOX_RUN_ANIMATION: usize = 2; +const MASK_GROUP_BUTTON_WIDTH: f32 = 250.0; // The names of the bones that each mask group consists of. Each mask group is // defined as a (prefix, suffix) tuple. The mask group consists of a single @@ -25,11 +28,16 @@ const FOX_RUN_ANIMATION: usize = 2; // "A/B/C" and the suffix is "D/E", then the bones that will be included in the // mask group are "A/B/C", "A/B/C/D", and "A/B/C/D/E". // -// The fact that our mask groups are single chains of bones isn't anything -// specific to Bevy; it just so happens to be the case for the model we're -// using. A mask group can consist of any set of animation targets, regardless -// of whether they form a single chain. -const MASK_GROUP_PATHS: [(&str, &str); 5] = [ +// The fact that our mask groups are single chains of bones isn't an engine +// requirement; it just so happens to be the case for the model we're using. A +// mask group can consist of any set of animation targets, regardless of whether +// they form a single chain. +const MASK_GROUP_PATHS: [(&str, &str); 6] = [ + // Head + ( + "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03", + "b_Neck_04/b_Head_05", + ), // Left front leg ( "root/_rootJoint/b_Root_00/b_Hip_01/b_Spine01_02/b_Spine02_03/b_LeftUpperArm_09", @@ -57,19 +65,30 @@ const MASK_GROUP_PATHS: [(&str, &str); 5] = [ ), ]; -// A component that identifies a clickable button that allows the user to toggle -// a mask group on or off. -#[derive(Component)] -struct MaskGroupControl { +#[derive(Clone, Copy, Component)] +struct AnimationControl { // The ID of the mask group that this button controls. group_id: u32, + label: AnimationLabel, +} + +#[derive(Clone, Copy, Component, PartialEq, Debug)] +enum AnimationLabel { + Idle = 0, + Walk = 1, + Run = 2, + Off = 3, +} + +#[derive(Clone, Debug, Resource)] +struct AnimationNodes([AnimationNodeIndex; 3]); + +#[derive(Clone, Copy, Debug, Resource)] +struct AppState([MaskGroupState; 6]); - // Whether animations are playing for this mask group. - // - // Note that this is the opposite of the `mask` field in `AnimationGraph`: - // i.e. it's true if the group is *not* presently masked, and false if the - // group *is* masked. - enabled: bool, +#[derive(Clone, Copy, Debug)] +struct MaskGroupState { + clip: u8, } // The application entry point. @@ -85,10 +104,12 @@ fn main() { .add_systems(Startup, (setup_scene, setup_ui)) .add_systems(Update, setup_animation_graph_once_loaded) .add_systems(Update, handle_button_toggles) + .add_systems(Update, update_ui) .insert_resource(AmbientLight { color: WHITE.into(), brightness: 100.0, }) + .init_resource::() .run(); } @@ -169,6 +190,8 @@ fn setup_ui(mut commands: Commands) { ..default() }; + add_mask_group_control(parent, "Head", Val::Auto, MASK_GROUP_HEAD); + parent .spawn(NodeBundle { style: row_style.clone(), @@ -178,13 +201,13 @@ fn setup_ui(mut commands: Commands) { add_mask_group_control( parent, "Left Front Leg", - Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + Val::Px(MASK_GROUP_BUTTON_WIDTH), MASK_GROUP_LEFT_FRONT_LEG, ); add_mask_group_control( parent, "Right Front Leg", - Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + Val::Px(MASK_GROUP_BUTTON_WIDTH), MASK_GROUP_RIGHT_FRONT_LEG, ); }); @@ -198,13 +221,13 @@ fn setup_ui(mut commands: Commands) { add_mask_group_control( parent, "Left Hind Leg", - Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + Val::Px(MASK_GROUP_BUTTON_WIDTH), MASK_GROUP_LEFT_HIND_LEG, ); add_mask_group_control( parent, "Right Hind Leg", - Val::Px(MASK_GROUP_SMALL_BUTTON_WIDTH), + Val::Px(MASK_GROUP_BUTTON_WIDTH), MASK_GROUP_RIGHT_HIND_LEG, ); }); @@ -218,34 +241,129 @@ fn setup_ui(mut commands: Commands) { // The button will automatically become a child of the parent that owns the // given `ChildBuilder`. fn add_mask_group_control(parent: &mut ChildBuilder, label: &str, width: Val, mask_group_id: u32) { + let button_text_style = TextStyle { + font_size: 14.0, + color: Color::WHITE, + ..default() + }; + let selected_button_text_style = TextStyle { + color: Color::BLACK, + ..button_text_style.clone() + }; + let label_text_style = TextStyle { + color: Color::Srgba(LIGHT_GRAY), + ..button_text_style.clone() + }; + parent - .spawn(ButtonBundle { + .spawn(NodeBundle { style: Style { border: UiRect::all(Val::Px(1.0)), width, + flex_direction: FlexDirection::Column, justify_content: JustifyContent::Center, align_items: AlignItems::Center, - padding: UiRect::all(Val::Px(6.0)), + padding: UiRect::ZERO, margin: UiRect::ZERO, ..default() }, border_color: BorderColor(Color::WHITE), border_radius: BorderRadius::all(Val::Px(3.0)), - background_color: Color::WHITE.into(), + background_color: Color::BLACK.into(), ..default() }) - .insert(MaskGroupControl { - group_id: mask_group_id, - enabled: true, - }) - .with_child(TextBundle::from_section( - label, - TextStyle { - font_size: 14.0, - color: Color::BLACK, - ..default() - }, - )); + .with_children(|builder| { + builder + .spawn(NodeBundle { + style: Style { + border: UiRect::ZERO, + width: Val::Percent(100.0), + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + padding: UiRect::ZERO, + margin: UiRect::ZERO, + ..default() + }, + background_color: Color::BLACK.into(), + ..default() + }) + .with_child(TextBundle { + text: Text::from_section(label, label_text_style.clone()), + style: Style { + margin: UiRect::vertical(Val::Px(3.0)), + ..default() + }, + ..default() + }); + + builder + .spawn(NodeBundle { + style: Style { + width: Val::Percent(100.0), + flex_direction: FlexDirection::Row, + justify_content: JustifyContent::Center, + align_items: AlignItems::Center, + border: UiRect::top(Val::Px(1.0)), + ..default() + }, + border_color: BorderColor(Color::WHITE), + ..default() + }) + .with_children(|builder| { + for (index, label) in [ + AnimationLabel::Run, + AnimationLabel::Walk, + AnimationLabel::Idle, + AnimationLabel::Off, + ] + .iter() + .enumerate() + { + builder + .spawn(ButtonBundle { + background_color: if index > 0 { + Color::BLACK.into() + } else { + Color::WHITE.into() + }, + style: Style { + flex_grow: 1.0, + border: if index > 0 { + UiRect::left(Val::Px(1.0)) + } else { + UiRect::ZERO + }, + ..default() + }, + border_color: BorderColor(Color::WHITE), + ..default() + }) + .with_child( + TextBundle { + style: Style { + flex_grow: 1.0, + margin: UiRect::vertical(Val::Px(3.0)), + ..default() + }, + text: Text::from_section( + format!("{:?}", label), + if index > 0 { + button_text_style.clone() + } else { + selected_button_text_style.clone() + }, + ), + ..default() + } + .with_text_justify(JustifyText::Center), + ) + .insert(AnimationControl { + group_id: mask_group_id, + label: *label, + }); + } + }); + }); } // Builds up the animation graph, including the mask groups, and adds it to the @@ -255,14 +373,25 @@ fn setup_animation_graph_once_loaded( asset_server: Res, mut animation_graphs: ResMut>, mut players: Query<(Entity, &mut AnimationPlayer), Added>, + targets: Query<(Entity, &AnimationTarget)>, ) { for (entity, mut player) in &mut players { // Load the animation clip from the glTF file. - let (mut animation_graph, node_index) = AnimationGraph::from_clip(asset_server.load( - GltfAssetLabel::Animation(FOX_RUN_ANIMATION).from_asset("models/animated/Fox.glb"), - )); + let mut animation_graph = AnimationGraph::new(); + let blend_node = animation_graph.add_additive_blend(1.0, animation_graph.root); + + let animation_graph_nodes: [AnimationNodeIndex; 3] = + std::array::from_fn(|animation_index| { + let handle = asset_server.load( + GltfAssetLabel::Animation(animation_index) + .from_asset("models/animated/Fox.glb"), + ); + let mask = if animation_index == 0 { 0 } else { 0x3f }; + animation_graph.add_clip_with_mask(handle, mask, 0.0, blend_node) + }); // Create each mask group. + let mut all_animation_target_ids = HashSet::new(); for (mask_group_index, (mask_group_prefix, mask_group_suffix)) in MASK_GROUP_PATHS.iter().enumerate() { @@ -277,6 +406,7 @@ fn setup_animation_graph_once_loaded( ); animation_graph .add_target_to_mask_group(animation_target_id, mask_group_index as u32); + all_animation_target_ids.insert(animation_target_id); } } @@ -284,81 +414,100 @@ fn setup_animation_graph_once_loaded( let animation_graph = animation_graphs.add(animation_graph); commands.entity(entity).insert(animation_graph); - // Finally, play the animation. - player.play(node_index).repeat(); + // Remove animation targets that aren't in any of the mask groups. If we + // don't do that, those bones will play all animations at once, which is + // ugly. + for (target_entity, target) in &targets { + if !all_animation_target_ids.contains(&target.id) { + commands.entity(target_entity).remove::(); + } + } + + // Play the animation. + for animation_graph_node in animation_graph_nodes { + player.play(animation_graph_node).repeat(); + } + + // Record the graph nodes. + commands.insert_resource(AnimationNodes(animation_graph_nodes)); } } // A system that handles requests from the user to toggle mask groups on and // off. fn handle_button_toggles( - mut interactions: Query< - ( - &Interaction, - &mut MaskGroupControl, - &mut BackgroundColor, - &Children, - ), - Changed, - >, - mut texts: Query<&mut Text>, - mut animation_players: Query<(&Handle, &AnimationPlayer)>, + mut interactions: Query<(&Interaction, &mut AnimationControl), Changed>, + mut animation_players: Query<&Handle, With>, mut animation_graphs: ResMut>, + mut animation_nodes: Option>, + mut app_state: ResMut, ) { - for (interaction, mut mask_group_control, mut button_background_color, children) in - interactions.iter_mut() - { + let Some(ref mut animation_nodes) = animation_nodes else { + return; + }; + + for (interaction, animation_control) in interactions.iter_mut() { // We only care about press events. if *interaction != Interaction::Pressed { continue; } - // Toggle the state of the mask. - mask_group_control.enabled = !mask_group_control.enabled; - - // Update the background color of the button. - button_background_color.0 = if mask_group_control.enabled { - Color::WHITE - } else { - Color::BLACK - }; - - // Update the text color of the button. - for &kid in children.iter() { - if let Ok(mut text) = texts.get_mut(kid) { - for section in &mut text.sections { - section.style.color = if mask_group_control.enabled { - Color::BLACK - } else { - Color::WHITE - }; - } - } - } + // Toggle the state of the clip. + app_state.0[animation_control.group_id as usize].clip = animation_control.label as u8; // Now grab the animation player. (There's only one in our case, but we // iterate just for clarity's sake.) - for (animation_graph_handle, animation_player) in animation_players.iter_mut() { + for animation_graph_handle in animation_players.iter_mut() { // The animation graph needs to have loaded. let Some(animation_graph) = animation_graphs.get_mut(animation_graph_handle) else { continue; }; - // Grab the animation graph node that's currently playing. - let Some((&animation_node_index, _)) = animation_player.playing_animations().next() - else { - continue; - }; - let Some(animation_node) = animation_graph.get_mut(animation_node_index) else { + for (clip_index, &animation_node_index) in animation_nodes.0.iter().enumerate() { + let Some(animation_node) = animation_graph.get_mut(animation_node_index) else { + continue; + }; + + if animation_control.label as usize == clip_index { + animation_node.mask &= !(1 << animation_control.group_id); + } else { + animation_node.mask |= 1 << animation_control.group_id; + } + } + } + } +} + +// A system that updates the UI based on the current app state. +fn update_ui( + mut animation_controls: Query<(&AnimationControl, &mut BackgroundColor, &Children)>, + mut texts: Query<&mut Text>, + app_state: Res, +) { + for (animation_control, mut background_color, kids) in animation_controls.iter_mut() { + let enabled = + app_state.0[animation_control.group_id as usize].clip == animation_control.label as u8; + + *background_color = if enabled { + BackgroundColor(Color::WHITE) + } else { + BackgroundColor(Color::BLACK) + }; + + for &kid in kids { + let Ok(mut text) = texts.get_mut(kid) else { continue; }; - // Enable or disable the mask group as appropriate. - if mask_group_control.enabled { - animation_node.mask &= !(1 << mask_group_control.group_id); - } else { - animation_node.mask |= 1 << mask_group_control.group_id; + for section in &mut text.sections { + section.style.color = if enabled { Color::BLACK } else { Color::WHITE }; } } } } + +impl Default for AppState { + fn default() -> Self { + AppState([MaskGroupState { clip: 0 }; 6]) + } +} From ddd4b4daf8bb3df1dac0fd5f453269ff4bd4f99a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristoffer=20S=C3=B8holm?= Date: Sat, 5 Oct 2024 00:51:23 +0200 Subject: [PATCH 025/546] Fix deferred rendering (#15656) # Objective Fixes #15525 The deferred and mesh pipelines tonemapping LUT bindings were accidentally out of sync, breaking deferred rendering. As noted in the issue it's still broken on wasm due to hitting a texture limit. ## Solution Add constants for these instead of hardcoding them. ## Testing Test with `cargo run --example deferred_rendering` and see it works, run the same on main and see it crash. --- crates/bevy_pbr/src/deferred/mod.rs | 7 ++++--- crates/bevy_pbr/src/lib.rs | 3 +++ crates/bevy_pbr/src/render/mesh.rs | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/crates/bevy_pbr/src/deferred/mod.rs b/crates/bevy_pbr/src/deferred/mod.rs index fe1a652d2404b7..bcb23a7048bfdb 100644 --- a/crates/bevy_pbr/src/deferred/mod.rs +++ b/crates/bevy_pbr/src/deferred/mod.rs @@ -2,7 +2,8 @@ use crate::{ graph::NodePbr, irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight, MeshPipeline, MeshViewBindGroup, RenderViewLightProbes, ScreenSpaceAmbientOcclusion, ScreenSpaceReflectionsUniform, ViewEnvironmentMapUniformOffset, ViewLightProbesUniformOffset, - ViewScreenSpaceReflectionsUniformOffset, + ViewScreenSpaceReflectionsUniformOffset, TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, + TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, }; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, Handle}; @@ -259,11 +260,11 @@ impl SpecializedRenderPipeline for DeferredLightingLayout { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), - 22, + TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, )); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), - 23, + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, )); let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 68f75af7bef86d..369efdcb59c0ba 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -158,6 +158,9 @@ pub const RGB9E5_FUNCTIONS_HANDLE: Handle = Handle::weak_from_u128(26590 const MESHLET_VISIBILITY_BUFFER_RESOLVE_SHADER_HANDLE: Handle = Handle::weak_from_u128(2325134235233421); +const TONEMAPPING_LUT_TEXTURE_BINDING_INDEX: u32 = 23; +const TONEMAPPING_LUT_SAMPLER_BINDING_INDEX: u32 = 24; + /// Sets up the entire PBR infrastructure of bevy. pub struct PbrPlugin { /// Controls if the prepass is enabled for the [`StandardMaterial`]. diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 91f93c61b25604..0b4a3e3bda4993 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -1842,11 +1842,11 @@ impl SpecializedMeshPipeline for MeshPipeline { shader_defs.push("TONEMAP_IN_SHADER".into()); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_TEXTURE_BINDING_INDEX".into(), - 23, + TONEMAPPING_LUT_TEXTURE_BINDING_INDEX, )); shader_defs.push(ShaderDefVal::UInt( "TONEMAPPING_LUT_SAMPLER_BINDING_INDEX".into(), - 24, + TONEMAPPING_LUT_SAMPLER_BINDING_INDEX, )); let method = key.intersection(MeshPipelineKey::TONEMAP_METHOD_RESERVED_BITS); From 2e89e9893159441459c971e9d6d35e438b83609c Mon Sep 17 00:00:00 2001 From: urben1680 <55257931+urben1680@users.noreply.github.com> Date: Sat, 5 Oct 2024 03:35:44 +0200 Subject: [PATCH 026/546] Deprecate `Events::oldest_id` (#15658) # Objective Fixes #15617 ## Solution The original author confirmed it was not intentional that both these methods exist. They do the same, one has the better implementation and the other the better name. ## Testing I just ran the unit tests of the module. --- ## Migration Guide - Change usages of `Events::oldest_id` to `Events::oldest_event_count` - If `Events::oldest_id` was used to get the actual oldest `EventId::id`, note that the deprecated method never reliably did that in the first place as the buffers may contain no id currently. --- crates/bevy_ecs/src/event/collections.rs | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ecs/src/event/collections.rs b/crates/bevy_ecs/src/event/collections.rs index 4c9bc1996f8da8..c04512ba62bbae 100644 --- a/crates/bevy_ecs/src/event/collections.rs +++ b/crates/bevy_ecs/src/event/collections.rs @@ -114,9 +114,7 @@ impl Default for Events { impl Events { /// Returns the index of the oldest event stored in the event buffer. pub fn oldest_event_count(&self) -> usize { - self.events_a - .start_event_count - .min(self.events_b.start_event_count) + self.events_a.start_event_count } /// "Sends" an `event` by writing it to the current event buffer. @@ -279,7 +277,7 @@ impl Events { /// Get a specific event by id if it still exists in the events buffer. pub fn get_event(&self, id: usize) -> Option<(&E, EventId)> { - if id < self.oldest_id() { + if id < self.oldest_event_count() { return None; } @@ -291,11 +289,6 @@ impl Events { .map(|instance| (&instance.event, instance.event_id)) } - /// Oldest id still in the events buffer. - pub fn oldest_id(&self) -> usize { - self.events_a.start_event_count - } - /// Which event buffer is this event id a part of. fn sequence(&self, id: usize) -> &EventSequence { if id < self.events_b.start_event_count { From ac9b0c848cb9e27ff31162353d03216b067a0669 Mon Sep 17 00:00:00 2001 From: m-edlund Date: Sat, 5 Oct 2024 03:36:47 +0200 Subject: [PATCH 027/546] fix: corrected projection calculation of camera sub view (#15646) # Objective - Fixes #15600 ## Solution - The projection calculations did not use the aspect ratio of `full_size`, this was amended ## Testing - I created a test example on [this fork](https://github.com/m-edlund/bevy/tree/bug/main/subViewProjectionBroken) to allow testing with different aspect ratios and offsets - The sub view is bound to a view port that can move across the screen. The image in the moving sub view should "match" the full image exactly ## Showcase Current version: https://github.com/user-attachments/assets/17ad1213-d5ae-4181-89c1-81146edede7d Fixed version: https://github.com/user-attachments/assets/398e0927-e1dd-4880-897d-8157aa4398e6 --- crates/bevy_render/src/camera/projection.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 832ba6e56f0bf1..e29d4ec869d8e2 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -206,10 +206,12 @@ impl CameraProjection for PerspectiveProjection { // Y-axis increases from top to bottom let offset_y = full_height - (sub_view.offset.y + sub_height); + let full_aspect = full_width / full_height; + // Original frustum parameters let top = self.near * ops::tan(0.5 * self.fov); let bottom = -top; - let right = top * self.aspect_ratio; + let right = top * full_aspect; let left = -right; // Calculate scaling factors @@ -450,11 +452,18 @@ impl CameraProjection for OrthographicProjection { let sub_width = sub_view.size.x as f32; let sub_height = sub_view.size.y as f32; - // Orthographic projection parameters + let full_aspect = full_width / full_height; + + // Base the vertical size on self.area and adjust the horizontal size let top = self.area.max.y; let bottom = self.area.min.y; - let right = self.area.max.x; - let left = self.area.min.x; + let ortho_height = top - bottom; + let ortho_width = ortho_height * full_aspect; + + // Center the orthographic area horizontally + let center_x = (self.area.max.x + self.area.min.x) / 2.0; + let left = center_x - ortho_width / 2.0; + let right = center_x + ortho_width / 2.0; // Calculate scaling factors let scale_w = (right - left) / full_width; From 25bfa80e60e886031dd6eb1a3fe2ea548fc0a6c6 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 5 Oct 2024 04:59:52 +0300 Subject: [PATCH 028/546] Migrate cameras to required components (#15641) # Objective Yet another PR for migrating stuff to required components. This time, cameras! ## Solution As per the [selected proposal](https://hackmd.io/tsYID4CGRiWxzsgawzxG_g#Combined-Proposal-1-Selected), deprecate `Camera2dBundle` and `Camera3dBundle` in favor of `Camera2d` and `Camera3d`. Adding a `Camera` without `Camera2d` or `Camera3d` now logs a warning, as suggested by Cart [on Discord](https://discord.com/channels/691052431525675048/1264881140007702558/1291506402832945273). I would personally like cameras to work a bit differently and be split into a few more components, to avoid some footguns and confusing semantics, but that is more controversial, and shouldn't block this core migration. ## Testing I ran a few 2D and 3D examples, and tried cameras with and without render graphs. --- ## Migration Guide `Camera2dBundle` and `Camera3dBundle` have been deprecated in favor of `Camera2d` and `Camera3d`. Inserting them will now also insert the other components required by them automatically. --- Cargo.toml | 2 +- .../src/core_2d/camera_2d.rs | 15 ++ .../src/core_3d/camera_3d.rs | 20 ++- crates/bevy_core_pipeline/src/lib.rs | 1 + .../bevy_core_pipeline/src/motion_blur/mod.rs | 4 +- .../bevy_core_pipeline/src/tonemapping/mod.rs | 6 +- .../src/ui_debug_overlay/mod.rs | 22 ++- crates/bevy_ecs/src/bundle.rs | 3 - crates/bevy_gltf/src/loader.rs | 10 +- crates/bevy_pbr/src/fog.rs | 5 +- crates/bevy_render/src/camera/camera.rs | 35 ++++- crates/bevy_render/src/primitives/mod.rs | 3 +- crates/bevy_text/src/text2d.rs | 2 +- crates/bevy_ui/src/layout/mod.rs | 14 +- crates/bevy_ui/src/ui_node.rs | 12 +- docs/cargo_features.md | 2 +- errors/B0004.md | 16 +-- examples/2d/2d_shapes.rs | 2 +- examples/2d/2d_viewport_to_world.rs | 2 +- examples/2d/bloom_2d.rs | 12 +- examples/2d/bounding_2d.rs | 2 +- examples/2d/custom_gltf_vertex_attribute.rs | 2 +- examples/2d/mesh2d.rs | 2 +- examples/2d/mesh2d_alpha_mode.rs | 2 +- examples/2d/mesh2d_arcs.rs | 8 +- examples/2d/mesh2d_manual.rs | 2 +- examples/2d/mesh2d_vertex_color_texture.rs | 2 +- examples/2d/move_sprite.rs | 2 +- examples/2d/pixel_grid_snap.rs | 23 +-- examples/2d/rotation.rs | 2 +- examples/2d/sprite.rs | 2 +- examples/2d/sprite_animation.rs | 2 +- examples/2d/sprite_flipping.rs | 2 +- examples/2d/sprite_sheet.rs | 2 +- examples/2d/sprite_slice.rs | 2 +- examples/2d/sprite_tile.rs | 2 +- examples/2d/text2d.rs | 2 +- examples/2d/texture_atlas.rs | 2 +- examples/2d/transparency_2d.rs | 2 +- examples/2d/wireframe_2d.rs | 2 +- examples/3d/3d_scene.rs | 8 +- examples/3d/3d_shapes.rs | 8 +- examples/3d/3d_viewport_to_world.rs | 8 +- examples/3d/animated_material.rs | 7 +- examples/3d/anisotropy.rs | 9 +- examples/3d/anti_aliasing.rs | 11 +- examples/3d/atmospheric_fog.rs | 7 +- examples/3d/auto_exposure.rs | 10 +- examples/3d/blend_modes.rs | 8 +- examples/3d/bloom_3d.rs | 12 +- examples/3d/camera_sub_view.rs | 136 +++++++++--------- examples/3d/clearcoat.rs | 14 +- examples/3d/color_grading.rs | 13 +- examples/3d/deferred_rendering.rs | 17 +-- examples/3d/depth_of_field.rs | 14 +- examples/3d/fog.rs | 2 +- examples/3d/fog_volumes.rs | 20 ++- examples/3d/generate_custom_mesh.rs | 5 +- examples/3d/irradiance_volumes.rs | 13 +- examples/3d/lighting.rs | 10 +- examples/3d/lightmaps.rs | 8 +- examples/3d/lines.rs | 8 +- examples/3d/load_gltf.rs | 7 +- examples/3d/load_gltf_extras.rs | 8 +- examples/3d/meshlet.rs | 9 +- examples/3d/motion_blur.rs | 2 +- examples/3d/orthographic.rs | 13 +- examples/3d/parallax_mapping.rs | 6 +- examples/3d/parenting.rs | 8 +- examples/3d/pbr.rs | 15 +- examples/3d/pcss.rs | 16 +-- examples/3d/post_processing.rs | 11 +- examples/3d/query_gltf_primitives.rs | 7 +- examples/3d/reflection_probes.rs | 10 +- examples/3d/render_to_texture.rs | 21 ++- examples/3d/rotate_environment_map.rs | 14 +- examples/3d/scrolling_fog.rs | 12 +- examples/3d/shadow_biases.rs | 7 +- examples/3d/shadow_caster_receiver.rs | 9 +- examples/3d/skybox.rs | 6 +- examples/3d/spherical_area_lights.rs | 8 +- examples/3d/split_screen.rs | 13 +- examples/3d/spotlight.rs | 10 +- examples/3d/ssao.rs | 12 +- examples/3d/ssr.rs | 13 +- examples/3d/texture.rs | 8 +- examples/3d/tonemapping.rs | 10 +- examples/3d/transmission.rs | 28 ++-- examples/3d/transparency_3d.rs | 8 +- examples/3d/two_passes.rs | 18 +-- examples/3d/update_gltf_scene.rs | 7 +- examples/3d/vertex_colors.rs | 8 +- examples/3d/visibility_range.rs | 8 +- examples/3d/volumetric_fog.rs | 15 +- examples/3d/wireframe.rs | 8 +- examples/animation/animated_fox.rs | 9 +- examples/animation/animated_transform.rs | 8 +- examples/animation/animated_ui.rs | 2 +- examples/animation/animation_graph.rs | 8 +- examples/animation/animation_masks.rs | 9 +- examples/animation/color_animation.rs | 2 +- examples/animation/cubic_curve.rs | 8 +- examples/animation/custom_skinned_mesh.rs | 8 +- examples/animation/gltf_skinned_mesh.rs | 9 +- examples/animation/morph_targets.rs | 8 +- examples/app/headless_renderer.rs | 12 +- examples/app/log_layers_ecs.rs | 2 +- examples/app/logs.rs | 2 +- examples/app/without_winit.rs | 2 +- examples/asset/alter_mesh.rs | 6 +- examples/asset/alter_sprite.rs | 2 +- examples/asset/asset_decompression.rs | 2 +- examples/asset/asset_loading.rs | 8 +- examples/asset/asset_settings.rs | 2 +- examples/asset/custom_asset_reader.rs | 2 +- examples/asset/embedded_asset.rs | 2 +- examples/asset/extra_source.rs | 2 +- examples/asset/hot_asset_reloading.rs | 8 +- examples/asset/multi_asset_sync.rs | 9 +- examples/asset/repeated_texture.rs | 8 +- examples/async_tasks/async_compute.rs | 8 +- .../external_source_external_thread.rs | 2 +- examples/audio/spatial_audio_2d.rs | 2 +- examples/audio/spatial_audio_3d.rs | 8 +- examples/camera/2d_top_down_camera.rs | 8 +- examples/camera/camera_orbit.rs | 6 +- examples/camera/first_person_view_model.rs | 30 ++-- examples/camera/projection_zoom.rs | 17 +-- examples/dev_tools/fps_overlay.rs | 2 +- examples/ecs/fallible_params.rs | 2 +- examples/ecs/hierarchy.rs | 2 +- examples/ecs/iter_combinations.rs | 8 +- examples/ecs/observers.rs | 2 +- examples/ecs/one_shot_systems.rs | 2 +- examples/ecs/parallel_query.rs | 2 +- examples/ecs/removal_detection.rs | 2 +- examples/games/alien_cake_addict.rs | 8 +- examples/games/breakout.rs | 2 +- examples/games/contributors.rs | 2 +- examples/games/desk_toy.rs | 2 +- examples/games/game_menu.rs | 2 +- examples/games/loading_screen.rs | 22 +-- examples/gizmos/2d_gizmos.rs | 2 +- examples/gizmos/3d_gizmos.rs | 6 +- examples/gizmos/axes.rs | 8 +- examples/gizmos/light_gizmos.rs | 8 +- examples/helpers/camera_controller.rs | 2 +- examples/input/text_input.rs | 2 +- examples/math/cubic_splines.rs | 2 +- examples/math/custom_primitives.rs | 6 +- examples/math/random_sampling.rs | 8 +- examples/math/render_primitives.rs | 15 +- examples/math/sampling_primitives.rs | 14 +- examples/mobile/src/lib.rs | 10 +- .../movement/physics_in_fixed_timestep.rs | 2 +- examples/movement/smooth_follow.rs | 8 +- examples/picking/simple_picking.rs | 8 +- examples/picking/sprite_picking.rs | 2 +- examples/remote/server.rs | 8 +- examples/scene/scene.rs | 2 +- examples/shader/animate_shader.rs | 8 +- examples/shader/array_texture.rs | 8 +- examples/shader/automatic_instancing.rs | 8 +- .../shader/compute_shader_game_of_life.rs | 2 +- examples/shader/custom_phase_item.rs | 8 +- examples/shader/custom_post_processing.rs | 11 +- examples/shader/custom_shader_instancing.rs | 8 +- examples/shader/custom_vertex_attribute.rs | 8 +- examples/shader/extended_material.rs | 8 +- examples/shader/fallback_image.rs | 8 +- examples/shader/shader_defs.rs | 8 +- examples/shader/shader_material.rs | 8 +- examples/shader/shader_material_2d.rs | 2 +- examples/shader/shader_material_glsl.rs | 8 +- .../shader_material_screenspace_texture.rs | 6 +- examples/shader/shader_prepass.rs | 10 +- examples/shader/specialized_mesh_pipeline.rs | 8 +- examples/shader/storage_buffer.rs | 8 +- examples/shader/texture_binding_array.rs | 8 +- examples/state/computed_states.rs | 2 +- examples/state/custom_transitions.rs | 2 +- examples/state/states.rs | 2 +- examples/state/sub_states.rs | 2 +- examples/stress_tests/bevymark.rs | 2 +- .../stress_tests/many_animated_sprites.rs | 2 +- examples/stress_tests/many_buttons.rs | 4 +- examples/stress_tests/many_cubes.rs | 7 +- examples/stress_tests/many_foxes.rs | 8 +- examples/stress_tests/many_gizmos.rs | 8 +- examples/stress_tests/many_glyphs.rs | 2 +- examples/stress_tests/many_lights.rs | 13 +- examples/stress_tests/many_sprites.rs | 2 +- examples/stress_tests/text_pipeline.rs | 2 +- examples/stress_tests/transform_hierarchy.rs | 5 +- examples/time/virtual_time.rs | 2 +- examples/tools/gamepad_viewer.rs | 2 +- examples/tools/scene_viewer/main.rs | 14 +- examples/transforms/3d_rotation.rs | 8 +- examples/transforms/align.rs | 8 +- examples/transforms/scale.rs | 8 +- examples/transforms/transform.rs | 8 +- examples/transforms/translation.rs | 8 +- examples/ui/borders.rs | 2 +- examples/ui/button.rs | 2 +- examples/ui/display_and_visibility.rs | 2 +- examples/ui/flex_layout.rs | 2 +- examples/ui/font_atlas_debug.rs | 2 +- examples/ui/ghost_nodes.rs | 2 +- examples/ui/grid.rs | 2 +- examples/ui/overflow.rs | 2 +- examples/ui/overflow_debug.rs | 2 +- examples/ui/relative_cursor_position.rs | 8 +- examples/ui/render_ui_to_texture.rs | 16 +-- examples/ui/scroll.rs | 2 +- examples/ui/size_constraints.rs | 2 +- examples/ui/text.rs | 2 +- examples/ui/text_debug.rs | 2 +- examples/ui/text_wrap_debug.rs | 2 +- examples/ui/transparency_ui.rs | 2 +- examples/ui/ui.rs | 2 +- examples/ui/ui_material.rs | 2 +- examples/ui/ui_scaling.rs | 2 +- examples/ui/ui_texture_atlas.rs | 2 +- examples/ui/ui_texture_atlas_slice.rs | 2 +- examples/ui/ui_texture_slice.rs | 2 +- examples/ui/ui_texture_slice_flip_and_tile.rs | 2 +- examples/ui/viewport_debug.rs | 2 +- examples/ui/window_fallthrough.rs | 2 +- examples/ui/z_index.rs | 2 +- examples/window/clear_color.rs | 2 +- examples/window/custom_user_event.rs | 2 +- examples/window/low_power.rs | 8 +- examples/window/monitor_info.rs | 8 +- examples/window/multiple_windows.rs | 18 +-- examples/window/scale_factor_override.rs | 2 +- examples/window/screenshot.rs | 8 +- examples/window/transparent_window.rs | 2 +- examples/window/window_resizing.rs | 2 +- tests/window/change_window_mode.rs | 65 +++++---- tests/window/minimising.rs | 16 +-- tests/window/resizing.rs | 16 +-- 241 files changed, 870 insertions(+), 969 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1775b2dd7b7d13..bd4efd833718d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -346,7 +346,7 @@ android_shared_stdcxx = ["bevy_internal/android_shared_stdcxx"] # Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in detailed_trace = ["bevy_internal/detailed_trace"] -# Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method on your `Camera2dBundle` or `Camera3dBundle`. +# Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method for your `Camera2d` or `Camera3d`. tonemapping_luts = ["bevy_internal/tonemapping_luts", "ktx2", "zstd"] # Include SMAA Look Up Tables KTX2 Files 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 03ddf5c0739154..5e3ef674d3bdf1 100644 --- a/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs +++ b/crates/bevy_core_pipeline/src/core_2d/camera_2d.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] + use crate::{ core_2d::graph::Core2d, tonemapping::{DebandDither, Tonemapping}, @@ -17,12 +19,25 @@ use bevy_render::{ }; use bevy_transform::prelude::{GlobalTransform, Transform}; +/// A 2D camera component. Enables the 2D render graph for a [`Camera`]. #[derive(Component, Default, Reflect, Clone, ExtractComponent)] #[extract_component_filter(With)] #[reflect(Component, Default)] +#[require( + Camera, + DebandDither, + CameraRenderGraph(|| CameraRenderGraph::new(Core2d)), + OrthographicProjection(OrthographicProjection::default_2d), + Frustum(|| OrthographicProjection::default_2d().compute_frustum(&GlobalTransform::from(Transform::default()))), + Tonemapping(|| Tonemapping::None), +)] pub struct Camera2d; #[derive(Bundle, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `Camera2d` component instead. Inserting it will now also insert the other components required by it automatically." +)] pub struct Camera2dBundle { pub camera: Camera, pub camera_render_graph: CameraRenderGraph, 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 454892a30684c0..81955f76b361e6 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] + use crate::{ core_3d::graph::Core3d, tonemapping::{DebandDither, Tonemapping}, @@ -15,12 +17,22 @@ use bevy_render::{ use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; -/// Configuration for the "main 3d render graph". -/// The camera coordinate space is right-handed x-right, y-up, z-back. +/// A 3D camera component. Enables the main 3D render graph for a [`Camera`]. +/// +/// The camera coordinate space is right-handed X-right, Y-up, Z-back. /// This means "forward" is -Z. #[derive(Component, Reflect, Clone, ExtractComponent)] #[extract_component_filter(With)] #[reflect(Component, Default)] +#[require( + Camera, + DebandDither(|| DebandDither::Enabled), + CameraRenderGraph(|| CameraRenderGraph::new(Core3d)), + Projection, + Tonemapping, + ColorGrading, + Exposure +)] pub struct Camera3d { /// The depth clear operation to perform for the main 3d pass. pub depth_load_op: Camera3dDepthLoadOp, @@ -139,6 +151,10 @@ pub enum ScreenSpaceTransmissionQuality { /// The camera coordinate space is right-handed x-right, y-up, z-back. /// This means "forward" is -Z. #[derive(Bundle, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `Camera3d` component instead. Inserting it will now also insert the other components required by it automatically." +)] pub struct Camera3dBundle { pub camera: Camera, pub camera_render_graph: CameraRenderGraph, diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 0ce36763d6e660..ac91fca61dfcbd 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -45,6 +45,7 @@ pub mod experimental { /// The core pipeline prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. +#[expect(deprecated)] pub mod prelude { #[doc(hidden)] pub use crate::{ diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index c5a4a9f212e5a3..2195e296f5590b 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -59,11 +59,11 @@ pub struct MotionBlurBundle { /// camera. /// /// ``` -/// # use bevy_core_pipeline::{core_3d::Camera3dBundle, motion_blur::MotionBlur}; +/// # use bevy_core_pipeline::{core_3d::Camera3d, motion_blur::MotionBlur}; /// # use bevy_ecs::prelude::*; /// # fn test(mut commands: Commands) { /// commands.spawn(( -/// Camera3dBundle::default(), +/// Camera3d::default(), /// MotionBlur::default(), /// )); /// # } diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 7614b50153a40e..972fd7836a3d72 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -263,7 +263,7 @@ impl SpecializedRenderPipeline for TonemappingPipeline { error!( "AgX tonemapping requires the `tonemapping_luts` feature. Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended), - or use a different `Tonemapping` method in your `Camera2dBundle`/`Camera3dBundle`." + or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`." ); shader_defs.push("TONEMAP_METHOD_AGX".into()); } @@ -275,7 +275,7 @@ impl SpecializedRenderPipeline for TonemappingPipeline { error!( "TonyMcMapFace tonemapping requires the `tonemapping_luts` feature. Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended), - or use a different `Tonemapping` method in your `Camera2dBundle`/`Camera3dBundle`." + or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`." ); shader_defs.push("TONEMAP_METHOD_TONY_MC_MAPFACE".into()); } @@ -284,7 +284,7 @@ impl SpecializedRenderPipeline for TonemappingPipeline { error!( "BlenderFilmic tonemapping requires the `tonemapping_luts` feature. Either enable the `tonemapping_luts` feature for bevy in `Cargo.toml` (recommended), - or use a different `Tonemapping` method in your `Camera2dBundle`/`Camera3dBundle`." + or use a different `Tonemapping` method for your `Camera2d`/`Camera3d`." ); shader_defs.push("TONEMAP_METHOD_BLENDER_FILMIC".into()); } diff --git a/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs b/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs index d84143943c9732..550cd1c29dde8e 100644 --- a/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs +++ b/crates/bevy_dev_tools/src/ui_debug_overlay/mod.rs @@ -4,7 +4,7 @@ use core::any::{Any, TypeId}; use bevy_app::{App, Plugin, PostUpdate}; use bevy_color::Hsla; use bevy_core::Name; -use bevy_core_pipeline::core_2d::Camera2dBundle; +use bevy_core_pipeline::core_2d::Camera2d; use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_gizmos::{config::GizmoConfigStore, prelude::Gizmos, AppGizmoBuilder}; use bevy_hierarchy::{Children, Parent}; @@ -88,17 +88,15 @@ fn update_debug_camera( } else { let spawn_cam = || { cmds.spawn(( - Camera2dBundle { - projection: OrthographicProjection { - far: 1000.0, - viewport_origin: Vec2::new(0.0, 0.0), - ..OrthographicProjection::default_3d() - }, - camera: Camera { - order: LAYOUT_DEBUG_CAMERA_ORDER, - clear_color: ClearColorConfig::None, - ..default() - }, + Camera2d, + OrthographicProjection { + far: 1000.0, + viewport_origin: Vec2::new(0.0, 0.0), + ..OrthographicProjection::default_3d() + }, + Camera { + order: LAYOUT_DEBUG_CAMERA_ORDER, + clear_color: ClearColorConfig::None, ..default() }, LAYOUT_DEBUG_LAYERS.clone(), diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 56e11e854e13e9..5075642da6d82e 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -55,9 +55,6 @@ use core::{any::TypeId, ptr::NonNull}; /// would create incoherent behavior. /// This would be unexpected if bundles were treated as an abstraction boundary, as /// the abstraction would be unmaintainable for these cases. -/// For example, both `Camera3dBundle` and `Camera2dBundle` contain the `CameraRenderGraph` -/// component, but specifying different render graphs to use. -/// If the bundles were both added to the same entity, only one of these two bundles would work. /// /// For this reason, there is intentionally no [`Query`] to match whether an entity /// contains the components of a bundle. diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 9bb72e23567299..046c0550a5aaec 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -9,7 +9,7 @@ use bevy_asset::{ }; use bevy_color::{Color, LinearRgba}; use bevy_core::Name; -use bevy_core_pipeline::prelude::Camera3dBundle; +use bevy_core_pipeline::prelude::Camera3d; use bevy_ecs::{ entity::{Entity, EntityHashMap}, world::World, @@ -1413,15 +1413,15 @@ fn load_node( Projection::Perspective(perspective_projection) } }; - node.insert(Camera3dBundle { + node.insert(( + Camera3d::default(), projection, transform, - camera: Camera { + Camera { is_active: !*active_camera_found, ..Default::default() }, - ..Default::default() - }); + )); *active_camera_found = true; } diff --git a/crates/bevy_pbr/src/fog.rs b/crates/bevy_pbr/src/fog.rs index c2ff30bb564112..198d218334dc27 100644 --- a/crates/bevy_pbr/src/fog.rs +++ b/crates/bevy_pbr/src/fog.rs @@ -29,10 +29,7 @@ use bevy_render::{extract_component::ExtractComponent, prelude::Camera}; /// # fn system(mut commands: Commands) { /// commands.spawn(( /// // Setup your camera as usual -/// Camera3dBundle { -/// // ... camera options -/// # ..Default::default() -/// }, +/// Camera3d::default(), /// // Add fog to the same entity /// DistanceFog { /// color: Color::WHITE, diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 702a3aea4b7c50..b34dcb4717f65a 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -8,27 +8,29 @@ use crate::{ render_resource::TextureView, texture::GpuImage, view::{ - ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, RenderLayers, VisibleEntities, + ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers, Visibility, + VisibleEntities, }, - world_sync::RenderEntity, + world_sync::{RenderEntity, SyncToRenderWorld}, Extract, }; use bevy_asset::{AssetEvent, AssetId, Assets, Handle}; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ change_detection::DetectChanges, - component::Component, + component::{Component, ComponentId}, entity::Entity, event::EventReader, prelude::With, query::Has, reflect::ReflectComponent, system::{Commands, Query, Res, ResMut, Resource}, + world::DeferredWorld, }; use bevy_math::{ops, vec2, Dir3, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3}; use bevy_reflect::prelude::*; use bevy_render_macros::ExtractComponent; -use bevy_transform::components::GlobalTransform; +use bevy_transform::components::{GlobalTransform, Transform}; use bevy_utils::{tracing::warn, warn_once, HashMap, HashSet}; use bevy_window::{ NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized, @@ -274,10 +276,25 @@ pub enum ViewportConversionError { /// to transform the 3D objects into a 2D image, as well as the render target into which that image /// is produced. /// -/// Adding a camera is typically done by adding a bundle, either the `Camera2dBundle` or the -/// `Camera3dBundle`. +/// Note that a [`Camera`] needs a [`CameraRenderGraph`] to render anything. +/// This is typically provided by adding a [`Camera2d`] or [`Camera3d`] component, +/// but custom render graphs can also be defined. Inserting a [`Camera`] with no render +/// graph will emit an error at runtime. +/// +/// [`Camera2d`]: https://docs.rs/crate/bevy_core_pipeline/latest/core_2d/struct.Camera2d.html +/// [`Camera3d`]: https://docs.rs/crate/bevy_core_pipeline/latest/core_3d/struct.Camera3d.html #[derive(Component, Debug, Reflect, Clone)] #[reflect(Component, Default, Debug)] +#[component(on_add = warn_on_no_render_graph)] +#[require( + Frustum, + CameraMainTextureUsages, + VisibleEntities, + Transform, + Visibility, + Msaa, + SyncToRenderWorld +)] pub struct Camera { /// If set, this camera will render to the given [`Viewport`] rectangle within the configured [`RenderTarget`]. pub viewport: Option, @@ -309,6 +326,12 @@ pub struct Camera { pub sub_camera_view: Option, } +fn warn_on_no_render_graph(world: DeferredWorld, entity: Entity, _: ComponentId) { + if !world.entity(entity).contains::() { + warn!("Entity {entity} has a `Camera` component, but it doesn't have a render graph configured. Consider adding a `Camera2d` or `Camera3d` component, or manually adding a `CameraRenderGraph` component if you need a custom render graph."); + } +} + impl Default for Camera { fn default() -> Self { Self { diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index 66925005ef956a..70a06ae6358408 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -200,8 +200,7 @@ impl HalfSpace { /// This process is called frustum culling, and entities can opt out of it using /// the [`NoFrustumCulling`] component. /// -/// The frustum component is typically added from a bundle, either the `Camera2dBundle` -/// or the `Camera3dBundle`. +/// The frustum component is typically added automatically for cameras, either `Camera2d` or `Camera3d`. /// It is usually updated automatically by [`update_frusta`] from the /// [`CameraProjection`] component and [`GlobalTransform`] of the camera entity. /// diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 0e0fa98468ac59..646e28a748e120 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -27,7 +27,7 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; use bevy_utils::HashSet; use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; -/// The bundle of components needed to draw text in a 2D scene via a 2D `Camera2dBundle`. +/// The bundle of components needed to draw text in a 2D scene via a `Camera2d`. /// [Example usage.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/text2d.rs) #[derive(Bundle, Clone, Debug, Default)] pub struct Text2dBundle { diff --git a/crates/bevy_ui/src/layout/mod.rs b/crates/bevy_ui/src/layout/mod.rs index 1965b24096d894..3e418c689d55ae 100644 --- a/crates/bevy_ui/src/layout/mod.rs +++ b/crates/bevy_ui/src/layout/mod.rs @@ -464,7 +464,7 @@ mod tests { use taffy::TraversePartialTree; use bevy_asset::{AssetEvent, Assets}; - use bevy_core_pipeline::core_2d::Camera2dBundle; + use bevy_core_pipeline::core_2d::Camera2d; use bevy_ecs::{ entity::Entity, event::Events, @@ -539,7 +539,7 @@ mod tests { }, PrimaryWindow, )); - world.spawn(Camera2dBundle::default()); + world.spawn(Camera2d); let mut ui_schedule = Schedule::default(); ui_schedule.add_systems( @@ -646,7 +646,7 @@ mod tests { assert!(ui_surface.camera_entity_to_taffy.is_empty()); // respawn camera - let camera_entity = world.spawn(Camera2dBundle::default()).id(); + let camera_entity = world.spawn(Camera2d).id(); let ui_entity = world .spawn((NodeBundle::default(), TargetCamera(camera_entity))) @@ -970,13 +970,13 @@ mod tests { let (mut world, mut ui_schedule) = setup_ui_test_world(); - world.spawn(Camera2dBundle { - camera: Camera { + world.spawn(( + Camera2d, + Camera { order: 1, ..default() }, - ..default() - }); + )); world.spawn(( NodeBundle { diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index dac55d0dd4826a..5812f6a40debc6 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -2437,7 +2437,7 @@ impl TargetCamera { /// # use bevy_ui::prelude::*; /// # use bevy_ecs::prelude::Commands; /// # use bevy_render::camera::{Camera, RenderTarget}; -/// # use bevy_core_pipeline::prelude::Camera2dBundle; +/// # use bevy_core_pipeline::prelude::Camera2d; /// # use bevy_window::{Window, WindowRef}; /// /// fn spawn_camera(mut commands: Commands) { @@ -2446,11 +2446,9 @@ impl TargetCamera { /// ..Default::default() /// }).id(); /// commands.spawn(( -/// Camera2dBundle { -/// camera: Camera { -/// target: RenderTarget::Window(WindowRef::Entity(another_window)), -/// ..Default::default() -/// }, +/// Camera2d, +/// Camera { +/// target: RenderTarget::Window(WindowRef::Entity(another_window)), /// ..Default::default() /// }, /// // We add the Marker here so all Ui will spawn in @@ -2502,7 +2500,7 @@ impl<'w, 's> DefaultUiCamera<'w, 's> { /// /// fn spawn_camera(mut commands: Commands) { /// commands.spawn(( -/// Camera2dBundle::default(), +/// Camera2d, /// // This will cause all Ui in this camera to be rendered without /// // anti-aliasing /// UiAntiAlias::Off, diff --git a/docs/cargo_features.md b/docs/cargo_features.md index bd29c14c43b27e..4b5f6f04264cf3 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -41,7 +41,7 @@ The default feature set enables most of the expected features of a game engine, |png|PNG image format support| |smaa_luts|Include SMAA Look Up Tables KTX2 Files| |sysinfo_plugin|Enables system information diagnostic plugin| -|tonemapping_luts|Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method on your `Camera2dBundle` or `Camera3dBundle`.| +|tonemapping_luts|Include tonemapping Look Up Tables KTX2 files. If everything is pink, you need to enable this feature or change the `Tonemapping` method for your `Camera2d` or `Camera3d`.| |vorbis|OGG/VORBIS audio format support| |webgl2|Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU.| |x11|X11 display server support| diff --git a/errors/B0004.md b/errors/B0004.md index 4a658c7aa01842..9344d47b633adb 100644 --- a/errors/B0004.md +++ b/errors/B0004.md @@ -40,10 +40,10 @@ fn setup_cube( }); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } fn main() { @@ -83,10 +83,10 @@ fn setup_cube( }); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } fn main() { diff --git a/examples/2d/2d_shapes.rs b/examples/2d/2d_shapes.rs index 2d59bbf56ccd86..56683fe2cc8fbb 100644 --- a/examples/2d/2d_shapes.rs +++ b/examples/2d/2d_shapes.rs @@ -27,7 +27,7 @@ fn setup( mut meshes: ResMut>, mut materials: ResMut>, ) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let shapes = [ meshes.add(Circle::new(50.0)), diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index 3913a5c5e93677..193fc281290a17 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -34,5 +34,5 @@ fn draw_cursor( } fn setup(mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); } diff --git a/examples/2d/bloom_2d.rs b/examples/2d/bloom_2d.rs index dcffdcafcc10a4..fcd5534632f681 100644 --- a/examples/2d/bloom_2d.rs +++ b/examples/2d/bloom_2d.rs @@ -23,15 +23,13 @@ fn setup( asset_server: Res, ) { commands.spawn(( - Camera2dBundle { - camera: Camera { - hdr: true, // 1. HDR is required for bloom - ..default() - }, - tonemapping: Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended + Camera2d, + Camera { + hdr: true, // 1. HDR is required for bloom ..default() }, - Bloom::default(), // 3. Enable bloom for the camera + Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended + Bloom::default(), // 3. Enable bloom for the camera )); // Sprite diff --git a/examples/2d/bounding_2d.rs b/examples/2d/bounding_2d.rs index 1690a4827a2f5d..0a44c6ae686fbf 100644 --- a/examples/2d/bounding_2d.rs +++ b/examples/2d/bounding_2d.rs @@ -206,7 +206,7 @@ const OFFSET_X: f32 = 125.; const OFFSET_Y: f32 = 75.; fn setup(mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.spawn(( SpatialBundle { transform: Transform::from_xyz(-OFFSET_X, OFFSET_Y, 0.), diff --git a/examples/2d/custom_gltf_vertex_attribute.rs b/examples/2d/custom_gltf_vertex_attribute.rs index 80323cb6ab4e16..6cb10bdb267e9b 100644 --- a/examples/2d/custom_gltf_vertex_attribute.rs +++ b/examples/2d/custom_gltf_vertex_attribute.rs @@ -60,7 +60,7 @@ fn setup( )); // Add a camera - commands.spawn(Camera2dBundle { ..default() }); + commands.spawn(Camera2d); } /// This custom material uses barycentric coordinates from diff --git a/examples/2d/mesh2d.rs b/examples/2d/mesh2d.rs index 2857a4d722d571..fbc507e2ef4a09 100644 --- a/examples/2d/mesh2d.rs +++ b/examples/2d/mesh2d.rs @@ -14,7 +14,7 @@ fn setup( mut meshes: ResMut>, mut materials: ResMut>, ) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.spawn(( Mesh2d(meshes.add(Rectangle::default())), MeshMaterial2d(materials.add(Color::from(PURPLE))), diff --git a/examples/2d/mesh2d_alpha_mode.rs b/examples/2d/mesh2d_alpha_mode.rs index ce552fd7115b4e..5d6d2fbb5e5ccf 100644 --- a/examples/2d/mesh2d_alpha_mode.rs +++ b/examples/2d/mesh2d_alpha_mode.rs @@ -20,7 +20,7 @@ fn setup( mut meshes: ResMut>, mut materials: ResMut>, ) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let texture_handle = asset_server.load("branding/icon.png"); let mesh_handle = meshes.add(Rectangle::from_size(Vec2::splat(256.0))); diff --git a/examples/2d/mesh2d_arcs.rs b/examples/2d/mesh2d_arcs.rs index 577c8398fffb32..975cc2cf8bbf0f 100644 --- a/examples/2d/mesh2d_arcs.rs +++ b/examples/2d/mesh2d_arcs.rs @@ -39,13 +39,13 @@ fn setup( ) { let material = materials.add(asset_server.load("branding/icon.png")); - commands.spawn(Camera2dBundle { - camera: Camera { + commands.spawn(( + Camera2d, + Camera { clear_color: ClearColorConfig::Custom(DARK_SLATE_GREY.into()), ..default() }, - ..default() - }); + )); const UPPER_Y: f32 = 50.0; const LOWER_Y: f32 = -50.0; diff --git a/examples/2d/mesh2d_manual.rs b/examples/2d/mesh2d_manual.rs index 0daf83d357d837..e3b471610b8b1e 100644 --- a/examples/2d/mesh2d_manual.rs +++ b/examples/2d/mesh2d_manual.rs @@ -115,7 +115,7 @@ fn star( )); // Spawn the camera - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); } // Require `HasMaterial2d` to indicate that no placeholder material should be rendeed. diff --git a/examples/2d/mesh2d_vertex_color_texture.rs b/examples/2d/mesh2d_vertex_color_texture.rs index 9fa00db852f802..24a242075e59fb 100644 --- a/examples/2d/mesh2d_vertex_color_texture.rs +++ b/examples/2d/mesh2d_vertex_color_texture.rs @@ -33,7 +33,7 @@ fn setup( let mesh_handle = meshes.add(mesh); // Spawn camera - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Spawn the quad with vertex colors commands.spawn(( diff --git a/examples/2d/move_sprite.rs b/examples/2d/move_sprite.rs index 6f0efa53cbe766..9bbbe496584c5f 100644 --- a/examples/2d/move_sprite.rs +++ b/examples/2d/move_sprite.rs @@ -17,7 +17,7 @@ enum Direction { } fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.spawn(( SpriteBundle { texture: asset_server.load("branding/icon.png"), diff --git a/examples/2d/pixel_grid_snap.rs b/examples/2d/pixel_grid_snap.rs index 5cac633a3cdcbd..7e2fcffb5c5b0c 100644 --- a/examples/2d/pixel_grid_snap.rs +++ b/examples/2d/pixel_grid_snap.rs @@ -119,16 +119,14 @@ fn setup_camera(mut commands: Commands, mut images: ResMut>) { // this camera renders whatever is on `PIXEL_PERFECT_LAYERS` to the canvas commands.spawn(( - Camera2dBundle { - camera: Camera { - // render before the "main pass" camera - order: -1, - target: RenderTarget::Image(image_handle.clone()), - ..default() - }, - msaa: Msaa::Off, + Camera2d, + Camera { + // render before the "main pass" camera + order: -1, + target: RenderTarget::Image(image_handle.clone()), ..default() }, + Msaa::Off, InGameCamera, PIXEL_PERFECT_LAYERS, )); @@ -145,14 +143,7 @@ fn setup_camera(mut commands: Commands, mut images: ResMut>) { // the "outer" camera renders whatever is on `HIGH_RES_LAYERS` to the screen. // here, the canvas and one of the sample sprites will be rendered by this camera - commands.spawn(( - Camera2dBundle { - msaa: Msaa::Off, - ..default() - }, - OuterCamera, - HIGH_RES_LAYERS, - )); + commands.spawn((Camera2d, Msaa::Off, OuterCamera, HIGH_RES_LAYERS)); } /// Rotates entities to demonstrate grid snapping. diff --git a/examples/2d/rotation.rs b/examples/2d/rotation.rs index 43227ed5027be4..85a8542a983066 100644 --- a/examples/2d/rotation.rs +++ b/examples/2d/rotation.rs @@ -55,7 +55,7 @@ fn setup(mut commands: Commands, asset_server: Res) { let enemy_b_handle = asset_server.load("textures/simplespace/enemy_B.png"); // 2D orthographic camera - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let horizontal_margin = BOUNDS.x / 4.0; let vertical_margin = BOUNDS.y / 4.0; diff --git a/examples/2d/sprite.rs b/examples/2d/sprite.rs index 5fdf172a5be197..1866f2d4fbf19c 100644 --- a/examples/2d/sprite.rs +++ b/examples/2d/sprite.rs @@ -10,7 +10,7 @@ fn main() { } fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.spawn(SpriteBundle { texture: asset_server.load("branding/bevy_bird_dark.png"), ..default() diff --git a/examples/2d/sprite_animation.rs b/examples/2d/sprite_animation.rs index 74d3a2bf43e8f0..57d9870750d200 100644 --- a/examples/2d/sprite_animation.rs +++ b/examples/2d/sprite_animation.rs @@ -90,7 +90,7 @@ fn setup( asset_server: Res, mut texture_atlas_layouts: ResMut>, ) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // load the sprite sheet using the `AssetServer` let texture = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png"); diff --git a/examples/2d/sprite_flipping.rs b/examples/2d/sprite_flipping.rs index 33ac914995a995..60bdfa8add974a 100644 --- a/examples/2d/sprite_flipping.rs +++ b/examples/2d/sprite_flipping.rs @@ -10,7 +10,7 @@ fn main() { } fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.spawn(SpriteBundle { texture: asset_server.load("branding/bevy_bird_dark.png"), sprite: Sprite { diff --git a/examples/2d/sprite_sheet.rs b/examples/2d/sprite_sheet.rs index c1c55914b78f39..42a7cd4dc7419d 100644 --- a/examples/2d/sprite_sheet.rs +++ b/examples/2d/sprite_sheet.rs @@ -46,7 +46,7 @@ fn setup( let texture_atlas_layout = texture_atlas_layouts.add(layout); // Use only the subset of sprites in the sheet that make up the run animation let animation_indices = AnimationIndices { first: 1, last: 6 }; - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.spawn(( SpriteBundle { transform: Transform::from_scale(Vec3::splat(6.0)), diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index 7afb0c975c4d65..7da31b4667876f 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -98,7 +98,7 @@ fn spawn_sprites( } fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let font = asset_server.load("fonts/FiraSans-Bold.ttf"); let style = TextStyle { font: font.clone(), diff --git a/examples/2d/sprite_tile.rs b/examples/2d/sprite_tile.rs index c9b0fe5ecb8113..57fecefe40c7d1 100644 --- a/examples/2d/sprite_tile.rs +++ b/examples/2d/sprite_tile.rs @@ -19,7 +19,7 @@ struct AnimationState { } fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); commands.insert_resource(AnimationState { min: 128.0, max: 512.0, diff --git a/examples/2d/text2d.rs b/examples/2d/text2d.rs index bda91e19bb266a..8bee817e0c574e 100644 --- a/examples/2d/text2d.rs +++ b/examples/2d/text2d.rs @@ -42,7 +42,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }; let text_justification = JustifyText::Center; // 2d camera - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Demonstrate changing translation commands.spawn(( Text2dBundle { diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs index d910f0ae9d2309..81ce1942f76142 100644 --- a/examples/2d/texture_atlas.rs +++ b/examples/2d/texture_atlas.rs @@ -94,7 +94,7 @@ fn setup( let atlas_nearest_padded_handle = texture_atlases.add(texture_atlas_nearest_padded); // setup 2d scene - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // padded textures are to the right, unpadded to the left diff --git a/examples/2d/transparency_2d.rs b/examples/2d/transparency_2d.rs index 069b73098e688b..fbcc535ec38e38 100644 --- a/examples/2d/transparency_2d.rs +++ b/examples/2d/transparency_2d.rs @@ -11,7 +11,7 @@ fn main() { } fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); let sprite_handle = asset_server.load("branding/icon.png"); diff --git a/examples/2d/wireframe_2d.rs b/examples/2d/wireframe_2d.rs index b53b20ea1e6dbe..7b75de4ca1910d 100644 --- a/examples/2d/wireframe_2d.rs +++ b/examples/2d/wireframe_2d.rs @@ -85,7 +85,7 @@ fn setup( )); // Camera - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Text used to show controls commands.spawn( diff --git a/examples/3d/3d_scene.rs b/examples/3d/3d_scene.rs index 437a8ff1ed0b0b..5ea7e20b296626 100644 --- a/examples/3d/3d_scene.rs +++ b/examples/3d/3d_scene.rs @@ -36,8 +36,8 @@ fn setup( Transform::from_xyz(4.0, 8.0, 4.0), )); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.5, 4.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index eae6d81aed95c4..e9c2c8a7c0f0e1 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -127,10 +127,10 @@ fn setup( MeshMaterial3d(materials.add(Color::from(SILVER))), )); - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 7., 14.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + )); #[cfg(not(target_arch = "wasm32"))] commands.spawn( diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index db11844e7e90d6..8b4d4abe3ce140 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -69,8 +69,8 @@ fn setup( )); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(15.0, 5.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(15.0, 5.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } diff --git a/examples/3d/animated_material.rs b/examples/3d/animated_material.rs index 53bb7163f745a8..53b9f7253aed87 100644 --- a/examples/3d/animated_material.rs +++ b/examples/3d/animated_material.rs @@ -17,11 +17,8 @@ fn setup( mut materials: ResMut>, ) { commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(3.0, 1.0, 3.0) - .looking_at(Vec3::new(0.0, -0.5, 0.0), Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(3.0, 1.0, 3.0).looking_at(Vec3::new(0.0, -0.5, 0.0), Vec3::Y), EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/anisotropy.rs b/examples/3d/anisotropy.rs index c7b92e0eec8013..ba36c8294e7631 100644 --- a/examples/3d/anisotropy.rs +++ b/examples/3d/anisotropy.rs @@ -64,11 +64,10 @@ fn main() { /// Creates the initial scene. fn setup(mut commands: Commands, asset_server: Res, app_status: Res) { - commands.spawn(Camera3dBundle { - transform: Transform::from_translation(CAMERA_INITIAL_POSITION) - .looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_translation(CAMERA_INITIAL_POSITION).looking_at(Vec3::ZERO, Vec3::Y), + )); spawn_directional_light(&mut commands); diff --git a/examples/3d/anti_aliasing.rs b/examples/3d/anti_aliasing.rs index 2e6fca92885a29..68872297e06599 100644 --- a/examples/3d/anti_aliasing.rs +++ b/examples/3d/anti_aliasing.rs @@ -301,15 +301,12 @@ fn setup( // Camera commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, - ..default() - }, - transform: Transform::from_xyz(0.7, 0.7, 1.0) - .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + Camera3d::default(), + Camera { + hdr: true, ..default() }, + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), ContrastAdaptiveSharpening { enabled: false, ..default() diff --git a/examples/3d/atmospheric_fog.rs b/examples/3d/atmospheric_fog.rs index 43f2ce4fc81c4f..d1bc5ba912a9ea 100644 --- a/examples/3d/atmospheric_fog.rs +++ b/examples/3d/atmospheric_fog.rs @@ -25,11 +25,8 @@ fn main() { fn setup_camera_fog(mut commands: Commands) { commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(-1.0, 0.1, 1.0) - .looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(-1.0, 0.1, 1.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y), DistanceFog { color: Color::srgba(0.35, 0.48, 0.66, 1.0), directional_light_color: Color::srgba(1.0, 0.95, 0.85, 0.5), diff --git a/examples/3d/auto_exposure.rs b/examples/3d/auto_exposure.rs index 7b3a30df1dc98c..2c5803815b0b78 100644 --- a/examples/3d/auto_exposure.rs +++ b/examples/3d/auto_exposure.rs @@ -39,14 +39,12 @@ fn setup( let metering_mask = asset_server.load("textures/basic_metering_mask.png"); commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, - ..default() - }, - transform: Transform::from_xyz(1.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), + Camera3d::default(), + Camera { + hdr: true, ..default() }, + Transform::from_xyz(1.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), AutoExposure { metering_mask: metering_mask.clone(), ..default() diff --git a/examples/3d/blend_modes.rs b/examples/3d/blend_modes.rs index cbf06c879fc114..b2b4e046ecc1dd 100644 --- a/examples/3d/blend_modes.rs +++ b/examples/3d/blend_modes.rs @@ -152,10 +152,10 @@ fn setup( commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 8.0, 4.0))); // Camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0.0, 2.5, 10.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 2.5, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + )); // Controls Text diff --git a/examples/3d/bloom_3d.rs b/examples/3d/bloom_3d.rs index c237245e90c380..51207183a821f6 100644 --- a/examples/3d/bloom_3d.rs +++ b/examples/3d/bloom_3d.rs @@ -28,15 +28,13 @@ fn setup_scene( mut materials: ResMut>, ) { commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, // 1. HDR is required for bloom - ..default() - }, - tonemapping: Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + Camera3d::default(), + Camera { + hdr: true, // 1. HDR is required for bloom ..default() }, + Tonemapping::TonyMcMapface, // 2. Using a tonemapper that desaturates to white is recommended + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), // 3. Enable bloom for the camera Bloom::NATURAL, )); diff --git a/examples/3d/camera_sub_view.rs b/examples/3d/camera_sub_view.rs index 92e168a966d669..1e707676c1caa2 100644 --- a/examples/3d/camera_sub_view.rs +++ b/examples/3d/camera_sub_view.rs @@ -72,8 +72,9 @@ fn setup( )); // Main perspective Camera - commands.spawn(Camera3dBundle { - camera: Camera { + commands.spawn(( + Camera3d::default(), + Camera { viewport: Option::from(Viewport { physical_size: UVec2::new(LARGE_SIZE, LARGE_SIZE), physical_position: UVec2::new(PADDING, PADDING * 2 + SMALL_SIZE), @@ -82,12 +83,12 @@ fn setup( ..default() }, transform, - ..default() - }); + )); // Perspective camera left half - commands.spawn(Camera3dBundle { - camera: Camera { + commands.spawn(( + Camera3d::default(), + Camera { viewport: Option::from(Viewport { physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), physical_position: UVec2::new(PADDING, PADDING), @@ -109,37 +110,35 @@ fn setup( ..default() }, transform, - ..default() - }); + )); // Perspective camera moving commands.spawn(( - Camera3dBundle { - camera: Camera { - viewport: Option::from(Viewport { - physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), - physical_position: UVec2::new(PADDING * 2 + SMALL_SIZE, PADDING), - ..default() - }), - sub_camera_view: Some(SubCameraView { - // Set the sub view camera to a fifth of the full view and - // move it in another system - full_size: UVec2::new(500, 500), - offset: Vec2::ZERO, - size: UVec2::new(100, 100), - }), - order: 2, + Camera3d::default(), + Camera { + viewport: Option::from(Viewport { + physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), + physical_position: UVec2::new(PADDING * 2 + SMALL_SIZE, PADDING), ..default() - }, - transform, + }), + sub_camera_view: Some(SubCameraView { + // Set the sub view camera to a fifth of the full view and + // move it in another system + full_size: UVec2::new(500, 500), + offset: Vec2::ZERO, + size: UVec2::new(100, 100), + }), + order: 2, ..default() }, + transform, MovingCameraMarker, )); // Perspective camera control - commands.spawn(Camera3dBundle { - camera: Camera { + commands.spawn(( + Camera3d::default(), + Camera { viewport: Option::from(Viewport { physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), physical_position: UVec2::new(PADDING * 3 + SMALL_SIZE * 2, PADDING), @@ -156,17 +155,16 @@ fn setup( ..default() }, transform, - ..default() - }); + )); // Main orthographic camera - commands.spawn(Camera3dBundle { - projection: OrthographicProjection { + commands.spawn(( + Camera3d::default(), + Projection::from(OrthographicProjection { scaling_mode: ScalingMode::FixedVertical(6.0), ..OrthographicProjection::default_3d() - } - .into(), - camera: Camera { + }), + Camera { viewport: Option::from(Viewport { physical_size: UVec2::new(LARGE_SIZE, LARGE_SIZE), physical_position: UVec2::new(PADDING * 2 + LARGE_SIZE, PADDING * 2 + SMALL_SIZE), @@ -176,17 +174,16 @@ fn setup( ..default() }, transform, - ..default() - }); + )); // Orthographic camera left half - commands.spawn(Camera3dBundle { - projection: OrthographicProjection { + commands.spawn(( + Camera3d::default(), + Projection::from(OrthographicProjection { scaling_mode: ScalingMode::FixedVertical(6.0), ..OrthographicProjection::default_3d() - } - .into(), - camera: Camera { + }), + Camera { viewport: Option::from(Viewport { physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), physical_position: UVec2::new(PADDING * 5 + SMALL_SIZE * 4, PADDING), @@ -206,47 +203,43 @@ fn setup( ..default() }, transform, - ..default() - }); + )); // Orthographic camera moving commands.spawn(( - Camera3dBundle { - projection: OrthographicProjection { - scaling_mode: ScalingMode::FixedVertical(6.0), - ..OrthographicProjection::default_3d() - } - .into(), - camera: Camera { - viewport: Option::from(Viewport { - physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), - physical_position: UVec2::new(PADDING * 6 + SMALL_SIZE * 5, PADDING), - ..default() - }), - sub_camera_view: Some(SubCameraView { - // Set the sub view camera to a fifth of the full view and - // move it in another system - full_size: UVec2::new(500, 500), - offset: Vec2::ZERO, - size: UVec2::new(100, 100), - }), - order: 6, + Camera3d::default(), + Projection::from(OrthographicProjection { + scaling_mode: ScalingMode::FixedVertical(6.0), + ..OrthographicProjection::default_3d() + }), + Camera { + viewport: Option::from(Viewport { + physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), + physical_position: UVec2::new(PADDING * 6 + SMALL_SIZE * 5, PADDING), ..default() - }, - transform, + }), + sub_camera_view: Some(SubCameraView { + // Set the sub view camera to a fifth of the full view and + // move it in another system + full_size: UVec2::new(500, 500), + offset: Vec2::ZERO, + size: UVec2::new(100, 100), + }), + order: 6, ..default() }, + transform, MovingCameraMarker, )); // Orthographic camera control - commands.spawn(Camera3dBundle { - projection: OrthographicProjection { + commands.spawn(( + Camera3d::default(), + Projection::from(OrthographicProjection { scaling_mode: ScalingMode::FixedVertical(6.0), ..OrthographicProjection::default_3d() - } - .into(), - camera: Camera { + }), + Camera { viewport: Option::from(Viewport { physical_size: UVec2::new(SMALL_SIZE, SMALL_SIZE), physical_position: UVec2::new(PADDING * 7 + SMALL_SIZE * 6, PADDING), @@ -263,8 +256,7 @@ fn setup( ..default() }, transform, - ..default() - }); + )); } fn move_camera_view( diff --git a/examples/3d/clearcoat.rs b/examples/3d/clearcoat.rs index 30dd0120a3b610..9602044e232be2 100644 --- a/examples/3d/clearcoat.rs +++ b/examples/3d/clearcoat.rs @@ -189,19 +189,19 @@ fn spawn_light(commands: &mut Commands) { /// Spawns a camera with associated skybox and environment map. fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { commands - .spawn(Camera3dBundle { - camera: Camera { + .spawn(( + Camera3d::default(), + Camera { hdr: true, ..default() }, - projection: Projection::Perspective(PerspectiveProjection { + Projection::Perspective(PerspectiveProjection { fov: 27.0 / 180.0 * PI, ..default() }), - transform: Transform::from_xyz(0.0, 0.0, 10.0), - tonemapping: AcesFitted, - ..default() - }) + Transform::from_xyz(0.0, 0.0, 10.0), + AcesFitted, + )) .insert(Skybox { brightness: 5000.0, image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/color_grading.rs b/examples/3d/color_grading.rs index d40225f2d3e9c8..6762640b668612 100644 --- a/examples/3d/color_grading.rs +++ b/examples/3d/color_grading.rs @@ -352,16 +352,13 @@ fn add_text<'a>( fn add_camera(commands: &mut Commands, asset_server: &AssetServer, color_grading: ColorGrading) { commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, - ..default() - }, - transform: Transform::from_xyz(0.7, 0.7, 1.0) - .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), - color_grading, + Camera3d::default(), + Camera { + hdr: true, ..default() }, + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + color_grading, DistanceFog { color: Color::srgb_u8(43, 44, 47), falloff: FogFalloff::Linear { diff --git a/examples/3d/deferred_rendering.rs b/examples/3d/deferred_rendering.rs index abdcd2da187c98..ec6d44c291f1d4 100644 --- a/examples/3d/deferred_rendering.rs +++ b/examples/3d/deferred_rendering.rs @@ -34,18 +34,15 @@ fn setup( mut meshes: ResMut>, ) { commands.spawn(( - Camera3dBundle { - camera: Camera { - // Deferred both supports both hdr: true and hdr: false - hdr: false, - ..default() - }, - transform: Transform::from_xyz(0.7, 0.7, 1.0) - .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), - // MSAA needs to be off for Deferred rendering - msaa: Msaa::Off, + Camera3d::default(), + Camera { + // Deferred both supports both hdr: true and hdr: false + hdr: false, ..default() }, + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + // MSAA needs to be off for Deferred rendering + Msaa::Off, DistanceFog { color: Color::srgb_u8(43, 44, 47), falloff: FogFalloff::Linear { diff --git a/examples/3d/depth_of_field.rs b/examples/3d/depth_of_field.rs index e56988858ebb76..4bfe584b010df3 100644 --- a/examples/3d/depth_of_field.rs +++ b/examples/3d/depth_of_field.rs @@ -70,16 +70,16 @@ fn main() { fn setup(mut commands: Commands, asset_server: Res, app_settings: Res) { // Spawn the camera. Enable HDR and bloom, as that highlights the depth of // field effect. - let mut camera = commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0.0, 4.5, 8.25).looking_at(Vec3::ZERO, Vec3::Y), - camera: Camera { + let mut camera = commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 4.5, 8.25).looking_at(Vec3::ZERO, Vec3::Y), + Camera { hdr: true, ..default() }, - tonemapping: Tonemapping::TonyMcMapface, - ..default() - }); - camera.insert(Bloom::NATURAL); + Tonemapping::TonyMcMapface, + Bloom::NATURAL, + )); // Insert the depth of field settings. if let Some(depth_of_field) = Option::::from(*app_settings) { diff --git a/examples/3d/fog.rs b/examples/3d/fog.rs index 7ef09ef2a823fb..e63f9e255e3d8d 100644 --- a/examples/3d/fog.rs +++ b/examples/3d/fog.rs @@ -34,7 +34,7 @@ fn main() { fn setup_camera_fog(mut commands: Commands) { commands.spawn(( - Camera3dBundle::default(), + Camera3d::default(), DistanceFog { color: Color::srgb(0.25, 0.25, 0.25), falloff: FogFalloff::Linear { diff --git a/examples/3d/fog_volumes.rs b/examples/3d/fog_volumes.rs index c161240b3de672..82e38e21989ae3 100644 --- a/examples/3d/fog_volumes.rs +++ b/examples/3d/fog_volumes.rs @@ -61,23 +61,21 @@ fn setup(mut commands: Commands, asset_server: Res) { )); // Spawn a camera. - commands - .spawn(Camera3dBundle { - transform: Transform::from_xyz(-0.75, 1.0, 2.0) - .looking_at(vec3(0.0, 0.0, 0.0), Vec3::Y), - camera: Camera { - hdr: true, - ..default() - }, + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-0.75, 1.0, 2.0).looking_at(vec3(0.0, 0.0, 0.0), Vec3::Y), + Camera { + hdr: true, ..default() - }) - .insert(VolumetricFog { + }, + VolumetricFog { // Make this relatively high in order to increase the fog quality. step_count: 64, // Disable ambient light. ambient_intensity: 0.0, ..default() - }); + }, + )); } /// Rotates the camera a bit every frame. diff --git a/examples/3d/generate_custom_mesh.rs b/examples/3d/generate_custom_mesh.rs index c13cdf40ca9e3a..5718ff5ab27e1b 100644 --- a/examples/3d/generate_custom_mesh.rs +++ b/examples/3d/generate_custom_mesh.rs @@ -51,10 +51,7 @@ fn setup( Transform::from_xyz(1.8, 1.8, 1.8).looking_at(Vec3::ZERO, Vec3::Y); // Camera in 3D space. - commands.spawn(Camera3dBundle { - transform: camera_and_light_transform, - ..default() - }); + commands.spawn((Camera3d::default(), camera_and_light_transform)); // Light up the scene. commands.spawn((PointLight::default(), camera_and_light_transform)); diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 77612d11d75247..f4ef9a229128b0 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -231,16 +231,15 @@ fn spawn_main_scene(commands: &mut Commands, assets: &ExampleAssets) { } fn spawn_camera(commands: &mut Commands, assets: &ExampleAssets) { - commands - .spawn(Camera3dBundle { - transform: Transform::from_xyz(-10.012, 4.8605, 13.281).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }) - .insert(Skybox { + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-10.012, 4.8605, 13.281).looking_at(Vec3::ZERO, Vec3::Y), + Skybox { image: assets.skybox.clone(), brightness: 150.0, ..default() - }); + }, + )); } fn spawn_irradiance_volume(commands: &mut Commands, assets: &ExampleAssets) { diff --git a/examples/3d/lighting.rs b/examples/3d/lighting.rs index 8269dc289e1364..99bcf8ac965927 100644 --- a/examples/3d/lighting.rs +++ b/examples/3d/lighting.rs @@ -244,11 +244,11 @@ fn setup( ); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - exposure: Exposure::from_physical_camera(**parameters), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + Exposure::from_physical_camera(**parameters), + )); } fn update_exposure( diff --git a/examples/3d/lightmaps.rs b/examples/3d/lightmaps.rs index c77d0c72c24e52..a6899cb368e7f4 100644 --- a/examples/3d/lightmaps.rs +++ b/examples/3d/lightmaps.rs @@ -16,10 +16,10 @@ fn setup(mut commands: Commands, asset_server: Res) { GltfAssetLabel::Scene(0).from_asset("models/CornellBox/CornellBox.glb"), ))); - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-278.0, 273.0, 800.0), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-278.0, 273.0, 800.0), + )); } fn add_lightmaps_to_meshes( diff --git a/examples/3d/lines.rs b/examples/3d/lines.rs index 985ec2c6fca4ad..d755aa434a2bb3 100644 --- a/examples/3d/lines.rs +++ b/examples/3d/lines.rs @@ -59,10 +59,10 @@ fn setup( )); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } #[derive(Asset, TypePath, Default, AsBindGroup, Debug, Clone)] diff --git a/examples/3d/load_gltf.rs b/examples/3d/load_gltf.rs index 07d7e3a99a00ad..174eb5cab62c1c 100644 --- a/examples/3d/load_gltf.rs +++ b/examples/3d/load_gltf.rs @@ -17,11 +17,8 @@ fn main() { fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(0.7, 0.7, 1.0) - .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/load_gltf_extras.rs b/examples/3d/load_gltf_extras.rs index 250c230e71cb35..6ea8406fe2bafc 100644 --- a/examples/3d/load_gltf_extras.rs +++ b/examples/3d/load_gltf_extras.rs @@ -17,10 +17,10 @@ fn main() { struct ExampleDisplay; fn setup(mut commands: Commands, asset_server: Res) { - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(2.0, 2.0, 2.0).looking_at(Vec3::ZERO, Vec3::Y), + )); commands.spawn(DirectionalLight { shadows_enabled: true, diff --git a/examples/3d/meshlet.rs b/examples/3d/meshlet.rs index f7c9bc3a52dfdd..21d1bb0f9bdb28 100644 --- a/examples/3d/meshlet.rs +++ b/examples/3d/meshlet.rs @@ -49,12 +49,9 @@ fn setup( mut meshes: ResMut>, ) { commands.spawn(( - Camera3dBundle { - transform: Transform::from_translation(Vec3::new(1.8, 0.4, -0.1)) - .looking_at(Vec3::ZERO, Vec3::Y), - msaa: Msaa::Off, - ..default() - }, + Camera3d::default(), + Transform::from_translation(Vec3::new(1.8, 0.4, -0.1)).looking_at(Vec3::ZERO, Vec3::Y), + Msaa::Off, EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/motion_blur.rs b/examples/3d/motion_blur.rs index 8d2ebbcaf8a44a..3f9e2f50040152 100644 --- a/examples/3d/motion_blur.rs +++ b/examples/3d/motion_blur.rs @@ -18,7 +18,7 @@ fn main() { fn setup_camera(mut commands: Commands) { commands.spawn(( - Camera3dBundle::default(), + Camera3d::default(), // Add the `MotionBlur` component to a camera to enable motion blur. // Motion blur requires the depth and motion vector prepass, which this bundle adds. // Configure the amount and quality of motion blur per-camera using this component. diff --git a/examples/3d/orthographic.rs b/examples/3d/orthographic.rs index be24a361d34cfc..4b4070285cbaa2 100644 --- a/examples/3d/orthographic.rs +++ b/examples/3d/orthographic.rs @@ -16,16 +16,15 @@ fn setup( mut materials: ResMut>, ) { // camera - commands.spawn(Camera3dBundle { - projection: OrthographicProjection { + commands.spawn(( + Camera3d::default(), + Projection::from(OrthographicProjection { // 6 world units per window height. scaling_mode: ScalingMode::FixedVertical(6.0), ..OrthographicProjection::default_3d() - } - .into(), - transform: Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + }), + Transform::from_xyz(5.0, 5.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); // plane commands.spawn(( diff --git a/examples/3d/parallax_mapping.rs b/examples/3d/parallax_mapping.rs index eee56aa817b13a..1cc39aaad65533 100644 --- a/examples/3d/parallax_mapping.rs +++ b/examples/3d/parallax_mapping.rs @@ -212,10 +212,8 @@ fn setup( // Camera commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(1.5, 1.5, 1.5).looking_at(Vec3::ZERO, Vec3::Y), CameraController, )); diff --git a/examples/3d/parenting.rs b/examples/3d/parenting.rs index 4be7173e41ad69..348de61a2d8c54 100644 --- a/examples/3d/parenting.rs +++ b/examples/3d/parenting.rs @@ -53,8 +53,8 @@ fn setup( // light commands.spawn((PointLight::default(), Transform::from_xyz(4.0, 5.0, -4.0))); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(5.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(5.0, 10.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } diff --git a/examples/3d/pbr.rs b/examples/3d/pbr.rs index fef786f562110d..5442a026fb9817 100644 --- a/examples/3d/pbr.rs +++ b/examples/3d/pbr.rs @@ -114,15 +114,12 @@ fn setup( // camera commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y), - projection: OrthographicProjection { - scaling_mode: ScalingMode::WindowSize(100.0), - ..OrthographicProjection::default_3d() - } - .into(), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::default(), Vec3::Y), + Projection::from(OrthographicProjection { + scaling_mode: ScalingMode::WindowSize(100.0), + ..OrthographicProjection::default_3d() + }), EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/pcss.rs b/examples/3d/pcss.rs index ee790004655c62..04b94a228b9bb6 100644 --- a/examples/3d/pcss.rs +++ b/examples/3d/pcss.rs @@ -151,16 +151,12 @@ fn setup(mut commands: Commands, asset_server: Res, app_status: Res /// Spawns the camera, with the initial shadow filtering method. fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { commands - .spawn(Camera3dBundle { - transform: Transform::from_xyz(-12.912 * 0.7, 4.466 * 0.7, -10.624 * 0.7) - .with_rotation(Quat::from_euler( - EulerRot::YXZ, - -134.76 / 180.0 * PI, - -0.175, - 0.0, - )), - ..default() - }) + .spawn(( + Camera3d::default(), + Transform::from_xyz(-12.912 * 0.7, 4.466 * 0.7, -10.624 * 0.7).with_rotation( + Quat::from_euler(EulerRot::YXZ, -134.76 / 180.0 * PI, -0.175, 0.0), + ), + )) .insert(ShadowFilteringMethod::Gaussian) // `TemporalJitter` is needed for TAA. Note that it does nothing without // `TemporalAntiAliasSettings`. diff --git a/examples/3d/post_processing.rs b/examples/3d/post_processing.rs index 8e033c0acf713e..7355171ff33c94 100644 --- a/examples/3d/post_processing.rs +++ b/examples/3d/post_processing.rs @@ -59,15 +59,12 @@ fn setup(mut commands: Commands, asset_server: Res, app_settings: R /// Spawns the camera, including the [`ChromaticAberration`] component. fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, - ..default() - }, - transform: Transform::from_xyz(0.7, 0.7, 1.0) - .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), + Camera3d::default(), + Camera { + hdr: true, ..default() }, + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::Y), DistanceFog { color: Color::srgb_u8(43, 44, 47), falloff: FogFalloff::Linear { diff --git a/examples/3d/query_gltf_primitives.rs b/examples/3d/query_gltf_primitives.rs index 9a58e74b9c082c..11af68c71f69be 100644 --- a/examples/3d/query_gltf_primitives.rs +++ b/examples/3d/query_gltf_primitives.rs @@ -54,11 +54,8 @@ fn find_top_material_and_mesh( fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(0.6, 1.6, 11.3) - .looking_at(Vec3::new(0.0, 0.0, 3.0), Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(0.6, 1.6, 11.3).looking_at(Vec3::new(0.0, 0.0, 3.0), Vec3::Y), EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/reflection_probes.rs b/examples/3d/reflection_probes.rs index 1a4fd3cfe4cad9..17c76576e7aa60 100644 --- a/examples/3d/reflection_probes.rs +++ b/examples/3d/reflection_probes.rs @@ -103,14 +103,14 @@ fn spawn_scene(commands: &mut Commands, asset_server: &AssetServer) { // Spawns the camera. fn spawn_camera(commands: &mut Commands) { - commands.spawn(Camera3dBundle { - camera: Camera { + commands.spawn(( + Camera3d::default(), + Camera { hdr: true, ..default() }, - transform: Transform::from_xyz(-6.483, 0.325, 4.381).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + Transform::from_xyz(-6.483, 0.325, 4.381).looking_at(Vec3::ZERO, Vec3::Y), + )); } // Creates the sphere mesh and spawns it. diff --git a/examples/3d/render_to_texture.rs b/examples/3d/render_to_texture.rs index 519792c5a2269d..40ec99eac38c4a 100644 --- a/examples/3d/render_to_texture.rs +++ b/examples/3d/render_to_texture.rs @@ -84,16 +84,13 @@ fn setup( )); commands.spawn(( - Camera3dBundle { - camera: Camera { - target: image_handle.clone().into(), - clear_color: Color::WHITE.into(), - ..default() - }, - transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) - .looking_at(Vec3::ZERO, Vec3::Y), + Camera3d::default(), + Camera { + target: image_handle.clone().into(), + clear_color: Color::WHITE.into(), ..default() }, + Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)).looking_at(Vec3::ZERO, Vec3::Y), first_pass_layer, )); @@ -117,10 +114,10 @@ fn setup( )); // The main pass camera. - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, 15.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } /// Rotates the inner cube (first pass) diff --git a/examples/3d/rotate_environment_map.rs b/examples/3d/rotate_environment_map.rs index 68f20d5fb64c18..1071a119d30cf2 100644 --- a/examples/3d/rotate_environment_map.rs +++ b/examples/3d/rotate_environment_map.rs @@ -93,19 +93,19 @@ fn spawn_light(commands: &mut Commands) { /// Spawns a camera with associated skybox and environment map. fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { commands - .spawn(Camera3dBundle { - camera: Camera { + .spawn(( + Camera3d::default(), + Camera { hdr: true, ..default() }, - projection: Projection::Perspective(PerspectiveProjection { + Projection::Perspective(PerspectiveProjection { fov: 27.0 / 180.0 * PI, ..default() }), - transform: Transform::from_xyz(0.0, 0.0, 10.0), - tonemapping: AcesFitted, - ..default() - }) + Transform::from_xyz(0.0, 0.0, 10.0), + AcesFitted, + )) .insert(Skybox { brightness: 5000.0, image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/scrolling_fog.rs b/examples/3d/scrolling_fog.rs index 4085ba20978464..8b0427ff0079f5 100644 --- a/examples/3d/scrolling_fog.rs +++ b/examples/3d/scrolling_fog.rs @@ -49,14 +49,10 @@ fn setup( ) { // Spawn camera with temporal anti-aliasing and a VolumetricFog configuration. commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(0.0, 2.0, 0.0) - .looking_at(Vec3::new(-5.0, 3.5, -6.0), Vec3::Y), - camera: Camera { - hdr: true, - ..default() - }, - msaa: Msaa::Off, + Camera3d::default(), + Transform::from_xyz(0.0, 2.0, 0.0).looking_at(Vec3::new(-5.0, 3.5, -6.0), Vec3::Y), + Camera { + hdr: true, ..default() }, TemporalAntiAliasing::default(), diff --git a/examples/3d/shadow_biases.rs b/examples/3d/shadow_biases.rs index bc0a62819a8de8..6b5d2d86eaf761 100644 --- a/examples/3d/shadow_biases.rs +++ b/examples/3d/shadow_biases.rs @@ -73,11 +73,8 @@ fn setup( // camera commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(-1.0, 1.0, 1.0) - .looking_at(Vec3::new(-1.0, 1.0, 0.0), Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(-1.0, 1.0, 1.0).looking_at(Vec3::new(-1.0, 1.0, 0.0), Vec3::Y), CameraController::default(), ShadowFilteringMethod::Hardware2x2, )); diff --git a/examples/3d/shadow_caster_receiver.rs b/examples/3d/shadow_caster_receiver.rs index b9cb8382bd7b7b..edc8631aa9abe2 100644 --- a/examples/3d/shadow_caster_receiver.rs +++ b/examples/3d/shadow_caster_receiver.rs @@ -98,11 +98,10 @@ fn setup( )); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-5.0, 5.0, 5.0) - .looking_at(Vec3::new(-1.0, 1.0, 0.0), Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-5.0, 5.0, 5.0).looking_at(Vec3::new(-1.0, 1.0, 0.0), Vec3::Y), + )); } fn toggle_light( diff --git a/examples/3d/skybox.rs b/examples/3d/skybox.rs index e499c809a55cf7..aaf62d33035c6a 100644 --- a/examples/3d/skybox.rs +++ b/examples/3d/skybox.rs @@ -71,10 +71,8 @@ fn setup(mut commands: Commands, asset_server: Res) { let skybox_handle = asset_server.load(CUBEMAPS[0].0); // camera commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), CameraController::default(), Skybox { image: skybox_handle.clone(), diff --git a/examples/3d/spherical_area_lights.rs b/examples/3d/spherical_area_lights.rs index e311d1af14156e..c3e945a8f34929 100644 --- a/examples/3d/spherical_area_lights.rs +++ b/examples/3d/spherical_area_lights.rs @@ -19,10 +19,10 @@ fn setup( mut materials: ResMut>, ) { // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0.2, 1.5, 2.5).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0.2, 1.5, 2.5).looking_at(Vec3::ZERO, Vec3::Y), + )); // plane commands.spawn(( diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index 95b6ac52c5f180..b7ca9c5da5a618 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -68,14 +68,11 @@ fn setup( { let camera = commands .spawn(( - Camera3dBundle { - transform: Transform::from_translation(*camera_pos) - .looking_at(Vec3::ZERO, Vec3::Y), - camera: Camera { - // Renders cameras with different priorities to prevent ambiguities - order: index as isize, - ..default() - }, + Camera3d::default(), + Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y), + Camera { + // Renders cameras with different priorities to prevent ambiguities + order: index as isize, ..default() }, CameraPosition { diff --git a/examples/3d/spotlight.rs b/examples/3d/spotlight.rs index b4a900f79deefb..48d891252cdfdf 100644 --- a/examples/3d/spotlight.rs +++ b/examples/3d/spotlight.rs @@ -117,14 +117,14 @@ fn setup( } // camera - commands.spawn(Camera3dBundle { - camera: Camera { + commands.spawn(( + Camera3d::default(), + Camera { hdr: true, ..default() }, - transform: Transform::from_xyz(-4.0, 5.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + Transform::from_xyz(-4.0, 5.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + )); commands.spawn( TextBundle::from_section(INSTRUCTIONS, TextStyle::default()).with_style(Style { diff --git a/examples/3d/ssao.rs b/examples/3d/ssao.rs index e8b13978486087..5ae09985690cf2 100644 --- a/examples/3d/ssao.rs +++ b/examples/3d/ssao.rs @@ -27,15 +27,13 @@ fn setup( mut materials: ResMut>, ) { commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, - ..default() - }, - transform: Transform::from_xyz(-2.0, 2.0, -2.0).looking_at(Vec3::ZERO, Vec3::Y), - msaa: Msaa::Off, + Camera3d::default(), + Camera { + hdr: true, ..default() }, + Transform::from_xyz(-2.0, 2.0, -2.0).looking_at(Vec3::ZERO, Vec3::Y), + Msaa::Off, ScreenSpaceAmbientOcclusion::default(), TemporalAntiAliasing::default(), )); diff --git a/examples/3d/ssr.rs b/examples/3d/ssr.rs index eabb0f31c9cfcc..4153ab619b7b69 100644 --- a/examples/3d/ssr.rs +++ b/examples/3d/ssr.rs @@ -225,16 +225,15 @@ fn spawn_camera(commands: &mut Commands, asset_server: &AssetServer) { // rendering by adding depth and deferred prepasses. Turn on FXAA to make // the scene look a little nicer. Finally, add screen space reflections. commands - .spawn(Camera3dBundle { - transform: Transform::from_translation(vec3(-1.25, 2.25, 4.5)) - .looking_at(Vec3::ZERO, Vec3::Y), - camera: Camera { + .spawn(( + Camera3d::default(), + Transform::from_translation(vec3(-1.25, 2.25, 4.5)).looking_at(Vec3::ZERO, Vec3::Y), + Camera { hdr: true, ..default() }, - msaa: Msaa::Off, - ..default() - }) + Msaa::Off, + )) .insert(EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/texture.rs b/examples/3d/texture.rs index 31606609bedf80..4b3e70dd5bffd1 100644 --- a/examples/3d/texture.rs +++ b/examples/3d/texture.rs @@ -71,8 +71,8 @@ fn setup( Transform::from_xyz(0.0, 0.0, -1.5).with_rotation(Quat::from_rotation_x(-PI / 5.0)), )); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(3.0, 5.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(3.0, 5.0, 8.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } diff --git a/examples/3d/tonemapping.rs b/examples/3d/tonemapping.rs index aa27c822b16468..8129b3e8e0aaf7 100644 --- a/examples/3d/tonemapping.rs +++ b/examples/3d/tonemapping.rs @@ -58,14 +58,12 @@ fn setup( ) { // camera commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, - ..default() - }, - transform: camera_transform.0, + Camera3d::default(), + Camera { + hdr: true, ..default() }, + camera_transform.0, DistanceFog { color: Color::srgb_u8(43, 44, 47), falloff: FogFalloff::Linear { diff --git a/examples/3d/transmission.rs b/examples/3d/transmission.rs index 62b5cca5b2b0c6..ae637d2267aba3 100644 --- a/examples/3d/transmission.rs +++ b/examples/3d/transmission.rs @@ -302,25 +302,23 @@ fn setup( // Camera commands.spawn(( - Camera3dBundle { - camera: Camera { - hdr: true, - ..default() - }, - transform: Transform::from_xyz(1.0, 1.8, 7.0).looking_at(Vec3::ZERO, Vec3::Y), - color_grading: ColorGrading { - global: ColorGradingGlobal { - post_saturation: 1.2, - ..default() - }, + Camera3d::default(), + Camera { + hdr: true, + ..default() + }, + Transform::from_xyz(1.0, 1.8, 7.0).looking_at(Vec3::ZERO, Vec3::Y), + ColorGrading { + global: ColorGradingGlobal { + post_saturation: 1.2, ..default() }, - tonemapping: Tonemapping::TonyMcMapface, - exposure: Exposure { ev100: 6.0 }, - #[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))] - msaa: Msaa::Off, ..default() }, + Tonemapping::TonyMcMapface, + Exposure { ev100: 6.0 }, + #[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))] + Msaa::Off, #[cfg(not(all(feature = "webgl2", target_arch = "wasm32")))] TemporalAntiAliasing::default(), EnvironmentMapLight { diff --git a/examples/3d/transparency_3d.rs b/examples/3d/transparency_3d.rs index f08f593fdbbc04..ab5426e13328e3 100644 --- a/examples/3d/transparency_3d.rs +++ b/examples/3d/transparency_3d.rs @@ -90,10 +90,10 @@ fn setup( )); // Camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 3.0, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } /// Fades the alpha channel of all materials between 0 and 1 over time. diff --git a/examples/3d/two_passes.rs b/examples/3d/two_passes.rs index ac97a240b7c8c1..f80125472e7b0b 100644 --- a/examples/3d/two_passes.rs +++ b/examples/3d/two_passes.rs @@ -38,20 +38,20 @@ fn setup( )); // Camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(10.0, 10., -5.0).looking_at(Vec3::ZERO, Vec3::Y), - camera: Camera { + commands.spawn(( + Camera3d::default(), + Camera { // renders after / on top of the main camera order: 1, clear_color: ClearColorConfig::None, ..default() }, - ..default() - }); + Transform::from_xyz(10.0, 10., -5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } diff --git a/examples/3d/update_gltf_scene.rs b/examples/3d/update_gltf_scene.rs index ac9df29e6d909d..7c14ea0237e03d 100644 --- a/examples/3d/update_gltf_scene.rs +++ b/examples/3d/update_gltf_scene.rs @@ -24,11 +24,8 @@ fn setup(mut commands: Commands, asset_server: Res) { }, )); commands.spawn(( - Camera3dBundle { - transform: Transform::from_xyz(-0.5, 0.9, 1.5) - .looking_at(Vec3::new(-0.5, 0.3, 0.0), Vec3::Y), - ..default() - }, + Camera3d::default(), + Transform::from_xyz(-0.5, 0.9, 1.5).looking_at(Vec3::new(-0.5, 0.3, 0.0), Vec3::Y), EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/vertex_colors.rs b/examples/3d/vertex_colors.rs index 315d370a136948..90eb67abd7ce3d 100644 --- a/examples/3d/vertex_colors.rs +++ b/examples/3d/vertex_colors.rs @@ -51,8 +51,8 @@ fn setup( )); // Camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); } diff --git a/examples/3d/visibility_range.rs b/examples/3d/visibility_range.rs index b237d1301d24f0..f8f90919575eec 100644 --- a/examples/3d/visibility_range.rs +++ b/examples/3d/visibility_range.rs @@ -137,10 +137,10 @@ fn setup( // Spawn a camera. commands - .spawn(Camera3dBundle { - transform: Transform::from_xyz(0.7, 0.7, 1.0).looking_at(CAMERA_FOCAL_POINT, Vec3::Y), - ..default() - }) + .spawn(( + Camera3d::default(), + Transform::from_xyz(0.7, 0.7, 1.0).looking_at(CAMERA_FOCAL_POINT, Vec3::Y), + )) .insert(EnvironmentMapLight { diffuse_map: asset_server.load("environment_maps/pisa_diffuse_rgb9e5_zstd.ktx2"), specular_map: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), diff --git a/examples/3d/volumetric_fog.rs b/examples/3d/volumetric_fog.rs index b100ebc923484e..8f710bb85cac96 100644 --- a/examples/3d/volumetric_fog.rs +++ b/examples/3d/volumetric_fog.rs @@ -63,17 +63,16 @@ fn setup(mut commands: Commands, asset_server: Res, app_settings: R // Spawn the camera. commands - .spawn(Camera3dBundle { - transform: Transform::from_xyz(-1.7, 1.5, 4.5) - .looking_at(vec3(-1.5, 1.7, 3.5), Vec3::Y), - camera: Camera { + .spawn(( + Camera3d::default(), + Camera { hdr: true, ..default() }, - ..default() - }) - .insert(Tonemapping::TonyMcMapface) - .insert(Bloom::default()) + Transform::from_xyz(-1.7, 1.5, 4.5).looking_at(vec3(-1.5, 1.7, 3.5), Vec3::Y), + Tonemapping::TonyMcMapface, + Bloom::default(), + )) .insert(Skybox { image: asset_server.load("environment_maps/pisa_specular_rgb9e5_zstd.ktx2"), brightness: 1000.0, diff --git a/examples/3d/wireframe.rs b/examples/3d/wireframe.rs index e424c5c58924dc..9509c689dfe1b6 100644 --- a/examples/3d/wireframe.rs +++ b/examples/3d/wireframe.rs @@ -93,10 +93,10 @@ fn setup( commands.spawn((PointLight::default(), Transform::from_xyz(2.0, 4.0, 2.0))); // camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); // Text used to show controls commands.spawn( diff --git a/examples/animation/animated_fox.rs b/examples/animation/animated_fox.rs index 73a662526e1905..1cc0192b914897 100644 --- a/examples/animation/animated_fox.rs +++ b/examples/animation/animated_fox.rs @@ -52,11 +52,10 @@ fn setup( }); // Camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(100.0, 100.0, 150.0) - .looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(100.0, 100.0, 150.0).looking_at(Vec3::new(0.0, 20.0, 0.0), Vec3::Y), + )); // Plane commands.spawn(( diff --git a/examples/animation/animated_transform.rs b/examples/animation/animated_transform.rs index b6ca201cc59178..f5eecd3d8ddb26 100644 --- a/examples/animation/animated_transform.rs +++ b/examples/animation/animated_transform.rs @@ -26,10 +26,10 @@ fn setup( mut graphs: ResMut>, ) { // Camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + )); // Light commands.spawn(( diff --git a/examples/animation/animated_ui.rs b/examples/animation/animated_ui.rs index 448f692e0962c7..c04abb6a0a2db8 100644 --- a/examples/animation/animated_ui.rs +++ b/examples/animation/animated_ui.rs @@ -144,7 +144,7 @@ fn setup( animation_player.play(animation_node_index).repeat(); // Add a camera. - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // Build the UI. We have a parent node that covers the whole screen and // contains the `AnimationPlayer`, as well as a child node that contains the diff --git a/examples/animation/animation_graph.rs b/examples/animation/animation_graph.rs index 4336151fefa64a..ecc47389d2ce10 100644 --- a/examples/animation/animation_graph.rs +++ b/examples/animation/animation_graph.rs @@ -218,10 +218,10 @@ fn setup_scene( mut meshes: ResMut>, mut materials: ResMut>, ) { - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-10.0, 5.0, 13.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-10.0, 5.0, 13.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + )); commands.spawn(( PointLight { diff --git a/examples/animation/animation_masks.rs b/examples/animation/animation_masks.rs index 4a9177074dd062..9057ae9e8d01ba 100644 --- a/examples/animation/animation_masks.rs +++ b/examples/animation/animation_masks.rs @@ -122,11 +122,10 @@ fn setup_scene( mut materials: ResMut>, ) { // Spawn the camera. - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(-15.0, 10.0, 20.0) - .looking_at(Vec3::new(0., 1., 0.), Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(-15.0, 10.0, 20.0).looking_at(Vec3::new(0., 1., 0.), Vec3::Y), + )); // Spawn the light. commands.spawn(( diff --git a/examples/animation/color_animation.rs b/examples/animation/color_animation.rs index e1b364e39b5e8f..dff3175c4e9c35 100644 --- a/examples/animation/color_animation.rs +++ b/examples/animation/color_animation.rs @@ -35,7 +35,7 @@ fn main() { } fn setup(mut commands: Commands) { - commands.spawn(Camera2dBundle::default()); + commands.spawn(Camera2d); // The color spaces `Oklaba`, `Laba`, `LinearRgba`, `Srgba` and `Xyza` all are either perceptually or physically linear. // This property allows us to define curves, e.g. bezier curves through these spaces. diff --git a/examples/animation/cubic_curve.rs b/examples/animation/cubic_curve.rs index 2c2ed2486fe662..702346cdb28307 100644 --- a/examples/animation/cubic_curve.rs +++ b/examples/animation/cubic_curve.rs @@ -62,10 +62,10 @@ fn setup( )); // The camera - commands.spawn(Camera3dBundle { - transform: Transform::from_xyz(0., 6., 12.).looking_at(Vec3::new(0., 3., 0.), Vec3::Y), - ..default() - }); + commands.spawn(( + Camera3d::default(), + Transform::from_xyz(0., 6., 12.).looking_at(Vec3::new(0., 3., 0.), Vec3::Y), + )); } fn animate_cube(time: Res

where P: AnimatableProperty, @@ -346,8 +345,7 @@ pub struct TranslationCurve(pub C); /// /// You shouldn't need to instantiate this manually; Bevy will automatically do /// so. -#[derive(Reflect, FromReflect)] -#[reflect(from_reflect = false)] +#[derive(Reflect)] pub struct TranslationCurveEvaluator { evaluator: BasicAnimationCurveEvaluator, } @@ -444,8 +442,7 @@ pub struct RotationCurve(pub C); /// /// You shouldn't need to instantiate this manually; Bevy will automatically do /// so. -#[derive(Reflect, FromReflect)] -#[reflect(from_reflect = false)] +#[derive(Reflect)] pub struct RotationCurveEvaluator { evaluator: BasicAnimationCurveEvaluator, } @@ -542,8 +539,7 @@ pub struct ScaleCurve(pub C); /// /// You shouldn't need to instantiate this manually; Bevy will automatically do /// so. -#[derive(Reflect, FromReflect)] -#[reflect(from_reflect = false)] +#[derive(Reflect)] pub struct ScaleCurveEvaluator { evaluator: BasicAnimationCurveEvaluator, } @@ -636,8 +632,7 @@ impl AnimationCurveEvaluator for ScaleCurveEvaluator { #[reflect(from_reflect = false)] pub struct WeightsCurve(pub C); -#[derive(Reflect, FromReflect)] -#[reflect(from_reflect = false)] +#[derive(Reflect)] struct WeightsCurveEvaluator { /// The values of the stack, in which each element is a list of morph target /// weights. @@ -825,8 +820,7 @@ impl AnimationCurveEvaluator for WeightsCurveEvaluator { } } -#[derive(Reflect, FromReflect)] -#[reflect(from_reflect = false)] +#[derive(Reflect)] struct BasicAnimationCurveEvaluator where A: Animatable, @@ -835,8 +829,7 @@ where blend_register: Option<(A, f32)>, } -#[derive(Reflect, FromReflect)] -#[reflect(from_reflect = false)] +#[derive(Reflect)] struct BasicAnimationCurveEvaluatorStackElement where A: Animatable, From 7c03ca2562781ad74092cf9798e81e0bcfac284f Mon Sep 17 00:00:00 2001 From: mgi388 <135186256+mgi388@users.noreply.github.com> Date: Sun, 6 Oct 2024 09:24:03 +1100 Subject: [PATCH 030/546] Fix QuerySingle -> Single missed in example (#15667) Missed in https://github.com/bevyengine/bevy/pull/15507 --- examples/ecs/fallible_params.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/ecs/fallible_params.rs b/examples/ecs/fallible_params.rs index b4f2ec7a19e2a2..6100154c2828ef 100644 --- a/examples/ecs/fallible_params.rs +++ b/examples/ecs/fallible_params.rs @@ -134,9 +134,9 @@ fn move_targets(mut enemies: Populated<(&mut Transform, &mut Enemy)>, time: Res< /// If there is one, player will track it. /// If there are too many enemies, the player will cease all action (the system will not run). fn move_pointer( - // `QuerySingle` ensures the system runs ONLY when exactly one matching entity exists. + // `Single` ensures the system runs ONLY when exactly one matching entity exists. mut player: Single<(&mut Transform, &Player)>, - // `Option` ensures that the system runs ONLY when zero or one matching entity exists. + // `Option` ensures that the system runs ONLY when zero or one matching entity exists. enemy: Option, Without)>>, time: Res

Release notes

Sourced from crate-ci/typos's releases.

v1.25.0

[1.25.0] - 2024-10-01

Fixes

Changelog

Sourced from crate-ci/typos's changelog.

[1.25.0] - 2024-10-01

Fixes

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=crate-ci/typos&package-manager=github_actions&previous-version=1.24.6&new-version=1.25.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0303f1dc9ca594..4b8bcf4454dc40 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -218,7 +218,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Check for typos - uses: crate-ci/typos@v1.24.6 + uses: crate-ci/typos@v1.25.0 - name: Typos info if: failure() run: | From d3657a04cdea09588ea4831fb911c7caa5df6a7b Mon Sep 17 00:00:00 2001 From: Matty Date: Mon, 7 Oct 2024 03:30:00 -0400 Subject: [PATCH 046/546] Fixes to animation graph evaluation (#15689) # Objective Fix a couple of substantial errors found during the development of #15665: - `AnimationCurveEvaluator::add` was secretly unreachable. In other words, additive blending never actually occurred. - Weights from the animation graph nodes were ignored, and only `ActiveAnimation`'s weights were used. ## Solution Made additive blending reachable and included the graph node weight in the weight of the stack elements appended in the curve application loop of `animate_targets`. ## Testing Tested on existing examples and on the new example added in #15665. --- crates/bevy_animation/src/lib.rs | 39 ++++++++++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/bevy_animation/src/lib.rs b/crates/bevy_animation/src/lib.rs index 8e74f5ddb15ba9..c4d54d7f5fa34c 100755 --- a/crates/bevy_animation/src/lib.rs +++ b/crates/bevy_animation/src/lib.rs @@ -1102,7 +1102,7 @@ pub fn animate_targets( }; match animation_graph_node.node_type { - AnimationNodeType::Blend | AnimationNodeType::Add => { + AnimationNodeType::Blend => { // This is a blend node. for edge_index in threaded_animation_graph.sorted_edge_ranges [animation_graph_node_index.index()] @@ -1123,6 +1123,27 @@ pub fn animate_targets( } } + AnimationNodeType::Add => { + // This is an additive blend node. + for edge_index in threaded_animation_graph.sorted_edge_ranges + [animation_graph_node_index.index()] + .clone() + { + if let Err(err) = evaluation_state + .add_all(threaded_animation_graph.sorted_edges[edge_index as usize]) + { + warn!("Failed to blend animation: {:?}", err); + } + } + + if let Err(err) = evaluation_state.push_blend_register_all( + animation_graph_node.weight, + animation_graph_node_index, + ) { + warn!("Animation blending failed: {:?}", err); + } + } + AnimationNodeType::Clip(ref animation_clip_handle) => { // This is a clip node. let Some(active_animation) = animation_player @@ -1175,7 +1196,7 @@ pub fn animate_targets( continue; }; - let weight = active_animation.weight; + let weight = active_animation.weight * animation_graph_node.weight; let seek_time = active_animation.seek_time; for curve in curves { @@ -1323,6 +1344,20 @@ impl AnimationEvaluationState { Ok(()) } + /// Calls [`AnimationCurveEvaluator::add`] on all curve evaluator types + /// that we've been building up for a single target. + /// + /// The given `node_index` is the node that we're evaluating. + fn add_all(&mut self, node_index: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + for curve_evaluator_type in self.current_curve_evaluator_types.keys() { + self.curve_evaluators + .get_mut(curve_evaluator_type) + .unwrap() + .add(node_index)?; + } + Ok(()) + } + /// Calls [`AnimationCurveEvaluator::push_blend_register`] on all curve /// evaluator types that we've been building up for a single target. /// From 9a61e83d73ea6cad25da0805e2ce3f740b94cf59 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:30:37 +0200 Subject: [PATCH 047/546] Bump actions/setup-java from 3 to 4 (#15695) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [actions/setup-java](https://github.com/actions/setup-java) from 3 to 4.
Release notes

Sourced from actions/setup-java's releases.

v4.0.0

What's Changed

In the scope of this release, the version of the Node.js runtime was updated to 20. The majority of dependencies were updated to the latest versions. From now on, the code for the setup-java will run on Node.js 20 instead of Node.js 16.

Breaking changes

Non-breaking changes

New Contributors

Full Changelog: https://github.com/actions/setup-java/compare/v3...v4.0.0

v3.13.0

What's changed

In the scope of this release, support for Dragonwell JDK was added by @​Accelerator1996 in actions/setup-java#532

steps:
 - name: Checkout
   uses: actions/checkout@v3
 - name: Setup-java
   uses: actions/setup-java@v3
   with:
     distribution: 'dragonwell'
     java-version: '17'

Several inaccuracies were also fixed:

New Contributors

Full Changelog: https://github.com/actions/setup-java/compare/v3...v3.13.0

v3.12.0

... (truncated)

Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=actions/setup-java&package-manager=github_actions&previous-version=3&new-version=4)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/daily.yml | 2 +- .github/workflows/validation-jobs.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index b9e8959df08c8c..d187165a763e9c 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -47,7 +47,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index 3372fc1b0353c8..e45492ecebb762 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -50,7 +50,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Set up JDK 17 - uses: actions/setup-java@v3 + uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' From aa56d4831ac63f95e1fe158ec794d8fe53695063 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 07:31:17 +0000 Subject: [PATCH 048/546] Update sysinfo requirement from 0.31.0 to 0.32.0 (#15697) Updates the requirements on [sysinfo](https://github.com/GuillaumeGomez/sysinfo) to permit the latest version.
Changelog

Sourced from sysinfo's changelog.

0.32.0

  • Add new Disk::is_read_only API.
  • Add new remove_dead_processes argument to System::refresh_processes and System::refresh_processes_specifics.
  • macOS: Fix memory leak in disk refresh.

0.31.4

  • macOS: Force memory cleanup in disk list retrieval.

0.31.3

  • Raspberry Pi: Fix temperature retrieval.

0.31.2

  • Remove bstr dependency (needed for rustc development).

0.31.1

  • Downgrade version of memchr (needed for rustc development).

0.31.0

  • Split crate in features to only enable what you need.
  • Remove System::refresh_process, System::refresh_process_specifics and System::refresh_pids methods.
  • Add new argument of type ProcessesToUpdate to System::refresh_processes and System::refresh_processes_specifics methods.
  • Add new NetworkData::ip_networks method.
  • Add new System::refresh_cpu_list method.
  • Global CPU now only contains CPU usage.
  • Rename TermalSensorType to ThermalSensorType.
  • Process names is now an OsString.
  • Remove System::global_cpu_info.
  • Add System::global_cpu_usage.
  • macOS: Fix invalid CPU computation when single processes are refreshed one after the other.
  • Windows: Fix virtual memory computation.
  • Windows: Fix WoW64 parent process refresh.
  • Linux: Retrieve RSS (Resident Set Size) memory for cgroups.

0.30.13

  • macOS: Fix segfault when calling Components::refresh_list multiple times.
  • Windows: Fix CPU arch retrieval.

0.30.12

  • FreeBSD: Fix network interfaces retrieval (one was always missing).

0.30.11

... (truncated)

Commits
  • e022ae4 Merge pull request #1354 from GuillaumeGomez/update
  • 0c5ca6a Update migration guide for 0.32
  • 9f14cba Update crate version to 0.32.0
  • eb7f147 Update CHANGELOG for 0.32.0
  • 9c86e25 Fix new clippy lints
  • 2fb2903 Merge pull request #1353 from GuillaumeGomez/rm-dead-processes
  • 7452b8d Update System::refresh_processes API to give control over when to remove de...
  • 6f1d382 Merge pull request #1348 from kevinbaker/master
  • 6d5ea97 add dependency on windows SystemServices for disk
  • 1c87f50 win: add correct location of FILE_READ_ONLY_VOLUME, correct call
  • Additional commits viewable in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot show ignore conditions` will show all of the ignore conditions of the specified dependency - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- crates/bevy_diagnostic/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/bevy_diagnostic/Cargo.toml b/crates/bevy_diagnostic/Cargo.toml index 972493e22d1b1d..cc9d7f1019ee08 100644 --- a/crates/bevy_diagnostic/Cargo.toml +++ b/crates/bevy_diagnostic/Cargo.toml @@ -27,14 +27,14 @@ const-fnv1a-hash = "1.1.0" # macOS [target.'cfg(all(target_os="macos"))'.dependencies] # Some features of sysinfo are not supported by apple. This will disable those features on apple devices -sysinfo = { version = "0.31.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.32.0", optional = true, default-features = false, features = [ "apple-app-store", "system", ] } # Only include when on linux/windows/android/freebsd [target.'cfg(any(target_os = "linux", target_os = "windows", target_os = "android", target_os = "freebsd"))'.dependencies] -sysinfo = { version = "0.31.0", optional = true, default-features = false, features = [ +sysinfo = { version = "0.32.0", optional = true, default-features = false, features = [ "system", ] } From 31409ebc61f1d03680822c33d68ee98954b76375 Mon Sep 17 00:00:00 2001 From: "Ida \"Iyes" <40234599+inodentry@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:38:41 +0300 Subject: [PATCH 049/546] Add `Image` methods for easy access to a pixel's color (#10392) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective If you want to draw / generate images from the CPU, such as: - to create procedurally-generated assets - for games whose artstyle is best implemented by poking pixels directly from the CPU, instead of using shaders It is currently very unergonomic to do in Bevy, because you have to deal with the raw bytes inside `image.data`, take care of the pixel format, etc. ## Solution This PR adds some helper methods to `Image` for pixel manipulation. These methods allow you to use Bevy's user-friendly `Color` struct to read and write the colors of pixels, at arbitrary coordinates (specified as `UVec3` to support any texture dimension). They handle encoding/decoding to the `Image`s `TextureFormat`, incl. any sRGB conversion. While we are at it, also add methods to help with direct access to the raw bytes. It is now easy to compute the offset where the bytes of a specific pixel coordinate are found, or to just get a Rust slice to access them. Caveat: `Color` roundtrips are obviously going to be lossy for non-float `TextureFormat`s. Using `set_color_at` followed by `get_color_at` will return a different value, due to the data conversions involved (such as `f32` -> `u8` -> `f32` for the common `Rgba8UnormSrgb` texture format). Be careful when comparing colors (such as checking for a color you wrote before)! Also adding a new example: `cpu_draw` (under `2d`), to showcase these new APIs. --- ## Changelog ### Added - `Image` APIs for easy access to the colors of specific pixels. --------- Co-authored-by: Pascal Hertleif Co-authored-by: François Co-authored-by: ltdk --- Cargo.toml | 11 + crates/bevy_image/src/image.rs | 453 ++++++++++++++++++++++++++++++++- examples/2d/cpu_draw.rs | 133 ++++++++++ examples/README.md | 1 + 4 files changed, 596 insertions(+), 2 deletions(-) create mode 100644 examples/2d/cpu_draw.rs diff --git a/Cargo.toml b/Cargo.toml index 563cc9bb206447..15bc3e8b7c0def 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -578,6 +578,17 @@ description = "Renders a glTF mesh in 2D with a custom vertex attribute" category = "2D Rendering" wasm = true +[[example]] +name = "cpu_draw" +path = "examples/2d/cpu_draw.rs" +doc-scrape-examples = true + +[package.metadata.example.cpu_draw] +name = "CPU Drawing" +description = "Manually read/write the pixels of a texture" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite" path = "examples/2d/sprite.rs" diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index ad0b046e28cb4a..6965f55ddec53c 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -6,9 +6,11 @@ use super::dds::*; use super::ktx2::*; use bevy_asset::{Asset, RenderAssetUsages}; -use bevy_math::{AspectRatio, UVec2, Vec2}; -use bevy_reflect::prelude::*; +use bevy_color::{Color, ColorToComponents, Gray, LinearRgba, Srgba, Xyza}; +use bevy_math::{AspectRatio, UVec2, UVec3, Vec2}; +use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; +use core::hash::Hash; use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu::{Extent3d, TextureDimension, TextureFormat, TextureViewDescriptor}; @@ -817,6 +819,442 @@ impl Image { .required_features() .contains(wgpu::Features::TEXTURE_COMPRESSION_ETC2) } + + /// Compute the byte offset where the data of a specific pixel is stored + /// + /// Returns None if the provided coordinates are out of bounds. + /// + /// For 2D textures, Z is ignored. For 1D textures, Y and Z are ignored. + #[inline(always)] + pub fn pixel_data_offset(&self, coords: UVec3) -> Option { + let width = self.texture_descriptor.size.width; + let height = self.texture_descriptor.size.height; + let depth = self.texture_descriptor.size.depth_or_array_layers; + + let pixel_size = self.texture_descriptor.format.pixel_size(); + let pixel_offset = match self.texture_descriptor.dimension { + TextureDimension::D3 => { + if coords.x > width || coords.y > height || coords.z > depth { + return None; + } + coords.z * height * width + coords.y * width + coords.x + } + TextureDimension::D2 => { + if coords.x > width || coords.y > height { + return None; + } + coords.y * width + coords.x + } + TextureDimension::D1 => { + if coords.x > width { + return None; + } + coords.x + } + }; + + Some(pixel_offset as usize * pixel_size) + } + + /// Get a reference to the data bytes where a specific pixel's value is stored + #[inline(always)] + pub fn pixel_bytes(&self, coords: UVec3) -> Option<&[u8]> { + let len = self.texture_descriptor.format.pixel_size(); + self.pixel_data_offset(coords) + .map(|start| &self.data[start..(start + len)]) + } + + /// Get a mutable reference to the data bytes where a specific pixel's value is stored + #[inline(always)] + pub fn pixel_bytes_mut(&mut self, coords: UVec3) -> Option<&mut [u8]> { + let len = self.texture_descriptor.format.pixel_size(); + self.pixel_data_offset(coords) + .map(|start| &mut self.data[start..(start + len)]) + } + + /// Read the color of a specific pixel (1D texture). + /// + /// See [`get_color_at`](Self::get_color_at) for more details. + #[inline(always)] + pub fn get_color_at_1d(&self, x: u32) -> Result { + if self.texture_descriptor.dimension != TextureDimension::D1 { + return Err(TextureAccessError::WrongDimension); + } + self.get_color_at_internal(UVec3::new(x, 0, 0)) + } + + /// Read the color of a specific pixel (2D texture). + /// + /// This function will find the raw byte data of a specific pixel and + /// decode it into a user-friendly [`Color`] struct for you. + /// + /// Supports many of the common [`TextureFormat`]s: + /// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear + /// - 16-bit and 32-bit unsigned integer + /// - 32-bit float + /// + /// Be careful: as the data is converted to [`Color`] (which uses `f32` internally), + /// there may be issues with precision when using non-float [`TextureFormat`]s. + /// If you read a value you previously wrote using `set_color_at`, it will not match. + /// If you are working with a 32-bit integer [`TextureFormat`], the value will be + /// inaccurate (as `f32` does not have enough bits to represent it exactly). + /// + /// Single channel (R) formats are assumed to represent greyscale, so the value + /// will be copied to all three RGB channels in the resulting [`Color`]. + /// + /// Other [`TextureFormat`]s are unsupported, such as: + /// - block-compressed formats + /// - non-byte-aligned formats like 10-bit + /// - 16-bit float formats + /// - signed integer formats + #[inline(always)] + pub fn get_color_at(&self, x: u32, y: u32) -> Result { + if self.texture_descriptor.dimension != TextureDimension::D2 { + return Err(TextureAccessError::WrongDimension); + } + self.get_color_at_internal(UVec3::new(x, y, 0)) + } + + /// Read the color of a specific pixel (3D texture). + /// + /// See [`get_color_at`](Self::get_color_at) for more details. + #[inline(always)] + pub fn get_color_at_3d(&self, x: u32, y: u32, z: u32) -> Result { + if self.texture_descriptor.dimension != TextureDimension::D3 { + return Err(TextureAccessError::WrongDimension); + } + self.get_color_at_internal(UVec3::new(x, y, z)) + } + + /// Change the color of a specific pixel (1D texture). + /// + /// See [`set_color_at`](Self::set_color_at) for more details. + #[inline(always)] + pub fn set_color_at_1d(&mut self, x: u32, color: Color) -> Result<(), TextureAccessError> { + if self.texture_descriptor.dimension != TextureDimension::D1 { + return Err(TextureAccessError::WrongDimension); + } + self.set_color_at_internal(UVec3::new(x, 0, 0), color) + } + + /// Change the color of a specific pixel (2D texture). + /// + /// This function will find the raw byte data of a specific pixel and + /// change it according to a [`Color`] you provide. The [`Color`] struct + /// will be encoded into the [`Image`]'s [`TextureFormat`]. + /// + /// Supports many of the common [`TextureFormat`]s: + /// - RGBA/BGRA 8-bit unsigned integer, both sRGB and Linear + /// - 16-bit and 32-bit unsigned integer (with possibly-limited precision, as [`Color`] uses `f32`) + /// - 32-bit float + /// + /// Be careful: writing to non-float [`TextureFormat`]s is lossy! The data has to be converted, + /// so if you read it back using `get_color_at`, the `Color` you get will not equal the value + /// you used when writing it using this function. + /// + /// For R and RG formats, only the respective values from the linear RGB [`Color`] will be used. + /// + /// Other [`TextureFormat`]s are unsupported, such as: + /// - block-compressed formats + /// - non-byte-aligned formats like 10-bit + /// - 16-bit float formats + /// - signed integer formats + #[inline(always)] + pub fn set_color_at(&mut self, x: u32, y: u32, color: Color) -> Result<(), TextureAccessError> { + if self.texture_descriptor.dimension != TextureDimension::D2 { + return Err(TextureAccessError::WrongDimension); + } + self.set_color_at_internal(UVec3::new(x, y, 0), color) + } + + /// Change the color of a specific pixel (3D texture). + /// + /// See [`set_color_at`](Self::set_color_at) for more details. + #[inline(always)] + pub fn set_color_at_3d( + &mut self, + x: u32, + y: u32, + z: u32, + color: Color, + ) -> Result<(), TextureAccessError> { + if self.texture_descriptor.dimension != TextureDimension::D3 { + return Err(TextureAccessError::WrongDimension); + } + self.set_color_at_internal(UVec3::new(x, y, z), color) + } + + #[inline(always)] + fn get_color_at_internal(&self, coords: UVec3) -> Result { + let Some(bytes) = self.pixel_bytes(coords) else { + return Err(TextureAccessError::OutOfBounds { + x: coords.x, + y: coords.y, + z: coords.z, + }); + }; + + // NOTE: GPUs are always Little Endian. + // Make sure to respect that when we create color values from bytes. + match self.texture_descriptor.format { + TextureFormat::Rgba8UnormSrgb => Ok(Color::srgba( + bytes[0] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[2] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => Ok(Color::linear_rgba( + bytes[0] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[2] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Bgra8UnormSrgb => Ok(Color::srgba( + bytes[2] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[0] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Bgra8Unorm => Ok(Color::linear_rgba( + bytes[2] as f32 / u8::MAX as f32, + bytes[1] as f32 / u8::MAX as f32, + bytes[0] as f32 / u8::MAX as f32, + bytes[3] as f32 / u8::MAX as f32, + )), + TextureFormat::Rgba32Float => Ok(Color::linear_rgba( + f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + f32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), + f32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), + )), + TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => { + let (r, g, b, a) = ( + u16::from_le_bytes([bytes[0], bytes[1]]), + u16::from_le_bytes([bytes[2], bytes[3]]), + u16::from_le_bytes([bytes[4], bytes[5]]), + u16::from_le_bytes([bytes[6], bytes[7]]), + ); + Ok(Color::linear_rgba( + // going via f64 to avoid rounding errors with large numbers and division + (r as f64 / u16::MAX as f64) as f32, + (g as f64 / u16::MAX as f64) as f32, + (b as f64 / u16::MAX as f64) as f32, + (a as f64 / u16::MAX as f64) as f32, + )) + } + TextureFormat::Rgba32Uint => { + let (r, g, b, a) = ( + u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]), + u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]), + u32::from_le_bytes([bytes[8], bytes[9], bytes[10], bytes[11]]), + u32::from_le_bytes([bytes[12], bytes[13], bytes[14], bytes[15]]), + ); + Ok(Color::linear_rgba( + // going via f64 to avoid rounding errors with large numbers and division + (r as f64 / u32::MAX as f64) as f32, + (g as f64 / u32::MAX as f64) as f32, + (b as f64 / u32::MAX as f64) as f32, + (a as f64 / u32::MAX as f64) as f32, + )) + } + // assume R-only texture format means grayscale (linear) + // copy value to all of RGB in Color + TextureFormat::R8Unorm | TextureFormat::R8Uint => { + let x = bytes[0] as f32 / u8::MAX as f32; + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::R16Unorm | TextureFormat::R16Uint => { + let x = u16::from_le_bytes([bytes[0], bytes[1]]); + // going via f64 to avoid rounding errors with large numbers and division + let x = (x as f64 / u16::MAX as f64) as f32; + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::R32Uint => { + let x = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + // going via f64 to avoid rounding errors with large numbers and division + let x = (x as f64 / u32::MAX as f64) as f32; + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::R32Float => { + let x = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + Ok(Color::linear_rgb(x, x, x)) + } + TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => { + let r = bytes[0] as f32 / u8::MAX as f32; + let g = bytes[1] as f32 / u8::MAX as f32; + Ok(Color::linear_rgb(r, g, 0.0)) + } + TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => { + let r = u16::from_le_bytes([bytes[0], bytes[1]]); + let g = u16::from_le_bytes([bytes[2], bytes[3]]); + // going via f64 to avoid rounding errors with large numbers and division + let r = (r as f64 / u16::MAX as f64) as f32; + let g = (g as f64 / u16::MAX as f64) as f32; + Ok(Color::linear_rgb(r, g, 0.0)) + } + TextureFormat::Rg32Uint => { + let r = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let g = u32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + // going via f64 to avoid rounding errors with large numbers and division + let r = (r as f64 / u32::MAX as f64) as f32; + let g = (g as f64 / u32::MAX as f64) as f32; + Ok(Color::linear_rgb(r, g, 0.0)) + } + TextureFormat::Rg32Float => { + let r = f32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]); + let g = f32::from_le_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]); + Ok(Color::linear_rgb(r, g, 0.0)) + } + _ => Err(TextureAccessError::UnsupportedTextureFormat( + self.texture_descriptor.format, + )), + } + } + + #[inline(always)] + fn set_color_at_internal( + &mut self, + coords: UVec3, + color: Color, + ) -> Result<(), TextureAccessError> { + let format = self.texture_descriptor.format; + + let Some(bytes) = self.pixel_bytes_mut(coords) else { + return Err(TextureAccessError::OutOfBounds { + x: coords.x, + y: coords.y, + z: coords.z, + }); + }; + + // NOTE: GPUs are always Little Endian. + // Make sure to respect that when we convert color values to bytes. + match format { + TextureFormat::Rgba8UnormSrgb => { + let [r, g, b, a] = Srgba::from(color).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (b * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Rgba8Unorm | TextureFormat::Rgba8Uint => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (b * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Bgra8UnormSrgb => { + let [r, g, b, a] = Srgba::from(color).to_f32_array(); + bytes[0] = (b * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (r * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Bgra8Unorm => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + bytes[0] = (b * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + bytes[2] = (r * u8::MAX as f32) as u8; + bytes[3] = (a * u8::MAX as f32) as u8; + } + TextureFormat::Rgba32Float => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + bytes[0..4].copy_from_slice(&f32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&f32::to_le_bytes(g)); + bytes[8..12].copy_from_slice(&f32::to_le_bytes(b)); + bytes[12..16].copy_from_slice(&f32::to_le_bytes(a)); + } + TextureFormat::Rgba16Unorm | TextureFormat::Rgba16Uint => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + let [r, g, b, a] = [ + (r * u16::MAX as f32) as u16, + (g * u16::MAX as f32) as u16, + (b * u16::MAX as f32) as u16, + (a * u16::MAX as f32) as u16, + ]; + bytes[0..2].copy_from_slice(&u16::to_le_bytes(r)); + bytes[2..4].copy_from_slice(&u16::to_le_bytes(g)); + bytes[4..6].copy_from_slice(&u16::to_le_bytes(b)); + bytes[6..8].copy_from_slice(&u16::to_le_bytes(a)); + } + TextureFormat::Rgba32Uint => { + let [r, g, b, a] = LinearRgba::from(color).to_f32_array(); + let [r, g, b, a] = [ + (r * u32::MAX as f32) as u32, + (g * u32::MAX as f32) as u32, + (b * u32::MAX as f32) as u32, + (a * u32::MAX as f32) as u32, + ]; + bytes[0..4].copy_from_slice(&u32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&u32::to_le_bytes(g)); + bytes[8..12].copy_from_slice(&u32::to_le_bytes(b)); + bytes[12..16].copy_from_slice(&u32::to_le_bytes(a)); + } + TextureFormat::R8Unorm | TextureFormat::R8Uint => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + } + TextureFormat::R16Unorm | TextureFormat::R16Uint => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + let r = (r * u16::MAX as f32) as u16; + bytes[0..2].copy_from_slice(&u16::to_le_bytes(r)); + } + TextureFormat::R32Uint => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + // go via f64 to avoid imprecision + let r = (r as f64 * u32::MAX as f64) as u32; + bytes[0..4].copy_from_slice(&u32::to_le_bytes(r)); + } + TextureFormat::R32Float => { + // Convert to grayscale with minimal loss if color is already gray + let linear = LinearRgba::from(color); + let luminance = Xyza::from(linear).y; + let [r, _, _, _] = LinearRgba::gray(luminance).to_f32_array(); + bytes[0..4].copy_from_slice(&f32::to_le_bytes(r)); + } + TextureFormat::Rg8Unorm | TextureFormat::Rg8Uint => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + bytes[0] = (r * u8::MAX as f32) as u8; + bytes[1] = (g * u8::MAX as f32) as u8; + } + TextureFormat::Rg16Unorm | TextureFormat::Rg16Uint => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + let r = (r * u16::MAX as f32) as u16; + let g = (g * u16::MAX as f32) as u16; + bytes[0..2].copy_from_slice(&u16::to_le_bytes(r)); + bytes[2..4].copy_from_slice(&u16::to_le_bytes(g)); + } + TextureFormat::Rg32Uint => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + // go via f64 to avoid imprecision + let r = (r as f64 * u32::MAX as f64) as u32; + let g = (g as f64 * u32::MAX as f64) as u32; + bytes[0..4].copy_from_slice(&u32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&u32::to_le_bytes(g)); + } + TextureFormat::Rg32Float => { + let [r, g, _, _] = LinearRgba::from(color).to_f32_array(); + bytes[0..4].copy_from_slice(&f32::to_le_bytes(r)); + bytes[4..8].copy_from_slice(&f32::to_le_bytes(g)); + } + _ => { + return Err(TextureAccessError::UnsupportedTextureFormat( + self.texture_descriptor.format, + )); + } + } + Ok(()) + } } #[derive(Clone, Copy, Debug)] @@ -840,6 +1278,17 @@ pub enum TranscodeFormat { Rgb8, } +/// An error that occurs when accessing specific pixels in a texture +#[derive(Error, Debug)] +pub enum TextureAccessError { + #[error("out of bounds (x: {x}, y: {y}, z: {z})")] + OutOfBounds { x: u32, y: u32, z: u32 }, + #[error("unsupported texture format: {0:?}")] + UnsupportedTextureFormat(TextureFormat), + #[error("attempt to access texture with different dimension")] + WrongDimension, +} + /// An error that occurs when loading a texture #[derive(Error, Debug)] pub enum TextureError { diff --git a/examples/2d/cpu_draw.rs b/examples/2d/cpu_draw.rs new file mode 100644 index 00000000000000..0fa6e81a62bcf7 --- /dev/null +++ b/examples/2d/cpu_draw.rs @@ -0,0 +1,133 @@ +//! Example of how to draw to a texture from the CPU. +//! +//! You can set the values of individual pixels to whatever you want. +//! Bevy provides user-friendly APIs that work with [`Color`](bevy::color::Color) +//! values and automatically perform any necessary conversions and encoding +//! into the texture's native pixel format. + +use bevy::color::{color_difference::EuclideanDistance, palettes::css}; +use bevy::prelude::*; +use bevy::render::{ + render_asset::RenderAssetUsages, + render_resource::{Extent3d, TextureDimension, TextureFormat}, +}; +use rand::Rng; + +const IMAGE_WIDTH: u32 = 256; +const IMAGE_HEIGHT: u32 = 256; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + // In this example, we will use a fixed timestep to draw a pattern on the screen + // one pixel at a time, so the pattern will gradually emerge over time, and + // the speed at which it appears is not tied to the framerate. + // Let's make the fixed update very fast, so it doesn't take too long. :) + .insert_resource(Time::::from_hz(1024.0)) + .add_systems(Startup, setup) + .add_systems(FixedUpdate, draw) + .run(); +} + +/// Store the image handle that we will draw to, here. +#[derive(Resource)] +struct MyProcGenImage(Handle); + +fn setup(mut commands: Commands, mut images: ResMut>) { + // spawn a camera + commands.spawn(Camera2d); + + // create an image that we are going to draw into + let mut image = Image::new_fill( + // 2D image of size 256x256 + Extent3d { + width: IMAGE_WIDTH, + height: IMAGE_HEIGHT, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + // Initialize it with a beige color + &(css::BEIGE.to_u8_array()), + // Use the same encoding as the color we set + TextureFormat::Rgba8UnormSrgb, + RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD, + ); + + // to make it extra fancy, we can set the Alpha of each pixel + // so that it fades out in a circular fashion + for y in 0..IMAGE_HEIGHT { + for x in 0..IMAGE_WIDTH { + let center = Vec2::new(IMAGE_WIDTH as f32 / 2.0, IMAGE_HEIGHT as f32 / 2.0); + let max_radius = IMAGE_HEIGHT.min(IMAGE_WIDTH) as f32 / 2.0; + let r = Vec2::new(x as f32, y as f32).distance(center); + let a = 1.0 - (r / max_radius).clamp(0.0, 1.0); + + // here we will set the A value by accessing the raw data bytes + // (it is the 4th byte of each pixel, as per our `TextureFormat`) + + // find our pixel by its coordinates + let pixel_bytes = image.pixel_bytes_mut(UVec3::new(x, y, 0)).unwrap(); + // convert our f32 to u8 + pixel_bytes[3] = (a * u8::MAX as f32) as u8; + } + } + + // add it to Bevy's assets, so it can be used for rendering + // this will give us a handle we can use + // (to display it in a sprite, or as part of UI, etc.) + let handle = images.add(image); + + // create a sprite entity using our image + commands.spawn(SpriteBundle { + texture: handle.clone(), + ..Default::default() + }); + + commands.insert_resource(MyProcGenImage(handle)); +} + +/// Every fixed update tick, draw one more pixel to make a spiral pattern +fn draw( + my_handle: Res, + mut images: ResMut>, + // used to keep track of where we are + mut i: Local, + mut draw_color: Local, +) { + let mut rng = rand::thread_rng(); + + if *i == 0 { + // Generate a random color on first run. + *draw_color = Color::linear_rgb(rng.gen(), rng.gen(), rng.gen()); + } + + // Get the image from Bevy's asset storage. + let image = images.get_mut(&my_handle.0).expect("Image not found"); + + // Compute the position of the pixel to draw. + + let center = Vec2::new(IMAGE_WIDTH as f32 / 2.0, IMAGE_HEIGHT as f32 / 2.0); + let max_radius = IMAGE_HEIGHT.min(IMAGE_WIDTH) as f32 / 2.0; + let rot_speed = 0.0123; + let period = 0.12345; + + let r = ops::sin(*i as f32 * period) * max_radius; + let xy = Vec2::from_angle(*i as f32 * rot_speed) * r + center; + let (x, y) = (xy.x as u32, xy.y as u32); + + // Get the old color of that pixel. + let old_color = image.get_color_at(x, y).unwrap(); + + // If the old color is our current color, change our drawing color. + let tolerance = 1.0 / 255.0; + if old_color.distance(&draw_color) <= tolerance { + *draw_color = Color::linear_rgb(rng.gen(), rng.gen(), rng.gen()); + } + + // Set the new color, but keep old alpha value from image. + image + .set_color_at(x, y, draw_color.with_alpha(old_color.alpha())) + .unwrap(); + + *i += 1; +} diff --git a/examples/README.md b/examples/README.md index 7754978c9307df..a195a0cf44bbcf 100644 --- a/examples/README.md +++ b/examples/README.md @@ -109,6 +109,7 @@ Example | Description [2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method [2D Wireframe](../examples/2d/wireframe_2d.rs) | Showcases wireframes for 2d meshes [Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of the circular segment and sector primitives +[CPU Drawing](../examples/2d/cpu_draw.rs) | Manually read/write the pixels of a texture [Custom glTF vertex attribute 2D](../examples/2d/custom_gltf_vertex_attribute.rs) | Renders a glTF mesh in 2D with a custom vertex attribute [Manual Mesh 2D](../examples/2d/mesh2d_manual.rs) | Renders a custom mesh "manually" with "mid-level" renderer apis [Mesh 2D](../examples/2d/mesh2d.rs) | Renders a 2d mesh From 584d14808adfbe4c249fecebe7a4e95c2e29f4fb Mon Sep 17 00:00:00 2001 From: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> Date: Mon, 7 Oct 2024 08:21:40 -0700 Subject: [PATCH 050/546] Allow `World::entity` family of functions to take multiple entities and get multiple references back (#15614) # Objective Following the pattern established in #15593, we can reduce the API surface of `World` by providing a single function to grab both a singular entity reference, or multiple entity references. ## Solution The following functions can now also take multiple entity IDs and will return multiple entity references back: - `World::entity` - `World::get_entity` - `World::entity_mut` - `World::get_entity_mut` - `DeferredWorld::entity_mut` - `DeferredWorld::get_entity_mut` If you pass in X, you receive Y: - give a single `Entity`, receive a single `EntityRef`/`EntityWorldMut` (matches current behavior) - give a `[Entity; N]`/`&[Entity; N]` (array), receive an equally-sized `[EntityRef; N]`/`[EntityMut; N]` - give a `&[Entity]` (slice), receive a `Vec`/`Vec` - give a `&EntityHashSet`, receive a `EntityHashMap`/`EntityHashMap` Note that `EntityWorldMut` is only returned in the single-entity case, because having multiple at the same time would lead to UB. Also, `DeferredWorld` receives an `EntityMut` in the single-entity case because it does not allow structural access. ## Testing - Added doc-tests on `World::entity`, `World::entity_mut`, and `DeferredWorld::entity_mut` - Added tests for aliased mutability and entity existence --- ## Showcase
Click to view showcase The APIs for fetching `EntityRef`s and `EntityMut`s from the `World` have been unified. ```rust // This code will be referred to by subsequent code blocks. let world = World::new(); let e1 = world.spawn_empty().id(); let e2 = world.spawn_empty().id(); let e3 = world.spawn_empty().id(); ``` Querying for a single entity remains mostly the same: ```rust // 0.14 let eref: EntityRef = world.entity(e1); let emut: EntityWorldMut = world.entity_mut(e1); let eref: Option = world.get_entity(e1); let emut: Option = world.get_entity_mut(e1); // 0.15 let eref: EntityRef = world.entity(e1); let emut: EntityWorldMut = world.entity_mut(e1); let eref: Result = world.get_entity(e1); let emut: Result = world.get_entity_mut(e1); ``` Querying for multiple entities with an array has changed: ```rust // 0.14 let erefs: [EntityRef; 2] = world.many_entities([e1, e2]); let emuts: [EntityMut; 2] = world.many_entities_mut([e1, e2]); let erefs: Result<[EntityRef; 2], Entity> = world.get_many_entities([e1, e2]); let emuts: Result<[EntityMut; 2], QueryEntityError> = world.get_many_entities_mut([e1, e2]); // 0.15 let erefs: [EntityRef; 2] = world.entity([e1, e2]); let emuts: [EntityMut; 2] = world.entity_mut([e1, e2]); let erefs: Result<[EntityRef; 2], Entity> = world.get_entity([e1, e2]); let emuts: Result<[EntityMut; 2], EntityFetchError> = world.get_entity_mut([e1, e2]); ``` Querying for multiple entities with a slice has changed: ```rust let ids = vec![e1, e2, e3]); // 0.14 let erefs: Result, Entity> = world.get_many_entities_dynamic(&ids[..]); let emuts: Result, QueryEntityError> = world.get_many_entities_dynamic_mut(&ids[..]); // 0.15 let erefs: Result, Entity> = world.get_entity(&ids[..]); let emuts: Result, EntityFetchError> = world.get_entity_mut(&ids[..]); let erefs: Vec = world.entity(&ids[..]); // Newly possible! let emuts: Vec = world.entity_mut(&ids[..]); // Newly possible! ``` Querying for multiple entities with an `EntityHashSet` has changed: ```rust let set = EntityHashSet::from_iter([e1, e2, e3]); // 0.14 let emuts: Result, QueryEntityError> = world.get_many_entities_from_set_mut(&set); // 0.15 let emuts: Result, EntityFetchError> = world.get_entity_mut(&set); let erefs: Result, EntityFetchError> = world.get_entity(&set); // Newly possible! let emuts: EntityHashMap = world.entity_mut(&set); // Newly possible! let erefs: EntityHashMap = world.entity(&set); // Newly possible! ```
## Migration Guide - `World::get_entity` now returns `Result<_, Entity>` instead of `Option<_>`. - Use `world.get_entity(..).ok()` to return to the previous behavior. - `World::get_entity_mut` and `DeferredWorld::get_entity_mut` now return `Result<_, EntityFetchError>` instead of `Option<_>`. - Use `world.get_entity_mut(..).ok()` to return to the previous behavior. - Type inference for `World::entity`, `World::entity_mut`, `World::get_entity`, `World::get_entity_mut`, `DeferredWorld::entity_mut`, and `DeferredWorld::get_entity_mut` has changed, and might now require the input argument's type to be explicitly written when inside closures. - The following functions have been deprecated, and should be replaced as such: - `World::many_entities` -> `World::entity::<[Entity; N]>` - `World::many_entities_mut` -> `World::entity_mut::<[Entity; N]>` - `World::get_many_entities` -> `World::get_entity::<[Entity; N]>` - `World::get_many_entities_dynamic` -> `World::get_entity::<&[Entity]>` - `World::get_many_entities_mut` -> `World::get_entity_mut::<[Entity; N]>` - The equivalent return type has changed from `Result<_, QueryEntityError>` to `Result<_, EntityFetchError>` - `World::get_many_entities_dynamic_mut` -> `World::get_entity_mut::<&[Entity]>1 - The equivalent return type has changed from `Result<_, QueryEntityError>` to `Result<_, EntityFetchError>` - `World::get_many_entities_from_set_mut` -> `World::get_entity_mut::<&EntityHashSet>` - The equivalent return type has changed from `Result, QueryEntityError>` to `Result, EntityFetchError>`. If necessary, you can still convert the `EntityHashMap` into a `Vec`. --- crates/bevy_ecs/src/lib.rs | 8 +- .../bevy_ecs/src/observer/entity_observer.rs | 2 +- .../bevy_ecs/src/reflect/entity_commands.rs | 4 +- crates/bevy_ecs/src/system/commands/mod.rs | 26 +- crates/bevy_ecs/src/system/system.rs | 2 +- crates/bevy_ecs/src/system/system_registry.rs | 12 +- crates/bevy_ecs/src/world/deferred_world.rs | 190 +++++- crates/bevy_ecs/src/world/entity_fetch.rs | 331 ++++++++++ crates/bevy_ecs/src/world/error.rs | 13 +- crates/bevy_ecs/src/world/mod.rs | 622 ++++++++++++------ crates/bevy_hierarchy/src/child_builder.rs | 2 +- crates/bevy_hierarchy/src/hierarchy.rs | 4 +- crates/bevy_remote/src/builtin_methods.rs | 4 +- crates/bevy_render/src/world_sync.rs | 4 +- crates/bevy_scene/src/bundle.rs | 2 +- crates/bevy_scene/src/scene_spawner.rs | 4 +- crates/bevy_scene/src/serde.rs | 2 +- crates/bevy_transform/src/commands.rs | 19 +- 18 files changed, 967 insertions(+), 284 deletions(-) create mode 100644 crates/bevy_ecs/src/world/entity_fetch.rs diff --git a/crates/bevy_ecs/src/lib.rs b/crates/bevy_ecs/src/lib.rs index 4c7737328a4065..a62d23afb69cdc 100644 --- a/crates/bevy_ecs/src/lib.rs +++ b/crates/bevy_ecs/src/lib.rs @@ -1637,13 +1637,13 @@ mod tests { "new entity is created immediately after world_a's max entity" ); assert!(world_b.get::(e1).is_none()); - assert!(world_b.get_entity(e1).is_none()); + assert!(world_b.get_entity(e1).is_err()); assert!(world_b.get::(e2).is_none()); - assert!(world_b.get_entity(e2).is_none()); + assert!(world_b.get_entity(e2).is_err()); assert!(world_b.get::(e3).is_none()); - assert!(world_b.get_entity(e3).is_none()); + assert!(world_b.get_entity(e3).is_err()); world_b.get_or_spawn(e1).unwrap().insert(B(1)); assert_eq!( @@ -1694,7 +1694,7 @@ mod tests { let high_non_existent_but_reserved_entity = Entity::from_raw(5); assert!( - world_b.get_entity(high_non_existent_but_reserved_entity).is_none(), + world_b.get_entity(high_non_existent_but_reserved_entity).is_err(), "entities between high-newly allocated entity and continuous block of existing entities don't exist" ); diff --git a/crates/bevy_ecs/src/observer/entity_observer.rs b/crates/bevy_ecs/src/observer/entity_observer.rs index 0f9baba760a856..a5f332e6f3b8c8 100644 --- a/crates/bevy_ecs/src/observer/entity_observer.rs +++ b/crates/bevy_ecs/src/observer/entity_observer.rs @@ -19,7 +19,7 @@ impl Component for ObservedBy { }; for e in observed_by { let (total_entities, despawned_watched_entities) = { - let Some(mut entity_mut) = world.get_entity_mut(e) else { + let Ok(mut entity_mut) = world.get_entity_mut(e) else { continue; }; let Some(mut state) = entity_mut.get_mut::() else { diff --git a/crates/bevy_ecs/src/reflect/entity_commands.rs b/crates/bevy_ecs/src/reflect/entity_commands.rs index 3979eee22903fb..60c89c7f7f9ff0 100644 --- a/crates/bevy_ecs/src/reflect/entity_commands.rs +++ b/crates/bevy_ecs/src/reflect/entity_commands.rs @@ -221,7 +221,7 @@ fn insert_reflect( .get_represented_type_info() .expect("component should represent a type."); let type_path = type_info.type_path(); - let Some(mut entity) = world.get_entity_mut(entity) else { + let Ok(mut entity) = world.get_entity_mut(entity) else { panic!("error[B0003]: Could not insert a reflected component (of type {type_path}) for entity {entity:?} because it doesn't exist in this World. See: https://bevyengine.org/learn/errors/b0003"); }; let Some(type_registration) = type_registry.get(type_info.type_id()) else { @@ -284,7 +284,7 @@ fn remove_reflect( type_registry: &TypeRegistry, component_type_path: Cow<'static, str>, ) { - let Some(mut entity) = world.get_entity_mut(entity) else { + let Ok(mut entity) = world.get_entity_mut(entity) else { return; }; let Some(type_registration) = type_registry.get_with_type_path(&component_type_path) else { diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index bb40dd48a1c4c0..2ba77daa30ac56 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -1757,7 +1757,7 @@ fn try_despawn() -> impl EntityCommand { fn insert(bundle: T, mode: InsertMode) -> impl EntityCommand { let caller = Location::caller(); move |entity: Entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.insert_with_caller( bundle, mode, @@ -1776,7 +1776,7 @@ fn insert_from_world(mode: InsertMode) -> impl EntityC let caller = Location::caller(); move |entity: Entity, world: &mut World| { let value = T::from_world(world); - if let Some(mut entity) = world.get_entity_mut(entity) { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.insert_with_caller( value, mode, @@ -1795,8 +1795,8 @@ fn insert_from_world(mode: InsertMode) -> impl EntityC fn try_insert(bundle: impl Bundle, mode: InsertMode) -> impl EntityCommand { #[cfg(feature = "track_change_detection")] let caller = Location::caller(); - move |entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { + move |entity: Entity, world: &mut World| { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.insert_with_caller( bundle, mode, @@ -1818,8 +1818,8 @@ unsafe fn insert_by_id( value: T, on_none_entity: impl FnOnce(Entity) + Send + 'static, ) -> impl EntityCommand { - move |entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { + move |entity: Entity, world: &mut World| { + if let Ok(mut entity) = world.get_entity_mut(entity) { // SAFETY: // - `component_id` safety is ensured by the caller // - `ptr` is valid within the `make` block; @@ -1837,7 +1837,7 @@ unsafe fn insert_by_id( /// For a [`Bundle`] type `T`, this will remove any components in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. fn remove(entity: Entity, world: &mut World) { - if let Some(mut entity) = world.get_entity_mut(entity) { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.remove::(); } } @@ -1848,7 +1848,7 @@ fn remove(entity: Entity, world: &mut World) { /// Panics if the provided [`ComponentId`] does not exist in the [`World`]. fn remove_by_id(component_id: ComponentId) -> impl EntityCommand { move |entity: Entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.remove_by_id(component_id); } } @@ -1856,7 +1856,7 @@ fn remove_by_id(component_id: ComponentId) -> impl EntityCommand { /// An [`EntityCommand`] that remove all components in the bundle and remove all required components for each component in the bundle. fn remove_with_requires(entity: Entity, world: &mut World) { - if let Some(mut entity) = world.get_entity_mut(entity) { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.remove_with_requires::(); } } @@ -1864,7 +1864,7 @@ fn remove_with_requires(entity: Entity, world: &mut World) { /// An [`EntityCommand`] that removes all components associated with a provided entity. fn clear() -> impl EntityCommand { move |entity: Entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.clear(); } } @@ -1875,7 +1875,7 @@ fn clear() -> impl EntityCommand { /// For a [`Bundle`] type `T`, this will remove all components except those in the bundle. /// Any components in the bundle that aren't found on the entity will be ignored. fn retain(entity: Entity, world: &mut World) { - if let Some(mut entity_mut) = world.get_entity_mut(entity) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.retain::(); } } @@ -1919,8 +1919,8 @@ fn log_components(entity: Entity, world: &mut World) { fn observe( observer: impl IntoObserverSystem, ) -> impl EntityCommand { - move |entity, world: &mut World| { - if let Some(mut entity) = world.get_entity_mut(entity) { + move |entity: Entity, world: &mut World| { + if let Ok(mut entity) = world.get_entity_mut(entity) { entity.observe_entity(observer); } } diff --git a/crates/bevy_ecs/src/system/system.rs b/crates/bevy_ecs/src/system/system.rs index ded89235e72ef4..c1c628cb2e1aa5 100644 --- a/crates/bevy_ecs/src/system/system.rs +++ b/crates/bevy_ecs/src/system/system.rs @@ -271,7 +271,7 @@ where /// let entity = world.run_system_once(|mut commands: Commands| { /// commands.spawn_empty().id() /// }).unwrap(); -/// # assert!(world.get_entity(entity).is_some()); +/// # assert!(world.get_entity(entity).is_ok()); /// ``` /// /// ## Immediate Queries diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 4b5508190acf1e..fd4518e633e67c 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -181,7 +181,7 @@ impl World { O: 'static, { match self.get_entity_mut(id.entity) { - Some(mut entity) => { + Ok(mut entity) => { let registered_system = entity .take::>() .ok_or(RegisteredSystemError::SelfRemove(id))?; @@ -191,7 +191,7 @@ impl World { system: registered_system.system, }) } - None => Err(RegisteredSystemError::SystemIdNotRegistered(id)), + Err(_) => Err(RegisteredSystemError::SystemIdNotRegistered(id)), } } @@ -327,7 +327,7 @@ impl World { // lookup let mut entity = self .get_entity_mut(id.entity) - .ok_or(RegisteredSystemError::SystemIdNotRegistered(id))?; + .map_err(|_| RegisteredSystemError::SystemIdNotRegistered(id))?; // take ownership of system trait object let RegisteredSystem { @@ -350,7 +350,7 @@ impl World { }; // return ownership of system trait object (if entity still exists) - if let Some(mut entity) = self.get_entity_mut(id.entity) { + if let Ok(mut entity) = self.get_entity_mut(id.entity) { entity.insert::>(RegisteredSystem { initialized, system, @@ -398,7 +398,7 @@ impl World { } self.resource_scope(|world, mut id: Mut>| { - if let Some(mut entity) = world.get_entity_mut(id.0.entity()) { + if let Ok(mut entity) = world.get_entity_mut(id.0.entity()) { if !entity.contains::>() { entity.insert(system_bundle(Box::new(IntoSystem::into_system(system)))); } @@ -538,7 +538,7 @@ where O: Send + 'static, { fn apply(self, world: &mut World) { - if let Some(mut entity) = world.get_entity_mut(self.entity) { + if let Ok(mut entity) = world.get_entity_mut(self.entity) { entity.insert(system_bundle(self.system)); } } diff --git a/crates/bevy_ecs/src/world/deferred_world.rs b/crates/bevy_ecs/src/world/deferred_world.rs index 8b3b0468591378..45e7a7cf873ee5 100644 --- a/crates/bevy_ecs/src/world/deferred_world.rs +++ b/crates/bevy_ecs/src/world/deferred_world.rs @@ -11,12 +11,10 @@ use crate::{ query::{QueryData, QueryFilter}, system::{Commands, Query, Resource}, traversal::Traversal, + world::{error::EntityFetchError, WorldEntityFetch}, }; -use super::{ - unsafe_world_cell::{UnsafeEntityCell, UnsafeWorldCell}, - EntityMut, Mut, World, -}; +use super::{unsafe_world_cell::UnsafeWorldCell, Mut, World}; /// A [`World`] reference that disallows structural ECS changes. /// This includes initializing resources, registering components or spawning entities. @@ -81,35 +79,168 @@ impl<'w> DeferredWorld<'w> { unsafe { self.world.get_entity(entity)?.get_mut() } } - /// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`. - /// Returns [`None`] if the `entity` does not exist. - /// Instead of unwrapping the value returned from this function, prefer [`Self::entity_mut`]. + /// Returns [`EntityMut`]s that expose read and write operations for the + /// given `entities`, returning [`Err`] if any of the given entities do not + /// exist. Instead of immediately unwrapping the value returned from this + /// function, prefer [`World::entity_mut`]. + /// + /// This function supports fetching a single entity or multiple entities: + /// - Pass an [`Entity`] to receive a single [`EntityMut`]. + /// - Pass a slice of [`Entity`]s to receive a [`Vec`]. + /// - Pass an array of [`Entity`]s to receive an equally-sized array of [`EntityMut`]s. + /// - Pass an [`&EntityHashSet`] to receive an [`EntityHashMap`]. + /// + /// **As [`DeferredWorld`] does not allow structural changes, all returned + /// references are [`EntityMut`]s, which do not allow structural changes + /// (i.e. adding/removing components or despawning the entity).** + /// + /// # Errors + /// + /// - Returns [`EntityFetchError::NoSuchEntity`] if any of the given `entities` do not exist in the world. + /// - Only the first entity found to be missing will be returned. + /// - Returns [`EntityFetchError::AliasedMutability`] if the same entity is requested multiple times. + /// + /// # Examples + /// + /// For examples, see [`DeferredWorld::entity_mut`]. + /// + /// [`EntityMut`]: crate::world::EntityMut + /// [`&EntityHashSet`]: crate::entity::EntityHashSet + /// [`EntityHashMap`]: crate::entity::EntityHashMap #[inline] - pub fn get_entity_mut(&mut self, entity: Entity) -> Option { - let location = self.entities.get(entity)?; - // SAFETY: if the Entity is invalid, the function returns early. - // Additionally, Entities::get(entity) returns the correct EntityLocation if the entity exists. - let entity_cell = UnsafeEntityCell::new(self.as_unsafe_world_cell(), entity, location); - // SAFETY: The UnsafeEntityCell has read access to the entire world. - let entity_ref = unsafe { EntityMut::new(entity_cell) }; - Some(entity_ref) + pub fn get_entity_mut( + &mut self, + entities: F, + ) -> Result, EntityFetchError> { + let cell = self.as_unsafe_world_cell(); + // SAFETY: `&mut self` gives mutable access to the entire world, + // and prevents any other access to the world. + unsafe { entities.fetch_deferred_mut(cell) } } - /// Retrieves an [`EntityMut`] that exposes read and write operations for the given `entity`. - /// This will panic if the `entity` does not exist. Use [`Self::get_entity_mut`] if you want - /// to check for entity existence instead of implicitly panic-ing. + /// Returns [`EntityMut`]s that expose read and write operations for the + /// given `entities`. This will panic if any of the given entities do not + /// exist. Use [`DeferredWorld::get_entity_mut`] if you want to check for + /// entity existence instead of implicitly panicking. + /// + /// This function supports fetching a single entity or multiple entities: + /// - Pass an [`Entity`] to receive a single [`EntityMut`]. + /// - Pass a slice of [`Entity`]s to receive a [`Vec`]. + /// - Pass an array of [`Entity`]s to receive an equally-sized array of [`EntityMut`]s. + /// - Pass an [`&EntityHashSet`] to receive an [`EntityHashMap`]. + /// + /// **As [`DeferredWorld`] does not allow structural changes, all returned + /// references are [`EntityMut`]s, which do not allow structural changes + /// (i.e. adding/removing components or despawning the entity).** + /// + /// # Panics + /// + /// If any of the given `entities` do not exist in the world. + /// + /// # Examples + /// + /// ## Single [`Entity`] + /// + /// ``` + /// # use bevy_ecs::{prelude::*, world::DeferredWorld}; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// # let mut world = World::new(); + /// # let entity = world.spawn(Position { x: 0.0, y: 0.0 }).id(); + /// let mut world: DeferredWorld = // ... + /// # DeferredWorld::from(&mut world); + /// + /// let mut entity_mut = world.entity_mut(entity); + /// let mut position = entity_mut.get_mut::().unwrap(); + /// position.y = 1.0; + /// assert_eq!(position.x, 0.0); + /// ``` + /// + /// ## Array of [`Entity`]s + /// + /// ``` + /// # use bevy_ecs::{prelude::*, world::DeferredWorld}; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// # let mut world = World::new(); + /// # let e1 = world.spawn(Position { x: 0.0, y: 0.0 }).id(); + /// # let e2 = world.spawn(Position { x: 1.0, y: 1.0 }).id(); + /// let mut world: DeferredWorld = // ... + /// # DeferredWorld::from(&mut world); + /// + /// let [mut e1_ref, mut e2_ref] = world.entity_mut([e1, e2]); + /// let mut e1_position = e1_ref.get_mut::().unwrap(); + /// e1_position.x = 1.0; + /// assert_eq!(e1_position.x, 1.0); + /// let mut e2_position = e2_ref.get_mut::().unwrap(); + /// e2_position.x = 2.0; + /// assert_eq!(e2_position.x, 2.0); + /// ``` + /// + /// ## Slice of [`Entity`]s + /// + /// ``` + /// # use bevy_ecs::{prelude::*, world::DeferredWorld}; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// # let mut world = World::new(); + /// # let e1 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// # let e2 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// # let e3 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let mut world: DeferredWorld = // ... + /// # DeferredWorld::from(&mut world); + /// + /// let ids = vec![e1, e2, e3]; + /// for mut eref in world.entity_mut(&ids[..]) { + /// let mut pos = eref.get_mut::().unwrap(); + /// pos.y = 2.0; + /// assert_eq!(pos.y, 2.0); + /// } + /// ``` + /// + /// ## [`&EntityHashSet`] + /// + /// ``` + /// # use bevy_ecs::{prelude::*, entity::EntityHashSet, world::DeferredWorld}; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// # let mut world = World::new(); + /// # let e1 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// # let e2 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// # let e3 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let mut world: DeferredWorld = // ... + /// # DeferredWorld::from(&mut world); + /// + /// let ids = EntityHashSet::from_iter([e1, e2, e3]); + /// for (_id, mut eref) in world.entity_mut(&ids) { + /// let mut pos = eref.get_mut::().unwrap(); + /// pos.y = 2.0; + /// assert_eq!(pos.y, 2.0); + /// } + /// ``` + /// + /// [`EntityMut`]: crate::world::EntityMut + /// [`&EntityHashSet`]: crate::entity::EntityHashSet + /// [`EntityHashMap`]: crate::entity::EntityHashMap #[inline] - pub fn entity_mut(&mut self, entity: Entity) -> EntityMut { - #[inline(never)] - #[cold] - fn panic_no_entity(entity: Entity) -> ! { - panic!("Entity {entity:?} does not exist"); - } - - match self.get_entity_mut(entity) { - Some(entity) => entity, - None => panic_no_entity(entity), - } + pub fn entity_mut(&mut self, entities: F) -> F::DeferredMut<'_> { + self.get_entity_mut(entities).unwrap() } /// Returns [`Query`] for the given [`QueryState`], which is used to efficiently @@ -411,6 +542,7 @@ impl<'w> DeferredWorld<'w> { } if let Some(traverse_to) = self .get_entity(entity) + .ok() .and_then(|entity| entity.get_components::()) .and_then(T::traverse) { diff --git a/crates/bevy_ecs/src/world/entity_fetch.rs b/crates/bevy_ecs/src/world/entity_fetch.rs new file mode 100644 index 00000000000000..62d63ced54fb76 --- /dev/null +++ b/crates/bevy_ecs/src/world/entity_fetch.rs @@ -0,0 +1,331 @@ +use core::mem::MaybeUninit; + +use crate::{ + entity::{Entity, EntityHash, EntityHashMap, EntityHashSet}, + world::{ + error::EntityFetchError, unsafe_world_cell::UnsafeWorldCell, EntityMut, EntityRef, + EntityWorldMut, + }, +}; + +/// Types that can be used to fetch [`Entity`] references from a [`World`]. +/// +/// Provided implementations are: +/// - [`Entity`]: Fetch a single entity. +/// - `[Entity; N]`/`&[Entity; N]`: Fetch multiple entities, receiving a +/// same-sized array of references. +/// - `&[Entity]`: Fetch multiple entities, receiving a vector of references. +/// - [`&EntityHashSet`](EntityHashSet): Fetch multiple entities, receiving a +/// hash map of [`Entity`] IDs to references. +/// +/// # Performance +/// +/// - The slice and array implementations perform an aliased mutabiltiy check +/// in [`WorldEntityFetch::fetch_mut`] that is `O(N^2)`. +/// - The [`EntityHashSet`] implementation performs no such check as the type +/// itself guarantees no duplicates. +/// - The single [`Entity`] implementation performs no such check as only one +/// reference is returned. +/// +/// # Safety +/// +/// Implementor must ensure that: +/// - No aliased mutability is caused by the returned references. +/// - [`WorldEntityFetch::fetch_ref`] returns only read-only references. +/// - [`WorldEntityFetch::fetch_deferred_mut`] returns only non-structurally-mutable references. +/// +/// [`World`]: crate::world::World +pub unsafe trait WorldEntityFetch { + /// The read-only reference type returned by [`WorldEntityFetch::fetch_ref`]. + type Ref<'w>; + + /// The mutable reference type returned by [`WorldEntityFetch::fetch_mut`]. + type Mut<'w>; + + /// The mutable reference type returned by [`WorldEntityFetch::fetch_deferred_mut`], + /// but without structural mutability. + type DeferredMut<'w>; + + /// Returns read-only reference(s) to the entities with the given + /// [`Entity`] IDs, as determined by `self`. + /// + /// # Safety + /// + /// It is the caller's responsibility to ensure that: + /// - The given [`UnsafeWorldCell`] has read-only access to the fetched entities. + /// - No other mutable references to the fetched entities exist at the same time. + /// + /// # Errors + /// + /// - Returns [`Entity`] if the entity does not exist. + unsafe fn fetch_ref(self, cell: UnsafeWorldCell<'_>) -> Result, Entity>; + + /// Returns mutable reference(s) to the entities with the given [`Entity`] + /// IDs, as determined by `self`. + /// + /// # Safety + /// + /// It is the caller's responsibility to ensure that: + /// - The given [`UnsafeWorldCell`] has mutable access to the fetched entities. + /// - No other references to the fetched entities exist at the same time. + /// + /// # Errors + /// + /// - Returns [`EntityFetchError::NoSuchEntity`] if the entity does not exist. + /// - Returns [`EntityFetchError::AliasedMutability`] if the entity was + /// requested mutably more than once. + unsafe fn fetch_mut(self, cell: UnsafeWorldCell<'_>) + -> Result, EntityFetchError>; + + /// Returns mutable reference(s) to the entities with the given [`Entity`] + /// IDs, as determined by `self`, but without structural mutability. + /// + /// No structural mutability means components cannot be removed from the + /// entity, new components cannot be added to the entity, and the entity + /// cannot be despawned. + /// + /// # Safety + /// + /// It is the caller's responsibility to ensure that: + /// - The given [`UnsafeWorldCell`] has mutable access to the fetched entities. + /// - No other references to the fetched entities exist at the same time. + /// + /// # Errors + /// + /// - Returns [`EntityFetchError::NoSuchEntity`] if the entity does not exist. + /// - Returns [`EntityFetchError::AliasedMutability`] if the entity was + /// requested mutably more than once. + unsafe fn fetch_deferred_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError>; +} + +// SAFETY: +// - No aliased mutability is caused because a single reference is returned. +// - No mutable references are returned by `fetch_ref`. +// - No structurally-mutable references are returned by `fetch_deferred_mut`. +unsafe impl WorldEntityFetch for Entity { + type Ref<'w> = EntityRef<'w>; + type Mut<'w> = EntityWorldMut<'w>; + type DeferredMut<'w> = EntityMut<'w>; + + unsafe fn fetch_ref(self, cell: UnsafeWorldCell<'_>) -> Result, Entity> { + let ecell = cell.get_entity(self).ok_or(self)?; + // SAFETY: caller ensures that the world cell has read-only access to the entity. + Ok(unsafe { EntityRef::new(ecell) }) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + let location = cell + .entities() + .get(self) + .ok_or(EntityFetchError::NoSuchEntity(self))?; + // SAFETY: caller ensures that the world cell has mutable access to the entity. + let world = unsafe { cell.world_mut() }; + // SAFETY: location was fetched from the same world's `Entities`. + Ok(unsafe { EntityWorldMut::new(world, self, location) }) + } + + unsafe fn fetch_deferred_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + let ecell = cell + .get_entity(self) + .ok_or(EntityFetchError::NoSuchEntity(self))?; + // SAFETY: caller ensures that the world cell has mutable access to the entity. + Ok(unsafe { EntityMut::new(ecell) }) + } +} + +// SAFETY: +// - No aliased mutability is caused because the array is checked for duplicates. +// - No mutable references are returned by `fetch_ref`. +// - No structurally-mutable references are returned by `fetch_deferred_mut`. +unsafe impl WorldEntityFetch for [Entity; N] { + type Ref<'w> = [EntityRef<'w>; N]; + type Mut<'w> = [EntityMut<'w>; N]; + type DeferredMut<'w> = [EntityMut<'w>; N]; + + unsafe fn fetch_ref(self, cell: UnsafeWorldCell<'_>) -> Result, Entity> { + <&Self>::fetch_ref(&self, cell) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + <&Self>::fetch_mut(&self, cell) + } + + unsafe fn fetch_deferred_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + <&Self>::fetch_deferred_mut(&self, cell) + } +} + +// SAFETY: +// - No aliased mutability is caused because the array is checked for duplicates. +// - No mutable references are returned by `fetch_ref`. +// - No structurally-mutable references are returned by `fetch_deferred_mut`. +unsafe impl WorldEntityFetch for &'_ [Entity; N] { + type Ref<'w> = [EntityRef<'w>; N]; + type Mut<'w> = [EntityMut<'w>; N]; + type DeferredMut<'w> = [EntityMut<'w>; N]; + + unsafe fn fetch_ref(self, cell: UnsafeWorldCell<'_>) -> Result, Entity> { + let mut refs = [MaybeUninit::uninit(); N]; + for (r, &id) in core::iter::zip(&mut refs, self) { + let ecell = cell.get_entity(id).ok_or(id)?; + // SAFETY: caller ensures that the world cell has read-only access to the entity. + *r = MaybeUninit::new(unsafe { EntityRef::new(ecell) }); + } + + // SAFETY: Each item was initialized in the loop above. + let refs = refs.map(|r| unsafe { MaybeUninit::assume_init(r) }); + + Ok(refs) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + // Check for duplicate entities. + for i in 0..self.len() { + for j in 0..i { + if self[i] == self[j] { + return Err(EntityFetchError::AliasedMutability(self[i])); + } + } + } + + let mut refs = [const { MaybeUninit::uninit() }; N]; + for (r, &id) in core::iter::zip(&mut refs, self) { + let ecell = cell + .get_entity(id) + .ok_or(EntityFetchError::NoSuchEntity(id))?; + // SAFETY: caller ensures that the world cell has mutable access to the entity. + *r = MaybeUninit::new(unsafe { EntityMut::new(ecell) }); + } + + // SAFETY: Each item was initialized in the loop above. + let refs = refs.map(|r| unsafe { MaybeUninit::assume_init(r) }); + + Ok(refs) + } + + unsafe fn fetch_deferred_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + // SAFETY: caller ensures that the world cell has mutable access to the entity, + // and `fetch_mut` does not return structurally-mutable references. + unsafe { self.fetch_mut(cell) } + } +} + +// SAFETY: +// - No aliased mutability is caused because the slice is checked for duplicates. +// - No mutable references are returned by `fetch_ref`. +// - No structurally-mutable references are returned by `fetch_deferred_mut`. +unsafe impl WorldEntityFetch for &'_ [Entity] { + type Ref<'w> = Vec>; + type Mut<'w> = Vec>; + type DeferredMut<'w> = Vec>; + + unsafe fn fetch_ref(self, cell: UnsafeWorldCell<'_>) -> Result, Entity> { + let mut refs = Vec::with_capacity(self.len()); + for &id in self { + let ecell = cell.get_entity(id).ok_or(id)?; + // SAFETY: caller ensures that the world cell has read-only access to the entity. + refs.push(unsafe { EntityRef::new(ecell) }); + } + + Ok(refs) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + // Check for duplicate entities. + for i in 0..self.len() { + for j in 0..i { + if self[i] == self[j] { + return Err(EntityFetchError::AliasedMutability(self[i])); + } + } + } + + let mut refs = Vec::with_capacity(self.len()); + for &id in self { + let ecell = cell + .get_entity(id) + .ok_or(EntityFetchError::NoSuchEntity(id))?; + // SAFETY: caller ensures that the world cell has mutable access to the entity. + refs.push(unsafe { EntityMut::new(ecell) }); + } + + Ok(refs) + } + + unsafe fn fetch_deferred_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + // SAFETY: caller ensures that the world cell has mutable access to the entity, + // and `fetch_mut` does not return structurally-mutable references. + unsafe { self.fetch_mut(cell) } + } +} + +// SAFETY: +// - No aliased mutability is caused because `EntityHashSet` guarantees no duplicates. +// - No mutable references are returned by `fetch_ref`. +// - No structurally-mutable references are returned by `fetch_deferred_mut`. +unsafe impl WorldEntityFetch for &'_ EntityHashSet { + type Ref<'w> = EntityHashMap>; + type Mut<'w> = EntityHashMap>; + type DeferredMut<'w> = EntityHashMap>; + + unsafe fn fetch_ref(self, cell: UnsafeWorldCell<'_>) -> Result, Entity> { + let mut refs = EntityHashMap::with_capacity_and_hasher(self.len(), EntityHash); + for &id in self { + let ecell = cell.get_entity(id).ok_or(id)?; + // SAFETY: caller ensures that the world cell has read-only access to the entity. + refs.insert(id, unsafe { EntityRef::new(ecell) }); + } + Ok(refs) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + let mut refs = EntityHashMap::with_capacity_and_hasher(self.len(), EntityHash); + for &id in self { + let ecell = cell + .get_entity(id) + .ok_or(EntityFetchError::NoSuchEntity(id))?; + // SAFETY: caller ensures that the world cell has mutable access to the entity. + refs.insert(id, unsafe { EntityMut::new(ecell) }); + } + Ok(refs) + } + + unsafe fn fetch_deferred_mut( + self, + cell: UnsafeWorldCell<'_>, + ) -> Result, EntityFetchError> { + // SAFETY: caller ensures that the world cell has mutable access to the entity, + // and `fetch_mut` does not return structurally-mutable references. + unsafe { self.fetch_mut(cell) } + } +} diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 5fc4264e07cd34..91cd2fa0340181 100644 --- a/crates/bevy_ecs/src/world/error.rs +++ b/crates/bevy_ecs/src/world/error.rs @@ -2,7 +2,7 @@ use thiserror::Error; -use crate::{component::ComponentId, schedule::InternedScheduleLabel}; +use crate::{component::ComponentId, entity::Entity, schedule::InternedScheduleLabel}; /// The error type returned by [`World::try_run_schedule`] if the provided schedule does not exist. /// @@ -21,3 +21,14 @@ pub enum EntityComponentError { #[error("The component with ID {0:?} was requested mutably more than once.")] AliasedMutability(ComponentId), } + +/// An error that occurs when fetching entities mutably from a world. +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntityFetchError { + /// The entity with the given ID does not exist. + #[error("The entity with ID {0:?} does not exist.")] + NoSuchEntity(Entity), + /// The entity with the given ID was requested mutably more than once. + #[error("The entity with ID {0:?} was requested mutably more than once.")] + AliasedMutability(Entity), +} diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 7dae46a97a8570..7c4daffb322712 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -3,6 +3,7 @@ pub(crate) mod command_queue; mod component_constants; mod deferred_world; +mod entity_fetch; mod entity_ref; pub mod error; mod filtered_resource; @@ -19,6 +20,7 @@ pub use crate::{ }; pub use component_constants::*; pub use deferred_world::DeferredWorld; +pub use entity_fetch::WorldEntityFetch; pub use entity_ref::{ EntityMut, EntityMutExcept, EntityRef, EntityRefExcept, EntityWorldMut, Entry, FilteredEntityMut, FilteredEntityRef, OccupiedEntry, VacantEntry, @@ -43,14 +45,16 @@ use crate::{ schedule::{Schedule, ScheduleLabel, Schedules}, storage::{ResourceData, Storages}, system::{Commands, Resource}, - world::{command_queue::RawCommandQueue, error::TryRunScheduleError}, + world::{ + command_queue::RawCommandQueue, + error::{EntityFetchError, TryRunScheduleError}, + }, }; use bevy_ptr::{OwningPtr, Ptr}; use bevy_utils::tracing::warn; use core::{ any::TypeId, fmt, - mem::MaybeUninit, sync::atomic::{AtomicU32, Ordering}, }; @@ -595,13 +599,28 @@ impl World { self.components.get_resource_id(TypeId::of::()) } - /// Retrieves an [`EntityRef`] that exposes read-only operations for the given `entity`. - /// This will panic if the `entity` does not exist. Use [`World::get_entity`] if you want - /// to check for entity existence instead of implicitly panic-ing. + /// Returns [`EntityRef`]s that expose read-only operations for the given + /// `entities`. This will panic if any of the given entities do not exist. Use + /// [`World::get_entity`] if you want to check for entity existence instead + /// of implicitly panicking. /// - /// ``` - /// use bevy_ecs::{component::Component, world::World}; + /// This function supports fetching a single entity or multiple entities: + /// - Pass an [`Entity`] to receive a single [`EntityRef`]. + /// - Pass a slice of [`Entity`]s to receive a [`Vec`]. + /// - Pass an array of [`Entity`]s to receive an equally-sized array of [`EntityRef`]s. + /// - Pass a reference to a [`EntityHashSet`] to receive an + /// [`EntityHashMap`](crate::entity::EntityHashMap). + /// + /// # Panics + /// + /// If any of the given `entities` do not exist in the world. + /// + /// # Examples /// + /// ## Single [`Entity`] + /// + /// ``` + /// # use bevy_ecs::prelude::*; /// #[derive(Component)] /// struct Position { /// x: f32, @@ -610,12 +629,76 @@ impl World { /// /// let mut world = World::new(); /// let entity = world.spawn(Position { x: 0.0, y: 0.0 }).id(); + /// /// let position = world.entity(entity).get::().unwrap(); /// assert_eq!(position.x, 0.0); /// ``` + /// + /// ## Array of [`Entity`]s + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// let mut world = World::new(); + /// let e1 = world.spawn(Position { x: 0.0, y: 0.0 }).id(); + /// let e2 = world.spawn(Position { x: 1.0, y: 1.0 }).id(); + /// + /// let [e1_ref, e2_ref] = world.entity([e1, e2]); + /// let e1_position = e1_ref.get::().unwrap(); + /// assert_eq!(e1_position.x, 0.0); + /// let e2_position = e2_ref.get::().unwrap(); + /// assert_eq!(e2_position.x, 1.0); + /// ``` + /// + /// ## Slice of [`Entity`]s + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// let mut world = World::new(); + /// let e1 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e2 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e3 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// + /// let ids = vec![e1, e2, e3]; + /// for eref in world.entity(&ids[..]) { + /// assert_eq!(eref.get::().unwrap().y, 1.0); + /// } + /// ``` + /// + /// ## [`EntityHashSet`] + /// + /// ``` + /// # use bevy_ecs::{prelude::*, entity::EntityHashSet}; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// let mut world = World::new(); + /// let e1 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e2 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e3 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// + /// let ids = EntityHashSet::from_iter([e1, e2, e3]); + /// for (_id, eref) in world.entity(&ids) { + /// assert_eq!(eref.get::().unwrap().y, 1.0); + /// } + /// ``` #[inline] #[track_caller] - pub fn entity(&self, entity: Entity) -> EntityRef { + pub fn entity(&self, entities: F) -> F::Ref<'_> { #[inline(never)] #[cold] #[track_caller] @@ -623,19 +706,42 @@ impl World { panic!("Entity {entity:?} does not exist"); } - match self.get_entity(entity) { - Some(entity) => entity, - None => panic_no_entity(entity), + match self.get_entity(entities) { + Ok(fetched) => fetched, + Err(entity) => panic_no_entity(entity), } } - /// Retrieves an [`EntityWorldMut`] that exposes read and write operations for the given `entity`. - /// This will panic if the `entity` does not exist. Use [`World::get_entity_mut`] if you want - /// to check for entity existence instead of implicitly panic-ing. + /// Returns [`EntityMut`]s that expose read and write operations for the + /// given `entities`. This will panic if any of the given entities do not + /// exist. Use [`World::get_entity_mut`] if you want to check for entity + /// existence instead of implicitly panicking. /// - /// ``` - /// use bevy_ecs::{component::Component, world::World}; + /// This function supports fetching a single entity or multiple entities: + /// - Pass an [`Entity`] to receive a single [`EntityWorldMut`]. + /// - This reference type allows for structural changes to the entity, + /// such as adding or removing components, or despawning the entity. + /// - Pass a slice of [`Entity`]s to receive a [`Vec`]. + /// - Pass an array of [`Entity`]s to receive an equally-sized array of [`EntityMut`]s. + /// - Pass a reference to a [`EntityHashSet`] to receive an + /// [`EntityHashMap`](crate::entity::EntityHashMap). + /// + /// In order to perform structural changes on the returned entity reference, + /// such as adding or removing components, or despawning the entity, only a + /// single [`Entity`] can be passed to this function. Allowing multiple + /// entities at the same time with structural access would lead to undefined + /// behavior, so [`EntityMut`] is returned when requesting multiple entities. /// + /// # Panics + /// + /// If any of the given `entities` do not exist in the world. + /// + /// # Examples + /// + /// ## Single [`Entity`] + /// + /// ``` + /// # use bevy_ecs::prelude::*; /// #[derive(Component)] /// struct Position { /// x: f32, @@ -644,23 +750,96 @@ impl World { /// /// let mut world = World::new(); /// let entity = world.spawn(Position { x: 0.0, y: 0.0 }).id(); + /// /// let mut entity_mut = world.entity_mut(entity); /// let mut position = entity_mut.get_mut::().unwrap(); - /// position.x = 1.0; + /// position.y = 1.0; + /// assert_eq!(position.x, 0.0); + /// entity_mut.despawn(); + /// # assert!(world.get_entity_mut(entity).is_err()); + /// ``` + /// + /// ## Array of [`Entity`]s + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// let mut world = World::new(); + /// let e1 = world.spawn(Position { x: 0.0, y: 0.0 }).id(); + /// let e2 = world.spawn(Position { x: 1.0, y: 1.0 }).id(); + /// + /// let [mut e1_ref, mut e2_ref] = world.entity_mut([e1, e2]); + /// let mut e1_position = e1_ref.get_mut::().unwrap(); + /// e1_position.x = 1.0; + /// assert_eq!(e1_position.x, 1.0); + /// let mut e2_position = e2_ref.get_mut::().unwrap(); + /// e2_position.x = 2.0; + /// assert_eq!(e2_position.x, 2.0); + /// ``` + /// + /// ## Slice of [`Entity`]s + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// let mut world = World::new(); + /// let e1 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e2 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e3 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// + /// let ids = vec![e1, e2, e3]; + /// for mut eref in world.entity_mut(&ids[..]) { + /// let mut pos = eref.get_mut::().unwrap(); + /// pos.y = 2.0; + /// assert_eq!(pos.y, 2.0); + /// } + /// ``` + /// + /// ## [`EntityHashSet`] + /// + /// ``` + /// # use bevy_ecs::{prelude::*, entity::EntityHashSet}; + /// #[derive(Component)] + /// struct Position { + /// x: f32, + /// y: f32, + /// } + /// + /// let mut world = World::new(); + /// let e1 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e2 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// let e3 = world.spawn(Position { x: 0.0, y: 1.0 }).id(); + /// + /// let ids = EntityHashSet::from_iter([e1, e2, e3]); + /// for (_id, mut eref) in world.entity_mut(&ids) { + /// let mut pos = eref.get_mut::().unwrap(); + /// pos.y = 2.0; + /// assert_eq!(pos.y, 2.0); + /// } /// ``` #[inline] #[track_caller] - pub fn entity_mut(&mut self, entity: Entity) -> EntityWorldMut { + pub fn entity_mut(&mut self, entities: F) -> F::Mut<'_> { #[inline(never)] #[cold] #[track_caller] - fn panic_no_entity(entity: Entity) -> ! { - panic!("Entity {entity:?} does not exist"); + fn panic_on_err(e: EntityFetchError) -> ! { + panic!("{e}"); } - match self.get_entity_mut(entity) { - Some(entity) => entity, - None => panic_no_entity(entity), + match self.get_entity_mut(entities) { + Ok(fetched) => fetched, + Err(e) => panic_on_err(e), } } @@ -690,18 +869,9 @@ impl World { /// world.despawn(id2); /// world.many_entities([id1, id2]); /// ``` + #[deprecated(since = "0.15.0", note = "Use `World::entity::<[Entity; N]>` instead")] pub fn many_entities(&mut self, entities: [Entity; N]) -> [EntityRef<'_>; N] { - #[inline(never)] - #[cold] - #[track_caller] - fn panic_no_entity(entity: Entity) -> ! { - panic!("Entity {entity:?} does not exist"); - } - - match self.get_many_entities(entities) { - Ok(refs) => refs, - Err(entity) => panic_no_entity(entity), - } + self.entity(entities) } /// Gets mutable access to multiple entities at once. @@ -732,21 +902,15 @@ impl World { /// # let id = world.spawn_empty().id(); /// world.many_entities_mut([id, id]); /// ``` + #[deprecated( + since = "0.15.0", + note = "Use `World::entity_mut::<[Entity; N]>` instead" + )] pub fn many_entities_mut( &mut self, entities: [Entity; N], ) -> [EntityMut<'_>; N] { - #[inline(never)] - #[cold] - #[track_caller] - fn panic_on_err(e: QueryEntityError) -> ! { - panic!("{e}"); - } - - match self.get_many_entities_mut(entities) { - Ok(borrows) => borrows, - Err(e) => panic_on_err(e), - } + self.entity_mut(entities) } /// Returns the components of an [`Entity`] through [`ComponentInfo`]. @@ -795,35 +959,31 @@ impl World { } } - /// Retrieves an [`EntityRef`] that exposes read-only operations for the given `entity`. - /// Returns [`None`] if the `entity` does not exist. - /// Instead of unwrapping the value returned from this function, prefer [`World::entity`]. + /// Returns [`EntityRef`]s that expose read-only operations for the given + /// `entities`, returning [`Err`] if any of the given entities do not exist. + /// Instead of immediately unwrapping the value returned from this function, + /// prefer [`World::entity`]. /// - /// ``` - /// use bevy_ecs::{component::Component, world::World}; + /// This function supports fetching a single entity or multiple entities: + /// - Pass an [`Entity`] to receive a single [`EntityRef`]. + /// - Pass a slice of [`Entity`]s to receive a [`Vec`]. + /// - Pass an array of [`Entity`]s to receive an equally-sized array of [`EntityRef`]s. + /// - Pass a reference to a [`EntityHashSet`] to receive an + /// [`EntityHashMap`](crate::entity::EntityHashMap). /// - /// #[derive(Component)] - /// struct Position { - /// x: f32, - /// y: f32, - /// } + /// # Errors /// - /// let mut world = World::new(); - /// let entity = world.spawn(Position { x: 0.0, y: 0.0 }).id(); - /// let entity_ref = world.get_entity(entity).unwrap(); - /// let position = entity_ref.get::().unwrap(); - /// assert_eq!(position.x, 0.0); - /// ``` + /// If any of the given `entities` do not exist in the world, the first + /// [`Entity`] found to be missing will be returned in the [`Err`]. + /// + /// # Examples + /// + /// For examples, see [`World::entity`]. #[inline] - pub fn get_entity(&self, entity: Entity) -> Option { - let location = self.entities.get(entity)?; - // SAFETY: if the Entity is invalid, the function returns early. - // Additionally, Entities::get(entity) returns the correct EntityLocation if the entity exists. - let entity_cell = - UnsafeEntityCell::new(self.as_unsafe_world_cell_readonly(), entity, location); - // SAFETY: The UnsafeEntityCell has read access to the entire world. - let entity_ref = unsafe { EntityRef::new(entity_cell) }; - Some(entity_ref) + pub fn get_entity(&self, entities: F) -> Result, Entity> { + let cell = self.as_unsafe_world_cell_readonly(); + // SAFETY: `&self` gives read access to the entire world, and prevents mutable access. + unsafe { entities.fetch_ref(cell) } } /// Gets an [`EntityRef`] for multiple entities at once. @@ -846,19 +1006,15 @@ impl World { /// world.despawn(id2); /// assert!(world.get_many_entities([id1, id2]).is_err()); /// ``` + #[deprecated( + since = "0.15.0", + note = "Use `World::get_entity::<[Entity; N]>` instead" + )] pub fn get_many_entities( &self, entities: [Entity; N], ) -> Result<[EntityRef<'_>; N], Entity> { - let mut refs = [MaybeUninit::uninit(); N]; - for (r, id) in core::iter::zip(&mut refs, entities) { - *r = MaybeUninit::new(self.get_entity(id).ok_or(id)?); - } - - // SAFETY: Each item was initialized in the above loop. - let refs = refs.map(|r| unsafe { MaybeUninit::assume_init(r) }); - - Ok(refs) + self.get_entity(entities) } /// Gets an [`EntityRef`] for multiple entities at once, whose number is determined at runtime. @@ -883,16 +1039,55 @@ impl World { /// world.despawn(id2); /// assert!(world.get_many_entities_dynamic(&[id1, id2]).is_err()); /// ``` + #[deprecated( + since = "0.15.0", + note = "Use `World::get_entity::<&[Entity]>` instead" + )] pub fn get_many_entities_dynamic<'w>( &'w self, entities: &[Entity], ) -> Result>, Entity> { - let mut borrows = Vec::with_capacity(entities.len()); - for &id in entities { - borrows.push(self.get_entity(id).ok_or(id)?); - } + self.get_entity(entities) + } - Ok(borrows) + /// Returns [`EntityMut`]s that expose read and write operations for the + /// given `entities`, returning [`Err`] if any of the given entities do not + /// exist. Instead of immediately unwrapping the value returned from this + /// function, prefer [`World::entity_mut`]. + /// + /// This function supports fetching a single entity or multiple entities: + /// - Pass an [`Entity`] to receive a single [`EntityWorldMut`]. + /// - This reference type allows for structural changes to the entity, + /// such as adding or removing components, or despawning the entity. + /// - Pass a slice of [`Entity`]s to receive a [`Vec`]. + /// - Pass an array of [`Entity`]s to receive an equally-sized array of [`EntityMut`]s. + /// - Pass a reference to a [`EntityHashSet`] to receive an + /// [`EntityHashMap`](crate::entity::EntityHashMap). + /// + /// In order to perform structural changes on the returned entity reference, + /// such as adding or removing components, or despawning the entity, only a + /// single [`Entity`] can be passed to this function. Allowing multiple + /// entities at the same time with structural access would lead to undefined + /// behavior, so [`EntityMut`] is returned when requesting multiple entities. + /// + /// # Errors + /// + /// - Returns [`EntityFetchError::NoSuchEntity`] if any of the given `entities` do not exist in the world. + /// - Only the first entity found to be missing will be returned. + /// - Returns [`EntityFetchError::AliasedMutability`] if the same entity is requested multiple times. + /// + /// # Examples + /// + /// For examples, see [`World::entity_mut`]. + #[inline] + pub fn get_entity_mut( + &mut self, + entities: F, + ) -> Result, EntityFetchError> { + let cell = self.as_unsafe_world_cell(); + // SAFETY: `&mut self` gives mutable access to the entire world, + // and prevents any other access to the world. + unsafe { entities.fetch_mut(cell) } } /// Returns an [`Entity`] iterator of current entities. @@ -952,49 +1147,6 @@ impl World { }) } - /// Retrieves an [`EntityWorldMut`] that exposes read and write operations for the given `entity`. - /// Returns [`None`] if the `entity` does not exist. - /// Instead of unwrapping the value returned from this function, prefer [`World::entity_mut`]. - /// - /// ``` - /// use bevy_ecs::{component::Component, world::World}; - /// - /// #[derive(Component)] - /// struct Position { - /// x: f32, - /// y: f32, - /// } - /// - /// let mut world = World::new(); - /// let entity = world.spawn(Position { x: 0.0, y: 0.0 }).id(); - /// let mut entity_mut = world.get_entity_mut(entity).unwrap(); - /// let mut position = entity_mut.get_mut::().unwrap(); - /// position.x = 1.0; - /// ``` - #[inline] - pub fn get_entity_mut(&mut self, entity: Entity) -> Option { - let location = self.entities.get(entity)?; - // SAFETY: `entity` exists and `location` is that entity's location - Some(unsafe { EntityWorldMut::new(self, entity, location) }) - } - - /// Verify that no duplicate entities are present in the given slice. - /// Does NOT check if the entities actually exist in the world. - /// - /// # Errors - /// - /// If any entities are duplicated. - fn verify_unique_entities(entities: &[Entity]) -> Result<(), QueryEntityError<'static>> { - for i in 0..entities.len() { - for j in 0..i { - if entities[i] == entities[j] { - return Err(QueryEntityError::AliasedMutability(entities[i])); - } - } - } - Ok(()) - } - /// Gets mutable access to multiple entities. /// /// # Errors @@ -1015,42 +1167,20 @@ impl World { /// // Trying to access the same entity multiple times will fail. /// assert!(world.get_many_entities_mut([id1, id1]).is_err()); /// ``` + #[deprecated( + since = "0.15.0", + note = "Use `World::get_entity_mut::<[Entity; N]>` instead" + )] pub fn get_many_entities_mut( &mut self, entities: [Entity; N], ) -> Result<[EntityMut<'_>; N], QueryEntityError> { - Self::verify_unique_entities(&entities)?; - - // SAFETY: Each entity is unique. - unsafe { self.get_entities_mut_unchecked(entities) } - } - - /// # Safety - /// `entities` must contain no duplicate [`Entity`] IDs. - unsafe fn get_entities_mut_unchecked( - &mut self, - entities: [Entity; N], - ) -> Result<[EntityMut<'_>; N], QueryEntityError> { - let world_cell = self.as_unsafe_world_cell(); - - let mut cells = [MaybeUninit::uninit(); N]; - for (cell, id) in core::iter::zip(&mut cells, entities) { - *cell = MaybeUninit::new( - world_cell - .get_entity(id) - .ok_or(QueryEntityError::NoSuchEntity(id))?, - ); - } - // SAFETY: Each item was initialized in the loop above. - let cells = cells.map(|c| unsafe { MaybeUninit::assume_init(c) }); - - // SAFETY: - // - `world_cell` has exclusive access to the entire world. - // - The caller ensures that each entity is unique, so none - // of the borrows will conflict with one another. - let borrows = cells.map(|c| unsafe { EntityMut::new(c) }); - - Ok(borrows) + self.get_entity_mut(entities).map_err(|e| match e { + EntityFetchError::NoSuchEntity(entity) => QueryEntityError::NoSuchEntity(entity), + EntityFetchError::AliasedMutability(entity) => { + QueryEntityError::AliasedMutability(entity) + } + }) } /// Gets mutable access to multiple entities, whose number is determined at runtime. @@ -1074,14 +1204,20 @@ impl World { /// // Trying to access the same entity multiple times will fail. /// assert!(world.get_many_entities_dynamic_mut(&[id1, id1]).is_err()); /// ``` + #[deprecated( + since = "0.15.0", + note = "Use `World::get_entity_mut::<&[Entity]>` instead" + )] pub fn get_many_entities_dynamic_mut<'w>( &'w mut self, entities: &[Entity], ) -> Result>, QueryEntityError> { - Self::verify_unique_entities(entities)?; - - // SAFETY: Each entity is unique. - unsafe { self.get_entities_dynamic_mut_unchecked(entities.iter().copied()) } + self.get_entity_mut(entities).map_err(|e| match e { + EntityFetchError::NoSuchEntity(entity) => QueryEntityError::NoSuchEntity(entity), + EntityFetchError::AliasedMutability(entity) => { + QueryEntityError::AliasedMutability(entity) + } + }) } /// Gets mutable access to multiple entities, contained in a [`EntityHashSet`]. @@ -1111,41 +1247,22 @@ impl World { /// let mut entities = world.get_many_entities_from_set_mut(&set).unwrap(); /// let entity1 = entities.get_mut(0).unwrap(); /// ``` + #[deprecated( + since = "0.15.0", + note = "Use `World::get_entity_mut::<&EntityHashSet>` instead." + )] pub fn get_many_entities_from_set_mut<'w>( &'w mut self, entities: &EntityHashSet, ) -> Result>, QueryEntityError> { - // SAFETY: Each entity is unique. - unsafe { self.get_entities_dynamic_mut_unchecked(entities.iter().copied()) } - } - - /// # Safety - /// `entities` must produce no duplicate [`Entity`] IDs. - unsafe fn get_entities_dynamic_mut_unchecked( - &mut self, - entities: impl ExactSizeIterator, - ) -> Result>, QueryEntityError> { - let world_cell = self.as_unsafe_world_cell(); - - let mut cells = Vec::with_capacity(entities.len()); - for id in entities { - cells.push( - world_cell - .get_entity(id) - .ok_or(QueryEntityError::NoSuchEntity(id))?, - ); - } - - let borrows = cells - .into_iter() - // SAFETY: - // - `world_cell` has exclusive access to the entire world. - // - The caller ensures that each entity is unique, so none - // of the borrows will conflict with one another. - .map(|c| unsafe { EntityMut::new(c) }) - .collect(); - - Ok(borrows) + self.get_entity_mut(entities) + .map(|fetched| fetched.into_values().collect()) + .map_err(|e| match e { + EntityFetchError::NoSuchEntity(entity) => QueryEntityError::NoSuchEntity(entity), + EntityFetchError::AliasedMutability(entity) => { + QueryEntityError::AliasedMutability(entity) + } + }) } /// Spawns a new [`Entity`] and returns a corresponding [`EntityWorldMut`], which can be used @@ -1332,7 +1449,7 @@ impl World { /// ``` #[inline] pub fn get(&self, entity: Entity) -> Option<&T> { - self.get_entity(entity)?.get() + self.get_entity(entity).ok()?.get() } /// Retrieves a mutable reference to the given `entity`'s [`Component`] of the given type. @@ -1381,7 +1498,7 @@ impl World { /// let mut world = World::new(); /// let entity = world.spawn(Position { x: 0.0, y: 0.0 }).id(); /// assert!(world.despawn(entity)); - /// assert!(world.get_entity(entity).is_none()); + /// assert!(world.get_entity(entity).is_err()); /// assert!(world.get::(entity).is_none()); /// ``` #[track_caller] @@ -1406,7 +1523,7 @@ impl World { log_warning: bool, ) -> bool { self.flush(); - if let Some(entity) = self.get_entity_mut(entity) { + if let Ok(entity) = self.get_entity_mut(entity) { entity.despawn(); true } else { @@ -3282,8 +3399,10 @@ mod tests { use crate::{ change_detection::DetectChangesMut, component::{ComponentDescriptor, ComponentInfo, StorageType}, + entity::EntityHashSet, ptr::OwningPtr, system::Resource, + world::error::EntityFetchError, }; use alloc::sync::Arc; use bevy_ecs_macros::Component; @@ -3822,21 +3941,102 @@ mod tests { } #[test] - fn test_verify_unique_entities() { + fn get_entity() { + let mut world = World::new(); + + let e1 = world.spawn_empty().id(); + let e2 = world.spawn_empty().id(); + + assert!(world.get_entity(e1).is_ok()); + assert!(world.get_entity([e1, e2]).is_ok()); + assert!(world + .get_entity(&[e1, e2] /* this is an array not a slice */) + .is_ok()); + assert!(world.get_entity(&vec![e1, e2][..]).is_ok()); + assert!(world + .get_entity(&EntityHashSet::from_iter([e1, e2])) + .is_ok()); + + world.entity_mut(e1).despawn(); + + assert_eq!(Err(e1), world.get_entity(e1).map(|_| {})); + assert_eq!(Err(e1), world.get_entity([e1, e2]).map(|_| {})); + assert_eq!( + Err(e1), + world + .get_entity(&[e1, e2] /* this is an array not a slice */) + .map(|_| {}) + ); + assert_eq!(Err(e1), world.get_entity(&vec![e1, e2][..]).map(|_| {})); + assert_eq!( + Err(e1), + world + .get_entity(&EntityHashSet::from_iter([e1, e2])) + .map(|_| {}) + ); + } + + #[test] + fn get_entity_mut() { let mut world = World::new(); - let entity1 = world.spawn(()).id(); - let entity2 = world.spawn(()).id(); - let entity3 = world.spawn(()).id(); - let entity4 = world.spawn(()).id(); - let entity5 = world.spawn(()).id(); - - assert!( - World::verify_unique_entities(&[entity1, entity2, entity3, entity4, entity5]).is_ok() + + let e1 = world.spawn_empty().id(); + let e2 = world.spawn_empty().id(); + + assert!(world.get_entity_mut(e1).is_ok()); + assert!(world.get_entity_mut([e1, e2]).is_ok()); + assert!(world + .get_entity_mut(&[e1, e2] /* this is an array not a slice */) + .is_ok()); + assert!(world.get_entity_mut(&vec![e1, e2][..]).is_ok()); + assert!(world + .get_entity_mut(&EntityHashSet::from_iter([e1, e2])) + .is_ok()); + + assert_eq!( + Err(EntityFetchError::AliasedMutability(e1)), + world.get_entity_mut([e1, e2, e1]).map(|_| {}) + ); + assert_eq!( + Err(EntityFetchError::AliasedMutability(e1)), + world + .get_entity_mut(&[e1, e2, e1] /* this is an array not a slice */) + .map(|_| {}) + ); + assert_eq!( + Err(EntityFetchError::AliasedMutability(e1)), + world.get_entity_mut(&vec![e1, e2, e1][..]).map(|_| {}) + ); + // Aliased mutability isn't allowed by HashSets + assert!(world + .get_entity_mut(&EntityHashSet::from_iter([e1, e2, e1])) + .is_ok()); + + world.entity_mut(e1).despawn(); + + assert_eq!( + Err(EntityFetchError::NoSuchEntity(e1)), + world.get_entity_mut(e1).map(|_| {}) + ); + assert_eq!( + Err(EntityFetchError::NoSuchEntity(e1)), + world.get_entity_mut([e1, e2]).map(|_| {}) + ); + assert_eq!( + Err(EntityFetchError::NoSuchEntity(e1)), + world + .get_entity_mut(&[e1, e2] /* this is an array not a slice */) + .map(|_| {}) + ); + assert_eq!( + Err(EntityFetchError::NoSuchEntity(e1)), + world.get_entity_mut(&vec![e1, e2][..]).map(|_| {}) + ); + assert_eq!( + Err(EntityFetchError::NoSuchEntity(e1)), + world + .get_entity_mut(&EntityHashSet::from_iter([e1, e2])) + .map(|_| {}) ); - assert!(World::verify_unique_entities(&[entity1, entity1, entity2, entity5]).is_err()); - assert!(World::verify_unique_entities(&[ - entity1, entity2, entity3, entity4, entity5, entity1 - ]) - .is_err()); } } diff --git a/crates/bevy_hierarchy/src/child_builder.rs b/crates/bevy_hierarchy/src/child_builder.rs index 47c74bb4f6cdc8..ad5819479d0499 100644 --- a/crates/bevy_hierarchy/src/child_builder.rs +++ b/crates/bevy_hierarchy/src/child_builder.rs @@ -45,7 +45,7 @@ fn update_parent(world: &mut World, child: Entity, new_parent: Entity) -> Option /// /// Removes the [`Children`] component from the parent if it's empty. fn remove_from_children(world: &mut World, parent: Entity, child: Entity) { - let Some(mut parent) = world.get_entity_mut(parent) else { + let Ok(mut parent) = world.get_entity_mut(parent) else { return; }; let Some(mut children) = parent.get_mut::() else { diff --git a/crates/bevy_hierarchy/src/hierarchy.rs b/crates/bevy_hierarchy/src/hierarchy.rs index 351f36ae7d747a..4a9d71ace7d13e 100644 --- a/crates/bevy_hierarchy/src/hierarchy.rs +++ b/crates/bevy_hierarchy/src/hierarchy.rs @@ -312,7 +312,7 @@ mod tests { // The parent's Children component should be removed. assert!(world.entity(parent).get::().is_none()); // The child should be despawned. - assert!(world.get_entity(child).is_none()); + assert!(world.get_entity(child).is_err()); } #[test] @@ -340,6 +340,6 @@ mod tests { assert!(children.is_some()); assert_eq!(children.unwrap().len(), 2_usize); // The original child should be despawned. - assert!(world.get_entity(child).is_none()); + assert!(world.get_entity(child).is_err()); } } diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 168999d6226bc4..f2e958189205ef 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -601,7 +601,7 @@ pub fn process_remote_list_request(In(params): In>, world: &World) fn get_entity(world: &World, entity: Entity) -> Result, BrpError> { world .get_entity(entity) - .ok_or_else(|| BrpError::entity_not_found(entity)) + .map_err(|_| BrpError::entity_not_found(entity)) } /// Mutably retrieves an entity from the [`World`], returning an error if the @@ -609,7 +609,7 @@ fn get_entity(world: &World, entity: Entity) -> Result, BrpError> fn get_entity_mut(world: &mut World, entity: Entity) -> Result, BrpError> { world .get_entity_mut(entity) - .ok_or_else(|| BrpError::entity_not_found(entity)) + .map_err(|_| BrpError::entity_not_found(entity)) } /// Returns the [`TypeId`] and [`ComponentId`] of the components with the given diff --git a/crates/bevy_render/src/world_sync.rs b/crates/bevy_render/src/world_sync.rs index cec1ce748c3863..e0f81fdc02692a 100644 --- a/crates/bevy_render/src/world_sync.rs +++ b/crates/bevy_render/src/world_sync.rs @@ -148,7 +148,7 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl for record in pending.drain(..) { match record { EntityRecord::Added(e) => { - if let Some(mut entity) = world.get_entity_mut(e) { + if let Ok(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!"); @@ -162,7 +162,7 @@ pub(crate) fn entity_sync_system(main_world: &mut World, render_world: &mut Worl } } EntityRecord::Removed(e) => { - if let Some(ec) = render_world.get_entity_mut(e) { + if let Ok(ec) = render_world.get_entity_mut(e) { ec.despawn(); }; } diff --git a/crates/bevy_scene/src/bundle.rs b/crates/bevy_scene/src/bundle.rs index b0f8b38a7734ff..0024b2f77729b3 100644 --- a/crates/bevy_scene/src/bundle.rs +++ b/crates/bevy_scene/src/bundle.rs @@ -180,7 +180,7 @@ mod tests { app.update(); // the scene entity does not exist anymore - assert!(app.world().get_entity(scene_entity).is_none()); + assert!(app.world().get_entity(scene_entity).is_err()); // the root entity does not have any children anymore assert!(app.world().entity(entity).get::().is_none()); diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index b354b52b09175a..1fa42e60191433 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -191,7 +191,7 @@ impl SceneSpawner { pub fn despawn_instance_sync(&mut self, world: &mut World, instance_id: &InstanceId) { if let Some(instance) = self.spawned_instances.remove(instance_id) { for &entity in instance.entity_map.values() { - if let Some(mut entity_mut) = world.get_entity_mut(entity) { + if let Ok(mut entity_mut) = world.get_entity_mut(entity) { entity_mut.remove_parent(); entity_mut.despawn_recursive(); }; @@ -427,7 +427,7 @@ pub fn scene_spawner_system(world: &mut World) { scene_spawner .scenes_with_parent .retain(|(instance, parent)| { - let retain = world.get_entity(*parent).is_some(); + let retain = world.get_entity(*parent).is_ok(); if !retain { dead_instances.insert(*instance); diff --git a/crates/bevy_scene/src/serde.rs b/crates/bevy_scene/src/serde.rs index d854d280a18aa9..0cf4efa128fffe 100644 --- a/crates/bevy_scene/src/serde.rs +++ b/crates/bevy_scene/src/serde.rs @@ -775,7 +775,7 @@ mod tests { assert!(dst_world .query_filtered::<&MyEntityRef, With>() .iter(&dst_world) - .all(|r| world.get_entity(r.0).is_none())); + .all(|r| world.get_entity(r.0).is_err())); } #[test] diff --git a/crates/bevy_transform/src/commands.rs b/crates/bevy_transform/src/commands.rs index 4e02105bfe85b9..2860ddf381ca73 100644 --- a/crates/bevy_transform/src/commands.rs +++ b/crates/bevy_transform/src/commands.rs @@ -29,9 +29,15 @@ impl Command for AddChildInPlace { hierarchy_command.apply(world); // FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436. let mut update_transform = || { - let parent = *world.get_entity(self.parent)?.get::()?; - let child_global = *world.get_entity(self.child)?.get::()?; - let mut child_entity = world.get_entity_mut(self.child)?; + let parent = *world + .get_entity(self.parent) + .ok()? + .get::()?; + let child_global = *world + .get_entity(self.child) + .ok()? + .get::()?; + let mut child_entity = world.get_entity_mut(self.child).ok()?; let mut child = child_entity.get_mut::()?; *child = child_global.reparented_to(&parent); Some(()) @@ -54,8 +60,11 @@ impl Command for RemoveParentInPlace { hierarchy_command.apply(world); // FIXME: Replace this closure with a `try` block. See: https://github.com/rust-lang/rust/issues/31436. let mut update_transform = || { - let child_global = *world.get_entity(self.child)?.get::()?; - let mut child_entity = world.get_entity_mut(self.child)?; + let child_global = *world + .get_entity(self.child) + .ok()? + .get::()?; + let mut child_entity = world.get_entity_mut(self.child).ok()?; let mut child = child_entity.get_mut::()?; *child = child_global.compute_transform(); Some(()) From 0a150b0d22711443a943d37a98b7afb7e9862130 Mon Sep 17 00:00:00 2001 From: Alice Cecile Date: Mon, 7 Oct 2024 11:24:57 -0400 Subject: [PATCH 051/546] Add more tools for traversing hierarchies (#15627) # Objective - Working with hierarchies in Bevy is far too tedious due to a lack of helper functions. - This is the first half of #15609. ## Solution Extend [`HierarchyQueryExt`](https://docs.rs/bevy/latest/bevy/hierarchy/trait.HierarchyQueryExt) with the following methods: - `parent` - `children` - `root_parent` - `iter_leaves` - `iter_siblings` - `iter_descendants_depth_first` I've opted to make both `iter_leaves` and `iter_siblings` collect the list of matching Entities for now, rather that operate by reference like the existing `iter_descendants`. This was simpler, and in the case of `iter_siblings` especially, the number of matching entities is likely to be much smaller. I've kept the generics in the type signature however, so we can go back and optimize that freely without a breaking change whenever we want. ## Testing I've added some basic testing, but they're currently failing. If you'd like to help, I'd welcome suggestions or a PR to my PR over the weekend <3 --------- Co-authored-by: Viktor Gustavsson Co-authored-by: poopy Co-authored-by: Christian Hughes <9044780+ItsDoot@users.noreply.github.com> --- crates/bevy_hierarchy/src/query_extension.rs | 250 ++++++++++++++++++- 1 file changed, 241 insertions(+), 9 deletions(-) diff --git a/crates/bevy_hierarchy/src/query_extension.rs b/crates/bevy_hierarchy/src/query_extension.rs index 36bf790cec1bd0..5cd86313107622 100644 --- a/crates/bevy_hierarchy/src/query_extension.rs +++ b/crates/bevy_hierarchy/src/query_extension.rs @@ -5,16 +5,54 @@ use bevy_ecs::{ query::{QueryData, QueryFilter, WorldQuery}, system::Query, }; +use smallvec::SmallVec; use crate::{Children, Parent}; /// An extension trait for [`Query`] that adds hierarchy related methods. pub trait HierarchyQueryExt<'w, 's, D: QueryData, F: QueryFilter> { + /// Returns the parent [`Entity`] of the given `entity`, if any. + fn parent(&'w self, entity: Entity) -> Option + where + D::ReadOnly: WorldQuery = &'w Parent>; + + /// Returns a slice over the [`Children`] of the given `entity`. + /// + /// This may be empty if the `entity` has no children. + fn children(&'w self, entity: Entity) -> &'w [Entity] + where + D::ReadOnly: WorldQuery = &'w Children>; + + /// Returns the topmost ancestor of the given `entity`. + /// + /// This may be the entity itself if it has no parent. + fn root_ancestor(&'w self, entity: Entity) -> Entity + where + D::ReadOnly: WorldQuery = &'w Parent>; + + /// Returns an [`Iterator`] of [`Entity`]s over the leaves of the hierarchy that are underneath this `entity`. + /// + /// Only entities which have no children are considered leaves. + /// This will not include the entity itself, and will not include any entities which are not descendants of the entity, + /// even if they are leaves in the same hierarchical tree. + /// + /// Traverses the hierarchy depth-first. + fn iter_leaves(&'w self, entity: Entity) -> impl Iterator + 'w + where + D::ReadOnly: WorldQuery = &'w Children>; + + /// Returns an [`Iterator`] of [`Entity`]s over the `entity`s immediate siblings, who share the same parent. + /// + /// The entity itself is not included in the iterator. + fn iter_siblings(&'w self, entity: Entity) -> impl Iterator + where + D::ReadOnly: WorldQuery = (Option<&'w Parent>, Option<&'w Children>)>; + /// Returns an [`Iterator`] of [`Entity`]s over all of `entity`s descendants. /// /// Can only be called on a [`Query`] of [`Children`] (i.e. `Query<&Children>`). /// - /// Traverses the hierarchy breadth-first. + /// Traverses the hierarchy breadth-first and does not include the entity itself. /// /// # Examples /// ``` @@ -34,8 +72,21 @@ pub trait HierarchyQueryExt<'w, 's, D: QueryData, F: QueryFilter> { where D::ReadOnly: WorldQuery = &'w Children>; + /// Returns an [`Iterator`] of [`Entity`]s over all of `entity`s descendants. + /// + /// Can only be called on a [`Query`] of [`Children`] (i.e. `Query<&Children>`). + /// + /// This is a depth-first alternative to [`HierarchyQueryExt::iter_descendants`]. + fn iter_descendants_depth_first( + &'w self, + entity: Entity, + ) -> DescendantDepthFirstIter<'w, 's, D, F> + where + D::ReadOnly: WorldQuery = &'w Children>; + /// Returns an [`Iterator`] of [`Entity`]s over all of `entity`s ancestors. /// + /// Does not include the entity itself. /// Can only be called on a [`Query`] of [`Parent`] (i.e. `Query<&Parent>`). /// /// # Examples @@ -58,6 +109,59 @@ pub trait HierarchyQueryExt<'w, 's, D: QueryData, F: QueryFilter> { } impl<'w, 's, D: QueryData, F: QueryFilter> HierarchyQueryExt<'w, 's, D, F> for Query<'w, 's, D, F> { + fn parent(&'w self, entity: Entity) -> Option + where + ::ReadOnly: WorldQuery = &'w Parent>, + { + self.get(entity).map(Parent::get).ok() + } + + fn children(&'w self, entity: Entity) -> &'w [Entity] + where + ::ReadOnly: WorldQuery = &'w Children>, + { + self.get(entity) + .map_or(&[] as &[Entity], |children| children) + } + + fn root_ancestor(&'w self, entity: Entity) -> Entity + where + ::ReadOnly: WorldQuery = &'w Parent>, + { + // Recursively search up the tree until we're out of parents + match self.get(entity) { + Ok(parent) => self.root_ancestor(parent.get()), + Err(_) => entity, + } + } + + fn iter_leaves(&'w self, entity: Entity) -> impl Iterator + where + ::ReadOnly: WorldQuery = &'w Children>, + { + self.iter_descendants_depth_first(entity).filter(|entity| { + self.get(*entity) + // These are leaf nodes if they have the `Children` component but it's empty + .map(|children| children.is_empty()) + // Or if they don't have the `Children` component at all + .unwrap_or(true) + }) + } + + fn iter_siblings(&'w self, entity: Entity) -> impl Iterator + where + D::ReadOnly: WorldQuery = (Option<&'w Parent>, Option<&'w Children>)>, + { + self.get(entity) + .ok() + .and_then(|(maybe_parent, _)| maybe_parent.map(Parent::get)) + .and_then(|parent| self.get(parent).ok()) + .and_then(|(_, maybe_children)| maybe_children) + .into_iter() + .flat_map(move |children| children.iter().filter(move |child| **child != entity)) + .copied() + } + fn iter_descendants(&'w self, entity: Entity) -> DescendantIter<'w, 's, D, F> where D::ReadOnly: WorldQuery = &'w Children>, @@ -65,6 +169,16 @@ impl<'w, 's, D: QueryData, F: QueryFilter> HierarchyQueryExt<'w, 's, D, F> for Q DescendantIter::new(self, entity) } + fn iter_descendants_depth_first( + &'w self, + entity: Entity, + ) -> DescendantDepthFirstIter<'w, 's, D, F> + where + D::ReadOnly: WorldQuery = &'w Children>, + { + DescendantDepthFirstIter::new(self, entity) + } + fn iter_ancestors(&'w self, entity: Entity) -> AncestorIter<'w, 's, D, F> where D::ReadOnly: WorldQuery = &'w Parent>, @@ -119,6 +233,51 @@ where } } +/// An [`Iterator`] of [`Entity`]s over the descendants of an [`Entity`]. +/// +/// Traverses the hierarchy depth-first. +pub struct DescendantDepthFirstIter<'w, 's, D: QueryData, F: QueryFilter> +where + D::ReadOnly: WorldQuery = &'w Children>, +{ + children_query: &'w Query<'w, 's, D, F>, + stack: SmallVec<[Entity; 8]>, +} + +impl<'w, 's, D: QueryData, F: QueryFilter> DescendantDepthFirstIter<'w, 's, D, F> +where + D::ReadOnly: WorldQuery = &'w Children>, +{ + /// Returns a new [`DescendantDepthFirstIter`]. + pub fn new(children_query: &'w Query<'w, 's, D, F>, entity: Entity) -> Self { + DescendantDepthFirstIter { + children_query, + stack: children_query + .get(entity) + .map_or(SmallVec::new(), |children| { + children.iter().rev().copied().collect() + }), + } + } +} + +impl<'w, 's, D: QueryData, F: QueryFilter> Iterator for DescendantDepthFirstIter<'w, 's, D, F> +where + D::ReadOnly: WorldQuery = &'w Children>, +{ + type Item = Entity; + + fn next(&mut self) -> Option { + let entity = self.stack.pop()?; + + if let Ok(children) = self.children_query.get(entity) { + self.stack.extend(children.iter().rev().copied()); + } + + Some(entity) + } +} + /// An [`Iterator`] of [`Entity`]s over the ancestors of an [`Entity`]. pub struct AncestorIter<'w, 's, D: QueryData, F: QueryFilter> where @@ -170,35 +329,108 @@ mod tests { fn descendant_iter() { let world = &mut World::new(); - let [a, b, c, d] = core::array::from_fn(|i| world.spawn(A(i)).id()); + let [a0, a1, a2, a3] = core::array::from_fn(|i| world.spawn(A(i)).id()); - world.entity_mut(a).add_children(&[b, c]); - world.entity_mut(c).add_children(&[d]); + world.entity_mut(a0).add_children(&[a1, a2]); + world.entity_mut(a1).add_children(&[a3]); let mut system_state = SystemState::<(Query<&Children>, Query<&A>)>::new(world); let (children_query, a_query) = system_state.get(world); let result: Vec<_> = a_query - .iter_many(children_query.iter_descendants(a)) + .iter_many(children_query.iter_descendants(a0)) .collect(); assert_eq!([&A(1), &A(2), &A(3)], result.as_slice()); } + #[test] + fn descendant_depth_first_iter() { + let world = &mut World::new(); + + let [a0, a1, a2, a3] = core::array::from_fn(|i| world.spawn(A(i)).id()); + + world.entity_mut(a0).add_children(&[a1, a2]); + world.entity_mut(a1).add_children(&[a3]); + + let mut system_state = SystemState::<(Query<&Children>, Query<&A>)>::new(world); + let (children_query, a_query) = system_state.get(world); + + let result: Vec<_> = a_query + .iter_many(children_query.iter_descendants_depth_first(a0)) + .collect(); + + assert_eq!([&A(1), &A(3), &A(2)], result.as_slice()); + } + #[test] fn ancestor_iter() { let world = &mut World::new(); - let [a, b, c] = core::array::from_fn(|i| world.spawn(A(i)).id()); + let [a0, a1, a2] = core::array::from_fn(|i| world.spawn(A(i)).id()); - world.entity_mut(a).add_children(&[b]); - world.entity_mut(b).add_children(&[c]); + world.entity_mut(a0).add_children(&[a1]); + world.entity_mut(a1).add_children(&[a2]); let mut system_state = SystemState::<(Query<&Parent>, Query<&A>)>::new(world); let (parent_query, a_query) = system_state.get(world); - let result: Vec<_> = a_query.iter_many(parent_query.iter_ancestors(c)).collect(); + let result: Vec<_> = a_query.iter_many(parent_query.iter_ancestors(a2)).collect(); assert_eq!([&A(1), &A(0)], result.as_slice()); } + + #[test] + fn root_ancestor() { + let world = &mut World::new(); + + let [a0, a1, a2] = core::array::from_fn(|i| world.spawn(A(i)).id()); + + world.entity_mut(a0).add_children(&[a1]); + world.entity_mut(a1).add_children(&[a2]); + + let mut system_state = SystemState::>::new(world); + let parent_query = system_state.get(world); + + assert_eq!(a0, parent_query.root_ancestor(a2)); + assert_eq!(a0, parent_query.root_ancestor(a1)); + assert_eq!(a0, parent_query.root_ancestor(a0)); + } + + #[test] + fn leaf_iter() { + let world = &mut World::new(); + + let [a0, a1, a2, a3] = core::array::from_fn(|i| world.spawn(A(i)).id()); + + world.entity_mut(a0).add_children(&[a1, a2]); + world.entity_mut(a1).add_children(&[a3]); + + let mut system_state = SystemState::<(Query<&Children>, Query<&A>)>::new(world); + let (children_query, a_query) = system_state.get(world); + + let result: Vec<_> = a_query.iter_many(children_query.iter_leaves(a0)).collect(); + + assert_eq!([&A(3), &A(2)], result.as_slice()); + } + + #[test] + fn siblings() { + let world = &mut World::new(); + + let [a0, a1, a2, a3, a4] = core::array::from_fn(|i| world.spawn(A(i)).id()); + + world.entity_mut(a0).add_children(&[a1, a2, a3]); + world.entity_mut(a2).add_children(&[a4]); + + let mut system_state = + SystemState::<(Query<(Option<&Parent>, Option<&Children>)>, Query<&A>)>::new(world); + let (hierarchy_query, a_query) = system_state.get(world); + + let result: Vec<_> = a_query + .iter_many(hierarchy_query.iter_siblings(a1)) + .collect(); + + assert_eq!([&A(2), &A(3)], result.as_slice()); + } } From 4357539e067c309130adaa412792f296d8c3db69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Mon, 7 Oct 2024 17:56:06 +0200 Subject: [PATCH 052/546] Add most common interpolations (#15675) # Objective - Followup for #14788 - Support most usual ease function ## Solution - Use the crate [`interpolation`](https://docs.rs/interpolation/0.3.0/interpolation/trait.Ease.html) which has them all - it's already used by bevy_easings, bevy_tweening, be_tween, bevy_tweening_captured, bevy_enoki, kayak_ui in the Bevy ecosystem for various easing/tweening/interpolation --- crates/bevy_math/Cargo.toml | 1 + crates/bevy_math/src/curve/easing.rs | 136 +++++++++++++++++++++++++-- 2 files changed, 131 insertions(+), 6 deletions(-) diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index 8662b029b2c7b1..7ceecc10652dd2 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -21,6 +21,7 @@ rand = { version = "0.8", features = [ ], default-features = false, optional = true } rand_distr = { version = "0.4.3", optional = true } smallvec = { version = "1.11" } +interpolation = "0.3" bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ "glam", diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs index d9e885faea1a60..5e4891566d4308 100644 --- a/crates/bevy_math/src/curve/easing.rs +++ b/crates/bevy_math/src/curve/easing.rs @@ -5,6 +5,7 @@ use crate::{ ops::{self, FloatPow}, VectorSpace, }; +use interpolation::Ease; use super::{Curve, FunctionCurve, Interval}; @@ -84,6 +85,51 @@ where } impl EasingCurve f32>> { + /// A [`Curve`] mapping the [unit interval] to itself. + /// + /// [unit interval]: `Interval::UNIT` + pub fn ease(function: EaseFunction) -> Self { + Self { + start: 0.0, + end: 1.0, + easing: FunctionCurve::new( + Interval::UNIT, + match function { + EaseFunction::QuadraticIn => Ease::quadratic_in, + EaseFunction::QuadraticOut => Ease::quadratic_out, + EaseFunction::QuadraticInOut => Ease::quadratic_in_out, + EaseFunction::CubicIn => Ease::cubic_in, + EaseFunction::CubicOut => Ease::cubic_out, + EaseFunction::CubicInOut => Ease::cubic_in_out, + EaseFunction::QuarticIn => Ease::quartic_in, + EaseFunction::QuarticOut => Ease::quartic_out, + EaseFunction::QuarticInOut => Ease::quartic_in_out, + EaseFunction::QuinticIn => Ease::quintic_in, + EaseFunction::QuinticOut => Ease::quintic_out, + EaseFunction::QuinticInOut => Ease::quintic_in_out, + EaseFunction::SineIn => Ease::sine_in, + EaseFunction::SineOut => Ease::sine_out, + EaseFunction::SineInOut => Ease::sine_in_out, + EaseFunction::CircularIn => Ease::circular_in, + EaseFunction::CircularOut => Ease::circular_out, + EaseFunction::CircularInOut => Ease::circular_in_out, + EaseFunction::ExponentialIn => Ease::exponential_in, + EaseFunction::ExponentialOut => Ease::exponential_out, + EaseFunction::ExponentialInOut => Ease::exponential_in_out, + EaseFunction::ElasticIn => Ease::elastic_in, + EaseFunction::ElasticOut => Ease::elastic_out, + EaseFunction::ElasticInOut => Ease::elastic_in_out, + EaseFunction::BackIn => Ease::back_in, + EaseFunction::BackOut => Ease::back_out, + EaseFunction::BackInOut => Ease::back_in_out, + EaseFunction::BounceIn => Ease::bounce_in, + EaseFunction::BounceOut => Ease::bounce_out, + EaseFunction::BounceInOut => Ease::bounce_in_out, + }, + ), + } + } + /// A [`Curve`] mapping the [unit interval] to itself. /// /// Quadratic easing functions can have exactly one critical point. This is a point on the function @@ -92,7 +138,7 @@ impl EasingCurve f32>> { /// /// It uses the function `f(t) = t²` /// - /// [unit domain]: `Interval::UNIT` + /// [unit interval]: `Interval::UNIT` /// [`t = 1`]: `Self::quadratic_ease_out` pub fn quadratic_ease_in() -> Self { Self { @@ -110,7 +156,7 @@ impl EasingCurve f32>> { /// /// It uses the function `f(t) = 1 - (1 - t)²` /// - /// [unit domain]: `Interval::UNIT` + /// [unit interval]: `Interval::UNIT` /// [`t = 0`]: `Self::quadratic_ease_in` pub fn quadratic_ease_out() -> Self { fn f(t: f32) -> f32 { @@ -132,7 +178,7 @@ impl EasingCurve f32>> { /// /// It uses the function `f(t) = t² * (3 - 2t)` /// - /// [unit domain]: `Interval::UNIT` + /// [unit interval]: `Interval::UNIT` /// [sigmoid function]: https://en.wikipedia.org/wiki/Sigmoid_function /// [smoothstep function]: https://en.wikipedia.org/wiki/Smoothstep pub fn smoothstep() -> Self { @@ -150,7 +196,7 @@ impl EasingCurve f32>> { /// /// It uses the function `f(t) = t` /// - /// [unit domain]: `Interval::UNIT` + /// [unit interval]: `Interval::UNIT` pub fn identity() -> Self { Self { start: 0.0, @@ -219,7 +265,7 @@ where /// - for `n >= 2` the curve has a start segment and an end segment of length `1 / (2 * n)` and in /// between there are `n - 1` segments of length `1 / n` /// -/// [unit domain]: `Interval::UNIT` +/// [unit interval]: `Interval::UNIT` /// [`constant_curve(Interval::UNIT, 0.0)`]: `crate::curve::constant_curve` #[derive(Clone, Debug)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -265,7 +311,7 @@ impl StepCurve { /// /// parametrized by `omega` /// -/// [unit domain]: `Interval::UNIT` +/// [unit interval]: `Interval::UNIT` /// [smoothstep function]: https://en.wikipedia.org/wiki/Smoothstep /// [spring-mass-system]: https://notes.yvt.jp/Graphics/Easing-Functions/#elastic-easing #[derive(Clone, Debug)] @@ -296,3 +342,81 @@ impl ElasticCurve { Self { omega } } } + +/// Curve functions over the [unit interval], commonly used for easing transitions. +/// +/// [unit interval]: `Interval::UNIT` +#[derive(Debug, Copy, Clone, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub enum EaseFunction { + /// `f(t) = t²` + QuadraticIn, + /// `f(t) = -(t * (t - 2.0))` + QuadraticOut, + /// Behaves as `EaseFunction::QuadraticIn` for t < 0.5 and as `EaseFunction::QuadraticOut` for t >= 0.5 + QuadraticInOut, + + /// `f(t) = t³` + CubicIn, + /// `f(t) = (t - 1.0)³ + 1.0` + CubicOut, + /// Behaves as `EaseFunction::CubicIn` for t < 0.5 and as `EaseFunction::CubicOut` for t >= 0.5 + CubicInOut, + + /// `f(t) = t⁴` + QuarticIn, + /// `f(t) = (t - 1.0)³ * (1.0 - t) + 1.0` + QuarticOut, + /// Behaves as `EaseFunction::QuarticIn` for t < 0.5 and as `EaseFunction::QuarticOut` for t >= 0.5 + QuarticInOut, + + /// `f(t) = t⁵` + QuinticIn, + /// `f(t) = (t - 1.0)⁵ + 1.0` + QuinticOut, + /// Behaves as `EaseFunction::QuinticIn` for t < 0.5 and as `EaseFunction::QuinticOut` for t >= 0.5 + QuinticInOut, + + /// `f(t) = sin((t - 1.0) * π / 2.0) + 1.0` + SineIn, + /// `f(t) = sin(t * π / 2.0)` + SineOut, + /// Behaves as `EaseFunction::SineIn` for t < 0.5 and as `EaseFunction::SineOut` for t >= 0.5 + SineInOut, + + /// `f(t) = 1.0 - sqrt(1.0 - t²)` + CircularIn, + /// `f(t) = sqrt((2.0 - t) * t)` + CircularOut, + /// Behaves as `EaseFunction::CircularIn` for t < 0.5 and as `EaseFunction::CircularOut` for t >= 0.5 + CircularInOut, + + /// `f(t) = 2.0.powf(10.0 * (t - 1.0))` + ExponentialIn, + /// `f(t) = 1.0 - 2.0.powf(-10.0 * t)` + ExponentialOut, + /// Behaves as `EaseFunction::ExponentialIn` for t < 0.5 and as `EaseFunction::ExponentialOut` for t >= 0.5 + ExponentialInOut, + + /// `f(t) = sin(13.0 * π / 2.0 * t) * 2.0.powf(10.0 * (t - 1.0))` + ElasticIn, + /// `f(t) = sin(-13.0 * π / 2.0 * (t + 1.0)) * 2.0.powf(-10.0 * t) + 1.0` + ElasticOut, + /// Behaves as `EaseFunction::ElasticIn` for t < 0.5 and as `EaseFunction::ElasticOut` for t >= 0.5 + ElasticInOut, + + /// `f(t) = t³ - t * sin(t * π)` + BackIn, + /// `f(t) = 1.0 - (1.0 - t)³ - t * sin((1.0 - t) * π))` + BackOut, + /// Behaves as `EaseFunction::BackIn` for t < 0.5 and as `EaseFunction::BackOut` for t >= 0.5 + BackInOut, + + /// bouncy at the start! + BounceIn, + /// bouncy at the end! + BounceOut, + /// Behaves as `EaseFunction::BounceIn` for t < 0.5 and as `EaseFunction::BounceOut` for t >= 0.5 + BounceInOut, +} From 037464800eff97442d38932b817fcc7606b7c401 Mon Sep 17 00:00:00 2001 From: charlotte Date: Mon, 7 Oct 2024 08:59:51 -0700 Subject: [PATCH 053/546] Use global clear color for camera driver node. (#15688) When no cameras are configured, the `ClearColor` resource has no effect on the default window. Fixes https://discord.com/channels/691052431525675048/866787577687310356/1292601838075379796 ![image](https://github.com/user-attachments/assets/f42479c0-b239-4660-acd0-daa859b1f815) ![image](https://github.com/user-attachments/assets/4d625960-f105-4a29-91a3-44f4baadac30) --- crates/bevy_render/src/camera/camera_driver_node.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/camera/camera_driver_node.rs b/crates/bevy_render/src/camera/camera_driver_node.rs index 5845f216bb1bc0..2548c416ce1854 100644 --- a/crates/bevy_render/src/camera/camera_driver_node.rs +++ b/crates/bevy_render/src/camera/camera_driver_node.rs @@ -1,5 +1,5 @@ use crate::{ - camera::{ExtractedCamera, NormalizedRenderTarget, SortedCameras}, + camera::{ClearColor, ExtractedCamera, NormalizedRenderTarget, SortedCameras}, render_graph::{Node, NodeRunError, RenderGraphContext}, renderer::RenderContext, view::ExtractedWindows, @@ -53,6 +53,8 @@ impl Node for CameraDriverNode { } } + let clear_color_global = world.resource::(); + // wgpu (and some backends) require doing work for swap chains if you call `get_current_texture()` and `present()` // This ensures that Bevy doesn't crash, even when there are no cameras (and therefore no work submitted). for (id, window) in world.resource::().iter() { @@ -72,7 +74,7 @@ impl Node for CameraDriverNode { view: swap_chain_texture, resolve_target: None, ops: Operations { - load: LoadOp::Clear(wgpu::Color::BLACK), + load: LoadOp::Clear(clear_color_global.to_linear().into()), store: StoreOp::Store, }, })], From d1bd46d45e7bf7b42051837be4f0e1e1061d1267 Mon Sep 17 00:00:00 2001 From: Trashtalk217 Date: Mon, 7 Oct 2024 18:08:22 +0200 Subject: [PATCH 054/546] Deprecate `get_or_spawn` (#15652) # Objective After merging retained rendering world #15320, we now have a good way of creating a link between worlds (*HIYAA intensifies*). This means that `get_or_spawn` is no longer necessary for that function. Entity should be opaque as the warning above `get_or_spawn` says. This is also part of #15459. I'm deprecating `get_or_spawn_batch` in a different PR in order to keep the PR small in size. ## Solution Deprecate `get_or_spawn` and replace it with `get_entity` in most contexts. If it's possible to query `&RenderEntity`, then the entity is synced and `render_entity.id()` is initialized in the render world. ## Migration Guide If you are given an `Entity` and you want to do something with it, use `Commands.entity(...)` or `World.entity(...)`. If instead you want to spawn something use `Commands.spawn(...)` or `World.spawn(...)`. If you are not sure if an entity exists, you can always use `get_entity` and match on the `Option<...>` that is returned. --------- Co-authored-by: Alice Cecile --- benches/benches/bevy_ecs/world/commands.rs | 38 ------- benches/benches/bevy_ecs/world/mod.rs | 1 - crates/bevy_core_pipeline/src/core_3d/mod.rs | 3 +- crates/bevy_core_pipeline/src/dof/mod.rs | 32 +++--- crates/bevy_core_pipeline/src/taa/mod.rs | 3 +- crates/bevy_ecs/src/lib.rs | 101 ------------------- crates/bevy_ecs/src/system/commands/mod.rs | 2 + crates/bevy_ecs/src/world/mod.rs | 1 + crates/bevy_pbr/src/cluster/mod.rs | 21 ++-- crates/bevy_pbr/src/light_probe/mod.rs | 9 +- crates/bevy_pbr/src/prepass/mod.rs | 4 +- crates/bevy_pbr/src/render/light.rs | 45 +++++---- crates/bevy_pbr/src/ssao/mod.rs | 3 +- crates/bevy_pbr/src/volumetric_fog/render.rs | 13 ++- crates/bevy_render/src/extract_param.rs | 6 +- crates/bevy_ui/src/render/mod.rs | 4 +- examples/stress_tests/transform_hierarchy.rs | 4 +- 17 files changed, 90 insertions(+), 200 deletions(-) diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 19128f80ba7daa..e7e0483bc8a8f7 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -209,41 +209,3 @@ pub fn medium_sized_commands(criterion: &mut Criterion) { pub fn large_sized_commands(criterion: &mut Criterion) { sized_commands_impl::>(criterion); } - -pub fn get_or_spawn(criterion: &mut Criterion) { - let mut group = criterion.benchmark_group("get_or_spawn"); - group.warm_up_time(core::time::Duration::from_millis(500)); - group.measurement_time(core::time::Duration::from_secs(4)); - - group.bench_function("individual", |bencher| { - let mut world = World::default(); - let mut command_queue = CommandQueue::default(); - - bencher.iter(|| { - let mut commands = Commands::new(&mut command_queue, &world); - for i in 0..10_000 { - commands - .get_or_spawn(Entity::from_raw(i)) - .insert((Matrix::default(), Vec3::default())); - } - command_queue.apply(&mut world); - }); - }); - - group.bench_function("batched", |bencher| { - let mut world = World::default(); - let mut command_queue = CommandQueue::default(); - - bencher.iter(|| { - let mut commands = Commands::new(&mut command_queue, &world); - let mut values = Vec::with_capacity(10_000); - for i in 0..10_000 { - values.push((Entity::from_raw(i), (Matrix::default(), Vec3::default()))); - } - commands.insert_or_spawn_batch(values); - command_queue.apply(&mut world); - }); - }); - - group.finish(); -} diff --git a/benches/benches/bevy_ecs/world/mod.rs b/benches/benches/bevy_ecs/world/mod.rs index 8af5e399018a18..a88f034776852e 100644 --- a/benches/benches/bevy_ecs/world/mod.rs +++ b/benches/benches/bevy_ecs/world/mod.rs @@ -27,7 +27,6 @@ criterion_group!( zero_sized_commands, medium_sized_commands, large_sized_commands, - get_or_spawn, world_entity, world_get, world_query_get, diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 71f1f031979d51..0c94bc6767e383 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -590,7 +590,8 @@ pub fn extract_camera_prepass_phase( live_entities.insert(entity); commands - .get_or_spawn(entity) + .get_entity(entity) + .expect("Camera entity wasn't synced.") .insert_if(DepthPrepass, || depth_prepass) .insert_if(NormalPrepass, || normal_prepass) .insert_if(MotionVectorPrepass, || motion_vector_prepass) diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index 100b7f20ffc4d0..76087b63a69bd1 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -830,20 +830,24 @@ fn extract_depth_of_field_settings( calculate_focal_length(depth_of_field.sensor_height, perspective_projection.fov); // Convert `DepthOfField` to `DepthOfFieldUniform`. - commands.get_or_spawn(entity).insert(( - *depth_of_field, - DepthOfFieldUniform { - focal_distance: depth_of_field.focal_distance, - focal_length, - coc_scale_factor: focal_length * focal_length - / (depth_of_field.sensor_height * depth_of_field.aperture_f_stops), - max_circle_of_confusion_diameter: depth_of_field.max_circle_of_confusion_diameter, - max_depth: depth_of_field.max_depth, - pad_a: 0, - pad_b: 0, - pad_c: 0, - }, - )); + commands + .get_entity(entity) + .expect("Depth of field entity wasn't synced.") + .insert(( + *depth_of_field, + DepthOfFieldUniform { + focal_distance: depth_of_field.focal_distance, + focal_length, + coc_scale_factor: focal_length * focal_length + / (depth_of_field.sensor_height * depth_of_field.aperture_f_stops), + max_circle_of_confusion_diameter: depth_of_field + .max_circle_of_confusion_diameter, + max_depth: depth_of_field.max_depth, + pad_a: 0, + pad_b: 0, + pad_c: 0, + }, + )); } } diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index f3895d1e26241a..d5b66a51b8d3ca 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -375,7 +375,8 @@ fn extract_taa_settings(mut commands: Commands, mut main_world: ResMut, Changed)>]); } - #[test] - fn reserve_entities_across_worlds() { - let mut world_a = World::default(); - let mut world_b = World::default(); - - let e1 = world_a.spawn(A(1)).id(); - let e2 = world_a.spawn(A(2)).id(); - let e3 = world_a.entities().reserve_entity(); - world_a.flush_entities(); - - let world_a_max_entities = world_a.entities().len(); - world_b.entities.reserve_entities(world_a_max_entities); - world_b.entities.flush_as_invalid(); - - let e4 = world_b.spawn(A(4)).id(); - assert_eq!( - e4, - Entity::from_raw(3), - "new entity is created immediately after world_a's max entity" - ); - assert!(world_b.get::(e1).is_none()); - assert!(world_b.get_entity(e1).is_err()); - - assert!(world_b.get::(e2).is_none()); - assert!(world_b.get_entity(e2).is_err()); - - assert!(world_b.get::(e3).is_none()); - assert!(world_b.get_entity(e3).is_err()); - - world_b.get_or_spawn(e1).unwrap().insert(B(1)); - assert_eq!( - world_b.get::(e1), - Some(&B(1)), - "spawning into 'world_a' entities works" - ); - - world_b.get_or_spawn(e4).unwrap().insert(B(4)); - assert_eq!( - world_b.get::(e4), - Some(&B(4)), - "spawning into existing `world_b` entities works" - ); - assert_eq!( - world_b.get::(e4), - Some(&A(4)), - "spawning into existing `world_b` entities works" - ); - - let e4_mismatched_generation = - Entity::from_raw_and_generation(3, NonZero::::new(2).unwrap()); - assert!( - world_b.get_or_spawn(e4_mismatched_generation).is_none(), - "attempting to spawn on top of an entity with a mismatched entity generation fails" - ); - assert_eq!( - world_b.get::(e4), - Some(&B(4)), - "failed mismatched spawn doesn't change existing entity" - ); - assert_eq!( - world_b.get::(e4), - Some(&A(4)), - "failed mismatched spawn doesn't change existing entity" - ); - - let high_non_existent_entity = Entity::from_raw(6); - world_b - .get_or_spawn(high_non_existent_entity) - .unwrap() - .insert(B(10)); - assert_eq!( - world_b.get::(high_non_existent_entity), - Some(&B(10)), - "inserting into newly allocated high / non-continuous entity id works" - ); - - let high_non_existent_but_reserved_entity = Entity::from_raw(5); - assert!( - world_b.get_entity(high_non_existent_but_reserved_entity).is_err(), - "entities between high-newly allocated entity and continuous block of existing entities don't exist" - ); - - let reserved_entities = vec![ - world_b.entities().reserve_entity(), - world_b.entities().reserve_entity(), - world_b.entities().reserve_entity(), - world_b.entities().reserve_entity(), - ]; - - assert_eq!( - reserved_entities, - vec![ - Entity::from_raw(5), - Entity::from_raw(4), - Entity::from_raw(7), - Entity::from_raw(8), - ], - "space between original entities and high entities is used for new entity ids" - ); - } - #[test] fn insert_or_spawn_batch() { let mut world = World::default(); diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 2ba77daa30ac56..35f3aad376529c 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -325,8 +325,10 @@ impl<'w, 's> Commands<'w, 's> { /// [`Commands::spawn`]. This method should generally only be used for sharing entities across /// apps, and only when they have a scheme worked out to share an ID space (which doesn't happen /// by default). + #[deprecated(since = "0.15.0", note = "use Commands::spawn instead")] pub fn get_or_spawn(&mut self, entity: Entity) -> EntityCommands { self.queue(move |world: &mut World| { + #[allow(deprecated)] world.get_or_spawn(entity); }); EntityCommands { diff --git a/crates/bevy_ecs/src/world/mod.rs b/crates/bevy_ecs/src/world/mod.rs index 7c4daffb322712..6b194a0df5733b 100644 --- a/crates/bevy_ecs/src/world/mod.rs +++ b/crates/bevy_ecs/src/world/mod.rs @@ -944,6 +944,7 @@ impl World { /// This method should generally only be used for sharing entities across apps, and only when they have a /// scheme worked out to share an ID space (which doesn't happen by default). #[inline] + #[deprecated(since = "0.15.0", note = "use `World::spawn` instead")] pub fn get_or_spawn(&mut self, entity: Entity) -> Option { self.flush(); match self.entities.alloc_at_without_replacement(entity) { diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index 9c77a02149fc33..375861e2949077 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -554,14 +554,17 @@ pub fn extract_clusters( } } - commands.get_or_spawn(entity.id()).insert(( - ExtractedClusterableObjects { data }, - ExtractedClusterConfig { - near: clusters.near, - far: clusters.far, - dimensions: clusters.dimensions, - }, - )); + commands + .get_entity(entity.id()) + .expect("Clusters entity wasn't synced.") + .insert(( + ExtractedClusterableObjects { data }, + ExtractedClusterConfig { + near: clusters.near, + far: clusters.far, + dimensions: clusters.dimensions, + }, + )); } } @@ -617,7 +620,7 @@ pub fn prepare_clusters( view_clusters_bindings.write_buffers(render_device, &render_queue); - commands.get_or_spawn(entity).insert(view_clusters_bindings); + commands.entity(entity).insert(view_clusters_bindings); } } diff --git a/crates/bevy_pbr/src/light_probe/mod.rs b/crates/bevy_pbr/src/light_probe/mod.rs index eb1b4ccec4f91c..2bfd2408a9fe4c 100644 --- a/crates/bevy_pbr/src/light_probe/mod.rs +++ b/crates/bevy_pbr/src/light_probe/mod.rs @@ -386,7 +386,8 @@ fn gather_environment_map_uniform( EnvironmentMapUniform::default() }; commands - .get_or_spawn(view_entity.id()) + .get_entity(view_entity.id()) + .expect("Environment map light entity wasn't synced.") .insert(environment_map_uniform); } } @@ -440,11 +441,13 @@ fn gather_light_probes( // Record the per-view light probes. if render_view_light_probes.is_empty() { commands - .get_or_spawn(entity) + .get_entity(entity) + .expect("View entity wasn't synced.") .remove::>(); } else { commands - .get_or_spawn(entity) + .get_entity(entity) + .expect("View entity wasn't synced.") .insert(render_view_light_probes); } } diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 6aaeed96787eb0..dc4bb77be13251 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -585,7 +585,9 @@ pub fn extract_camera_previous_view_data( for (entity, camera, maybe_previous_view_data) in cameras_3d.iter() { if camera.is_active { let entity = entity.id(); - let mut entity = commands.get_or_spawn(entity); + let mut entity = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); if let Some(previous_view_data) = maybe_previous_view_data { entity.insert(previous_view_data.clone()); diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index ea0bfd25e7aed9..584c6f4fef4336 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -403,27 +403,30 @@ pub fn extract_lights( } } - commands.get_or_spawn(entity.id()).insert(( - ExtractedDirectionalLight { - color: directional_light.color.into(), - illuminance: directional_light.illuminance, - transform: *transform, - volumetric: volumetric_light.is_some(), - soft_shadow_size: directional_light.soft_shadow_size, - shadows_enabled: directional_light.shadows_enabled, - shadow_depth_bias: directional_light.shadow_depth_bias, - // The factor of SQRT_2 is for the worst-case diagonal offset - shadow_normal_bias: directional_light.shadow_normal_bias - * core::f32::consts::SQRT_2, - cascade_shadow_config: cascade_config.clone(), - cascades: extracted_cascades, - frusta: extracted_frusta, - render_layers: maybe_layers.unwrap_or_default().clone(), - }, - CascadesVisibleEntities { - entities: cascade_visible_entities, - }, - )); + commands + .get_entity(entity.id()) + .expect("Light entity wasn't synced.") + .insert(( + ExtractedDirectionalLight { + color: directional_light.color.into(), + illuminance: directional_light.illuminance, + transform: *transform, + volumetric: volumetric_light.is_some(), + soft_shadow_size: directional_light.soft_shadow_size, + shadows_enabled: directional_light.shadows_enabled, + shadow_depth_bias: directional_light.shadow_depth_bias, + // The factor of SQRT_2 is for the worst-case diagonal offset + shadow_normal_bias: directional_light.shadow_normal_bias + * core::f32::consts::SQRT_2, + cascade_shadow_config: cascade_config.clone(), + cascades: extracted_cascades, + frusta: extracted_frusta, + render_layers: maybe_layers.unwrap_or_default().clone(), + }, + CascadesVisibleEntities { + entities: cascade_visible_entities, + }, + )); } } diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index a37fb9c79985d4..b81a013d65f336 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -537,7 +537,8 @@ fn extract_ssao_settings( } if camera.is_active { commands - .get_or_spawn(entity.id()) + .get_entity(entity.id()) + .expect("SSAO entity wasn't synced.") .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 b833197d4ac998..ae1e0e7f81bc3f 100644 --- a/crates/bevy_pbr/src/volumetric_fog/render.rs +++ b/crates/bevy_pbr/src/volumetric_fog/render.rs @@ -280,18 +280,25 @@ pub fn extract_volumetric_fog( } for (entity, volumetric_fog) in view_targets.iter() { - commands.get_or_spawn(entity.id()).insert(*volumetric_fog); + commands + .get_entity(entity.id()) + .expect("Volumetric fog entity wasn't synced.") + .insert(*volumetric_fog); } for (entity, fog_volume, fog_transform) in fog_volumes.iter() { commands - .get_or_spawn(entity.id()) + .get_entity(entity.id()) + .expect("Fog volume entity wasn't synced.") .insert((*fog_volume).clone()) .insert(*fog_transform); } for (entity, volumetric_light) in volumetric_lights.iter() { - commands.get_or_spawn(entity.id()).insert(*volumetric_light); + commands + .get_entity(entity.id()) + .expect("Volumetric light entity wasn't synced.") + .insert(*volumetric_light); } } diff --git a/crates/bevy_render/src/extract_param.rs b/crates/bevy_render/src/extract_param.rs index 89b6edef1903df..801a6b2d1c20d1 100644 --- a/crates/bevy_render/src/extract_param.rs +++ b/crates/bevy_render/src/extract_param.rs @@ -30,11 +30,13 @@ use core::ops::{Deref, DerefMut}; /// ``` /// use bevy_ecs::prelude::*; /// use bevy_render::Extract; +/// use bevy_render::world_sync::RenderEntity; /// # #[derive(Component)] +/// // Do make sure to sync the cloud entities before extracting them. /// # struct Cloud; -/// fn extract_clouds(mut commands: Commands, clouds: Extract>>) { +/// fn extract_clouds(mut commands: Commands, clouds: Extract>>) { /// for cloud in &clouds { -/// commands.get_or_spawn(cloud).insert(Cloud); +/// commands.entity(cloud.id()).insert(Cloud); /// } /// } /// ``` diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index d8c9948d22a7b6..c136afdaa8af57 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -510,7 +510,9 @@ pub fn extract_default_ui_camera_view( TemporaryRenderEntity, )) .id(); - let mut entity_commands = commands.get_or_spawn(entity); + let mut entity_commands = commands + .get_entity(entity) + .expect("Camera entity wasn't synced."); entity_commands.insert(DefaultCameraView(default_camera_view)); if let Some(ui_anti_alias) = ui_anti_alias { entity_commands.insert(*ui_anti_alias); diff --git a/examples/stress_tests/transform_hierarchy.rs b/examples/stress_tests/transform_hierarchy.rs index 8607d489ec244a..ab669940357f01 100644 --- a/examples/stress_tests/transform_hierarchy.rs +++ b/examples/stress_tests/transform_hierarchy.rs @@ -428,9 +428,7 @@ fn spawn_tree( cmd.id() }; - commands - .get_or_spawn(ents[parent_idx]) - .add_child(child_entity); + commands.entity(ents[parent_idx]).add_child(child_entity); ents.push(child_entity); } From d1927736de9bd48c8188f1905f174da967f1efbd Mon Sep 17 00:00:00 2001 From: Emerson Coskey <56370779+ecoskey@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:26:37 -0700 Subject: [PATCH 055/546] Migrate bevy picking (#15690) # Objective Migrate `bevy_picking` to the required components API ## Solution - Made `PointerId` require `PointerLocation`, `PointerPress`, and `PointerInteraction` - Removed `PointerBundle` - Removed all engine uses of `PointerBundle` - Added convenience constructor `PointerLocation::new(location: Location)` ## Testing - ran unit tests - ran `sprite_picking` example, everything seemed fine. ## Migration Guide This API hasn't shipped yet, so I didn't bother with a deprecation. However, for any crates tracking main the changes are as follows: Previous api: ```rs commands.insert(PointerBundle::new(PointerId::Mouse)); commands.insert(PointerBundle::new(PointerId::Mouse).with_location(location)); ``` New api: ```rs commands.insert(PointerId::Mouse); commands.insert((PointerId::Mouse, PointerLocation::new(location))); ``` --- crates/bevy_picking/src/input.rs | 10 ++++----- crates/bevy_picking/src/lib.rs | 36 ------------------------------ crates/bevy_picking/src/pointer.rs | 8 +++++++ 3 files changed, 13 insertions(+), 41 deletions(-) diff --git a/crates/bevy_picking/src/input.rs b/crates/bevy_picking/src/input.rs index 0238f9d76be248..7c32ffac26f816 100644 --- a/crates/bevy_picking/src/input.rs +++ b/crates/bevy_picking/src/input.rs @@ -25,9 +25,9 @@ use bevy_render::camera::RenderTarget; use bevy_utils::{tracing::debug, HashMap, HashSet}; use bevy_window::{PrimaryWindow, WindowEvent, WindowRef}; -use crate::{ - pointer::{Location, PointerAction, PointerButton, PointerId, PointerInput, PressDirection}, - PointerBundle, +use crate::pointer::{ + Location, PointerAction, PointerButton, PointerId, PointerInput, PointerLocation, + PressDirection, }; use crate::PickSet; @@ -99,7 +99,7 @@ impl Plugin for PointerInputPlugin { /// Spawns the default mouse pointer. pub fn spawn_mouse_pointer(mut commands: Commands) { - commands.spawn((PointerBundle::new(PointerId::Mouse),)); + commands.spawn(PointerId::Mouse); } /// Sends mouse pointer events to be processed by the core plugin @@ -192,7 +192,7 @@ pub fn touch_pick_events( match touch.phase { TouchPhase::Started => { debug!("Spawning pointer {:?}", pointer); - commands.spawn(PointerBundle::new(pointer).with_location(location.clone())); + commands.spawn((pointer, PointerLocation::new(location.clone()))); pointer_events.send(PointerInput::new( pointer, diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index fdc710539c468e..ea20dd3b5fddfb 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -236,42 +236,6 @@ impl Default for Pickable { } } -/// Components needed to build a pointer. Multiple pointers can be active at once, with each pointer -/// being an entity. -/// -/// `Mouse` and `Touch` pointers are automatically spawned as needed. Use this bundle if you are -/// spawning a custom `PointerId::Custom` pointer, either for testing, as a software controlled -/// pointer, or if you are replacing the default touch and mouse inputs. -#[derive(Bundle)] -pub struct PointerBundle { - /// The pointer's unique [`PointerId`](pointer::PointerId). - pub id: pointer::PointerId, - /// Tracks the pointer's location. - pub location: pointer::PointerLocation, - /// Tracks the pointer's button press state. - pub click: pointer::PointerPress, - /// The interaction state of any hovered entities. - pub interaction: pointer::PointerInteraction, -} - -impl PointerBundle { - /// Create a new pointer with the provided [`PointerId`](pointer::PointerId). - pub fn new(id: pointer::PointerId) -> Self { - PointerBundle { - id, - location: pointer::PointerLocation::default(), - click: pointer::PointerPress::default(), - interaction: pointer::PointerInteraction::default(), - } - } - - /// Sets the location of the pointer bundle - pub fn with_location(mut self, location: pointer::Location) -> Self { - self.location.location = Some(location); - self - } -} - /// Groups the stages of the picking process under shared labels. #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum PickSet { diff --git a/crates/bevy_picking/src/pointer.rs b/crates/bevy_picking/src/pointer.rs index ee04fc732c8804..c7209459dda97e 100644 --- a/crates/bevy_picking/src/pointer.rs +++ b/crates/bevy_picking/src/pointer.rs @@ -26,6 +26,7 @@ use crate::backend::HitData; /// This component is needed because pointers can be spawned and despawned, but they need to have a /// stable ID that persists regardless of the Entity they are associated with. #[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Component, Reflect)] +#[require(PointerLocation, PointerPress, PointerInteraction)] #[reflect(Component, Default, Debug, Hash, PartialEq)] pub enum PointerId { /// The mouse pointer. @@ -164,6 +165,13 @@ pub struct PointerLocation { } impl PointerLocation { + ///Returns a [`PointerLocation`] associated with the given location + pub fn new(location: Location) -> Self { + Self { + location: Some(location), + } + } + /// Returns `Some(&`[`Location`]`)` if the pointer is active, or `None` if the pointer is /// inactive. pub fn location(&self) -> Option<&Location> { From 8039f34b0d4254ae3ca62041052f514f6fa34e7c Mon Sep 17 00:00:00 2001 From: Gino Valente <49806985+MrGVSV@users.noreply.github.com> Date: Mon, 7 Oct 2024 09:30:34 -0700 Subject: [PATCH 056/546] bevy_ecs: Replace panics in `QueryData` derive compile errors (#15691) # Objective The current `QueryData` derive panics when it encounters an error. Additionally, it doesn't provide the clearest error message: ```rust #[derive(QueryData)] #[query_data(mut)] struct Foo { // ... } ``` ``` error: proc-macro derive panicked --> src/foo.rs:16:10 | 16 | #[derive(QueryData)] | ^^^^^^^^^ | = help: message: Invalid `query_data` attribute format ``` ## Solution Updated the derive logic to not panic and gave a bit more detail in the error message. This is makes the error message just a bit clearer and maintains the correct span: ``` error: invalid attribute, expected `mutable` or `derive` --> src/foo.rs:17:14 | 17 | #[query_data(mut)] | ^^^ ``` ## Testing You can test locally by running the following in `crates/bevy_ecs/compile_fail`: ``` cargo test --target-dir ../../../target ``` --- .../tests/ui/world_query_derive.rs | 21 ++++++ crates/bevy_ecs/macros/src/query_data.rs | 65 +++++++------------ 2 files changed, 44 insertions(+), 42 deletions(-) diff --git a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs index d990f59ecc3c37..44e43430473f9d 100644 --- a/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs +++ b/crates/bevy_ecs/compile_fail/tests/ui/world_query_derive.rs @@ -10,6 +10,27 @@ struct MutableUnmarked { a: &'static mut Foo, } +#[derive(QueryData)] +#[query_data(mut)] +//~^ ERROR: invalid attribute, expected `mutable` or `derive` +struct MutableInvalidAttribute { + a: &'static mut Foo, +} + +#[derive(QueryData)] +#[query_data(mutable(foo))] +//~^ ERROR: `mutable` does not take any arguments +struct MutableInvalidAttributeParameters { + a: &'static mut Foo, +} + +#[derive(QueryData)] +#[query_data(derive)] +//~^ ERROR: `derive` requires at least one argument +struct MutableMissingAttributeParameters { + a: &'static mut Foo, +} + #[derive(QueryData)] #[query_data(mutable)] struct MutableMarked { diff --git a/crates/bevy_ecs/macros/src/query_data.rs b/crates/bevy_ecs/macros/src/query_data.rs index 0a78b705a062ec..3f198b1ad1b18f 100644 --- a/crates/bevy_ecs/macros/src/query_data.rs +++ b/crates/bevy_ecs/macros/src/query_data.rs @@ -1,13 +1,10 @@ use bevy_macro_utils::ensure_no_collision; use proc_macro::TokenStream; use proc_macro2::{Ident, Span}; -use quote::{format_ident, quote, ToTokens}; +use quote::{format_ident, quote}; use syn::{ - parse::{Parse, ParseStream}, - parse_macro_input, parse_quote, - punctuated::Punctuated, - token::Comma, - Attribute, Data, DataStruct, DeriveInput, Field, Index, Meta, + parse_macro_input, parse_quote, punctuated::Punctuated, token, token::Comma, Attribute, Data, + DataStruct, DeriveInput, Field, Index, Meta, }; use crate::{ @@ -47,45 +44,29 @@ pub fn derive_query_data_impl(input: TokenStream) -> TokenStream { continue; } - attr.parse_args_with(|input: ParseStream| { - let meta = input.parse_terminated(Meta::parse, Comma)?; - for meta in meta { - let ident = meta.path().get_ident().unwrap_or_else(|| { - panic!( - "Unrecognized attribute: `{}`", - meta.path().to_token_stream() - ) - }); - if ident == MUTABLE_ATTRIBUTE_NAME { - if let Meta::Path(_) = meta { - attributes.is_mutable = true; - } else { - panic!( - "The `{MUTABLE_ATTRIBUTE_NAME}` attribute is expected to have no value or arguments", - ); - } - } - else if ident == DERIVE_ATTRIBUTE_NAME { - if let Meta::List(meta_list) = meta { - meta_list.parse_nested_meta(|meta| { - attributes.derive_args.push(Meta::Path(meta.path)); - Ok(()) - })?; - } else { - panic!( - "Expected a structured list within the `{DERIVE_ATTRIBUTE_NAME}` attribute", - ); - } + let result = attr.parse_nested_meta(|meta| { + if meta.path.is_ident(MUTABLE_ATTRIBUTE_NAME) { + attributes.is_mutable = true; + if meta.input.peek(token::Paren) { + Err(meta.error(format_args!("`{MUTABLE_ATTRIBUTE_NAME}` does not take any arguments"))) } else { - panic!( - "Unrecognized attribute: `{}`", - meta.path().to_token_stream() - ); + Ok(()) } + } else if meta.path.is_ident(DERIVE_ATTRIBUTE_NAME) { + meta.parse_nested_meta(|meta| { + attributes.derive_args.push(Meta::Path(meta.path)); + Ok(()) + }).map_err(|_| { + meta.error(format_args!("`{DERIVE_ATTRIBUTE_NAME}` requires at least one argument")) + }) + } else { + Err(meta.error(format_args!("invalid attribute, expected `{MUTABLE_ATTRIBUTE_NAME}` or `{DERIVE_ATTRIBUTE_NAME}`"))) } - Ok(()) - }) - .unwrap_or_else(|_| panic!("Invalid `{QUERY_DATA_ATTRIBUTE_NAME}` attribute format")); + }); + + if let Err(err) = result { + return err.to_compile_error().into(); + } } let path = bevy_ecs_path(); From 0a1d60f3b086502ef7c5c7445830107d79738ed6 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Mon, 7 Oct 2024 09:33:15 -0700 Subject: [PATCH 057/546] Fix a system ordering issue with motion blur for skinned meshes. (#15693) Currently, it's possible for the `collect_meshes_for_gpu_building` system to run after `set_mesh_motion_vector_flags`. This will cause those motion vector flags to be overwritten, which will cause the shader to ignore the motion vectors for skinned meshes, which will cause graphical artifacts. This patch corrects the issue by forcing `set_mesh_motion_vector_flags` to run after `collect_meshes_for_gpu_building`. --- crates/bevy_pbr/src/render/mesh.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index 0b4a3e3bda4993..1ef92d63c1921a 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -163,7 +163,7 @@ impl Plugin for MeshRenderPlugin { .add_systems( Render, ( - set_mesh_motion_vector_flags.before(RenderSet::Queue), + set_mesh_motion_vector_flags.in_set(RenderSet::PrepareAssets), prepare_skins.in_set(RenderSet::PrepareResources), prepare_morphs.in_set(RenderSet::PrepareResources), prepare_mesh_bind_group.in_set(RenderSet::PrepareBindGroups), @@ -208,7 +208,11 @@ impl Plugin for MeshRenderPlugin { .after(prepare_view_targets), collect_meshes_for_gpu_building .in_set(RenderSet::PrepareAssets) - .after(allocator::allocate_and_free_meshes), + .after(allocator::allocate_and_free_meshes) + // This must be before + // `set_mesh_motion_vector_flags` so it doesn't + // overwrite those flags. + .before(set_mesh_motion_vector_flags), ), ); } else { From 8adc9e9d6ea5d73d992830c2fc3f779f51c45488 Mon Sep 17 00:00:00 2001 From: Clar Fon <15850505+clarfonthey@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:37:45 -0400 Subject: [PATCH 058/546] Feature-gate all image formats (#15586) # Objective Bevy supports feature gates for each format it supports, but several formats that it loads via the `image` crate do not have feature gates. Additionally, the QOI format is supported by the `image` crate and wasn't available at all. This fixes that. ## Solution The following feature gates are added: * `avif` * `ff` (Farbfeld) * `gif` * `ico` * `qoi` * `tiff` None of these formats are enabled by default, despite the fact that all these formats appeared to be enabled by default before. Since `default-features` was disabled for the `image` crate, it's likely that using any of these formats would have errored by default before this change, although this probably needs additional testing. ## Testing The changes seemed minimal enough that a compile test would be sufficient. ## Migration guide Image formats that previously weren't feature-gated are now feature-gated, meaning they will have to be enabled if you use them: * `avif` * `ff` (Farbfeld) * `gif` * `ico` * `tiff` Additionally, the `qoi` feature has been added to support loading QOI format images. Previously, these formats appeared in the enum by default, but weren't actually enabled via the `image` crate, potentially resulting in weird bugs. Now, you should be able to add these features to your projects to support them properly. --- Cargo.toml | 87 +++--- crates/bevy_core_pipeline/Cargo.toml | 7 +- crates/bevy_gltf/Cargo.toml | 3 +- crates/bevy_image/Cargo.toml | 19 +- crates/bevy_image/src/image.rs | 286 +++++++++++++++++- crates/bevy_internal/Cargo.toml | 43 ++- crates/bevy_render/Cargo.toml | 23 +- .../bevy_render/src/texture/image_loader.rs | 31 +- crates/bevy_render/src/texture/mod.rs | 28 +- docs/cargo_features.md | 6 + 10 files changed, 389 insertions(+), 144 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 15bc3e8b7c0def..aa38f582c1067f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,39 +100,40 @@ unused_qualifications = "warn" [features] default = [ + "android-game-activity", + "android-game-activity", + "android_shared_stdcxx", "animation", "bevy_asset", - "bevy_state", "bevy_audio", "bevy_color", - "bevy_gilrs", - "bevy_scene", - "bevy_winit", "bevy_core_pipeline", + "bevy_gilrs", + "bevy_gizmos", + "bevy_gltf", "bevy_pbr", "bevy_picking", - "bevy_sprite_picking_backend", - "bevy_ui_picking_backend", - "bevy_gltf", + "bevy_remote", "bevy_render", + "bevy_scene", "bevy_sprite", + "bevy_sprite_picking_backend", + "bevy_state", "bevy_text", "bevy_ui", - "bevy_remote", + "bevy_ui_picking_backend", + "bevy_winit", + "custom_cursor", + "default_font", + "hdr", "multi_threaded", "png", - "hdr", - "vorbis", - "x11", - "bevy_gizmos", - "android_shared_stdcxx", - "tonemapping_luts", "smaa_luts", - "default_font", - "webgl2", "sysinfo_plugin", - "android-game-activity", - "custom_cursor", + "tonemapping_luts", + "vorbis", + "webgl2", + "x11", ] # Provides an implementation for picking sprites @@ -242,38 +243,56 @@ trace_tracy_memory = [ # Tracing support trace = ["bevy_internal/trace"] +# AVIF image format support +avif = ["bevy_internal/avif"] + +# Basis Universal compressed texture support +basis-universal = ["bevy_internal/basis-universal"] + +# BMP image format support +bmp = ["bevy_internal/bmp"] + +# DDS compressed texture support +dds = ["bevy_internal/dds"] + # EXR image format support exr = ["bevy_internal/exr"] +# Farbfeld image format support +ff = ["bevy_internal/ff"] + +# GIF image format support +gif = ["bevy_internal/gif"] + # HDR image format support hdr = ["bevy_internal/hdr"] -# PNG image format support -png = ["bevy_internal/png"] +# KTX2 compressed texture support +ktx2 = ["bevy_internal/ktx2"] -# TGA image format support -tga = ["bevy_internal/tga"] +# ICO image format support +ico = ["bevy_internal/ico"] # JPEG image format support jpeg = ["bevy_internal/jpeg"] -# BMP image format support -bmp = ["bevy_internal/bmp"] +# PNG image format support +png = ["bevy_internal/png"] -# WebP image format support -webp = ["bevy_internal/webp"] +# PNM image format support, includes pam, pbm, pgm and ppm +pnm = ["bevy_internal/pnm"] -# Basis Universal compressed texture support -basis-universal = ["bevy_internal/basis-universal"] +# QOI image format support +qoi = ["bevy_internal/qoi"] -# DDS compressed texture support -dds = ["bevy_internal/dds"] +# TGA image format support +tga = ["bevy_internal/tga"] -# KTX2 compressed texture support -ktx2 = ["bevy_internal/ktx2"] +# TIFF image format support +tiff = ["bevy_internal/tiff"] -# PNM image format support, includes pam, pbm, pgm and ppm -pnm = ["bevy_internal/pnm"] +# WebP image format support +webp = ["bevy_internal/webp"] # For KTX2 supercompression zlib = ["bevy_internal/zlib"] diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 9db01932f402fe..fbf070f4cbb61b 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -13,12 +13,12 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -dds = ["bevy_render/dds"] +dds = ["bevy_render/dds", "bevy_image/dds"] trace = [] webgl = [] webgpu = [] -tonemapping_luts = ["bevy_render/ktx2", "bevy_render/zstd"] -smaa_luts = ["bevy_render/ktx2", "bevy_render/zstd"] +tonemapping_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] +smaa_luts = ["bevy_render/ktx2", "bevy_image/ktx2", "bevy_image/zstd"] [dependencies] # bevy @@ -28,6 +28,7 @@ bevy_core = { path = "../bevy_core", version = "0.15.0-dev" } bevy_color = { path = "../bevy_color", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } diff --git a/crates/bevy_gltf/Cargo.toml b/crates/bevy_gltf/Cargo.toml index 6bc73daa1688ce..9b0ef61cbb8096 100644 --- a/crates/bevy_gltf/Cargo.toml +++ b/crates/bevy_gltf/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -dds = ["bevy_render/dds", "bevy_core_pipeline/dds"] +dds = ["bevy_render/dds", "bevy_image/dds", "bevy_core_pipeline/dds"] pbr_transmission_textures = ["bevy_pbr/pbr_transmission_textures"] pbr_multi_layer_material_textures = [ "bevy_pbr/pbr_multi_layer_material_textures", @@ -26,6 +26,7 @@ bevy_core = { path = "../bevy_core", version = "0.15.0-dev" } bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.15.0-dev" } bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev" } bevy_hierarchy = { path = "../bevy_hierarchy", version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", version = "0.15.0-dev" } bevy_math = { path = "../bevy_math", version = "0.15.0-dev" } bevy_pbr = { path = "../bevy_pbr", version = "0.15.0-dev" } bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [ diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index bc3e187ff893e6..a9b6c6fdc32692 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -9,15 +9,24 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -png = ["image/png"] +# Image formats +avif = ["image/avif"] +basis-universal = ["dep:basis-universal"] +bmp = ["image/bmp"] +dds = ["ddsfile"] exr = ["image/exr"] +ff = ["image/ff"] +gif = ["image/gif"] hdr = ["image/hdr"] -tga = ["image/tga"] +ktx2 = ["dep:ktx2"] +ico = ["image/ico"] jpeg = ["image/jpeg"] -bmp = ["image/bmp"] -webp = ["image/webp"] -dds = ["ddsfile"] +png = ["image/png"] pnm = ["image/pnm"] +qoi = ["image/qoi"] +tga = ["image/tga"] +tiff = ["image/tiff"] +webp = ["image/webp"] # For ktx2 supercompression zlib = ["flate2"] diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 6965f55ddec53c..61b271cb273daf 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -29,6 +29,7 @@ pub const SAMPLER_ASSET_INDEX: u64 = 1; #[derive(Debug, Serialize, Deserialize, Copy, Clone)] pub enum ImageFormat { + #[cfg(feature = "avif")] Avif, #[cfg(feature = "basis-universal")] Basis, @@ -36,12 +37,15 @@ pub enum ImageFormat { Bmp, #[cfg(feature = "dds")] Dds, + #[cfg(feature = "ff")] Farbfeld, + #[cfg(feature = "gif")] Gif, #[cfg(feature = "exr")] OpenExr, #[cfg(feature = "hdr")] Hdr, + #[cfg(feature = "ico")] Ico, #[cfg(feature = "jpeg")] Jpeg, @@ -51,8 +55,11 @@ pub enum ImageFormat { Png, #[cfg(feature = "pnm")] Pnm, + #[cfg(feature = "qoi")] + Qoi, #[cfg(feature = "tga")] Tga, + #[cfg(feature = "tiff")] Tiff, #[cfg(feature = "webp")] WebP, @@ -71,24 +78,261 @@ macro_rules! feature_gate { } impl ImageFormat { + /// Number of image formats, used for computing other constants. + const COUNT: usize = { + let mut count = 0; + #[cfg(feature = "avif")] + { + count += 1; + } + #[cfg(feature = "basis-universal")] + { + count += 1; + } + #[cfg(feature = "bmp")] + { + count += 1; + } + #[cfg(feature = "dds")] + { + count += 1; + } + #[cfg(feature = "ff")] + { + count += 1; + } + #[cfg(feature = "gif")] + { + count += 1; + } + #[cfg(feature = "exr")] + { + count += 1; + } + #[cfg(feature = "hdr")] + { + count += 1; + } + #[cfg(feature = "ico")] + { + count += 1; + } + #[cfg(feature = "jpeg")] + { + count += 1; + } + #[cfg(feature = "ktx2")] + { + count += 1; + } + #[cfg(feature = "pnm")] + { + count += 1; + } + #[cfg(feature = "png")] + { + count += 1; + } + #[cfg(feature = "qoi")] + { + count += 1; + } + #[cfg(feature = "tga")] + { + count += 1; + } + #[cfg(feature = "tiff")] + { + count += 1; + } + #[cfg(feature = "webp")] + { + count += 1; + } + count + }; + + /// Full list of supported formats. + pub const SUPPORTED: &'static [ImageFormat] = &[ + #[cfg(feature = "avif")] + ImageFormat::Avif, + #[cfg(feature = "basis-universal")] + ImageFormat::Basis, + #[cfg(feature = "bmp")] + ImageFormat::Bmp, + #[cfg(feature = "dds")] + ImageFormat::Dds, + #[cfg(feature = "ff")] + ImageFormat::Farbfeld, + #[cfg(feature = "gif")] + ImageFormat::Gif, + #[cfg(feature = "exr")] + ImageFormat::OpenExr, + #[cfg(feature = "hdr")] + ImageFormat::Hdr, + #[cfg(feature = "ico")] + ImageFormat::Ico, + #[cfg(feature = "jpeg")] + ImageFormat::Jpeg, + #[cfg(feature = "ktx2")] + ImageFormat::Ktx2, + #[cfg(feature = "png")] + ImageFormat::Png, + #[cfg(feature = "pnm")] + ImageFormat::Pnm, + #[cfg(feature = "qoi")] + ImageFormat::Qoi, + #[cfg(feature = "tga")] + ImageFormat::Tga, + #[cfg(feature = "tiff")] + ImageFormat::Tiff, + #[cfg(feature = "webp")] + ImageFormat::WebP, + ]; + + /// Total count of file extensions, for computing supported file extensions list. + const COUNT_FILE_EXTENSIONS: usize = { + let mut count = 0; + let mut idx = 0; + while idx < ImageFormat::COUNT { + count += ImageFormat::SUPPORTED[idx].to_file_extensions().len(); + idx += 1; + } + count + }; + + /// Gets the list of file extensions for all formats. + pub const SUPPORTED_FILE_EXTENSIONS: &'static [&'static str] = &{ + let mut exts = [""; ImageFormat::COUNT_FILE_EXTENSIONS]; + let mut ext_idx = 0; + let mut fmt_idx = 0; + while fmt_idx < ImageFormat::COUNT { + let mut off = 0; + let fmt_exts = ImageFormat::SUPPORTED[fmt_idx].to_file_extensions(); + while off < fmt_exts.len() { + exts[ext_idx] = fmt_exts[off]; + off += 1; + ext_idx += 1; + } + fmt_idx += 1; + } + exts + }; + + /// Gets the file extensions for a given format. + pub const fn to_file_extensions(&self) -> &'static [&'static str] { + match self { + #[cfg(feature = "avif")] + ImageFormat::Avif => &["avif"], + #[cfg(feature = "basis-universal")] + ImageFormat::Basis => &["basis"], + #[cfg(feature = "bmp")] + ImageFormat::Bmp => &["bmp"], + #[cfg(feature = "dds")] + ImageFormat::Dds => &["dds"], + #[cfg(feature = "ff")] + ImageFormat::Farbfeld => &["ff", "farbfeld"], + #[cfg(feature = "gif")] + ImageFormat::Gif => &["gif"], + #[cfg(feature = "exr")] + ImageFormat::OpenExr => &["exr"], + #[cfg(feature = "hdr")] + ImageFormat::Hdr => &["hdr"], + #[cfg(feature = "ico")] + ImageFormat::Ico => &["ico"], + #[cfg(feature = "jpeg")] + ImageFormat::Jpeg => &["jpg", "jpeg"], + #[cfg(feature = "ktx2")] + ImageFormat::Ktx2 => &["ktx2"], + #[cfg(feature = "pnm")] + ImageFormat::Pnm => &["pam", "pbm", "pgm", "ppm"], + #[cfg(feature = "png")] + ImageFormat::Png => &["png"], + #[cfg(feature = "qoi")] + ImageFormat::Qoi => &["qoi"], + #[cfg(feature = "tga")] + ImageFormat::Tga => &["tga"], + #[cfg(feature = "tiff")] + ImageFormat::Tiff => &["tif", "tiff"], + #[cfg(feature = "webp")] + ImageFormat::WebP => &["webp"], + // FIXME: https://github.com/rust-lang/rust/issues/129031 + #[allow(unreachable_patterns)] + _ => &[], + } + } + + /// Gets the MIME types for a given format. + /// + /// If a format doesn't have any dedicated MIME types, this list will be empty. + pub const fn to_mime_types(&self) -> &'static [&'static str] { + match self { + #[cfg(feature = "avif")] + ImageFormat::Avif => &["image/avif"], + #[cfg(feature = "basis-universal")] + ImageFormat::Basis => &["image/basis", "image/x-basis"], + #[cfg(feature = "bmp")] + ImageFormat::Bmp => &["image/bmp", "image/x-bmp"], + #[cfg(feature = "dds")] + ImageFormat::Dds => &["image/vnd-ms.dds"], + #[cfg(feature = "hdr")] + ImageFormat::Hdr => &["image/vnd.radiance"], + #[cfg(feature = "gif")] + ImageFormat::Gif => &["image/gif"], + #[cfg(feature = "ff")] + ImageFormat::Farbfeld => &[], + #[cfg(feature = "ico")] + ImageFormat::Ico => &["image/x-icon"], + #[cfg(feature = "jpeg")] + ImageFormat::Jpeg => &["image/jpeg"], + #[cfg(feature = "ktx2")] + ImageFormat::Ktx2 => &["image/ktx2"], + #[cfg(feature = "png")] + ImageFormat::Png => &["image/png"], + #[cfg(feature = "qoi")] + ImageFormat::Qoi => &["image/qoi", "image/x-qoi"], + #[cfg(feature = "exr")] + ImageFormat::OpenExr => &["image/x-exr"], + #[cfg(feature = "pnm")] + ImageFormat::Pnm => &[ + "image/x-portable-bitmap", + "image/x-portable-graymap", + "image/x-portable-pixmap", + "image/x-portable-anymap", + ], + #[cfg(feature = "tga")] + ImageFormat::Tga => &["image/x-targa", "image/x-tga"], + #[cfg(feature = "tiff")] + ImageFormat::Tiff => &["image/tiff"], + #[cfg(feature = "webp")] + ImageFormat::WebP => &["image/webp"], + // FIXME: https://github.com/rust-lang/rust/issues/129031 + #[allow(unreachable_patterns)] + _ => &[], + } + } + pub fn from_mime_type(mime_type: &str) -> Option { Some(match mime_type.to_ascii_lowercase().as_str() { - "image/avif" => ImageFormat::Avif, + // note: farbfeld does not have a MIME type + "image/avif" => feature_gate!("avif", Avif), + "image/basis" | "image/x-basis" => feature_gate!("basis-universal", Basis), "image/bmp" | "image/x-bmp" => feature_gate!("bmp", Bmp), "image/vnd-ms.dds" => feature_gate!("dds", Dds), "image/vnd.radiance" => feature_gate!("hdr", Hdr), - "image/gif" => ImageFormat::Gif, - "image/x-icon" => ImageFormat::Ico, + "image/gif" => feature_gate!("gif", Gif), + "image/x-icon" => feature_gate!("ico", Ico), "image/jpeg" => feature_gate!("jpeg", Jpeg), "image/ktx2" => feature_gate!("ktx2", Ktx2), "image/png" => feature_gate!("png", Png), + "image/qoi" | "image/x-qoi" => feature_gate!("qoi", Qoi), "image/x-exr" => feature_gate!("exr", OpenExr), "image/x-portable-bitmap" | "image/x-portable-graymap" | "image/x-portable-pixmap" | "image/x-portable-anymap" => feature_gate!("pnm", Pnm), "image/x-targa" | "image/x-tga" => feature_gate!("tga", Tga), - "image/tiff" => ImageFormat::Tiff, + "image/tiff" => feature_gate!("tiff", Tiff), "image/webp" => feature_gate!("webp", WebP), _ => return None, }) @@ -96,21 +340,22 @@ impl ImageFormat { pub fn from_extension(extension: &str) -> Option { Some(match extension.to_ascii_lowercase().as_str() { - "avif" => ImageFormat::Avif, + "avif" => feature_gate!("avif", Avif), "basis" => feature_gate!("basis-universal", Basis), "bmp" => feature_gate!("bmp", Bmp), "dds" => feature_gate!("dds", Dds), - "ff" | "farbfeld" => ImageFormat::Farbfeld, - "gif" => ImageFormat::Gif, + "ff" | "farbfeld" => feature_gate!("ff", Farbfeld), + "gif" => feature_gate!("gif", Gif), "exr" => feature_gate!("exr", OpenExr), "hdr" => feature_gate!("hdr", Hdr), - "ico" => ImageFormat::Ico, + "ico" => feature_gate!("ico", Ico), "jpg" | "jpeg" => feature_gate!("jpeg", Jpeg), "ktx2" => feature_gate!("ktx2", Ktx2), - "pbm" | "pam" | "ppm" | "pgm" => feature_gate!("pnm", Pnm), + "pam" | "pbm" | "pgm" | "ppm" => feature_gate!("pnm", Pnm), "png" => feature_gate!("png", Png), + "qoi" => feature_gate!("qoi", Qoi), "tga" => feature_gate!("tga", Tga), - "tif" | "tiff" => ImageFormat::Tiff, + "tif" | "tiff" => feature_gate!("tiff", Tiff), "webp" => feature_gate!("webp", WebP), _ => return None, }) @@ -118,17 +363,21 @@ impl ImageFormat { pub fn as_image_crate_format(&self) -> Option { Some(match self { + #[cfg(feature = "avif")] ImageFormat::Avif => image::ImageFormat::Avif, #[cfg(feature = "bmp")] ImageFormat::Bmp => image::ImageFormat::Bmp, #[cfg(feature = "dds")] ImageFormat::Dds => image::ImageFormat::Dds, + #[cfg(feature = "ff")] ImageFormat::Farbfeld => image::ImageFormat::Farbfeld, + #[cfg(feature = "gif")] ImageFormat::Gif => image::ImageFormat::Gif, #[cfg(feature = "exr")] ImageFormat::OpenExr => image::ImageFormat::OpenExr, #[cfg(feature = "hdr")] ImageFormat::Hdr => image::ImageFormat::Hdr, + #[cfg(feature = "ico")] ImageFormat::Ico => image::ImageFormat::Ico, #[cfg(feature = "jpeg")] ImageFormat::Jpeg => image::ImageFormat::Jpeg, @@ -136,8 +385,11 @@ impl ImageFormat { ImageFormat::Png => image::ImageFormat::Png, #[cfg(feature = "pnm")] ImageFormat::Pnm => image::ImageFormat::Pnm, + #[cfg(feature = "qoi")] + ImageFormat::Qoi => image::ImageFormat::Qoi, #[cfg(feature = "tga")] ImageFormat::Tga => image::ImageFormat::Tga, + #[cfg(feature = "tiff")] ImageFormat::Tiff => image::ImageFormat::Tiff, #[cfg(feature = "webp")] ImageFormat::WebP => image::ImageFormat::WebP, @@ -145,24 +397,28 @@ impl ImageFormat { ImageFormat::Basis => return None, #[cfg(feature = "ktx2")] ImageFormat::Ktx2 => return None, + // FIXME: https://github.com/rust-lang/rust/issues/129031 + #[allow(unreachable_patterns)] + _ => return None, }) } pub fn from_image_crate_format(format: image::ImageFormat) -> Option { Some(match format { - image::ImageFormat::Avif => ImageFormat::Avif, + image::ImageFormat::Avif => feature_gate!("avif", Avif), image::ImageFormat::Bmp => feature_gate!("bmp", Bmp), image::ImageFormat::Dds => feature_gate!("dds", Dds), - image::ImageFormat::Farbfeld => ImageFormat::Farbfeld, - image::ImageFormat::Gif => ImageFormat::Gif, + image::ImageFormat::Farbfeld => feature_gate!("ff", Farbfeld), + image::ImageFormat::Gif => feature_gate!("gif", Gif), image::ImageFormat::OpenExr => feature_gate!("exr", OpenExr), image::ImageFormat::Hdr => feature_gate!("hdr", Hdr), - image::ImageFormat::Ico => ImageFormat::Ico, + image::ImageFormat::Ico => feature_gate!("ico", Ico), image::ImageFormat::Jpeg => feature_gate!("jpeg", Jpeg), image::ImageFormat::Png => feature_gate!("png", Png), image::ImageFormat::Pnm => feature_gate!("pnm", Pnm), + image::ImageFormat::Qoi => feature_gate!("qoi", Qoi), image::ImageFormat::Tga => feature_gate!("tga", Tga), - image::ImageFormat::Tiff => ImageFormat::Tiff, + image::ImageFormat::Tiff => feature_gate!("tiff", Tiff), image::ImageFormat::WebP => feature_gate!("webp", WebP), _ => return None, }) diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index b8de4caf57d59c..2ea147a826a52f 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -28,21 +28,35 @@ detailed_trace = ["bevy_utils/detailed_trace"] sysinfo_plugin = ["bevy_diagnostic/sysinfo_plugin"] -# Image format support for texture loading (PNG and HDR are enabled by default) -exr = ["bevy_render/exr"] -hdr = ["bevy_render/hdr"] -png = ["bevy_render/png"] -tga = ["bevy_render/tga"] -jpeg = ["bevy_render/jpeg"] -bmp = ["bevy_render/bmp"] -webp = ["bevy_render/webp"] -basis-universal = ["bevy_render/basis-universal"] -dds = ["bevy_render/dds", "bevy_core_pipeline/dds", "bevy_gltf/dds"] -pnm = ["bevy_render/pnm"] -ktx2 = ["bevy_render/ktx2"] +# Texture formats that have specific rendering support (HDR enabled by default) +basis-universal = ["bevy_image/basis-universal", "bevy_render/basis-universal"] +dds = [ + "bevy_image/dds", + "bevy_render/dds", + "bevy_core_pipeline/dds", + "bevy_gltf/dds", +] +exr = ["bevy_image/exr", "bevy_render/exr"] +hdr = ["bevy_image/hdr", "bevy_render/hdr"] +ktx2 = ["bevy_image/ktx2", "bevy_render/ktx2"] + # For ktx2 supercompression -zlib = ["bevy_render/zlib"] -zstd = ["bevy_render/zstd"] +zlib = ["bevy_image/zlib"] +zstd = ["bevy_image/zstd"] + +# Image format support (PNG enabled by default) +avif = ["bevy_image/avif"] +bmp = ["bevy_image/bmp"] +ff = ["bevy_image/ff"] +gif = ["bevy_image/gif"] +ico = ["bevy_image/ico"] +jpeg = ["bevy_image/jpeg"] +png = ["bevy_image/png"] +pnm = ["bevy_image/pnm"] +qoi = ["bevy_image/qoi"] +tga = ["bevy_image/tga"] +tiff = ["bevy_image/tiff"] +webp = ["bevy_image/webp"] # Enable SPIR-V passthrough spirv_shader_passthrough = ["bevy_render/spirv_shader_passthrough"] @@ -258,6 +272,7 @@ bevy_dev_tools = { path = "../bevy_dev_tools", optional = true, version = "0.15. bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.15.0-dev" } bevy_gizmos = { path = "../bevy_gizmos", optional = true, version = "0.15.0-dev", default-features = false } bevy_gltf = { path = "../bevy_gltf", optional = true, version = "0.15.0-dev" } +bevy_image = { path = "../bevy_image", optional = true, version = "0.15.0-dev" } bevy_pbr = { path = "../bevy_pbr", optional = true, version = "0.15.0-dev" } bevy_picking = { path = "../bevy_picking", optional = true, version = "0.15.0-dev" } bevy_remote = { path = "../bevy_remote", optional = true, version = "0.15.0-dev" } diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 46018bde2b07b5..ab3603e0f53a1a 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -9,31 +9,18 @@ license = "MIT OR Apache-2.0" keywords = ["bevy"] [features] -png = ["image/png", "bevy_image/png"] -exr = ["image/exr", "bevy_image/exr"] -hdr = ["image/hdr", "bevy_image/hdr"] -tga = ["image/tga", "bevy_image/tga"] -jpeg = ["image/jpeg", "bevy_image/jpeg"] -bmp = ["image/bmp", "bevy_image/bmp"] -webp = ["image/webp", "bevy_image/webp"] -dds = ["ddsfile", "bevy_image/dds"] -pnm = ["image/pnm", "bevy_image/pnm"] - -ddsfile = ["bevy_image/ddsfile"] -ktx2 = ["dep:ktx2", "bevy_image/ktx2"] -flate2 = ["bevy_image/flate2"] -ruzstd = ["bevy_image/ruzstd"] +# Texture formats (require more than just image support) basis-universal = ["dep:basis-universal", "bevy_image/basis-universal"] +dds = ["bevy_image/dds"] +exr = ["bevy_image/exr"] +hdr = ["bevy_image/hdr"] +ktx2 = ["dep:ktx2", "bevy_image/ktx2"] multi_threaded = ["bevy_tasks/multi_threaded"] shader_format_glsl = ["naga/glsl-in", "naga/wgsl-out", "naga_oil/glsl"] shader_format_spirv = ["wgpu/spirv", "naga/spv-in", "naga/spv-out"] -# For ktx2 supercompression -zlib = ["flate2", "bevy_image/zlib"] -zstd = ["ruzstd", "bevy_image/zstd"] - # Enable SPIR-V shader passthrough spirv_shader_passthrough = [] diff --git a/crates/bevy_render/src/texture/image_loader.rs b/crates/bevy_render/src/texture/image_loader.rs index c82c64807b8129..9f3c55502eddc5 100644 --- a/crates/bevy_render/src/texture/image_loader.rs +++ b/crates/bevy_render/src/texture/image_loader.rs @@ -17,35 +17,6 @@ pub struct ImageLoader { supported_compressed_formats: CompressedImageFormats, } -pub(crate) const IMG_FILE_EXTENSIONS: &[&str] = &[ - #[cfg(feature = "basis-universal")] - "basis", - #[cfg(feature = "bmp")] - "bmp", - #[cfg(feature = "png")] - "png", - #[cfg(feature = "dds")] - "dds", - #[cfg(feature = "tga")] - "tga", - #[cfg(feature = "jpeg")] - "jpg", - #[cfg(feature = "jpeg")] - "jpeg", - #[cfg(feature = "ktx2")] - "ktx2", - #[cfg(feature = "webp")] - "webp", - #[cfg(feature = "pnm")] - "pam", - #[cfg(feature = "pnm")] - "pbm", - #[cfg(feature = "pnm")] - "pgm", - #[cfg(feature = "pnm")] - "ppm", -]; - #[derive(Serialize, Deserialize, Default, Debug)] pub enum ImageFormatSetting { #[default] @@ -131,7 +102,7 @@ impl AssetLoader for ImageLoader { } fn extensions(&self) -> &[&str] { - IMG_FILE_EXTENSIONS + ImageFormat::SUPPORTED_FILE_EXTENSIONS } } diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 1c64dde93f57b2..49b74d4c6cf0e4 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -114,33 +114,13 @@ impl Plugin for ImagePlugin { ); } - #[cfg(any( - feature = "png", - feature = "dds", - feature = "tga", - feature = "jpeg", - feature = "bmp", - feature = "basis-universal", - feature = "ktx2", - feature = "webp", - feature = "pnm" - ))] - app.preregister_asset_loader::(IMG_FILE_EXTENSIONS); + if !ImageFormat::SUPPORTED_FILE_EXTENSIONS.is_empty() { + app.preregister_asset_loader::(ImageFormat::SUPPORTED_FILE_EXTENSIONS); + } } fn finish(&self, app: &mut App) { - #[cfg(any( - feature = "png", - feature = "dds", - feature = "tga", - feature = "jpeg", - feature = "bmp", - feature = "basis-universal", - feature = "ktx2", - feature = "webp", - feature = "pnm" - ))] - { + if !ImageFormat::SUPPORTED.is_empty() { app.init_asset_loader::(); } diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 273ee171ffaec1..e45c379955740f 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -56,6 +56,7 @@ The default feature set enables most of the expected features of a game engine, |android-native-activity|Android NativeActivity support. Legacy, should be avoided for most new Android games.| |asset_processor|Enables the built-in asset processor for processed assets.| |async-io|Use async-io's implementation of block_on instead of futures-lite's implementation. This is preferred if your application uses async-io.| +|avif|AVIF image format support| |basis-universal|Basis Universal compressed texture support| |bevy_ci_testing|Enable systems that allow for automated testing on CI| |bevy_debug_stepping|Enable stepping-based debugging of Bevy systems| @@ -67,9 +68,12 @@ The default feature set enables most of the expected features of a game engine, |dynamic_linking|Force dynamic linking, which improves iterative compile times| |embedded_watcher|Enables watching in memory asset providers for Bevy Asset hot-reloading| |exr|EXR image format support| +|ff|Farbfeld image format support| |file_watcher|Enables watching the filesystem for Bevy Asset hot-reloading| |flac|FLAC audio format support| +|gif|GIF image format support| |glam_assert|Enable assertions to check the validity of parameters passed to glam| +|ico|ICO image format support| |ios_simulator|Enable support for the ios_simulator by downgrading some rendering capabilities| |jpeg|JPEG image format support| |meshlet|Enables the meshlet renderer for dense high-poly scenes (experimental)| @@ -80,6 +84,7 @@ The default feature set enables most of the expected features of a game engine, |pbr_multi_layer_material_textures|Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pnm|PNM image format support, includes pam, pbm, pgm and ppm| +|qoi|QOI image format support| |reflect_functions|Enable function reflection| |serialize|Enable serialization support through serde| |shader_format_glsl|Enable support for shaders in GLSL| @@ -92,6 +97,7 @@ The default feature set enables most of the expected features of a game engine, |symphonia-vorbis|OGG/VORBIS audio format support (through symphonia)| |symphonia-wav|WAV audio format support (through symphonia)| |tga|TGA image format support| +|tiff|TIFF image format support| |trace|Tracing support| |trace_chrome|Tracing support, saving a file in Chrome Tracing format| |trace_tracy|Tracing support, exposing a port for Tracy| From d454db8e5803df1e41b961f960b85cbe91e90880 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 7 Oct 2024 17:09:57 +0000 Subject: [PATCH 059/546] Rename the `Pickable` component and fix incorrect documentation (#15707) # Objective - Rename `Pickable` to `PickingBehavior` to counter the easily-made assumption that the component is required. It is optional - Fix and clarify documentation - The docs in `crates/bevy_ui/src/picking_backend.rs` were incorrect about the necessity of `Pickable` - Plus two minor code quality changes in this commit (7c2e75f48d20387ed27f38b3c9f2d28ae8adf84f) Closes #15632 --- crates/bevy_picking/src/backend.rs | 4 ++-- crates/bevy_picking/src/focus.rs | 26 +++++++++++------------ crates/bevy_picking/src/lib.rs | 18 ++++++++-------- crates/bevy_sprite/src/picking_backend.rs | 6 +++--- crates/bevy_ui/src/picking_backend.rs | 16 +++++++------- examples/ui/scroll.rs | 16 +++++++------- examples/ui/ui.rs | 6 +++--- 7 files changed, 46 insertions(+), 46 deletions(-) diff --git a/crates/bevy_picking/src/backend.rs b/crates/bevy_picking/src/backend.rs index 8f29a5a3003235..39c8f00ae97653 100644 --- a/crates/bevy_picking/src/backend.rs +++ b/crates/bevy_picking/src/backend.rs @@ -20,7 +20,7 @@ //! - The [`PointerHits`] events produced by a backend do **not** need to be sorted or filtered, all //! that is needed is an unordered list of entities and their [`HitData`]. //! -//! - Backends do not need to consider the [`Pickable`](crate::Pickable) component, though they may +//! - Backends do not need to consider the [`PickingBehavior`](crate::PickingBehavior) component, though they may //! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy //! may want to early exit if it intersects an entity that blocks lower entities from being //! picked. @@ -42,7 +42,7 @@ pub mod prelude { pub use super::{ray::RayMap, HitData, PointerHits}; pub use crate::{ pointer::{PointerId, PointerLocation}, - PickSet, Pickable, + PickSet, PickingBehavior, }; } diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/focus.rs index fdb5b862be980a..d07c0c095f4bc5 100644 --- a/crates/bevy_picking/src/focus.rs +++ b/crates/bevy_picking/src/focus.rs @@ -10,7 +10,7 @@ use std::collections::HashSet; use crate::{ backend::{self, HitData}, pointer::{PointerAction, PointerId, PointerInput, PointerInteraction, PointerPress}, - Pickable, + PickingBehavior, }; use bevy_derive::{Deref, DerefMut}; @@ -43,8 +43,8 @@ type OverMap = HashMap; /// between it and the pointer block interactions. /// /// For example, if a pointer is hitting a UI button and a 3d mesh, but the button is in front of -/// the mesh, and [`Pickable::should_block_lower`], the UI button will be hovered, but the mesh will -/// not. +/// the mesh, the UI button will be hovered, but the mesh will not. Unless, the [`PickingBehavior`] +/// component is present with [`should_block_lower`](PickingBehavior::should_block_lower) set to `false`. /// /// # Advanced Users /// @@ -64,7 +64,7 @@ pub struct PreviousHoverMap(pub HashMap>); /// This is the final focusing step to determine which entity the pointer is hovering over. pub fn update_focus( // Inputs - pickable: Query<&Pickable>, + picking_behavior: Query<&PickingBehavior>, pointers: Query<&PointerId>, mut under_pointer: EventReader, mut pointer_input: EventReader, @@ -81,7 +81,7 @@ pub fn update_focus( &pointers, ); build_over_map(&mut under_pointer, &mut over_map, &mut pointer_input); - build_hover_map(&pointers, pickable, &over_map, &mut hover_map); + build_hover_map(&pointers, picking_behavior, &over_map, &mut hover_map); } /// Clear non-empty local maps, reusing allocated memory. @@ -136,7 +136,7 @@ fn build_over_map( .or_insert_with(BTreeMap::new); for (entity, pick_data) in entities_under_pointer.picks.iter() { let layer = entities_under_pointer.order; - let hits = layer_map.entry(FloatOrd(layer)).or_insert_with(Vec::new); + let hits = layer_map.entry(FloatOrd(layer)).or_default(); hits.push((*entity, pick_data.clone())); } } @@ -148,26 +148,26 @@ fn build_over_map( } } -/// Build an unsorted set of hovered entities, accounting for depth, layer, and [`Pickable`]. Note -/// that unlike the pointer map, this uses [`Pickable`] to determine if lower entities receive hover +/// Build an unsorted set of hovered entities, accounting for depth, layer, and [`PickingBehavior`]. Note +/// that unlike the pointer map, this uses [`PickingBehavior`] to determine if lower entities receive hover /// focus. Often, only a single entity per pointer will be hovered. fn build_hover_map( pointers: &Query<&PointerId>, - pickable: Query<&Pickable>, + picking_behavior: Query<&PickingBehavior>, over_map: &Local, // Output hover_map: &mut HoverMap, ) { for pointer_id in pointers.iter() { - let pointer_entity_set = hover_map.entry(*pointer_id).or_insert_with(HashMap::new); + let pointer_entity_set = hover_map.entry(*pointer_id).or_default(); if let Some(layer_map) = over_map.get(pointer_id) { // Note we reverse here to start from the highest layer first. for (entity, pick_data) in layer_map.values().rev().flatten() { - if let Ok(pickable) = pickable.get(*entity) { - if pickable.is_hoverable { + if let Ok(picking_behavior) = picking_behavior.get(*entity) { + if picking_behavior.is_hoverable { pointer_entity_set.insert(*entity, pick_data.clone()); } - if pickable.should_block_lower { + if picking_behavior.should_block_lower { break; } } else { diff --git a/crates/bevy_picking/src/lib.rs b/crates/bevy_picking/src/lib.rs index ea20dd3b5fddfb..c1ec7a49e9d96e 100644 --- a/crates/bevy_picking/src/lib.rs +++ b/crates/bevy_picking/src/lib.rs @@ -135,8 +135,8 @@ //! just because a pointer is over an entity, it is not necessarily *hovering* that entity. Although //! multiple backends may be reporting that a pointer is hitting an entity, the focus system needs //! to determine which entities are actually being hovered by this pointer based on the pick depth, -//! order of the backend, and the [`Pickable`] state of the entity. In other words, if one entity is -//! in front of another, usually only the topmost one will be hovered. +//! order of the backend, and the optional [`PickingBehavior`] component of the entity. In other words, +//! if one entity is in front of another, usually only the topmost one will be hovered. //! //! #### Events ([`events`]) //! @@ -169,7 +169,7 @@ pub mod prelude { #[doc(hidden)] pub use crate::{ events::*, input::PointerInputPlugin, pointer::PointerButton, DefaultPickingPlugins, - InteractionPlugin, Pickable, PickingPlugin, + InteractionPlugin, PickingBehavior, PickingPlugin, }; } @@ -178,7 +178,7 @@ pub mod prelude { /// the fields for more details. #[derive(Component, Debug, Clone, Reflect, PartialEq, Eq)] #[reflect(Component, Default, Debug, PartialEq)] -pub struct Pickable { +pub struct PickingBehavior { /// Should this entity block entities below it from being picked? /// /// This is useful if you want picking to continue hitting entities below this one. Normally, @@ -198,7 +198,7 @@ pub struct Pickable { /// element will be marked as hovered. However, if this field is set to `false`, both the UI /// element *and* the mesh will be marked as hovered. /// - /// Entities without the [`Pickable`] component will block by default. + /// Entities without the [`PickingBehavior`] component will block by default. pub should_block_lower: bool, /// If this is set to `false` and `should_block_lower` is set to true, this entity will block @@ -213,11 +213,11 @@ pub struct Pickable { /// components mark it as hovered. This can be combined with the other field /// [`Self::should_block_lower`], which is orthogonal to this one. /// - /// Entities without the [`Pickable`] component are hoverable by default. + /// Entities without the [`PickingBehavior`] component are hoverable by default. pub is_hoverable: bool, } -impl Pickable { +impl PickingBehavior { /// This entity will not block entities beneath it, nor will it emit events. /// /// If a backend reports this entity as being hit, the picking plugin will completely ignore it. @@ -227,7 +227,7 @@ impl Pickable { }; } -impl Default for Pickable { +impl Default for PickingBehavior { fn default() -> Self { Self { should_block_lower: true, @@ -354,7 +354,7 @@ impl Plugin for PickingPlugin { .chain(), ) .register_type::() - .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 7199d7875dc6bb..f9f906536c722c 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -35,7 +35,7 @@ pub fn sprite_picking( Option<&TextureAtlas>, &Handle, &GlobalTransform, - Option<&Pickable>, + Option<&PickingBehavior>, &ViewVisibility, )>, mut output: EventWriter, @@ -78,7 +78,7 @@ pub fn sprite_picking( .copied() .filter(|(.., visibility)| visibility.get()) .filter_map( - |(entity, sprite, atlas, image, sprite_transform, pickable, ..)| { + |(entity, sprite, atlas, image, sprite_transform, picking_behavior, ..)| { if blocked { return None; } @@ -129,7 +129,7 @@ pub fn sprite_picking( let is_cursor_in_sprite = rect.contains(cursor_pos_sprite); blocked = is_cursor_in_sprite - && pickable.map(|p| p.should_block_lower) != Some(false); + && picking_behavior.map(|p| p.should_block_lower) != Some(false); is_cursor_in_sprite.then(|| { let hit_pos_world = diff --git a/crates/bevy_ui/src/picking_backend.rs b/crates/bevy_ui/src/picking_backend.rs index da708511bbe9f5..500fea213bf0ff 100644 --- a/crates/bevy_ui/src/picking_backend.rs +++ b/crates/bevy_ui/src/picking_backend.rs @@ -8,9 +8,9 @@ //! ## Important Note //! //! This backend completely ignores [`FocusPolicy`](crate::FocusPolicy). The design of `bevy_ui`'s -//! focus systems and the picking plugin are not compatible. Instead, use the [`Pickable`] component -//! to customize how an entity responds to picking focus. Nodes without the [`Pickable`] component -//! will not trigger events. +//! focus systems and the picking plugin are not compatible. Instead, use the optional [`PickingBehavior`] component +//! to override how an entity responds to picking focus. Nodes without the [`PickingBehavior`] component +//! will still trigger events and block items below it from being hovered. //! //! ## Implementation Notes //! @@ -50,7 +50,7 @@ pub struct NodeQuery { entity: Entity, node: &'static Node, global_transform: &'static GlobalTransform, - pickable: Option<&'static Pickable>, + picking_behavior: Option<&'static PickingBehavior>, calculated_clip: Option<&'static CalculatedClip>, view_visibility: Option<&'static ViewVisibility>, target_camera: Option<&'static TargetCamera>, @@ -197,13 +197,13 @@ pub fn ui_picking( picks.push((node.entity, HitData::new(camera_entity, depth, None, None))); - if let Some(pickable) = node.pickable { - // If an entity has a `Pickable` component, we will use that as the source of truth. - if pickable.should_block_lower { + if let Some(picking_behavior) = node.picking_behavior { + // If an entity has a `PickingBehavior` component, we will use that as the source of truth. + if picking_behavior.should_block_lower { break; } } else { - // If the Pickable component doesn't exist, default behavior is to block. + // If the PickingBehavior component doesn't exist, default behavior is to block. break; } diff --git a/examples/ui/scroll.rs b/examples/ui/scroll.rs index 9ce6f36337cd13..a8919e598b8909 100644 --- a/examples/ui/scroll.rs +++ b/examples/ui/scroll.rs @@ -40,7 +40,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }) - .insert(Pickable::IGNORE) + .insert(PickingBehavior::IGNORE) .with_children(|parent| { // horizontal scroll example parent @@ -98,7 +98,7 @@ fn setup(mut commands: Commands, asset_server: Res) { align_content: AlignContent::Center, ..default() }) - .insert(Pickable { + .insert(PickingBehavior { should_block_lower: false, ..default() }) @@ -177,7 +177,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }) - .insert(Pickable { + .insert(PickingBehavior { should_block_lower: false, ..default() }) @@ -198,7 +198,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Role::ListItem, )), )) - .insert(Pickable { + .insert(PickingBehavior { should_block_lower: false, ..default() }); @@ -256,7 +256,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }) - .insert(Pickable::IGNORE) + .insert(PickingBehavior::IGNORE) .with_children(|parent| { // Elements in each row for i in 0..25 { @@ -276,7 +276,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Role::ListItem, )), )) - .insert(Pickable { + .insert(PickingBehavior { should_block_lower: false, ..default() }); @@ -340,7 +340,7 @@ fn setup(mut commands: Commands, asset_server: Res) { .into(), ..default() }) - .insert(Pickable { + .insert(PickingBehavior { should_block_lower: false, ..default() }) @@ -362,7 +362,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Role::ListItem, )), )) - .insert(Pickable { + .insert(PickingBehavior { should_block_lower: false, ..default() }); diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index 009354634c1793..aa0608029f02f4 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -44,7 +44,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }) - .insert(Pickable::IGNORE) + .insert(PickingBehavior::IGNORE) .with_children(|parent| { // left vertical fill (border) parent @@ -167,7 +167,7 @@ fn setup(mut commands: Commands, asset_server: Res) { Label, AccessibilityNode(NodeBuilder::new(Role::ListItem)), )) - .insert(Pickable { + .insert(PickingBehavior { should_block_lower: false, ..default() }); @@ -214,7 +214,7 @@ fn setup(mut commands: Commands, asset_server: Res) { }, ..default() }) - .insert(Pickable::IGNORE) + .insert(PickingBehavior::IGNORE) .with_children(|parent| { parent .spawn(NodeBundle { From 01387101dfc5c0de52da3168cfa3400e4aebaa2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois=20Mockers?= Date: Mon, 7 Oct 2024 20:31:43 +0200 Subject: [PATCH 060/546] add example for ease functions (#15703) # Objective - Followup to #15675 - Add an example showcasing the functions ## Solution - Add an example showcasing the functions - Some of the functions from the interpolation crate are messed up, fixed in #15706 ![ease](https://github.com/user-attachments/assets/1f3b2b80-23d2-45c7-8b08-95b2e870aa02) --------- Co-authored-by: Alice Cecile Co-authored-by: Joona Aalto --- Cargo.toml | 11 ++ examples/README.md | 1 + examples/animation/easing_functions.rs | 178 +++++++++++++++++++++++++ 3 files changed, 190 insertions(+) create mode 100644 examples/animation/easing_functions.rs diff --git a/Cargo.toml b/Cargo.toml index aa38f582c1067f..e5c0078e2299bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1299,6 +1299,17 @@ description = "Bezier curve example showing a cube following a cubic curve" category = "Animation" wasm = true +[[example]] +name = "easing_functions" +path = "examples/animation/easing_functions.rs" +doc-scrape-examples = true + +[package.metadata.example.easing_functions] +name = "Easing Functions" +description = "Showcases the built-in easing functions" +category = "Animation" +wasm = true + [[example]] name = "custom_skinned_mesh" path = "examples/animation/custom_skinned_mesh.rs" diff --git a/examples/README.md b/examples/README.md index a195a0cf44bbcf..15349d35ef5dca 100644 --- a/examples/README.md +++ b/examples/README.md @@ -200,6 +200,7 @@ Example | Description [Color animation](../examples/animation/color_animation.rs) | Demonstrates how to animate colors using mixing and splines in different color spaces [Cubic Curve](../examples/animation/cubic_curve.rs) | Bezier curve example showing a cube following a cubic curve [Custom Skinned Mesh](../examples/animation/custom_skinned_mesh.rs) | Skinned mesh example with mesh and joints data defined in code +[Easing Functions](../examples/animation/easing_functions.rs) | Showcases the built-in easing functions [Morph Targets](../examples/animation/morph_targets.rs) | Plays an animation from a glTF file with meshes with morph targets [glTF Skinned Mesh](../examples/animation/gltf_skinned_mesh.rs) | Skinned mesh example with mesh and joints data loaded from a glTF file diff --git a/examples/animation/easing_functions.rs b/examples/animation/easing_functions.rs new file mode 100644 index 00000000000000..58e66aa65bd81f --- /dev/null +++ b/examples/animation/easing_functions.rs @@ -0,0 +1,178 @@ +//! Demonstrates the behavior of the built-in easing functions. + +use bevy::{prelude::*, sprite::Anchor}; + +#[derive(Component)] +struct SelectedEaseFunction(easing::EaseFunction, Color); + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, display_curves) + .run(); +} + +fn setup(mut commands: Commands) { + commands.spawn(Camera2d); + + let text_style = TextStyle { + font_size: 10.0, + ..default() + }; + + for (i, functions) in [ + easing::EaseFunction::QuadraticIn, + easing::EaseFunction::QuadraticOut, + easing::EaseFunction::QuadraticInOut, + easing::EaseFunction::CubicIn, + easing::EaseFunction::CubicOut, + easing::EaseFunction::CubicInOut, + easing::EaseFunction::QuarticIn, + easing::EaseFunction::QuarticOut, + easing::EaseFunction::QuarticInOut, + easing::EaseFunction::QuinticIn, + easing::EaseFunction::QuinticOut, + easing::EaseFunction::QuinticInOut, + easing::EaseFunction::CircularIn, + easing::EaseFunction::CircularOut, + easing::EaseFunction::CircularInOut, + easing::EaseFunction::ExponentialIn, + easing::EaseFunction::ExponentialOut, + easing::EaseFunction::ExponentialInOut, + easing::EaseFunction::SineIn, + easing::EaseFunction::SineOut, + easing::EaseFunction::SineInOut, + easing::EaseFunction::ElasticIn, + easing::EaseFunction::ElasticOut, + easing::EaseFunction::ElasticInOut, + easing::EaseFunction::BackIn, + easing::EaseFunction::BackOut, + easing::EaseFunction::BackInOut, + easing::EaseFunction::BounceIn, + easing::EaseFunction::BounceOut, + easing::EaseFunction::BounceInOut, + ] + .chunks(3) + .enumerate() + { + for (j, function) in functions.iter().enumerate() { + let color = Hsla::hsl(i as f32 / 10.0 * 360.0, 0.8, 0.75).into(); + commands + .spawn(( + Text2dBundle { + text: Text::from_section( + format!("{:?}", function), + TextStyle { + color, + ..text_style.clone() + }, + ), + transform: Transform::from_xyz( + i as f32 * 125.0 - 1280.0 / 2.0 + 25.0, + -100.0 - ((j as f32 * 250.0) - 300.0), + 0.0, + ), + text_anchor: Anchor::TopLeft, + ..default() + }, + SelectedEaseFunction(*function, color), + )) + .with_children(|p| { + p.spawn(SpriteBundle { + sprite: Sprite { + custom_size: Some(Vec2::new(5.0, 5.0)), + color, + ..default() + }, + transform: Transform::from_xyz(110.0, 15.0, 0.0), + ..default() + }); + p.spawn(SpriteBundle { + sprite: Sprite { + custom_size: Some(Vec2::new(4.0, 4.0)), + color, + ..default() + }, + transform: Transform::from_xyz(0.0, 0.0, 0.0), + ..default() + }); + }); + } + } + commands.spawn( + TextBundle::from_section("", TextStyle::default()).with_style(Style { + position_type: PositionType::Absolute, + bottom: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }), + ); +} + +fn display_curves( + mut gizmos: Gizmos, + ease_functions: Query<(&SelectedEaseFunction, &Transform, &Children)>, + mut transforms: Query<&mut Transform, Without>, + mut ui: Query<&mut Text, With>, + time: Res

for SetMeshViewBindGroup view_ssr, view_environment_map, mesh_view_bind_group, + maybe_oit_layers_count_offset, ): ROQueryItem<'w, Self::ViewQuery>, _entity: Option<()>, _: SystemParamItem<'w, '_, Self::Param>, pass: &mut TrackedRenderPass<'w>, ) -> RenderCommandResult { - pass.set_bind_group( - I, - &mesh_view_bind_group.value, - &[ - view_uniform.offset, - view_lights.offset, - view_fog.offset, - **view_light_probes, - **view_ssr, - **view_environment_map, - ], - ); + let mut offsets: SmallVec<[u32; 8]> = smallvec![ + view_uniform.offset, + view_lights.offset, + view_fog.offset, + **view_light_probes, + **view_ssr, + **view_environment_map, + ]; + if let Some(layers_count_offset) = maybe_oit_layers_count_offset { + offsets.push(layers_count_offset.offset); + } + pass.set_bind_group(I, &mesh_view_bind_group.value, &offsets); RenderCommandResult::Success } diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index 0973b6513609c3..2ad6ad8499e774 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -3,6 +3,7 @@ use core::{array, num::NonZero}; use bevy_core_pipeline::{ core_3d::ViewTransmissionTexture, + oit::{OitBuffers, OrderIndependentTransparencySettings}, prepass::ViewPrepassTextures, tonemapping::{ get_lut_bind_group_layout_entries, get_lut_bindings, Tonemapping, TonemappingLuts, @@ -12,6 +13,7 @@ use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ component::Component, entity::Entity, + query::Has, system::{Commands, Query, Res, Resource}, world::{FromWorld, World}, }; @@ -71,6 +73,7 @@ bitflags::bitflags! { const NORMAL_PREPASS = 1 << 2; const MOTION_VECTOR_PREPASS = 1 << 3; const DEFERRED_PREPASS = 1 << 4; + const OIT_ENABLED = 1 << 5; } } @@ -83,7 +86,7 @@ impl MeshPipelineViewLayoutKey { use MeshPipelineViewLayoutKey as Key; format!( - "mesh_view_layout{}{}{}{}{}", + "mesh_view_layout{}{}{}{}{}{}", self.contains(Key::MULTISAMPLED) .then_some("_multisampled") .unwrap_or_default(), @@ -99,6 +102,9 @@ impl MeshPipelineViewLayoutKey { self.contains(Key::DEFERRED_PREPASS) .then_some("_deferred") .unwrap_or_default(), + self.contains(Key::OIT_ENABLED) + .then_some("_oit") + .unwrap_or_default(), ) } } @@ -122,6 +128,9 @@ impl From for MeshPipelineViewLayoutKey { if value.contains(MeshPipelineKey::DEFERRED_PREPASS) { result |= MeshPipelineViewLayoutKey::DEFERRED_PREPASS; } + if value.contains(MeshPipelineKey::OIT_ENABLED) { + result |= MeshPipelineViewLayoutKey::OIT_ENABLED; + } result } @@ -348,6 +357,18 @@ fn layout_entries( (30, sampler(SamplerBindingType::Filtering)), )); + // OIT + if cfg!(not(feature = "webgl")) && layout_key.contains(MeshPipelineViewLayoutKey::OIT_ENABLED) { + entries = entries.extend_with_indices(( + // oit_layers + (31, storage_buffer_sized(false, None)), + // oit_layer_ids, + (32, storage_buffer_sized(false, None)), + // oit_layer_count + (33, uniform_buffer::(true)), + )); + } + entries.to_vec() } @@ -453,8 +474,7 @@ pub fn prepare_mesh_view_bind_groups( render_device: Res, mesh_pipeline: Res, shadow_samplers: Res, - light_meta: Res, - global_light_meta: Res, + (light_meta, global_light_meta): (Res, Res), fog_meta: Res, (view_uniforms, environment_map_uniform): (Res, Res), views: Query<( @@ -468,6 +488,7 @@ pub fn prepare_mesh_view_bind_groups( &Tonemapping, Option<&RenderViewLightProbes>, Option<&RenderViewLightProbes>, + Has, )>, (images, mut fallback_images, fallback_image, fallback_image_zero): ( Res>, @@ -480,6 +501,7 @@ pub fn prepare_mesh_view_bind_groups( light_probes_buffer: Res, visibility_ranges: Res, ssr_buffer: Res, + oit_buffers: Res, ) { if let ( Some(view_binding), @@ -513,6 +535,7 @@ pub fn prepare_mesh_view_bind_groups( tonemapping, render_view_environment_maps, render_view_irradiance_volumes, + has_oit, ) in &views { let fallback_ssao = fallback_images @@ -523,10 +546,13 @@ pub fn prepare_mesh_view_bind_groups( .map(|t| &t.screen_space_ambient_occlusion_texture.default_view) .unwrap_or(&fallback_ssao); - let layout = &mesh_pipeline.get_view_layout( - MeshPipelineViewLayoutKey::from(*msaa) - | MeshPipelineViewLayoutKey::from(prepass_textures), - ); + let mut layout_key = MeshPipelineViewLayoutKey::from(*msaa) + | MeshPipelineViewLayoutKey::from(prepass_textures); + if has_oit { + layout_key |= MeshPipelineViewLayoutKey::OIT_ENABLED; + } + + let layout = &mesh_pipeline.get_view_layout(layout_key); let mut entries = DynamicBindGroupEntries::new_with_indices(( (0, view_binding.clone()), @@ -645,6 +671,24 @@ pub fn prepare_mesh_view_bind_groups( entries = entries.extend_with_indices(((29, transmission_view), (30, transmission_sampler))); + if has_oit { + if let ( + Some(oit_layers_binding), + Some(oit_layer_ids_binding), + Some(oit_layers_count_uniforms_binding), + ) = ( + oit_buffers.layers.binding(), + oit_buffers.layer_ids.binding(), + oit_buffers.layers_count_uniforms.binding(), + ) { + entries = entries.extend_with_indices(( + (31, oit_layers_binding.clone()), + (32, oit_layer_ids_binding.clone()), + (33, oit_layers_count_uniforms_binding.clone()), + )); + } + } + commands.entity(entity).insert(MeshViewBindGroup { value: render_device.create_bind_group("mesh_view_bind_group", layout, &entries), }); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl index dfda3a576b3179..e777b203583301 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.wgsl @@ -101,3 +101,9 @@ const VISIBILITY_RANGE_UNIFORM_BUFFER_SIZE: u32 = 64u; @group(0) @binding(29) var view_transmission_texture: texture_2d; @group(0) @binding(30) var view_transmission_sampler: sampler; + +#ifdef OIT_ENABLED +@group(0) @binding(31) var oit_layers: array>; +@group(0) @binding(32) var oit_layer_ids: array>; +@group(0) @binding(33) var oit_layers_count: i32; +#endif OIT_ENABLED diff --git a/crates/bevy_pbr/src/render/pbr.wgsl b/crates/bevy_pbr/src/render/pbr.wgsl index 7b94f3cdf801c8..336c1724c548f3 100644 --- a/crates/bevy_pbr/src/render/pbr.wgsl +++ b/crates/bevy_pbr/src/render/pbr.wgsl @@ -1,4 +1,5 @@ #import bevy_pbr::{ + pbr_types, pbr_functions::alpha_discard, pbr_fragment::pbr_input_from_standard_material, } @@ -21,6 +22,10 @@ #import bevy_pbr::meshlet_visibility_buffer_resolve::resolve_vertex_output #endif +#ifdef OIT_ENABLED +#import bevy_core_pipeline::oit::oit_draw +#endif // OIT_ENABLED + @fragment fn fragment( #ifdef MESHLET_MESH_MATERIAL_PASS @@ -65,5 +70,13 @@ fn fragment( out.color = main_pass_post_lighting_processing(pbr_input, out.color); #endif +#ifdef OIT_ENABLED + let alpha_mode = pbr_input.material.flags & pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_RESERVED_BITS; + if alpha_mode != pbr_types::STANDARD_MATERIAL_FLAGS_ALPHA_MODE_OPAQUE { + // This will always return 0.0. The fragments will only be drawn during the oit resolve pass. + out.color = oit_draw(in.position, out.color); + } +#endif // OIT_ENABLED + return out; } diff --git a/examples/3d/order_independent_transparency.rs b/examples/3d/order_independent_transparency.rs new file mode 100644 index 00000000000000..79520d7431b87d --- /dev/null +++ b/examples/3d/order_independent_transparency.rs @@ -0,0 +1,236 @@ +//! A simple 3D scene showing how alpha blending can break and how order independent transparency (OIT) can fix it. +//! +//! See [`OrderIndependentTransparencyPlugin`] for the trade-offs of using OIT. +//! +//! [`OrderIndependentTransparencyPlugin`]: bevy::render::pipeline::OrderIndependentTransparencyPlugin +use bevy::{ + color::palettes::css::{BLUE, GREEN, RED}, + core_pipeline::oit::OrderIndependentTransparencySettings, + prelude::*, + render::view::RenderLayers, +}; + +fn main() { + std::env::set_var("RUST_BACKTRACE", "1"); + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (toggle_oit, cycle_scenes)) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // camera + commands + .spawn(( + Camera3d::default(), + Transform::from_xyz(0.0, 0.0, 10.0).looking_at(Vec3::ZERO, Vec3::Y), + // Add this component to this camera to render transparent meshes using OIT + OrderIndependentTransparencySettings::default(), + RenderLayers::layer(1), + )) + .insert( + // Msaa currently doesn't work with OIT + Msaa::Off, + ); + + // light + commands.spawn(( + PointLight { + shadows_enabled: false, + ..default() + }, + Transform::from_xyz(4.0, 8.0, 4.0), + RenderLayers::layer(1), + )); + + // spawn help text + commands.spawn(( + TextBundle::from_sections([ + TextSection::new("Press T to toggle OIT\n", TextStyle::default()), + TextSection::new("OIT Enabled", TextStyle::default()), + TextSection::new("\nPress C to cycle test scenes", TextStyle::default()), + ]), + RenderLayers::layer(1), + )); + + // spawn default scene + spawn_spheres(&mut commands, &mut meshes, &mut materials); +} + +fn toggle_oit( + mut commands: Commands, + mut text: Query<&mut Text>, + keyboard_input: Res>, + q: Query<(Entity, Has), With>, +) { + if keyboard_input.just_pressed(KeyCode::KeyT) { + let (e, has_oit) = q.single(); + text.single_mut().sections[1].value = if has_oit { + // Removing the component will completely disable OIT for this camera + commands + .entity(e) + .remove::(); + "OIT disabled".to_string() + } else { + // Adding the component to the camera will render any transparent meshes + // with OIT instead of alpha blending + commands + .entity(e) + .insert(OrderIndependentTransparencySettings::default()); + "OIT enabled".to_string() + }; + } +} + +fn cycle_scenes( + mut commands: Commands, + keyboard_input: Res>, + mut meshes: ResMut>, + mut materials: ResMut>, + q: Query>, + mut scene_id: Local, +) { + if keyboard_input.just_pressed(KeyCode::KeyC) { + // depsawn current scene + for e in &q { + commands.entity(e).despawn_recursive(); + } + // increment scene_id + *scene_id = (*scene_id + 1) % 2; + // spawn next scene + match *scene_id { + 0 => spawn_spheres(&mut commands, &mut meshes, &mut materials), + 1 => spawn_occlusion_test(&mut commands, &mut meshes, &mut materials), + _ => unreachable!(), + } + } +} + +/// Spawns 3 overlapping spheres +/// Technically, when using `alpha_to_coverage` with MSAA this particular example wouldn't break, +/// but it breaks when disabling MSAA and is enough to show the difference between OIT enabled vs disabled. +fn spawn_spheres( + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, +) { + let pos_a = Vec3::new(-1.0, 0.75, 0.0); + let pos_b = Vec3::new(0.0, -0.75, 0.0); + let pos_c = Vec3::new(1.0, 0.75, 0.0); + + let offset = Vec3::new(0.0, 0.0, 0.0); + + let sphere_handle = meshes.add(Sphere::new(2.0).mesh()); + + let alpha = 0.25; + + let render_layers = RenderLayers::layer(1); + + commands.spawn(( + Mesh3d(sphere_handle.clone()), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: RED.with_alpha(alpha).into(), + alpha_mode: AlphaMode::Blend, + ..default() + })), + Transform::from_translation(pos_a + offset), + render_layers.clone(), + )); + commands.spawn(( + Mesh3d(sphere_handle.clone()), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: GREEN.with_alpha(alpha).into(), + alpha_mode: AlphaMode::Blend, + ..default() + })), + Transform::from_translation(pos_b + offset), + render_layers.clone(), + )); + commands.spawn(( + Mesh3d(sphere_handle.clone()), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: BLUE.with_alpha(alpha).into(), + alpha_mode: AlphaMode::Blend, + ..default() + })), + Transform::from_translation(pos_c + offset), + render_layers.clone(), + )); +} + +/// Spawn a combination of opaque cubes and transparent spheres. +/// This is useful to make sure transparent meshes drawn with OIT +/// are properly occluded by opaque meshes. +fn spawn_occlusion_test( + commands: &mut Commands, + meshes: &mut Assets, + materials: &mut Assets, +) { + let sphere_handle = meshes.add(Sphere::new(1.0).mesh()); + let cube_handle = meshes.add(Cuboid::from_size(Vec3::ONE).mesh()); + let cube_material = materials.add(Color::srgb(0.8, 0.7, 0.6)); + + let render_layers = RenderLayers::layer(1); + + // front + let x = -2.5; + commands.spawn(( + Mesh3d(cube_handle.clone()), + MeshMaterial3d(cube_material.clone()), + Transform::from_xyz(x, 0.0, 2.0), + render_layers.clone(), + )); + commands.spawn(( + Mesh3d(sphere_handle.clone()), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: RED.with_alpha(0.5).into(), + alpha_mode: AlphaMode::Blend, + ..default() + })), + Transform::from_xyz(x, 0., 0.), + render_layers.clone(), + )); + + // intersection + commands.spawn(( + Mesh3d(cube_handle.clone()), + MeshMaterial3d(cube_material.clone()), + Transform::from_xyz(x, 0.0, 1.0), + render_layers.clone(), + )); + commands.spawn(( + Mesh3d(sphere_handle.clone()), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: RED.with_alpha(0.5).into(), + alpha_mode: AlphaMode::Blend, + ..default() + })), + Transform::from_xyz(0., 0., 0.), + render_layers.clone(), + )); + + // back + let x = 2.5; + commands.spawn(( + Mesh3d(cube_handle.clone()), + MeshMaterial3d(cube_material.clone()), + Transform::from_xyz(x, 0.0, -2.0), + render_layers.clone(), + )); + commands.spawn(( + Mesh3d(sphere_handle.clone()), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: RED.with_alpha(0.5).into(), + alpha_mode: AlphaMode::Blend, + ..default() + })), + Transform::from_xyz(x, 0., 0.), + render_layers.clone(), + )); +} diff --git a/examples/README.md b/examples/README.md index 1744816a8c9142..700bc6f01720da 100644 --- a/examples/README.md +++ b/examples/README.md @@ -158,6 +158,7 @@ Example | Description [Load glTF extras](../examples/3d/load_gltf_extras.rs) | Loads and renders a glTF file as a scene, including the gltf extras [Meshlet](../examples/3d/meshlet.rs) | Meshlet rendering for dense high-poly scenes (experimental) [Motion Blur](../examples/3d/motion_blur.rs) | Demonstrates per-pixel motion blur +[Order Independent Transparency](../examples/3d/order_independent_transparency.rs) | Demonstrates how to use OIT [Orthographic View](../examples/3d/orthographic.rs) | Shows how to create a 3D orthographic view (for isometric-look in games or CAD applications) [Parallax Mapping](../examples/3d/parallax_mapping.rs) | Demonstrates use of a normal map and depth map for parallax mapping [Parenting](../examples/3d/parenting.rs) | Demonstrates parent->child relationships and relative transformations From 48e20278272ca8c725c86e1ed02d1f912ed89ff7 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Tue, 8 Oct 2024 05:19:38 -0700 Subject: [PATCH 070/546] Add some missing features from the gamepads-as-entities change that were needed to update `leafwing-input-manager`. (#15685) The gamepads-as-entities change caused several regressions. This patch fixes each of them: 1. This PR introduces two new fields on `GamepadInfo`: `vendor_id`, and `product_id`, as well as associated methods. These fields are simply mirrored from the `gilrs` library. 2. That PR removed the methods that allowed iterating over all pressed and released buttons, as well as the method that allowed iterating over the axis values. (It was still technically possible to do so by using reflection to access the private fields of `Gamepad`.) 3. The `Gamepad` component wasn't marked reflectable. This PR fixes that problem. These changes allowed me to forward port `leafwing-input-manager`. --- crates/bevy_gilrs/src/gilrs_system.rs | 4 ++ crates/bevy_input/src/axis.rs | 14 +++++++ crates/bevy_input/src/gamepad.rs | 57 ++++++++++++++++++++++++++- crates/bevy_input/src/lib.rs | 15 +++++-- 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/crates/bevy_gilrs/src/gilrs_system.rs b/crates/bevy_gilrs/src/gilrs_system.rs index 4ea9d2bf787902..a0ed0e4e7659e3 100644 --- a/crates/bevy_gilrs/src/gilrs_system.rs +++ b/crates/bevy_gilrs/src/gilrs_system.rs @@ -28,6 +28,8 @@ pub fn gilrs_event_startup_system( let info = GamepadInfo { name: gamepad.name().into(), + vendor_id: gamepad.vendor_id(), + product_id: gamepad.product_id(), }; events.send(GamepadConnectionEvent { @@ -62,6 +64,8 @@ pub fn gilrs_event_system( let info = GamepadInfo { name: pad.name().into(), + vendor_id: pad.vendor_id(), + product_id: pad.product_id(), }; events.send( diff --git a/crates/bevy_input/src/axis.rs b/crates/bevy_input/src/axis.rs index df16c0babf76ce..f2e97777cb45e7 100644 --- a/crates/bevy_input/src/axis.rs +++ b/crates/bevy_input/src/axis.rs @@ -4,12 +4,16 @@ use bevy_ecs::system::Resource; use bevy_utils::HashMap; use core::hash::Hash; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; + /// Stores the position data of the input devices of type `T`. /// /// The values are stored as `f32`s, using [`Axis::set`]. /// Use [`Axis::get`] to retrieve the value clamped between [`Axis::MIN`] and [`Axis::MAX`] /// inclusive, or unclamped using [`Axis::get_unclamped`]. #[derive(Debug, Resource)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] pub struct Axis { /// The position data of the input devices. axis_data: HashMap, @@ -70,6 +74,16 @@ where pub fn remove(&mut self, input_device: T) -> Option { self.axis_data.remove(&input_device) } + + /// Returns an iterator over all axes. + pub fn all_axes(&self) -> impl Iterator { + self.axis_data.keys() + } + + /// Returns an iterator over all axes and their values. + pub fn all_axes_and_values(&self) -> impl Iterator { + self.axis_data.iter().map(|(axis, value)| (axis, *value)) + } } #[cfg(test)] diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 152efe74c0ea76..1e508a1e8f67d1 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -364,6 +364,7 @@ pub enum ButtonSettingsError { /// } /// ``` #[derive(Component, Debug)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug))] #[require(GamepadSettings)] pub struct Gamepad { info: GamepadInfo, @@ -374,7 +375,7 @@ pub struct Gamepad { } impl Gamepad { - /// Creates a gamepad with the given metadata + /// Creates a gamepad with the given metadata. fn new(info: GamepadInfo) -> Self { let mut analog = Axis::default(); for button in GamepadButton::all().iter().copied() { @@ -399,6 +400,18 @@ impl Gamepad { self.info.name.as_str() } + /// Returns the USB vendor ID as assigned by the USB-IF, if available. + pub fn vendor_id(&self) -> Option { + self.info.vendor_id + } + + /// Returns the USB product ID as assigned by the [vendor], if available. + /// + /// [vendor]: Self::vendor_id + pub fn product_id(&self) -> Option { + self.info.product_id + } + /// Returns the analog data of the provided [`GamepadAxis`] or [`GamepadButton`]. /// /// This will be clamped between [[`Axis::MIN`],[`Axis::MAX`]]. @@ -505,8 +518,39 @@ impl Gamepad { .into_iter() .all(|button_type| self.just_released(button_type)) } + + /// Returns an iterator over all digital [button]s that are pressed. + /// + /// [button]: GamepadButton + pub fn get_pressed(&self) -> impl Iterator { + self.digital.get_pressed() + } + + /// Returns an iterator over all digital [button]s that were just pressed. + /// + /// [button]: GamepadButton + pub fn get_just_pressed(&self) -> impl Iterator { + self.digital.get_just_pressed() + } + + /// Returns an iterator over all digital [button]s that were just released. + /// + /// [button]: GamepadButton + pub fn get_just_released(&self) -> impl Iterator { + self.digital.get_just_released() + } + + /// Returns an iterator over all analog [axes]. + /// + /// [axes]: GamepadInput + pub fn get_analog_axes(&self) -> impl Iterator { + self.analog.all_axes() + } } +// Note that we don't expose `gilrs::Gamepad::uuid` due to +// https://gitlab.com/gilrs-project/gilrs/-/issues/153. +// /// Metadata associated with a [`Gamepad`]. #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] @@ -522,6 +566,14 @@ pub struct GamepadInfo { /// /// For example on Windows the name may be "HID-compliant game controller". pub name: String, + + /// The USB vendor ID as assigned by the USB-IF, if available. + pub vendor_id: Option, + + /// The USB product ID as assigned by the [vendor], if available. + /// + /// [vendor]: Self::vendor_id + pub product_id: Option, } /// Represents gamepad input types that are mapped in the range [0.0, 1.0]. @@ -665,6 +717,7 @@ impl GamepadAxis { /// Encapsulation over [`GamepadAxis`] and [`GamepadButton`] // This is done so Gamepad can share a single Axis and simplifies the API by having only one get/get_unclamped method #[derive(Debug, Copy, Clone, Eq, Hash, PartialEq)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Debug, PartialEq))] pub enum GamepadInput { /// A [`GamepadAxis`] Axis(GamepadAxis), @@ -1988,6 +2041,8 @@ mod tests { gamepad, Connected(GamepadInfo { name: String::from("Gamepad test"), + vendor_id: None, + product_id: None, }), )); gamepad diff --git a/crates/bevy_input/src/lib.rs b/crates/bevy_input/src/lib.rs index ddcfa5bf17e896..f78b264977f5c2 100644 --- a/crates/bevy_input/src/lib.rs +++ b/crates/bevy_input/src/lib.rs @@ -51,11 +51,14 @@ use mouse::{ }; use touch::{touch_screen_input_system, TouchInput, Touches}; +#[cfg(feature = "bevy_reflect")] +use gamepad::Gamepad; use gamepad::{ - gamepad_connection_system, gamepad_event_processing_system, GamepadAxisChangedEvent, - GamepadButtonChangedEvent, GamepadButtonStateChangedEvent, GamepadConnection, - GamepadConnectionEvent, GamepadEvent, GamepadInfo, GamepadRumbleRequest, GamepadSettings, - RawGamepadAxisChangedEvent, RawGamepadButtonChangedEvent, RawGamepadEvent, + gamepad_connection_system, gamepad_event_processing_system, GamepadAxis, + GamepadAxisChangedEvent, GamepadButton, GamepadButtonChangedEvent, + GamepadButtonStateChangedEvent, GamepadConnection, GamepadConnectionEvent, GamepadEvent, + GamepadInfo, GamepadInput, GamepadRumbleRequest, GamepadSettings, RawGamepadAxisChangedEvent, + RawGamepadButtonChangedEvent, RawGamepadEvent, }; #[cfg(all(feature = "serialize", feature = "bevy_reflect"))] @@ -134,6 +137,7 @@ impl Plugin for InputPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() .register_type::() .register_type::() .register_type::() @@ -141,6 +145,9 @@ impl Plugin for InputPlugin { .register_type::() .register_type::() .register_type::() + .register_type::() + .register_type::() + .register_type::() .register_type::() .register_type::(); } From 320d53c1d2d7e1ff796daea1c2ab6eed171375fc Mon Sep 17 00:00:00 2001 From: Shane Celis Date: Tue, 8 Oct 2024 08:37:46 -0400 Subject: [PATCH 071/546] Vary transforms for custom_skinned_mesh example (#15710) # Objective Enhance the [custom skinned mesh example](https://bevyengine.org/examples/animation/custom-skinned-mesh/) to show some variety and clarify what the transform does to the mesh. ## Solution https://github.com/user-attachments/assets/c919db74-6e77-4f33-ba43-0f40a88042b3 Add variety and clarity with the following changes: - vary transform changes, - use a UV texture, - and show transform changes via gizmos. (Maybe it'd be worth turning on wireframe rendering to show what happens to the mesh. I think it'd be nice visually but might make the code a little noisy.) ## Testing I exercised it on my x86 macOS computer. It'd be good to have it validated on Windows, Linux, and WASM. --- ## Showcase - Custom skinned mesh example varies the transforms changes and uses a UV test texture. --- assets/textures/uv_checker_bw.png | Bin 0 -> 84853 bytes examples/animation/custom_skinned_mesh.rs | 95 +++++++++++++++++++--- 2 files changed, 83 insertions(+), 12 deletions(-) create mode 100644 assets/textures/uv_checker_bw.png diff --git a/assets/textures/uv_checker_bw.png b/assets/textures/uv_checker_bw.png new file mode 100644 index 0000000000000000000000000000000000000000..bef39cd66b9d1483dbcb89341ae9d5e4f0e8a25d GIT binary patch literal 84853 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∨r3bQo3wV-wJY*;O#_4^)h4G-KzS|$G0fa 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 literal 0 HcmV?d00001 diff --git a/examples/animation/custom_skinned_mesh.rs b/examples/animation/custom_skinned_mesh.rs index 383fbaefff3d32..bffcf7eb08dd46 100644 --- a/examples/animation/custom_skinned_mesh.rs +++ b/examples/animation/custom_skinned_mesh.rs @@ -31,13 +31,14 @@ fn main() { /// Used to mark a joint to be animated in the [`joint_animation`] system. #[derive(Component)] -struct AnimatedJoint; +struct AnimatedJoint(isize); /// Construct a mesh and a skeleton with 2 joints for that mesh, /// and mark the second joint to be animated. /// It is similar to the scene defined in `models/SimpleSkin/SimpleSkin.gltf` fn setup( mut commands: Commands, + asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, mut skinned_mesh_inverse_bindposes_assets: ResMut>, @@ -45,7 +46,7 @@ fn setup( // Create a camera commands.spawn(( Camera3d::default(), - Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + Transform::from_xyz(2.5, 2.5, 9.0).looking_at(Vec3::ZERO, Vec3::Y), )); // Create inverse bindpose matrices for a skeleton consists of 2 joints @@ -75,6 +76,23 @@ fn setup( [1.0, 2.0, 0.0], ], ) + // Add UV coordinates that map the left half of the texture since its a 1 x + // 2 rectangle. + .with_inserted_attribute( + Mesh::ATTRIBUTE_UV_0, + vec![ + [0.0, 0.00], + [0.5, 0.00], + [0.0, 0.25], + [0.5, 0.25], + [0.0, 0.50], + [0.5, 0.50], + [0.0, 0.75], + [0.5, 0.75], + [0.0, 1.00], + [0.5, 1.00], + ], + ) // Set mesh vertex normals .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 10]) // Set mesh vertex joint indices for mesh skinning. @@ -130,9 +148,15 @@ fn setup( for i in -5..5 { // Create joint entities let joint_0 = commands - .spawn(Transform::from_xyz(i as f32 * 1.5, 0.0, i as f32 * 0.1)) + .spawn(Transform::from_xyz( + i as f32 * 1.5, + 0.0, + // Move quads back a small amount to avoid Z-fighting and not + // obscure the transform gizmos. + -(i as f32 * 0.01).abs(), + )) .id(); - let joint_1 = commands.spawn((AnimatedJoint, Transform::IDENTITY)).id(); + let joint_1 = commands.spawn((AnimatedJoint(i), Transform::IDENTITY)).id(); // Set joint_1 as a child of joint_0. commands.entity(joint_0).add_children(&[joint_1]); @@ -143,11 +167,15 @@ fn setup( // Create skinned mesh renderer. Note that its transform doesn't affect the position of the mesh. commands.spawn(( Mesh3d(mesh.clone()), - MeshMaterial3d(materials.add(Color::srgb( - rng.gen_range(0.0..1.0), - rng.gen_range(0.0..1.0), - rng.gen_range(0.0..1.0), - ))), + MeshMaterial3d(materials.add(StandardMaterial { + base_color: Color::srgb( + rng.gen_range(0.0..1.0), + rng.gen_range(0.0..1.0), + rng.gen_range(0.0..1.0), + ), + base_color_texture: Some(asset_server.load("textures/uv_checker_bw.png")), + ..default() + })), SkinnedMesh { inverse_bindposes: inverse_bindposes.clone(), joints: joint_entities, @@ -157,8 +185,51 @@ fn setup( } /// Animate the joint marked with [`AnimatedJoint`] component. -fn joint_animation(time: Res