diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8bde4e09dc2e..0303f1dc9ca59 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -432,6 +432,6 @@ jobs: echo -e "$errors" echo " Avoid importing internal Bevy crates, they should not be used directly" echo " Fix the issue by replacing 'bevy_*' with 'bevy'" - echo " Example: 'use bevy::sprite::MaterialMesh2dBundle;' instead of 'bevy_internal::sprite::MaterialMesh2dBundle;'" + echo " Example: 'use bevy::sprite::Mesh2d;' instead of 'bevy_internal::sprite::Mesh2d;'" exit 1 fi diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index dab2789281eec..b9e8959df08c8 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -46,23 +46,32 @@ jobs: - uses: dtolnay/rust-toolchain@stable + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Add Android targets - run: rustup target add aarch64-linux-android armv7-linux-androideabi + run: rustup target add aarch64-linux-android - - name: Install Cargo APK - run: cargo install --force cargo-apk + - name: Install Cargo NDK + run: cargo install --force cargo-ndk - - name: Build app for Android - run: ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME cargo apk build --package bevy_mobile_example + - name: Build .so file + run: cargo ndk -t arm64-v8a -o android_example/app/src/main/jniLibs build --package bevy_mobile_example env: # This will reduce the APK size from 1GB to ~200MB CARGO_PROFILE_DEV_DEBUG: false + - name: Build app for Android + run: cd examples/mobile/android_example && chmod +x gradlew && ./gradlew build + - name: Upload to Browser Stack run: | curl -u "${{ secrets.BROWSERSTACK_USERNAME }}:${{ secrets.BROWSERSTACK_ACCESS_KEY }}" \ -X POST "https://api-cloud.browserstack.com/app-automate/upload" \ - -F "file=@target/debug/apk/bevyexample.apk" \ + -F "file=@app/build/outputs/apk/debug/app-debug.apk" \ -F "custom_id=$GITHUB_RUN_ID" nonce: diff --git a/.github/workflows/validation-jobs.yml b/.github/workflows/validation-jobs.yml index f5fab3f976d6d..3372fc1b0353c 100644 --- a/.github/workflows/validation-jobs.yml +++ b/.github/workflows/validation-jobs.yml @@ -49,6 +49,12 @@ jobs: - uses: dtolnay/rust-toolchain@stable + - name: Set up JDK 17 + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - uses: actions/cache@v4 with: path: | @@ -60,13 +66,16 @@ jobs: key: ${{ runner.os }}-cargo-build-android-${{ hashFiles('**/Cargo.toml') }} - name: Install Android targets - run: rustup target add aarch64-linux-android armv7-linux-androideabi + run: rustup target add aarch64-linux-android + + - name: Install Cargo NDK + run: cargo install --force cargo-ndk - - name: Install Cargo APK - run: cargo install --force cargo-apk + - name: Build .so file + run: cargo ndk -t arm64-v8a -o android_example/app/src/main/jniLibs build --package bevy_mobile_example - - name: Build APK - run: ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME cargo apk build --package bevy_mobile_example + - name: Build app for Android + run: cd examples/mobile/android_example && chmod +x gradlew && ./gradlew build run-examples-linux-vulkan: if: ${{ github.event_name == 'merge_group' }} diff --git a/Cargo.toml b/Cargo.toml index 8c05cfebf9478..1775b2dd7b7d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -131,6 +131,7 @@ default = [ "default_font", "webgl2", "sysinfo_plugin", + "android-game-activity", ] # Provides an implementation for picking sprites @@ -327,6 +328,12 @@ wayland = ["bevy_internal/wayland"] # X11 display server support x11 = ["bevy_internal/x11"] +# Android NativeActivity support. Legacy, should be avoided for most new Android games. +android-native-activity = ["bevy_internal/android-native-activity"] + +# Android GameActivity support. Default, choose between this and `android-native-activity`. +android-game-activity = ["bevy_internal/android-game-activity"] + # Enable systems that allow for automated testing on CI bevy_ci_testing = ["bevy_internal/bevy_ci_testing"] @@ -2965,6 +2972,17 @@ description = "Demonstrates text wrapping" category = "UI (User Interface)" wasm = true +[[example]] +name = "ghost_nodes" +path = "examples/ui/ghost_nodes.rs" +doc-scrape-examples = true + +[package.metadata.example.ghost_nodes] +name = "Ghost Nodes" +description = "Demonstrates the use of Ghost Nodes to skip entities in the UI layout hierarchy" +category = "UI (User Interface)" +wasm = true + [[example]] name = "grid" path = "examples/ui/grid.rs" @@ -3430,6 +3448,17 @@ description = "Demonstrates screen space reflections with water ripples" category = "3D Rendering" wasm = false +[[example]] +name = "camera_sub_view" +path = "examples/3d/camera_sub_view.rs" +doc-scrape-examples = true + +[package.metadata.example.camera_sub_view] +name = "Camera sub view" +description = "Demonstrates using different sub view effects on a camera" +category = "3D Rendering" +wasm = true + [[example]] name = "color_grading" path = "examples/3d/color_grading.rs" diff --git a/benches/benches/bevy_ecs/world/commands.rs b/benches/benches/bevy_ecs/world/commands.rs index 5f2f8a01f5213..19128f80ba7da 100644 --- a/benches/benches/bevy_ecs/world/commands.rs +++ b/benches/benches/bevy_ecs/world/commands.rs @@ -43,8 +43,8 @@ pub fn spawn_commands(criterion: &mut Criterion) { bencher.iter(|| { let mut commands = Commands::new(&mut command_queue, &world); for i in 0..entity_count { - let entity = commands - .spawn_empty() + let mut entity = commands.spawn_empty(); + entity .insert_if(A, || black_box(i % 2 == 0)) .insert_if(B, || black_box(i % 3 == 0)) .insert_if(C, || black_box(i % 4 == 0)); diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index ae1e8ee23cdc9..66d20c49fe872 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 746b6f8b9dd1e..26589b8e6e56f 100644 --- a/crates/bevy_animation/src/animation_curves.rs +++ b/crates/bevy_animation/src/animation_curves.rs @@ -65,7 +65,6 @@ //! - [`TranslationCurve`], which uses `Vec3` output to animate [`Transform::translation`] //! - [`RotationCurve`], which uses `Quat` output to animate [`Transform::rotation`] //! - [`ScaleCurve`], which uses `Vec3` output to animate [`Transform::scale`] -//! - [`TransformCurve`], which uses `Transform` output to animate the entire `Transform` //! //! ## Animatable properties //! @@ -90,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. /// @@ -189,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, @@ -242,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(()) } } @@ -268,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, @@ -280,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(()) } } @@ -305,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, @@ -317,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(()) } } @@ -342,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, @@ -354,57 +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 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); + 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(()) } } -/// This type allows a [curve] valued in `Transform` to become an [`AnimationCurve`] that animates -/// a transform. -/// -/// This exists primarily as a convenience to animate entities using the entire transform at once -/// instead of splitting it into pieces and animating each part (translation, rotation, scale). -/// -/// [curve]: Curve -#[derive(Debug, Clone, Reflect, FromReflect)] -#[reflect(from_reflect = false)] -pub struct TransformCurve(pub C); - -impl AnimationCurve for TransformCurve -where - C: AnimationCompatibleCurve, -{ - fn clone_value(&self) -> Box { - Box::new(self.clone()) +impl AnimationCurveEvaluator for ScaleCurveEvaluator { + fn blend(&mut self, graph_node: AnimationNodeIndex) -> Result<(), AnimationEvaluationError> { + self.evaluator.blend(graph_node) } - fn domain(&self) -> Interval { - self.0.domain() + fn push_blend_register( + &mut self, + weight: f32, + graph_node: AnimationNodeIndex, + ) -> Result<(), AnimationEvaluationError> { + self.evaluator.push_blend_register(weight, graph_node) } - fn apply<'a>( - &self, - t: f32, + fn commit<'a>( + &mut self, transform: Option>, - _entity: AnimationEntityMut<'a>, - weight: f32, + _: AnimationEntityMut<'a>, ) -> Result<(), AnimationEvaluationError> { let mut component = transform.ok_or_else(|| { AnimationEvaluationError::ComponentNotPresent(TypeId::of::()) })?; - let new_value = self.0.sample_clamped(t); - *component = ::interpolate(&component, &new_value, weight); + component.scale = self + .evaluator + .stack + .pop() + .ok_or_else(inconsistent::)? + .value; Ok(()) } } @@ -417,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, @@ -429,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 { @@ -477,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>; } @@ -536,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 5264cf9a23552..22c0e1a60842c 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 c9d821b3d57bb..23585432041e6 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, @@ -653,14 +655,12 @@ impl ActiveAnimation { /// Animation controls. /// -/// Automatically added to any root animations of a `SceneBundle` when it is +/// Automatically added to any root animations of a scene when it is /// spawned. #[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/crates/bevy_app/src/lib.rs b/crates/bevy_app/src/lib.rs index 830772803bec0..aac1cfddff875 100644 --- a/crates/bevy_app/src/lib.rs +++ b/crates/bevy_app/src/lib.rs @@ -1,4 +1,6 @@ -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +// `rustdoc_internals` is needed for `#[doc(fake_variadics)]` +#![allow(internal_features)] +#![cfg_attr(any(docsrs, docsrs_dep), feature(doc_auto_cfg, rustdoc_internals))] #![forbid(unsafe_code)] #![doc( html_logo_url = "https://bevyengine.org/assets/icon.png", diff --git a/crates/bevy_app/src/plugin.rs b/crates/bevy_app/src/plugin.rs index 0659c6a311884..c264264695c63 100644 --- a/crates/bevy_app/src/plugin.rs +++ b/crates/bevy_app/src/plugin.rs @@ -162,7 +162,8 @@ mod sealed { } macro_rules! impl_plugins_tuples { - ($(($param: ident, $plugins: ident)),*) => { + ($(#[$meta:meta])* $(($param: ident, $plugins: ident)),*) => { + $(#[$meta])* impl<$($param, $plugins),*> Plugins<(PluginsTupleMarker, $($param,)*)> for ($($plugins,)*) where $($plugins: Plugins<$param>),* @@ -179,5 +180,12 @@ mod sealed { } } - all_tuples!(impl_plugins_tuples, 0, 15, P, S); + all_tuples!( + #[doc(fake_variadic)] + impl_plugins_tuples, + 0, + 15, + P, + S + ); } diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index d05bb498888eb..4bee550a4879c 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -36,6 +36,7 @@ async-lock = "3.0" crossbeam-channel = "0.5" downcast-rs = "1.2" disqualified = "1.0" +either = "1.13" futures-io = "0.3" futures-lite = "2.0.1" blake3 = "1.5" @@ -46,7 +47,7 @@ thiserror = "1.0" uuid = { version = "1.0", features = ["v4"] } [target.'cfg(target_os = "android")'.dependencies] -bevy_winit = { path = "../bevy_winit", version = "0.15.0-dev" } +bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } [target.'cfg(target_arch = "wasm32")'.dependencies] wasm-bindgen = { version = "0.2" } diff --git a/crates/bevy_asset/src/io/android.rs b/crates/bevy_asset/src/io/android.rs index aa708f56ba724..b8b78a9681637 100644 --- a/crates/bevy_asset/src/io/android.rs +++ b/crates/bevy_asset/src/io/android.rs @@ -1,16 +1,15 @@ -use crate::io::{ - get_meta_path, AssetReader, AssetReaderError, EmptyPathStream, PathStream, Reader, VecReader, -}; -use alloc::ffi::CString; +use crate::io::{get_meta_path, AssetReader, AssetReaderError, PathStream, Reader, VecReader}; use bevy_utils::tracing::error; -use std::path::Path; +use futures_lite::stream; +use std::{ffi::CString, path::Path}; /// [`AssetReader`] implementation for Android devices, built on top of Android's [`AssetManager`]. /// /// Implementation details: /// -/// - [`load_path`](AssetIo::load_path) uses the [`AssetManager`] to load files. -/// - [`read_directory`](AssetIo::read_directory) always returns an empty iterator. +/// - All functions use the [`AssetManager`] to load files. +/// - [`is_directory`](AssetReader::is_directory) tries to open the path +/// as a normal file and treats an error as if the path is a directory. /// - Watching for changes is not supported. The watcher method will do nothing. /// /// [AssetManager]: https://developer.android.com/reference/android/content/res/AssetManager @@ -18,7 +17,7 @@ pub struct AndroidAssetReader; impl AssetReader for AndroidAssetReader { async fn read<'a>(&'a self, path: &'a Path) -> Result { - let asset_manager = bevy_winit::ANDROID_APP + let asset_manager = bevy_window::ANDROID_APP .get() .expect("Bevy must be setup with the #[bevy_main] macro on Android") .asset_manager(); @@ -32,7 +31,7 @@ impl AssetReader for AndroidAssetReader { async fn read_meta<'a>(&'a self, path: &'a Path) -> Result { let meta_path = get_meta_path(path); - let asset_manager = bevy_winit::ANDROID_APP + let asset_manager = bevy_window::ANDROID_APP .get() .expect("Bevy must be setup with the #[bevy_main] macro on Android") .asset_manager(); @@ -46,15 +45,53 @@ impl AssetReader for AndroidAssetReader { async fn read_directory<'a>( &'a self, - _path: &'a Path, + path: &'a Path, ) -> Result, AssetReaderError> { - let stream: Box = Box::new(EmptyPathStream); - error!("Reading directories is not supported with the AndroidAssetReader"); - Ok(stream) + let asset_manager = bevy_window::ANDROID_APP + .get() + .expect("Bevy must be setup with the #[bevy_main] macro on Android") + .asset_manager(); + let opened_assets_dir = asset_manager + .open_dir(&CString::new(path.to_str().unwrap()).unwrap()) + .ok_or(AssetReaderError::NotFound(path.to_path_buf()))?; + + let mapped_stream = opened_assets_dir + .filter_map(move |f| { + let file_path = path.join(Path::new(f.to_str().unwrap())); + // filter out meta files as they are not considered assets + if let Some(ext) = file_path.extension().and_then(|e| e.to_str()) { + if ext.eq_ignore_ascii_case("meta") { + return None; + } + } + Some(file_path.to_owned()) + }) + .collect::>(); + + let read_dir: Box = Box::new(stream::iter(mapped_stream)); + Ok(read_dir) } - async fn is_directory<'a>(&'a self, _path: &'a Path) -> Result { - error!("Reading directories is not supported with the AndroidAssetReader"); - Ok(false) + async fn is_directory<'a>( + &'a self, + path: &'a Path, + ) -> std::result::Result { + let asset_manager = bevy_window::ANDROID_APP + .get() + .expect("Bevy must be setup with the #[bevy_main] macro on Android") + .asset_manager(); + // HACK: `AssetManager` does not provide a way to check if path + // points to a directory or a file + // `open_dir` succeeds for both files and directories and will only + // fail if the path does not exist at all + // `open` will fail for directories, but it will work for files + // The solution here was to first use `open_dir` to eliminate the case + // when the path does not exist at all, and then to use `open` to + // see if that path is a file or a directory + let cpath = CString::new(path.to_str().unwrap()).unwrap(); + let _ = asset_manager + .open_dir(&cpath) + .ok_or(AssetReaderError::NotFound(path.to_path_buf()))?; + Ok(asset_manager.open(&cpath).is_none()) } } diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index af957b45454f4..08d08c4b1c2df 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -188,7 +188,7 @@ pub use handle::*; pub use id::*; pub use loader::*; pub use loader_builders::{ - DirectNestedLoader, NestedLoader, UntypedDirectNestedLoader, UntypedNestedLoader, + Deferred, DynamicTyped, Immediate, NestedLoader, StaticTyped, UnknownTyped, }; pub use path::*; pub use reflect::*; @@ -689,7 +689,7 @@ mod tests { for dep in ron.embedded_dependencies { let loaded = load_context .loader() - .direct() + .immediate() .load::(&dep) .await .map_err(|_| Self::Error::CannotLoadDependency { diff --git a/crates/bevy_asset/src/loader.rs b/crates/bevy_asset/src/loader.rs index aa66985631253..e1315a96e26b2 100644 --- a/crates/bevy_asset/src/loader.rs +++ b/crates/bevy_asset/src/loader.rs @@ -1,6 +1,6 @@ use crate::{ io::{AssetReaderError, MissingAssetSourceError, MissingProcessedAssetReaderError, Reader}, - loader_builders::NestedLoader, + loader_builders::{Deferred, NestedLoader, StaticTyped}, meta::{AssetHash, AssetMeta, AssetMetaDyn, ProcessedInfoMinimal, Settings}, path::AssetPath, Asset, AssetLoadError, AssetServer, AssetServerMode, Assets, Handle, UntypedAssetId, @@ -290,7 +290,11 @@ impl AssetContainer for A { } } -/// An error that occurs when attempting to call [`DirectNestedLoader::load`](crate::DirectNestedLoader::load) +/// An error that occurs when attempting to call [`NestedLoader::load`] which +/// is configured to work [immediately]. +/// +/// [`NestedLoader::load`]: crate::NestedLoader::load +/// [immediately]: crate::Immediate #[derive(Error, Debug)] #[error("Failed to load dependency {dependency:?} {error}")] pub struct LoadDirectError { @@ -550,7 +554,7 @@ impl<'a> LoadContext<'a> { /// Create a builder for loading nested assets in this context. #[must_use] - pub fn loader(&mut self) -> NestedLoader<'a, '_> { + pub fn loader(&mut self) -> NestedLoader<'a, '_, StaticTyped, Deferred> { NestedLoader::new(self) } diff --git a/crates/bevy_asset/src/loader_builders.rs b/crates/bevy_asset/src/loader_builders.rs index 23cf722803546..681bd16cbc94d 100644 --- a/crates/bevy_asset/src/loader_builders.rs +++ b/crates/bevy_asset/src/loader_builders.rs @@ -5,7 +5,7 @@ use crate::{ io::Reader, meta::{meta_transform_settings, AssetMetaDyn, MetaTransform, Settings}, Asset, AssetLoadError, AssetPath, ErasedAssetLoader, ErasedLoadedAsset, Handle, LoadContext, - LoadDirectError, LoadedAsset, LoadedUntypedAsset, + LoadDirectError, LoadedAsset, LoadedUntypedAsset, UntypedHandle, }; use alloc::sync::Arc; use core::any::TypeId; @@ -25,28 +25,157 @@ impl ReaderRef<'_> { } } -/// A builder for loading nested assets inside a `LoadContext`. +/// A builder for loading nested assets inside a [`LoadContext`]. +/// +/// # Loader state +/// +/// The type parameters `T` and `M` determine how this will load assets: +/// - `T`: the typing of this loader. How do we know what type of asset to load? +/// +/// See [`StaticTyped`] (the default), [`DynamicTyped`], and [`UnknownTyped`]. +/// +/// - `M`: the load mode. Do we want to load this asset right now (in which case +/// you will have to `await` the operation), or do we just want a [`Handle`], +/// and leave the actual asset loading to later? +/// +/// See [`Deferred`] (the default) and [`Immediate`]. +/// +/// When configuring this builder, you can freely switch between these modes +/// via functions like [`deferred`] and [`immediate`]. +/// +/// ## Typing +/// +/// To inform the loader of what type of asset to load: +/// - in [`StaticTyped`]: statically providing a type parameter `A: Asset` to +/// [`load`]. +/// +/// This is the simplest way to get a [`Handle`] to the loaded asset, as +/// long as you know the type of `A` at compile time. +/// +/// - in [`DynamicTyped`]: providing the [`TypeId`] of the asset at runtime. +/// +/// If you know the type ID of the asset at runtime, but not at compile time, +/// use [`with_dynamic_type`] followed by [`load`] to start loading an asset +/// of that type. This lets you get an [`UntypedHandle`] (via [`Deferred`]), +/// or a [`ErasedLoadedAsset`] (via [`Immediate`]). +/// +/// - in [`UnknownTyped`]: loading either a type-erased version of the asset +/// ([`ErasedLoadedAsset`]), or a handle *to a handle* of the actual asset +/// ([`LoadedUntypedAsset`]). +/// +/// If you have no idea what type of asset you will be loading (not even at +/// runtime with a [`TypeId`]), use this. +/// +/// ## Load mode +/// +/// To inform the loader how you want to load the asset: +/// - in [`Deferred`]: when you request to load the asset, you get a [`Handle`] +/// for it, but the actual loading won't be completed until later. +/// +/// Use this if you only need a [`Handle`] or [`UntypedHandle`]. +/// +/// - in [`Immediate`]: the load request will load the asset right then and +/// there, waiting until the asset is fully loaded and giving you access to +/// it. +/// +/// Note that this requires you to `await` a future, so you must be in an +/// async context to use direct loading. In an asset loader, you will be in +/// an async context. +/// +/// Use this if you need the *value* of another asset in order to load the +/// current asset. For example, if you are deriving a new asset from the +/// referenced asset, or you are building a collection of assets. This will +/// add the path of the asset as a "load dependency". +/// +/// If the current loader is used in a [`Process`] "asset preprocessor", +/// such as a [`LoadTransformAndSave`] preprocessor, changing a "load +/// dependency" will result in re-processing of the asset. +/// +/// # Load kickoff +/// +/// If the current context is a normal [`AssetServer::load`], an actual asset +/// load will be kicked off immediately, which ensures the load happens as soon +/// as possible. "Normal loads" kicked from within a normal Bevy App will +/// generally configure the context to kick off loads immediately. +/// +/// If the current context is configured to not load dependencies automatically +/// (ex: [`AssetProcessor`]), a load will not be kicked off automatically. It is +/// then the calling context's responsibility to begin a load if necessary. /// /// # Lifetimes +/// /// - `ctx`: the lifetime of the associated [`AssetServer`](crate::AssetServer) reference /// - `builder`: the lifetime of the temporary builder structs -pub struct NestedLoader<'ctx, 'builder> { +/// +/// [`deferred`]: Self::deferred +/// [`immediate`]: Self::immediate +/// [`load`]: Self::load +/// [`with_dynamic_type`]: Self::with_dynamic_type +/// [`AssetServer::load`]: crate::AssetServer::load +/// [`AssetProcessor`]: crate::processor::AssetProcessor +/// [`Process`]: crate::processor::Process +/// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave +pub struct NestedLoader<'ctx, 'builder, T, M> { load_context: &'builder mut LoadContext<'ctx>, meta_transform: Option, - asset_type_id: Option, + typing: T, + mode: M, +} + +mod sealed { + pub trait Typing {} + + pub trait Mode {} } -impl<'ctx, 'builder> NestedLoader<'ctx, 'builder> { - pub(crate) fn new( - load_context: &'builder mut LoadContext<'ctx>, - ) -> NestedLoader<'ctx, 'builder> { +/// [`NestedLoader`] will be provided the type of asset as a type parameter on +/// [`load`]. +/// +/// [`load`]: NestedLoader::load +pub struct StaticTyped(()); + +impl sealed::Typing for StaticTyped {} + +/// [`NestedLoader`] has been configured with info on what type of asset to load +/// at runtime. +pub struct DynamicTyped { + asset_type_id: TypeId, +} + +impl sealed::Typing for DynamicTyped {} + +/// [`NestedLoader`] does not know what type of asset it will be loading. +pub struct UnknownTyped(()); + +impl sealed::Typing for UnknownTyped {} + +/// [`NestedLoader`] will create and return asset handles immediately, but only +/// actually load the asset later. +pub struct Deferred(()); + +impl sealed::Mode for Deferred {} + +/// [`NestedLoader`] will immediately load an asset when requested. +pub struct Immediate<'builder, 'reader> { + reader: Option<&'builder mut (dyn Reader + 'reader)>, +} + +impl sealed::Mode for Immediate<'_, '_> {} + +// common to all states + +impl<'ctx, 'builder> NestedLoader<'ctx, 'builder, StaticTyped, Deferred> { + pub(crate) fn new(load_context: &'builder mut LoadContext<'ctx>) -> Self { NestedLoader { load_context, meta_transform: None, - asset_type_id: None, + typing: StaticTyped(()), + mode: Deferred(()), } } +} +impl<'ctx, 'builder, T: sealed::Typing, M: sealed::Mode> NestedLoader<'ctx, 'builder, T, M> { fn with_transform( mut self, transform: impl Fn(&mut dyn AssetMetaDyn) + Send + Sync + 'static, @@ -74,47 +203,105 @@ impl<'ctx, 'builder> NestedLoader<'ctx, 'builder> { self.with_transform(move |meta| meta_transform_settings(meta, &settings)) } - /// Specify the output asset type. + // convert between `T`s + + /// When [`load`]ing, you must pass in the asset type as a type parameter + /// statically. + /// + /// If you don't know the type statically (at compile time), consider + /// [`with_dynamic_type`] or [`with_unknown_type`]. + /// + /// [`load`]: Self::load + /// [`with_dynamic_type`]: Self::with_dynamic_type + /// [`with_unknown_type`]: Self::with_unknown_type #[must_use] - pub fn with_asset_type(mut self) -> Self { - self.asset_type_id = Some(TypeId::of::()); - self + pub fn with_static_type(self) -> NestedLoader<'ctx, 'builder, StaticTyped, M> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: StaticTyped(()), + mode: self.mode, + } } - /// Specify the output asset type. + /// When [`load`]ing, the loader will attempt to load an asset with the + /// given [`TypeId`]. + /// + /// [`load`]: Self::load #[must_use] - pub fn with_asset_type_id(mut self, asset_type_id: TypeId) -> Self { - self.asset_type_id = Some(asset_type_id); - self + pub fn with_dynamic_type( + self, + asset_type_id: TypeId, + ) -> NestedLoader<'ctx, 'builder, DynamicTyped, M> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: DynamicTyped { asset_type_id }, + mode: self.mode, + } } - /// Load assets directly, rather than creating handles. + /// When [`load`]ing, we will infer what type of asset to load from + /// metadata. + /// + /// [`load`]: Self::load #[must_use] - pub fn direct<'c>(self) -> DirectNestedLoader<'ctx, 'builder, 'c> { - DirectNestedLoader { - base: self, - reader: None, + pub fn with_unknown_type(self) -> NestedLoader<'ctx, 'builder, UnknownTyped, M> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: UnknownTyped(()), + mode: self.mode, } } - /// Load assets without static type information. + // convert between `M`s + + /// When [`load`]ing, create only asset handles, rather than returning the + /// actual asset. /// - /// If you need to specify the type of asset, but cannot do it statically, - /// use `.with_asset_type_id()`. + /// [`load`]: Self::load + pub fn deferred(self) -> NestedLoader<'ctx, 'builder, T, Deferred> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: self.typing, + mode: Deferred(()), + } + } + + /// The [`load`] call itself will load an asset, rather than scheduling the + /// loading to happen later. + /// + /// This gives you access to the loaded asset, but requires you to be in an + /// async context, and be able to `await` the resulting future. + /// + /// [`load`]: Self::load #[must_use] - pub fn untyped(self) -> UntypedNestedLoader<'ctx, 'builder> { - UntypedNestedLoader { base: self } + pub fn immediate<'c>(self) -> NestedLoader<'ctx, 'builder, T, Immediate<'builder, 'c>> { + NestedLoader { + load_context: self.load_context, + meta_transform: self.meta_transform, + typing: self.typing, + mode: Immediate { reader: None }, + } } +} - /// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset. - /// If the current context is a normal [`AssetServer::load`](crate::AssetServer::load), an actual asset - /// load will be kicked off immediately, which ensures the load happens as soon as possible. - /// "Normal loads" kicked from within a normal Bevy App will generally configure the context to kick off - /// loads immediately. - /// If the current context is configured to not load dependencies automatically - /// (ex: [`AssetProcessor`](crate::processor::AssetProcessor)), - /// a load will not be kicked off automatically. It is then the calling context's responsibility to begin - /// a load if necessary. +// deferred loading logic + +impl NestedLoader<'_, '_, StaticTyped, Deferred> { + /// Retrieves a handle for the asset at the given path and adds that path as + /// a dependency of this asset. + /// + /// This requires you to know the type of asset statically. + /// - If you have runtime info for what type of asset you're loading (e.g. a + /// [`TypeId`]), use [`with_dynamic_type`]. + /// - If you do not know at all what type of asset you're loading, use + /// [`with_unknown_type`]. + /// + /// [`with_dynamic_type`]: Self::with_dynamic_type + /// [`with_unknown_type`]: Self::with_unknown_type pub fn load<'c, A: Asset>(self, path: impl Into>) -> Handle { let path = path.into().to_owned(); let handle = if self.load_context.should_load_dependencies { @@ -131,74 +318,78 @@ impl<'ctx, 'builder> NestedLoader<'ctx, 'builder> { } } -/// A builder for loading untyped nested assets inside a [`LoadContext`]. -/// -/// # Lifetimes -/// - `ctx`: the lifetime of the associated [`AssetServer`](crate::AssetServer) reference -/// - `builder`: the lifetime of the temporary builder structs -pub struct UntypedNestedLoader<'ctx, 'builder> { - base: NestedLoader<'ctx, 'builder>, +impl NestedLoader<'_, '_, DynamicTyped, Deferred> { + /// Retrieves a handle for the asset at the given path and adds that path as + /// a dependency of this asset. + /// + /// This requires you to pass in the asset type ID into + /// [`with_dynamic_type`]. + /// + /// [`with_dynamic_type`]: Self::with_dynamic_type + pub fn load<'p>(self, path: impl Into>) -> UntypedHandle { + let path = path.into().to_owned(); + let handle = if self.load_context.should_load_dependencies { + self.load_context + .asset_server + .load_erased_with_meta_transform( + path, + self.typing.asset_type_id, + self.meta_transform, + (), + ) + } else { + self.load_context + .asset_server + .get_or_create_path_handle_erased( + path, + self.typing.asset_type_id, + self.meta_transform, + ) + }; + self.load_context.dependencies.insert(handle.id()); + handle + } } -impl<'ctx, 'builder> UntypedNestedLoader<'ctx, 'builder> { - /// Retrieves a handle for the asset at the given path and adds that path as a dependency of the asset without knowing its type. +impl NestedLoader<'_, '_, UnknownTyped, Deferred> { + /// Retrieves a handle for the asset at the given path and adds that path as + /// a dependency of this asset. + /// + /// This will infer the asset type from metadata. pub fn load<'p>(self, path: impl Into>) -> Handle { let path = path.into().to_owned(); - let handle = if self.base.load_context.should_load_dependencies { - self.base - .load_context + let handle = if self.load_context.should_load_dependencies { + self.load_context .asset_server - .load_untyped_with_meta_transform(path, self.base.meta_transform) + .load_unknown_type_with_meta_transform(path, self.meta_transform) } else { - self.base - .load_context + self.load_context .asset_server - .get_or_create_path_handle(path, self.base.meta_transform) + .get_or_create_path_handle(path, self.meta_transform) }; - self.base - .load_context - .dependencies - .insert(handle.id().untyped()); + self.load_context.dependencies.insert(handle.id().untyped()); handle } } -/// A builder for directly loading nested assets inside a `LoadContext`. -/// -/// # Lifetimes -/// - `ctx`: the lifetime of the associated [`AssetServer`][crate::AssetServer] reference -/// - `builder`: the lifetime of the temporary builder structs -/// - `reader`: the lifetime of the [`Reader`] reference used to read the asset data -pub struct DirectNestedLoader<'ctx, 'builder, 'reader> { - base: NestedLoader<'ctx, 'builder>, - reader: Option<&'builder mut (dyn Reader + 'reader)>, -} +// immediate loading logic -impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reader> { +impl<'builder, 'reader, T> NestedLoader<'_, '_, T, Immediate<'builder, 'reader>> { /// Specify the reader to use to read the asset data. #[must_use] pub fn with_reader(mut self, reader: &'builder mut (dyn Reader + 'reader)) -> Self { - self.reader = Some(reader); + self.mode.reader = Some(reader); self } - /// Load the asset without providing static type information. - /// - /// If you need to specify the type of asset, but cannot do it statically, - /// use `.with_asset_type_id()`. - #[must_use] - pub fn untyped(self) -> UntypedDirectNestedLoader<'ctx, 'builder, 'reader> { - UntypedDirectNestedLoader { base: self } - } - async fn load_internal( self, path: &AssetPath<'static>, + asset_type_id: Option, ) -> Result<(Arc, ErasedLoadedAsset), LoadDirectError> { - let (mut meta, loader, mut reader) = if let Some(reader) = self.reader { - let loader = if let Some(asset_type_id) = self.base.asset_type_id { - self.base - .load_context + let (mut meta, loader, mut reader) = if let Some(reader) = self.mode.reader { + let loader = if let Some(asset_type_id) = asset_type_id { + self.load_context .asset_server .get_asset_loader_with_asset_type_id(asset_type_id) .await @@ -207,8 +398,7 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade error: error.into(), })? } else { - self.base - .load_context + self.load_context .asset_server .get_path_asset_loader(path) .await @@ -221,10 +411,9 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade (meta, loader, ReaderRef::Borrowed(reader)) } else { let (meta, loader, reader) = self - .base .load_context .asset_server - .get_meta_loader_and_reader(path, self.base.asset_type_id) + .get_meta_loader_and_reader(path, asset_type_id) .await .map_err(|error| LoadDirectError { dependency: path.clone(), @@ -233,35 +422,35 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade (meta, loader, ReaderRef::Boxed(reader)) }; - if let Some(meta_transform) = self.base.meta_transform { + if let Some(meta_transform) = self.meta_transform { meta_transform(&mut *meta); } let asset = self - .base .load_context .load_direct_internal(path.clone(), meta, &*loader, reader.as_mut()) .await?; Ok((loader, asset)) } +} - /// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before - /// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are - /// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a - /// "load dependency". +impl NestedLoader<'_, '_, StaticTyped, Immediate<'_, '_>> { + /// Attempts to load the asset at the given `path` immediately. /// - /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor, - /// changing a "load dependency" will result in re-processing of the asset. + /// This requires you to know the type of asset statically. + /// - If you have runtime info for what type of asset you're loading (e.g. a + /// [`TypeId`]), use [`with_dynamic_type`]. + /// - If you do not know at all what type of asset you're loading, use + /// [`with_unknown_type`]. /// - /// [`Process`]: crate::processor::Process - /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + /// [`with_dynamic_type`]: Self::with_dynamic_type + /// [`with_unknown_type`]: Self::with_unknown_type pub async fn load<'p, A: Asset>( - mut self, + self, path: impl Into>, ) -> Result, LoadDirectError> { - self.base.asset_type_id = Some(TypeId::of::()); let path = path.into().into_owned(); - self.load_internal(&path) + self.load_internal(&path, Some(TypeId::of::())) .await .and_then(move |(loader, untyped_asset)| { untyped_asset.downcast::().map_err(|_| LoadDirectError { @@ -277,32 +466,36 @@ impl<'ctx: 'reader, 'builder, 'reader> DirectNestedLoader<'ctx, 'builder, 'reade } } -/// A builder for directly loading untyped nested assets inside a `LoadContext`. -/// -/// # Lifetimes -/// - `ctx`: the lifetime of the associated [`AssetServer`](crate::AssetServer) reference -/// - `builder`: the lifetime of the temporary builder structs -/// - `reader`: the lifetime of the [`Reader`] reference used to read the asset data -pub struct UntypedDirectNestedLoader<'ctx, 'builder, 'reader> { - base: DirectNestedLoader<'ctx, 'builder, 'reader>, +impl NestedLoader<'_, '_, DynamicTyped, Immediate<'_, '_>> { + /// Attempts to load the asset at the given `path` immediately. + /// + /// This requires you to pass in the asset type ID into + /// [`with_dynamic_type`]. + /// + /// [`with_dynamic_type`]: Self::with_dynamic_type + pub async fn load<'p>( + self, + path: impl Into>, + ) -> Result { + let path = path.into().into_owned(); + let asset_type_id = Some(self.typing.asset_type_id); + self.load_internal(&path, asset_type_id) + .await + .map(|(_, asset)| asset) + } } -impl<'ctx: 'reader, 'builder, 'reader> UntypedDirectNestedLoader<'ctx, 'builder, 'reader> { - /// Loads the asset at the given `path` directly. This is an async function that will wait until the asset is fully loaded before - /// returning. Use this if you need the _value_ of another asset in order to load the current asset. For example, if you are - /// deriving a new asset from the referenced asset, or you are building a collection of assets. This will add the `path` as a - /// "load dependency". - /// - /// If the current loader is used in a [`Process`] "asset preprocessor", such as a [`LoadTransformAndSave`] preprocessor, - /// changing a "load dependency" will result in re-processing of the asset. +impl NestedLoader<'_, '_, UnknownTyped, Immediate<'_, '_>> { + /// Attempts to load the asset at the given `path` immediately. /// - /// [`Process`]: crate::processor::Process - /// [`LoadTransformAndSave`]: crate::processor::LoadTransformAndSave + /// This will infer the asset type from metadata. pub async fn load<'p>( self, path: impl Into>, ) -> Result { let path = path.into().into_owned(); - self.base.load_internal(&path).await.map(|(_, asset)| asset) + self.load_internal(&path, None) + .await + .map(|(_, asset)| asset) } } diff --git a/crates/bevy_asset/src/server/info.rs b/crates/bevy_asset/src/server/info.rs index fdeaaff089604..85b3d02fa584a 100644 --- a/crates/bevy_asset/src/server/info.rs +++ b/crates/bevy_asset/src/server/info.rs @@ -10,6 +10,7 @@ use bevy_tasks::Task; use bevy_utils::{tracing::warn, Entry, HashMap, HashSet, TypeIdMap}; use core::any::TypeId; use crossbeam_channel::Sender; +use either::Either; use thiserror::Error; #[derive(Debug)] @@ -103,7 +104,7 @@ impl AssetInfos { None, true, ), - type_name, + Either::Left(type_name), ) .unwrap() } @@ -162,15 +163,15 @@ impl AssetInfos { ); // it is ok to unwrap because TypeId was specified above let (handle, should_load) = - unwrap_with_context(result, core::any::type_name::()).unwrap(); + unwrap_with_context(result, Either::Left(core::any::type_name::())).unwrap(); (handle.typed_unchecked(), should_load) } - pub(crate) fn get_or_create_path_handle_untyped( + pub(crate) fn get_or_create_path_handle_erased( &mut self, path: AssetPath<'static>, type_id: TypeId, - type_name: &'static str, + type_name: Option<&str>, loading_mode: HandleLoadingMode, meta_transform: Option, ) -> (UntypedHandle, bool) { @@ -180,8 +181,12 @@ impl AssetInfos { loading_mode, meta_transform, ); - // it is ok to unwrap because TypeId was specified above - unwrap_with_context(result, type_name).unwrap() + let type_info = match type_name { + Some(type_name) => Either::Left(type_name), + None => Either::Right(type_id), + }; + unwrap_with_context(result, type_info) + .expect("type should be correct since the `TypeId` is specified above") } /// Retrieves asset tracking data, or creates it if it doesn't exist. @@ -765,14 +770,20 @@ pub(crate) enum GetOrCreateHandleInternalError { pub(crate) fn unwrap_with_context( result: Result, - type_name: &'static str, + type_info: Either<&str, TypeId>, ) -> Option { match result { Ok(value) => Some(value), Err(GetOrCreateHandleInternalError::HandleMissingButTypeIdNotSpecified) => None, - Err(GetOrCreateHandleInternalError::MissingHandleProviderError(_)) => { - panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \ - Make sure you have called app.init_asset::<{type_name}>()") - } + Err(GetOrCreateHandleInternalError::MissingHandleProviderError(_)) => match type_info { + Either::Left(type_name) => { + panic!("Cannot allocate an Asset Handle of type '{type_name}' because the asset type has not been initialized. \ + Make sure you have called `app.init_asset::<{type_name}>()`"); + } + Either::Right(type_id) => { + panic!("Cannot allocate an AssetHandle of type '{type_id:?}' because the asset type has not been initialized. \ + Make sure you have called `app.init_asset::<(actual asset type)>()`") + } + }, } } diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 9d57808ed0bac..316642a45a260 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -31,6 +31,7 @@ use core::{ panic::AssertUnwindSafe, }; use crossbeam_channel::{Receiver, Sender}; +use either::Either; use futures_lite::{FutureExt, StreamExt}; use info::*; use loaders::*; @@ -382,25 +383,62 @@ impl AssetServer { ); if should_load { - let owned_handle = Some(handle.clone().untyped()); - let server = self.clone(); - let task = IoTaskPool::get().spawn(async move { - if let Err(err) = server.load_internal(owned_handle, path, false, None).await { - error!("{}", err); - } - drop(guard); - }); + self.spawn_load_task(handle.clone().untyped(), path, &mut infos, guard); + } - #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] - infos.pending_tasks.insert(handle.id().untyped(), task); + handle + } - #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] - task.detach(); + pub(crate) fn load_erased_with_meta_transform<'a, G: Send + Sync + 'static>( + &self, + path: impl Into>, + type_id: TypeId, + meta_transform: Option, + guard: G, + ) -> UntypedHandle { + let path = path.into().into_owned(); + let mut infos = self.data.infos.write(); + let (handle, should_load) = infos.get_or_create_path_handle_erased( + path.clone(), + type_id, + None, + HandleLoadingMode::Request, + meta_transform, + ); + + if should_load { + self.spawn_load_task(handle.clone(), path, &mut infos, guard); } handle } + pub(crate) fn spawn_load_task( + &self, + handle: UntypedHandle, + path: AssetPath<'static>, + infos: &mut AssetInfos, + guard: G, + ) { + let owned_handle = handle.clone(); + let server = self.clone(); + let task = IoTaskPool::get().spawn(async move { + if let Err(err) = server + .load_internal(Some(owned_handle), path, false, None) + .await + { + error!("{}", err); + } + drop(guard); + }); + + #[cfg(not(any(target_arch = "wasm32", not(feature = "multi_threaded"))))] + infos.pending_tasks.insert(handle.id(), task); + + #[cfg(any(target_arch = "wasm32", not(feature = "multi_threaded")))] + task.detach(); + } + /// Asynchronously load an asset that you do not know the type of statically. If you _do_ know the type of the asset, /// you should use [`AssetServer::load`]. If you don't know the type of the asset, but you can't use an async method, /// consider using [`AssetServer::load_untyped`]. @@ -413,7 +451,7 @@ impl AssetServer { self.load_internal(None, path, false, None).await } - pub(crate) fn load_untyped_with_meta_transform<'a>( + pub(crate) fn load_unknown_type_with_meta_transform<'a>( &self, path: impl Into>, meta_transform: Option, @@ -492,7 +530,7 @@ impl AssetServer { /// required to figure out the asset type before a handle can be created. #[must_use = "not using the returned strong handle may result in the unexpected release of the assets"] pub fn load_untyped<'a>(&self, path: impl Into>) -> Handle { - self.load_untyped_with_meta_transform(path, None) + self.load_unknown_type_with_meta_transform(path, None) } /// Performs an async asset load. @@ -558,7 +596,7 @@ impl AssetServer { HandleLoadingMode::Request, meta_transform, ); - unwrap_with_context(result, loader.asset_type_name()) + unwrap_with_context(result, Either::Left(loader.asset_type_name())) } }; @@ -588,10 +626,10 @@ impl AssetServer { let (base_handle, base_path) = if path.label().is_some() { let mut infos = self.data.infos.write(); let base_path = path.without_label().into_owned(); - let (base_handle, _) = infos.get_or_create_path_handle_untyped( + let (base_handle, _) = infos.get_or_create_path_handle_erased( base_path.clone(), loader.asset_type_id(), - loader.asset_type_name(), + Some(loader.asset_type_name()), HandleLoadingMode::Force, None, ); @@ -707,10 +745,10 @@ impl AssetServer { ) -> UntypedHandle { let loaded_asset = asset.into(); let handle = if let Some(path) = path { - let (handle, _) = self.data.infos.write().get_or_create_path_handle_untyped( + let (handle, _) = self.data.infos.write().get_or_create_path_handle_erased( path, loaded_asset.asset_type_id(), - loaded_asset.asset_type_name(), + Some(loaded_asset.asset_type_name()), HandleLoadingMode::NotLoading, None, ); @@ -1131,6 +1169,28 @@ impl AssetServer { .0 } + /// Retrieve a handle for the given path, where the asset type ID and name + /// are not known statically. + /// + /// This will create a handle (and [`AssetInfo`]) if it does not exist. + pub(crate) fn get_or_create_path_handle_erased<'a>( + &self, + path: impl Into>, + type_id: TypeId, + meta_transform: Option, + ) -> UntypedHandle { + let mut infos = self.data.infos.write(); + infos + .get_or_create_path_handle_erased( + path.into().into_owned(), + type_id, + None, + HandleLoadingMode::NotLoading, + meta_transform, + ) + .0 + } + pub(crate) async fn get_meta_loader_and_reader<'a>( &'a self, asset_path: &'a AssetPath<'_>, diff --git a/crates/bevy_audio/src/audio.rs b/crates/bevy_audio/src/audio.rs index d6af1906def4b..58dc0e49a1e6b 100644 --- a/crates/bevy_audio/src/audio.rs +++ b/crates/bevy_audio/src/audio.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] + use crate::{AudioSource, Decodable}; use bevy_asset::{Asset, Handle}; use bevy_derive::Deref; @@ -228,8 +230,40 @@ impl Default for SpatialScale { pub struct DefaultSpatialScale(pub SpatialScale); /// Bundle for playing a standard bevy audio asset +#[deprecated( + since = "0.15.0", + note = "Use the `AudioPlayer` component instead. Inserting it will now also insert a `PlaybackSettings` component automatically." +)] pub type AudioBundle = AudioSourceBundle; +/// A component for playing a sound. +/// +/// Insert this component onto an entity to trigger an audio source to begin playing. +/// +/// If the handle refers to an unavailable asset (such as if it has not finished loading yet), +/// the audio will not begin playing immediately. The audio will play when the asset is ready. +/// +/// When Bevy begins the audio playback, an [`AudioSink`][crate::AudioSink] component will be +/// added to the entity. You can use that component to control the audio settings during playback. +/// +/// Playback can be configured using the [`PlaybackSettings`] component. Note that changes to the +/// `PlaybackSettings` component will *not* affect already-playing audio. +#[derive(Component, Reflect)] +#[reflect(Component)] +#[require(PlaybackSettings)] +pub struct AudioPlayer(pub Handle) +where + Source: Asset + Decodable; + +impl Clone for AudioPlayer +where + Source: Asset + Decodable, +{ + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + /// Bundle for playing a sound. /// /// Insert this bundle onto an entity to trigger a sound source to begin playing. @@ -240,12 +274,16 @@ pub type AudioBundle = AudioSourceBundle; /// When Bevy begins the audio playback, an [`AudioSink`][crate::AudioSink] component will be /// added to the entity. You can use that component to control the audio settings during playback. #[derive(Bundle)] +#[deprecated( + since = "0.15.0", + note = "Use the `AudioPlayer` component instead. Inserting it will now also insert a `PlaybackSettings` component automatically." +)] pub struct AudioSourceBundle where Source: Asset + Decodable, { /// Asset containing the audio data to play. - pub source: Handle, + pub source: AudioPlayer, /// Initial settings that the audio starts playing with. /// If you would like to control the audio while it is playing, /// query for the [`AudioSink`][crate::AudioSink] component. @@ -265,7 +303,7 @@ impl Clone for AudioSourceBundle { impl Default for AudioSourceBundle { fn default() -> Self { Self { - source: Default::default(), + source: AudioPlayer(Handle::default()), settings: Default::default(), } } diff --git a/crates/bevy_audio/src/audio_output.rs b/crates/bevy_audio/src/audio_output.rs index 69a31acff1878..c595be70ca609 100644 --- a/crates/bevy_audio/src/audio_output.rs +++ b/crates/bevy_audio/src/audio_output.rs @@ -1,6 +1,6 @@ use crate::{ - AudioSourceBundle, Decodable, DefaultSpatialScale, GlobalVolume, PlaybackMode, - PlaybackSettings, SpatialAudioSink, SpatialListener, + AudioPlayer, Decodable, DefaultSpatialScale, GlobalVolume, PlaybackMode, PlaybackSettings, + SpatialAudioSink, SpatialListener, }; use bevy_asset::{Asset, Assets, Handle}; use bevy_ecs::{prelude::*, system::SystemParam}; @@ -89,8 +89,7 @@ impl<'w, 's> EarPositions<'w, 's> { /// Plays "queued" audio through the [`AudioOutput`] resource. /// -/// "Queued" audio is any audio entity (with the components from -/// [`AudioBundle`][crate::AudioBundle] that does not have an +/// "Queued" audio is any audio entity (with an [`AudioPlayer`] component) that does not have an /// [`AudioSink`]/[`SpatialAudioSink`] component. /// /// This system detects such entities, checks if their source asset @@ -141,7 +140,7 @@ pub(crate) fn play_queued_audio_system( let emitter_translation = if let Some(emitter_transform) = maybe_emitter_transform { (emitter_transform.translation() * scale).into() } else { - warn!("Spatial AudioBundle with no GlobalTransform component. Using zero."); + warn!("Spatial AudioPlayer with no GlobalTransform component. Using zero."); Vec3::ZERO.into() }; @@ -264,16 +263,22 @@ pub(crate) fn cleanup_finished_audio( } for (entity, sink) in &query_nonspatial_remove { if sink.sink.empty() { - commands - .entity(entity) - .remove::<(AudioSourceBundle, AudioSink, PlaybackRemoveMarker)>(); + commands.entity(entity).remove::<( + AudioPlayer, + AudioSink, + PlaybackSettings, + PlaybackRemoveMarker, + )>(); } } for (entity, sink) in &query_spatial_remove { if sink.sink.empty() { - commands - .entity(entity) - .remove::<(AudioSourceBundle, SpatialAudioSink, PlaybackRemoveMarker)>(); + commands.entity(entity).remove::<( + AudioPlayer, + SpatialAudioSink, + PlaybackSettings, + PlaybackRemoveMarker, + )>(); } } } diff --git a/crates/bevy_audio/src/lib.rs b/crates/bevy_audio/src/lib.rs index 6fae1e835d972..5120a341a1e91 100644 --- a/crates/bevy_audio/src/lib.rs +++ b/crates/bevy_audio/src/lib.rs @@ -9,7 +9,7 @@ //! //! ```no_run //! # use bevy_ecs::prelude::*; -//! # use bevy_audio::{AudioBundle, AudioPlugin, PlaybackSettings}; +//! # use bevy_audio::{AudioPlayer, AudioPlugin, AudioSource, PlaybackSettings}; //! # use bevy_asset::{AssetPlugin, AssetServer}; //! # use bevy_app::{App, AppExit, NoopPluginGroup as MinimalPlugins, Startup}; //! fn main() { @@ -20,10 +20,10 @@ //! } //! //! fn play_background_audio(asset_server: Res, mut commands: Commands) { -//! commands.spawn(AudioBundle { -//! source: asset_server.load("background_audio.ogg"), -//! settings: PlaybackSettings::LOOP, -//! }); +//! commands.spawn(( +//! AudioPlayer::(asset_server.load("background_audio.ogg")), +//! PlaybackSettings::LOOP, +//! )); //! } //! ``` @@ -38,11 +38,13 @@ mod sinks; /// The audio 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::{ - AudioBundle, AudioSink, AudioSinkPlayback, AudioSource, AudioSourceBundle, Decodable, - GlobalVolume, Pitch, PitchBundle, PlaybackSettings, SpatialAudioSink, SpatialListener, + AudioBundle, AudioPlayer, AudioSink, AudioSinkPlayback, AudioSource, AudioSourceBundle, + Decodable, GlobalVolume, Pitch, PitchBundle, PlaybackSettings, SpatialAudioSink, + SpatialListener, }; } @@ -66,7 +68,7 @@ struct AudioPlaySet; /// Adds support for audio playback to a Bevy Application /// -/// Insert an [`AudioBundle`] onto your entities to play audio. +/// Insert an [`AudioPlayer`] onto your entities to play audio. #[derive(Default)] pub struct AudioPlugin { /// The global volume for all audio entities. diff --git a/crates/bevy_audio/src/pitch.rs b/crates/bevy_audio/src/pitch.rs index 1f4c406a5d967..02863d6c62781 100644 --- a/crates/bevy_audio/src/pitch.rs +++ b/crates/bevy_audio/src/pitch.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] + use crate::{AudioSourceBundle, Decodable}; use bevy_asset::Asset; use bevy_reflect::TypePath; @@ -35,4 +37,8 @@ impl Decodable for Pitch { } /// Bundle for playing a bevy note sound +#[deprecated( + since = "0.15.0", + note = "Use the `AudioPlayer` component instead. Inserting it will now also insert a `PlaybackSettings` component automatically." +)] pub type PitchBundle = AudioSourceBundle; diff --git a/crates/bevy_audio/src/sinks.rs b/crates/bevy_audio/src/sinks.rs index 68fa1e99c4543..4c2cee3b935a7 100644 --- a/crates/bevy_audio/src/sinks.rs +++ b/crates/bevy_audio/src/sinks.rs @@ -76,7 +76,7 @@ pub trait AudioSinkPlayback { /// Used to control audio during playback. /// /// Bevy inserts this component onto your entities when it begins playing an audio source. -/// Use [`AudioBundle`][crate::AudioBundle] to trigger that to happen. +/// Use [`AudioPlayer`][crate::AudioPlayer] to trigger that to happen. /// /// You can use this component to modify the playback settings while the audio is playing. /// diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 27b9e9195f2e7..0ce36763d6e66 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -33,8 +33,8 @@ pub use skybox::Skybox; /// /// Expect bugs, missing features, compatibility issues, low performance, and/or future breaking changes. pub mod experimental { + #[expect(deprecated)] pub mod taa { - #[allow(deprecated)] pub use crate::taa::{ TemporalAntiAliasBundle, TemporalAntiAliasNode, TemporalAntiAliasPlugin, TemporalAntiAliasSettings, TemporalAntiAliasing, diff --git a/crates/bevy_core_pipeline/src/motion_blur/mod.rs b/crates/bevy_core_pipeline/src/motion_blur/mod.rs index 11fc5fbe112ac..c5a4a9f212e5a 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/mod.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/mod.rs @@ -1,7 +1,8 @@ //! Per-object, per-pixel motion blur. //! -//! Add the [`MotionBlurBundle`] to a camera to enable motion blur. See [`MotionBlur`] for more -//! documentation. +//! Add the [`MotionBlur`] component to a camera to enable motion blur. + +#![expect(deprecated)] use crate::{ core_3d::graph::{Core3d, Node3d}, @@ -27,6 +28,10 @@ pub mod pipeline; /// Adds [`MotionBlur`] and the required depth and motion vector prepasses to a camera entity. #[derive(Bundle, Default)] +#[deprecated( + since = "0.15.0", + note = "Use the `MotionBlur` component instead. Inserting it will now also insert the other components required by it automatically." +)] pub struct MotionBlurBundle { pub motion_blur: MotionBlur, pub depth_prepass: DepthPrepass, @@ -51,22 +56,22 @@ pub struct MotionBlurBundle { /// # Usage /// /// Add the [`MotionBlur`] component to a camera to enable and configure motion blur for that -/// camera. Motion blur also requires the depth and motion vector prepass, which can be added more -/// easily to the camera with the [`MotionBlurBundle`]. +/// camera. /// /// ``` -/// # use bevy_core_pipeline::{core_3d::Camera3dBundle, motion_blur::MotionBlurBundle}; +/// # use bevy_core_pipeline::{core_3d::Camera3dBundle, motion_blur::MotionBlur}; /// # use bevy_ecs::prelude::*; /// # fn test(mut commands: Commands) { /// commands.spawn(( /// Camera3dBundle::default(), -/// MotionBlurBundle::default(), +/// MotionBlur::default(), /// )); /// # } /// ```` #[derive(Reflect, Component, Clone, ExtractComponent, ShaderType)] #[reflect(Component, Default)] #[extract_component_filter(With)] +#[require(DepthPrepass, MotionVectorPrepass)] pub struct MotionBlur { /// The strength of motion blur from `0.0` to `1.0`. /// diff --git a/crates/bevy_core_pipeline/src/taa/mod.rs b/crates/bevy_core_pipeline/src/taa/mod.rs index 3ca33a21d6502..f3895d1e26241 100644 --- a/crates/bevy_core_pipeline/src/taa/mod.rs +++ b/crates/bevy_core_pipeline/src/taa/mod.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] + use crate::{ core_3d::graph::{Core3d, Node3d}, fullscreen_vertex_shader::fullscreen_shader_vertex_state, @@ -88,6 +90,10 @@ impl Plugin for TemporalAntiAliasPlugin { /// Bundle to apply temporal anti-aliasing. #[derive(Bundle, Default, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `TemporalAntiAlias` component instead. Inserting it will now also insert the other components required by it automatically." +)] pub struct TemporalAntiAliasBundle { pub settings: TemporalAntiAliasing, pub jitter: TemporalJitter, @@ -119,25 +125,24 @@ pub struct TemporalAntiAliasBundle { /// /// # Usage Notes /// +/// The [`TemporalAntiAliasPlugin`] must be added to your app. /// Any camera with this component must also disable [`Msaa`] by setting it to [`Msaa::Off`]. /// -/// Requires that you add [`TemporalAntiAliasPlugin`] to your app, -/// and add the [`DepthPrepass`], [`MotionVectorPrepass`], and [`TemporalJitter`] -/// components to your camera. -/// -/// [Currently](https://github.com/bevyengine/bevy/issues/8423) cannot be used with [`bevy_render::camera::OrthographicProjection`]. +/// [Currently](https://github.com/bevyengine/bevy/issues/8423), TAA cannot be used with [`bevy_render::camera::OrthographicProjection`]. /// -/// Does not work well with alpha-blended meshes as it requires depth writing to determine motion. +/// TAA also does not work well with alpha-blended meshes, as it requires depth writing to determine motion. /// /// It is very important that correct motion vectors are written for everything on screen. /// Failure to do so will lead to ghosting artifacts. For instance, if particle effects /// are added using a third party library, the library must either: +/// /// 1. Write particle motion vectors to the motion vectors prepass texture /// 2. Render particles after TAA /// -/// If no [`MipBias`] component is attached to the camera, TAA will add a MipBias(-1.0) component. +/// If no [`MipBias`] component is attached to the camera, TAA will add a `MipBias(-1.0)` component. #[derive(Component, Reflect, Clone)] #[reflect(Component, Default)] +#[require(TemporalJitter, DepthPrepass, MotionVectorPrepass)] #[doc(alias = "Taa")] pub struct TemporalAntiAliasing { /// Set to true to delete the saved temporal history (past frames). diff --git a/crates/bevy_derive/src/bevy_main.rs b/crates/bevy_derive/src/bevy_main.rs index d4504331a6467..8111a31338b56 100644 --- a/crates/bevy_derive/src/bevy_main.rs +++ b/crates/bevy_derive/src/bevy_main.rs @@ -12,8 +12,8 @@ pub fn bevy_main(_attr: TokenStream, item: TokenStream) -> TokenStream { TokenStream::from(quote! { #[no_mangle] #[cfg(target_os = "android")] - fn android_main(android_app: bevy::winit::android_activity::AndroidApp) { - let _ = bevy::winit::ANDROID_APP.set(android_app); + fn android_main(android_app: bevy::window::android_activity::AndroidApp) { + let _ = bevy::window::ANDROID_APP.set(android_app); main(); } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index c735bfea59226..10f794075466c 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -376,6 +376,10 @@ pub fn impl_param_set(_input: TokenStream) -> TokenStream { <(#(#param,)*) as SystemParam>::apply(state, system_meta, world); } + fn queue(state: &mut Self::State, system_meta: &SystemMeta, mut world: DeferredWorld) { + <(#(#param,)*) as SystemParam>::queue(state, system_meta, world.reborrow()); + } + #[inline] unsafe fn validate_param<'w, 's>( state: &'s Self::State, diff --git a/crates/bevy_ecs/src/bundle.rs b/crates/bevy_ecs/src/bundle.rs index 7a23ac3f11e06..446eb30921225 100644 --- a/crates/bevy_ecs/src/bundle.rs +++ b/crates/bevy_ecs/src/bundle.rs @@ -287,6 +287,7 @@ macro_rules! tuple_impl { } } + $(#[$meta])* impl<$($name: Bundle),*> DynamicBundle for ($($name,)*) { #[allow(unused_variables, unused_mut)] #[inline(always)] diff --git a/crates/bevy_ecs/src/entity/map_entities.rs b/crates/bevy_ecs/src/entity/map_entities.rs index 5bc0be1725774..5b0de2359d6b8 100644 --- a/crates/bevy_ecs/src/entity/map_entities.rs +++ b/crates/bevy_ecs/src/entity/map_entities.rs @@ -15,9 +15,16 @@ use super::{EntityHashMap, VisitEntitiesMut}; /// (usually by using an [`EntityHashMap`] between source entities and entities in the /// current world). /// +/// This trait is similar to [`VisitEntitiesMut`]. They differ in that [`VisitEntitiesMut`] operates +/// on `&mut Entity` and allows for in-place modification, while this trait makes no assumption that +/// such in-place modification is occurring, which is impossible for types such as [`HashSet`] +/// and [`EntityHashMap`] which must be rebuilt when their contained [`Entity`]s are remapped. +/// /// Implementing this trait correctly is required for properly loading components /// with entity references from scenes. /// +/// [`HashSet`]: bevy_utils::HashSet +/// /// ## Example /// /// ``` @@ -60,9 +67,6 @@ impl MapEntities for T { /// /// More generally, this can be used to map [`Entity`] references between any two [`Worlds`](World). /// -/// Note that this trait is _not_ [object safe](https://doc.rust-lang.org/reference/items/traits.html#object-safety). -/// Please see [`DynEntityMapper`] for an object safe alternative. -/// /// ## Example /// /// ``` @@ -79,64 +83,16 @@ impl MapEntities for T { /// fn map_entity(&mut self, entity: Entity) -> Entity { /// self.map.get(&entity).copied().unwrap_or(entity) /// } -/// -/// fn mappings(&self) -> impl Iterator { -/// self.map.iter().map(|(&source, &target)| (source, target)) -/// } /// } /// ``` pub trait EntityMapper { /// Map an entity to another entity fn map_entity(&mut self, entity: Entity) -> Entity; - - /// Iterate over all entity to entity mappings. - /// - /// # Examples - /// - /// ```rust - /// # use bevy_ecs::entity::{Entity, EntityMapper}; - /// # fn example(mapper: impl EntityMapper) { - /// for (source, target) in mapper.mappings() { - /// println!("Will map from {source} to {target}"); - /// } - /// # } - /// ``` - fn mappings(&self) -> impl Iterator; -} - -/// An [object safe](https://doc.rust-lang.org/reference/items/traits.html#object-safety) version -/// of [`EntityMapper`]. This trait is automatically implemented for type that implements `EntityMapper`. -pub trait DynEntityMapper { - /// Map an entity to another entity. - /// - /// This is an [object safe](https://doc.rust-lang.org/reference/items/traits.html#object-safety) - /// alternative to [`EntityMapper::map_entity`]. - fn dyn_map_entity(&mut self, entity: Entity) -> Entity; - - /// Iterate over all entity to entity mappings. - /// - /// This is an [object safe](https://doc.rust-lang.org/reference/items/traits.html#object-safety) - /// alternative to [`EntityMapper::mappings`]. - fn dyn_mappings(&self) -> Vec<(Entity, Entity)>; -} - -impl DynEntityMapper for T { - fn dyn_map_entity(&mut self, entity: Entity) -> Entity { - ::map_entity(self, entity) - } - - fn dyn_mappings(&self) -> Vec<(Entity, Entity)> { - ::mappings(self).collect() - } } -impl<'a> EntityMapper for &'a mut dyn DynEntityMapper { +impl EntityMapper for &mut dyn EntityMapper { fn map_entity(&mut self, entity: Entity) -> Entity { - (*self).dyn_map_entity(entity) - } - - fn mappings(&self) -> impl Iterator { - (*self).dyn_mappings().into_iter() + (*self).map_entity(entity) } } @@ -160,10 +116,6 @@ impl EntityMapper for SceneEntityMapper<'_> { new } - - fn mappings(&self) -> impl Iterator { - self.map.iter().map(|(&source, &target)| (source, target)) - } } /// A wrapper for [`EntityHashMap`], augmenting it with the ability to allocate new [`Entity`] references in a destination @@ -242,10 +194,9 @@ impl<'m> SceneEntityMapper<'m> { #[cfg(test)] mod tests { use crate::{ - entity::{DynEntityMapper, Entity, EntityHashMap, EntityMapper, SceneEntityMapper}, + entity::{Entity, EntityHashMap, EntityMapper, SceneEntityMapper}, world::World, }; - use bevy_utils::assert_object_safe; #[test] fn entity_mapper() { @@ -292,26 +243,6 @@ mod tests { assert!(entity.generation() > dead_ref.generation()); } - #[test] - fn entity_mapper_iteration() { - let mut old_world = World::new(); - let mut new_world = World::new(); - - let mut map = EntityHashMap::default(); - let mut mapper = SceneEntityMapper::new(&mut map, &mut new_world); - - assert_eq!(mapper.mappings().collect::>(), vec![]); - - let old_entity = old_world.spawn_empty().id(); - - let new_entity = mapper.map_entity(old_entity); - - assert_eq!( - mapper.mappings().collect::>(), - vec![(old_entity, new_entity)] - ); - } - #[test] fn entity_mapper_no_panic() { let mut world = World::new(); @@ -328,9 +259,4 @@ mod tests { // The SceneEntityMapper should leave `Entities` in a flushed state. assert!(!world.entities.needs_flush()); } - - #[test] - fn dyn_entity_mapper_object_safe() { - assert_object_safe::(); - } } diff --git a/crates/bevy_ecs/src/observer/mod.rs b/crates/bevy_ecs/src/observer/mod.rs index 41ba7f5289235..c74d78ae0318f 100644 --- a/crates/bevy_ecs/src/observer/mod.rs +++ b/crates/bevy_ecs/src/observer/mod.rs @@ -1211,4 +1211,27 @@ mod tests { assert!(world.get_resource::().is_none()); } + + #[test] + fn observer_apply_deferred_from_param_set() { + #[derive(Event)] + struct EventA; + + #[derive(Resource)] + struct ResA; + + let mut world = World::new(); + world.observe( + |_: Trigger, mut params: ParamSet<(Query, Commands)>| { + params.p1().insert_resource(ResA); + }, + ); + // TODO: ideally this flush is not necessary, but right now observe() returns WorldEntityMut + // and therefore does not automatically flush. + world.flush(); + world.trigger(EventA); + world.flush(); + + assert!(world.get_resource::().is_some()); + } } diff --git a/crates/bevy_ecs/src/query/fetch.rs b/crates/bevy_ecs/src/query/fetch.rs index 18a2ecda08155..17d92fc97930a 100644 --- a/crates/bevy_ecs/src/query/fetch.rs +++ b/crates/bevy_ecs/src/query/fetch.rs @@ -2014,7 +2014,6 @@ pub struct AnyOf(PhantomData); macro_rules! impl_tuple_query_data { ($(#[$meta:meta])* $(($name: ident, $state: ident)),*) => { - #[allow(non_snake_case)] #[allow(clippy::unused_unit)] $(#[$meta])* @@ -2023,6 +2022,7 @@ macro_rules! impl_tuple_query_data { type ReadOnly = ($($name::ReadOnly,)*); } + $(#[$meta])* /// SAFETY: each item in the tuple is read only unsafe impl<$($name: ReadOnlyQueryData),*> ReadOnlyQueryData for ($($name,)*) {} diff --git a/crates/bevy_ecs/src/reflect/map_entities.rs b/crates/bevy_ecs/src/reflect/map_entities.rs index dcf8c3ae86f30..49c018f50e00c 100644 --- a/crates/bevy_ecs/src/reflect/map_entities.rs +++ b/crates/bevy_ecs/src/reflect/map_entities.rs @@ -1,107 +1,37 @@ -use crate::{ - component::Component, - entity::{Entity, EntityHashMap, MapEntities, SceneEntityMapper}, - world::World, -}; -use bevy_reflect::FromType; +use crate::entity::{EntityMapper, MapEntities}; +use bevy_reflect::{FromReflect, FromType, PartialReflect}; -/// For a specific type of component, this maps any fields with values of type [`Entity`] to a new world. +/// For a specific type of value, this maps any fields with values of type [`Entity`] to a new world. /// /// Since a given `Entity` ID is only valid for the world it came from, when performing deserialization /// any stored IDs need to be re-allocated in the destination world. /// -/// See [`SceneEntityMapper`] and [`MapEntities`] for more information. +/// See [`EntityMapper`] and [`MapEntities`] for more information. +/// +/// [`Entity`]: crate::entity::Entity +/// [`EntityMapper`]: crate::entity::EntityMapper #[derive(Clone)] pub struct ReflectMapEntities { - map_all_entities: fn(&mut World, &mut SceneEntityMapper), - map_entities: fn(&mut World, &mut SceneEntityMapper, &[Entity]), + map_entities: fn(&mut dyn PartialReflect, &mut dyn EntityMapper), } impl ReflectMapEntities { - /// A general method for applying [`MapEntities`] behavior to all elements in an [`EntityHashMap`]. - /// - /// Be mindful in its usage: Works best in situations where the entities in the [`EntityHashMap`] are newly - /// created, before systems have a chance to add new components. If some of the entities referred to - /// by the [`EntityHashMap`] might already contain valid entity references, you should use [`map_entities`](Self::map_entities). - /// - /// An example of this: A scene can be loaded with `Parent` components, but then a `Parent` component can be added - /// to these entities after they have been loaded. If you reload the scene using [`map_all_entities`](Self::map_all_entities), those `Parent` - /// components with already valid entity references could be updated to point at something else entirely. - pub fn map_all_entities(&self, world: &mut World, entity_map: &mut EntityHashMap) { - SceneEntityMapper::world_scope(entity_map, world, self.map_all_entities); - } - - /// A general method for applying [`MapEntities`] behavior to elements in an [`EntityHashMap`]. Unlike - /// [`map_all_entities`](Self::map_all_entities), this is applied to specific entities, not all values - /// in the [`EntityHashMap`]. + /// A general method for remapping entities in a reflected value via an [`EntityMapper`]. /// - /// This is useful mostly for when you need to be careful not to update components that already contain valid entity - /// values. See [`map_all_entities`](Self::map_all_entities) for more details. - pub fn map_entities( - &self, - world: &mut World, - entity_map: &mut EntityHashMap, - entities: &[Entity], - ) { - SceneEntityMapper::world_scope(entity_map, world, |world, mapper| { - (self.map_entities)(world, mapper, entities); - }); + /// # Panics + /// Panics if the the type of the reflected value doesn't match. + pub fn map_entities(&self, reflected: &mut dyn PartialReflect, mapper: &mut dyn EntityMapper) { + (self.map_entities)(reflected, mapper); } } -impl FromType for ReflectMapEntities { +impl FromType for ReflectMapEntities { fn from_type() -> Self { ReflectMapEntities { - map_entities: |world, entity_mapper, entities| { - for &entity in entities { - if let Some(mut component) = world.get_mut::(entity) { - component.map_entities(entity_mapper); - } - } - }, - map_all_entities: |world, entity_mapper| { - let entities = entity_mapper - .get_map() - .values() - .copied() - .collect::>(); - for entity in &entities { - if let Some(mut component) = world.get_mut::(*entity) { - component.map_entities(entity_mapper); - } - } - }, - } - } -} - -/// For a specific type of resource, this maps any fields with values of type [`Entity`] to a new world. -/// -/// Since a given `Entity` ID is only valid for the world it came from, when performing deserialization -/// any stored IDs need to be re-allocated in the destination world. -/// -/// See [`SceneEntityMapper`] and [`MapEntities`] for more information. -#[derive(Clone)] -pub struct ReflectMapEntitiesResource { - map_entities: fn(&mut World, &mut SceneEntityMapper), -} - -impl ReflectMapEntitiesResource { - /// A method for applying [`MapEntities`] behavior to elements in an [`EntityHashMap`]. - pub fn map_entities(&self, world: &mut World, entity_map: &mut EntityHashMap) { - SceneEntityMapper::world_scope(entity_map, world, |world, mapper| { - (self.map_entities)(world, mapper); - }); - } -} - -impl FromType for ReflectMapEntitiesResource { - fn from_type() -> Self { - ReflectMapEntitiesResource { - map_entities: |world, entity_mapper| { - if let Some(mut resource) = world.get_resource_mut::() { - resource.map_entities(entity_mapper); - } + map_entities: |reflected, mut mapper| { + let mut concrete = C::from_reflect(reflected).expect("reflected type should match"); + concrete.map_entities(&mut mapper); + reflected.apply(&concrete); }, } } diff --git a/crates/bevy_ecs/src/reflect/mod.rs b/crates/bevy_ecs/src/reflect/mod.rs index 8b7f0ada6edb4..ba27538d2ade9 100644 --- a/crates/bevy_ecs/src/reflect/mod.rs +++ b/crates/bevy_ecs/src/reflect/mod.rs @@ -24,7 +24,7 @@ pub use bundle::{ReflectBundle, ReflectBundleFns}; pub use component::{ReflectComponent, ReflectComponentFns}; pub use entity_commands::ReflectCommandExt; pub use from_world::{ReflectFromWorld, ReflectFromWorldFns}; -pub use map_entities::{ReflectMapEntities, ReflectMapEntitiesResource}; +pub use map_entities::ReflectMapEntities; pub use resource::{ReflectResource, ReflectResourceFns}; pub use visit_entities::{ReflectVisitEntities, ReflectVisitEntitiesMut}; diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index 51ab7d3552fc9..d814f180e5398 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -390,7 +390,9 @@ impl<'w, 's> Commands<'w, 's> { /// - [`spawn_batch`](Self::spawn_batch) to spawn entities with a bundle each. #[track_caller] pub fn spawn(&mut self, bundle: T) -> EntityCommands { - self.spawn_empty().insert(bundle) + let mut entity = self.spawn_empty(); + entity.insert(bundle); + entity } /// Returns the [`EntityCommands`] for the requested [`Entity`]. @@ -1043,7 +1045,7 @@ impl EntityCommands<'_> { /// # bevy_ecs::system::assert_is_system(add_combat_stats_system); /// ``` #[track_caller] - pub fn insert(self, bundle: impl Bundle) -> Self { + pub fn insert(&mut self, bundle: impl Bundle) -> &mut Self { self.queue(insert(bundle, InsertMode::Replace)) } @@ -1077,7 +1079,7 @@ impl EntityCommands<'_> { /// # bevy_ecs::system::assert_is_system(add_health_system); /// ``` #[track_caller] - pub fn insert_if(self, bundle: impl Bundle, condition: F) -> Self + pub fn insert_if(&mut self, bundle: impl Bundle, condition: F) -> &mut Self where F: FnOnce() -> bool, { @@ -1102,7 +1104,7 @@ impl EntityCommands<'_> { /// The command will panic when applied if the associated entity does not exist. /// /// To avoid a panic in this case, use the command [`Self::try_insert_if_new`] instead. - pub fn insert_if_new(self, bundle: impl Bundle) -> Self { + pub fn insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { self.queue(insert(bundle, InsertMode::Keep)) } @@ -1120,7 +1122,7 @@ impl EntityCommands<'_> { /// /// To avoid a panic in this case, use the command [`Self::try_insert_if_new`] /// instead. - pub fn insert_if_new_and(self, bundle: impl Bundle, condition: F) -> Self + pub fn insert_if_new_and(&mut self, bundle: impl Bundle, condition: F) -> &mut Self where F: FnOnce() -> bool, { @@ -1147,10 +1149,10 @@ impl EntityCommands<'_> { /// - `T` must have the same layout as the one passed during `component_id` creation. #[track_caller] pub unsafe fn insert_by_id( - self, + &mut self, component_id: ComponentId, value: T, - ) -> Self { + ) -> &mut Self { let caller = Location::caller(); // SAFETY: same invariants as parent call self.queue(unsafe {insert_by_id(component_id, value, move |entity| { @@ -1167,10 +1169,10 @@ impl EntityCommands<'_> { /// - [`ComponentId`] must be from the same world as `self`. /// - `T` must have the same layout as the one passed during `component_id` creation. pub unsafe fn try_insert_by_id( - self, + &mut self, component_id: ComponentId, value: T, - ) -> Self { + ) -> &mut Self { // SAFETY: same invariants as parent call self.queue(unsafe { insert_by_id(component_id, value, |_| {}) }) } @@ -1224,7 +1226,7 @@ impl EntityCommands<'_> { /// # bevy_ecs::system::assert_is_system(add_combat_stats_system); /// ``` #[track_caller] - pub fn try_insert(self, bundle: impl Bundle) -> Self { + pub fn try_insert(&mut self, bundle: impl Bundle) -> &mut Self { self.queue(try_insert(bundle, InsertMode::Replace)) } @@ -1255,7 +1257,7 @@ impl EntityCommands<'_> { /// # bevy_ecs::system::assert_is_system(add_health_system); /// ``` #[track_caller] - pub fn try_insert_if(self, bundle: impl Bundle, condition: F) -> Self + pub fn try_insert_if(&mut self, bundle: impl Bundle, condition: F) -> &mut Self where F: FnOnce() -> bool, { @@ -1301,7 +1303,7 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(add_health_system); /// ``` - pub fn try_insert_if_new_and(self, bundle: impl Bundle, condition: F) -> Self + pub fn try_insert_if_new_and(&mut self, bundle: impl Bundle, condition: F) -> &mut Self where F: FnOnce() -> bool, { @@ -1321,7 +1323,7 @@ impl EntityCommands<'_> { /// # Note /// /// Unlike [`Self::insert_if_new`], this will not panic if the associated entity does not exist. - pub fn try_insert_if_new(self, bundle: impl Bundle) -> Self { + pub fn try_insert_if_new(&mut self, bundle: impl Bundle) -> &mut Self { self.queue(try_insert(bundle, InsertMode::Keep)) } @@ -1360,7 +1362,7 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` - pub fn remove(self) -> Self + pub fn remove(&mut self) -> &mut Self where T: Bundle, { @@ -1368,12 +1370,12 @@ impl EntityCommands<'_> { } /// Removes a component from the entity. - pub fn remove_by_id(self, component_id: ComponentId) -> Self { + pub fn remove_by_id(&mut self, component_id: ComponentId) -> &mut Self { self.queue(remove_by_id(component_id)) } /// Removes all components associated with the entity. - pub fn clear(self) -> Self { + pub fn clear(&mut self) -> &mut Self { self.queue(clear()) } @@ -1405,8 +1407,8 @@ impl EntityCommands<'_> { /// # bevy_ecs::system::assert_is_system(remove_character_system); /// ``` #[track_caller] - pub fn despawn(self) -> Self { - self.queue(despawn()) + pub fn despawn(&mut self) { + self.queue(despawn()); } /// Despawns the entity. @@ -1433,8 +1435,7 @@ impl EntityCommands<'_> { /// # } /// # bevy_ecs::system::assert_is_system(my_system); /// ``` - #[allow(clippy::should_implement_trait)] - pub fn queue(mut self, command: impl EntityCommand) -> Self { + pub fn queue(&mut self, command: impl EntityCommand) -> &mut Self { self.commands.queue(command.with_entity(self.entity)); self } @@ -1476,7 +1477,7 @@ impl EntityCommands<'_> { /// } /// # bevy_ecs::system::assert_is_system(remove_combat_stats_system); /// ``` - pub fn retain(self) -> Self + pub fn retain(&mut self) -> &mut Self where T: Bundle, { @@ -1488,7 +1489,7 @@ impl EntityCommands<'_> { /// # Panics /// /// The command will panic when applied if the associated entity does not exist. - pub fn log_components(self) -> Self { + pub fn log_components(&mut self) -> &mut Self { self.queue(log_components) } @@ -1501,13 +1502,16 @@ impl EntityCommands<'_> { /// watches this entity. /// /// [`Trigger`]: crate::observer::Trigger - pub fn trigger(mut self, event: impl Event) -> Self { + pub fn trigger(&mut self, event: impl Event) -> &mut Self { self.commands.trigger_targets(event, self.entity); self } /// Creates an [`Observer`] listening for a trigger of type `T` that targets this entity. - pub fn observe(self, system: impl IntoObserverSystem) -> Self { + pub fn observe( + &mut self, + system: impl IntoObserverSystem, + ) -> &mut Self { self.queue(observe(system)) } } @@ -1520,9 +1524,8 @@ pub struct EntityEntryCommands<'a, T> { impl<'a, T: Component> EntityEntryCommands<'a, T> { /// Modify the component `T` if it exists, using the the function `modify`. - pub fn and_modify(mut self, modify: impl FnOnce(Mut) + Send + Sync + 'static) -> Self { - self.entity_commands = self - .entity_commands + pub fn and_modify(&mut self, modify: impl FnOnce(Mut) + Send + Sync + 'static) -> &mut Self { + self.entity_commands .queue(move |mut entity: EntityWorldMut| { if let Some(value) = entity.get_mut() { modify(value); @@ -1540,9 +1543,8 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// Panics if the entity does not exist. /// See [`or_try_insert`](Self::or_try_insert) for a non-panicking version. #[track_caller] - pub fn or_insert(mut self, default: T) -> Self { - self.entity_commands = self - .entity_commands + pub fn or_insert(&mut self, default: T) -> &mut Self { + self.entity_commands .queue(insert(default, InsertMode::Keep)); self } @@ -1553,9 +1555,8 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// /// See also [`or_insert_with`](Self::or_insert_with). #[track_caller] - pub fn or_try_insert(mut self, default: T) -> Self { - self.entity_commands = self - .entity_commands + pub fn or_try_insert(&mut self, default: T) -> &mut Self { + self.entity_commands .queue(try_insert(default, InsertMode::Keep)); self } @@ -1569,7 +1570,7 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// Panics if the entity does not exist. /// See [`or_try_insert_with`](Self::or_try_insert_with) for a non-panicking version. #[track_caller] - pub fn or_insert_with(self, default: impl Fn() -> T) -> Self { + pub fn or_insert_with(&mut self, default: impl Fn() -> T) -> &mut Self { self.or_insert(default()) } @@ -1579,7 +1580,7 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// /// See also [`or_insert`](Self::or_insert) and [`or_try_insert`](Self::or_try_insert). #[track_caller] - pub fn or_try_insert_with(self, default: impl Fn() -> T) -> Self { + pub fn or_try_insert_with(&mut self, default: impl Fn() -> T) -> &mut Self { self.or_try_insert(default()) } @@ -1591,7 +1592,7 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// /// Panics if the entity does not exist. #[track_caller] - pub fn or_default(self) -> Self + pub fn or_default(&mut self) -> &mut Self where T: Default, { @@ -1608,12 +1609,11 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> { /// /// Panics if the entity does not exist. #[track_caller] - pub fn or_from_world(mut self) -> Self + pub fn or_from_world(&mut self) -> &mut Self where T: FromWorld, { - self.entity_commands = self - .entity_commands + self.entity_commands .queue(insert_from_world::(InsertMode::Keep)); self } diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index 480f16868bcfc..4b5508190acf1 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -1,3 +1,5 @@ +#[cfg(feature = "bevy_reflect")] +use crate::reflect::ReflectComponent; use crate::{ self as bevy_ecs, bundle::Bundle, @@ -7,6 +9,8 @@ use crate::{ world::{Command, World}, }; use bevy_ecs_macros::{Component, Resource}; +#[cfg(feature = "bevy_reflect")] +use bevy_reflect::Reflect; use core::marker::PhantomData; use thiserror::Error; @@ -19,6 +23,8 @@ struct RegisteredSystem { /// Marker [`Component`](bevy_ecs::component::Component) for identifying [`SystemId`] [`Entity`]s. #[derive(Component)] +#[cfg_attr(feature = "bevy_reflect", derive(Reflect))] +#[cfg_attr(feature = "bevy_reflect", reflect(Component))] pub struct SystemIdMarker; /// A system that has been removed from the registry. diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index 3eb20be577209..023db78f5dbdd 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -10,10 +10,11 @@ use crate::{ removal_detection::RemovedComponentEvents, storage::Storages, system::IntoObserverSystem, - world::{DeferredWorld, Mut, World}, + world::{error::EntityComponentError, DeferredWorld, Mut, World}, }; use bevy_ptr::{OwningPtr, Ptr}; -use core::{any::TypeId, marker::PhantomData}; +use bevy_utils::{HashMap, HashSet}; +use core::{any::TypeId, marker::PhantomData, mem::MaybeUninit}; use thiserror::Error; use super::{unsafe_world_cell::UnsafeEntityCell, Ref, ON_REMOVE, ON_REPLACE}; @@ -143,18 +144,117 @@ impl<'w> EntityRef<'w> { unsafe { self.0.get_change_ticks_by_id(component_id) } } - /// Gets the component of the given [`ComponentId`] from the entity. + /// Returns [untyped read-only reference(s)](Ptr) to component(s) for the + /// current entity, based on the given [`ComponentId`]s. /// - /// **You should prefer to use the typed API where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityRef::get`] where + /// possible and only use this in cases where the actual component types + /// are not known at compile time.** + /// + /// Unlike [`EntityRef::get`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors + /// + /// Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// + /// # Examples + /// + /// ## Single [`ComponentId`] + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Foo(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn(Foo(42)).id(); + /// + /// // Grab the component ID for `Foo` in whatever way you like. + /// let component_id = world.register_component::(); + /// + /// // Then, get the component by ID. + /// let ptr = world.entity(entity).get_by_id(component_id); + /// # assert_eq!(unsafe { ptr.unwrap().deref::() }, &Foo(42)); + /// ``` + /// + /// ## Array of [`ComponentId`]s + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct X(i32); + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Y(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn((X(42), Y(10))).id(); + /// + /// // Grab the component IDs for `X` and `Y` in whatever way you like. + /// let x_id = world.register_component::(); + /// let y_id = world.register_component::(); + /// + /// // Then, get the components by ID. You'll receive a same-sized array. + /// let Ok([x_ptr, y_ptr]) = world.entity(entity).get_by_id([x_id, y_id]) else { + /// // Up to you to handle if a component is missing from the entity. + /// # unreachable!(); + /// }; + /// # assert_eq!((unsafe { x_ptr.deref::() }, unsafe { y_ptr.deref::() }), (&X(42), &Y(10))); + /// ``` + /// + /// ## Slice of [`ComponentId`]s + /// + /// ``` + /// # use bevy_ecs::{prelude::*, component::ComponentId}; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct X(i32); + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Y(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn((X(42), Y(10))).id(); + /// + /// // Grab the component IDs for `X` and `Y` in whatever way you like. + /// let x_id = world.register_component::(); + /// let y_id = world.register_component::(); /// - /// Unlike [`EntityRef::get`], this returns a raw pointer to the component, - /// which is only valid while the `'w` borrow of the lifetime is active. + /// // Then, get the components by ID. You'll receive a vec of ptrs. + /// let ptrs = world.entity(entity).get_by_id(&[x_id, y_id] as &[ComponentId]); + /// # let ptrs = ptrs.unwrap(); + /// # assert_eq!((unsafe { ptrs[0].deref::() }, unsafe { ptrs[1].deref::() }), (&X(42), &Y(10))); + /// ``` + /// + /// ## [`HashSet`] of [`ComponentId`]s + /// + /// ``` + /// # use bevy_utils::HashSet; + /// # use bevy_ecs::{prelude::*, component::ComponentId}; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct X(i32); + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Y(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn((X(42), Y(10))).id(); + /// + /// // Grab the component IDs for `X` and `Y` in whatever way you like. + /// let x_id = world.register_component::(); + /// let y_id = world.register_component::(); + /// + /// // Then, get the components by ID. You'll receive a vec of ptrs. + /// let ptrs = world.entity(entity).get_by_id(&HashSet::from_iter([x_id, y_id])); + /// # let ptrs = ptrs.unwrap(); + /// # assert_eq!((unsafe { ptrs[&x_id].deref::() }, unsafe { ptrs[&y_id].deref::() }), (&X(42), &Y(10))); + /// ``` #[inline] - pub fn get_by_id(&self, component_id: ComponentId) -> Option> { + pub fn get_by_id( + &self, + component_ids: F, + ) -> Result, EntityComponentError> { // SAFETY: We have read-only access to all components of this entity. - unsafe { self.0.get_by_id(component_id) } + unsafe { component_ids.fetch_ref(self.0) } } /// Returns read-only components for the current entity that match the query `Q`. @@ -448,59 +548,219 @@ impl<'w> EntityMut<'w> { self.as_readonly().get_change_ticks_by_id(component_id) } - /// Gets the component of the given [`ComponentId`] from the entity. + /// Returns [untyped read-only reference(s)](Ptr) to component(s) for the + /// current entity, based on the given [`ComponentId`]s. /// - /// **You should prefer to use the typed API [`EntityWorldMut::get`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityMut::get`] where + /// possible and only use this in cases where the actual component types + /// are not known at compile time.** + /// + /// Unlike [`EntityMut::get`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors /// - /// Unlike [`EntityMut::get`], this returns a raw pointer to the component, - /// which is only valid while the [`EntityMut`] is alive. + /// Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// + /// # Examples + /// + /// For examples on how to use this method, see [`EntityRef::get_by_id`]. #[inline] - pub fn get_by_id(&self, component_id: ComponentId) -> Option> { - self.as_readonly().get_by_id(component_id) + pub fn get_by_id( + &self, + component_ids: F, + ) -> Result, EntityComponentError> { + self.as_readonly().get_by_id(component_ids) } - /// Consumes `self` and gets the component of the given [`ComponentId`] with - /// world `'w` lifetime from the entity. + /// Consumes `self` and returns [untyped read-only reference(s)](Ptr) to + /// component(s) with lifetime `'w` for the current entity, based on the + /// given [`ComponentId`]s. /// - /// **You should prefer to use the typed API [`EntityWorldMut::into_borrow`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityMut::into_borrow`] + /// where possible and only use this in cases where the actual component + /// types are not known at compile time.** + /// + /// Unlike [`EntityMut::into_borrow`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors + /// + /// Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// + /// # Examples + /// + /// For examples on how to use this method, see [`EntityRef::get_by_id`]. #[inline] - pub fn into_borrow_by_id(self, component_id: ComponentId) -> Option> { + pub fn into_borrow_by_id( + self, + component_ids: F, + ) -> Result, EntityComponentError> { // SAFETY: - // consuming `self` ensures that no references exist to this entity's components. - unsafe { self.0.get_by_id(component_id) } + // - We have read-only access to all components of this entity. + // - consuming `self` ensures that no references exist to this entity's components. + unsafe { component_ids.fetch_ref(self.0) } } - /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. + /// Returns [untyped mutable reference(s)](MutUntyped) to component(s) for + /// the current entity, based on the given [`ComponentId`]s. /// - /// **You should prefer to use the typed API [`EntityMut::get_mut`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityMut::get_mut`] where + /// possible and only use this in cases where the actual component types + /// are not known at compile time.** + /// + /// Unlike [`EntityMut::get_mut`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors + /// + /// - Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// - Returns [`EntityComponentError::AliasedMutability`] if a component + /// is requested multiple times. + /// + /// # Examples + /// + /// ## Single [`ComponentId`] + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Foo(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn(Foo(42)).id(); + /// + /// // Grab the component ID for `Foo` in whatever way you like. + /// let component_id = world.register_component::(); + /// + /// // Then, get the component by ID. + /// let mut entity_mut = world.entity_mut(entity); + /// let mut ptr = entity_mut.get_mut_by_id(component_id) + /// # .unwrap(); + /// # assert_eq!(unsafe { ptr.as_mut().deref_mut::() }, &mut Foo(42)); + /// ``` + /// + /// ## Array of [`ComponentId`]s + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct X(i32); + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Y(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn((X(42), Y(10))).id(); + /// + /// // Grab the component IDs for `X` and `Y` in whatever way you like. + /// let x_id = world.register_component::(); + /// let y_id = world.register_component::(); + /// + /// // Then, get the components by ID. You'll receive a same-sized array. + /// let mut entity_mut = world.entity_mut(entity); + /// let Ok([mut x_ptr, mut y_ptr]) = entity_mut.get_mut_by_id([x_id, y_id]) else { + /// // Up to you to handle if a component is missing from the entity. + /// # unreachable!(); + /// }; + /// # assert_eq!((unsafe { x_ptr.as_mut().deref_mut::() }, unsafe { y_ptr.as_mut().deref_mut::() }), (&mut X(42), &mut Y(10))); + /// ``` + /// + /// ## Slice of [`ComponentId`]s + /// + /// ``` + /// # use bevy_ecs::{prelude::*, component::ComponentId, change_detection::MutUntyped}; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct X(i32); + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Y(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn((X(42), Y(10))).id(); + /// + /// // Grab the component IDs for `X` and `Y` in whatever way you like. + /// let x_id = world.register_component::(); + /// let y_id = world.register_component::(); + /// + /// // Then, get the components by ID. You'll receive a vec of ptrs. + /// let mut entity_mut = world.entity_mut(entity); + /// let ptrs = entity_mut.get_mut_by_id(&[x_id, y_id] as &[ComponentId]) + /// # .unwrap(); + /// # let [mut x_ptr, mut y_ptr]: [MutUntyped; 2] = ptrs.try_into().unwrap(); + /// # assert_eq!((unsafe { x_ptr.as_mut().deref_mut::() }, unsafe { y_ptr.as_mut().deref_mut::() }), (&mut X(42), &mut Y(10))); + /// ``` /// - /// Unlike [`EntityMut::get_mut`], this returns a raw pointer to the component, - /// which is only valid while the [`EntityMut`] is alive. + /// ## [`HashSet`] of [`ComponentId`]s + /// + /// ``` + /// # use bevy_utils::HashSet; + /// # use bevy_ecs::{prelude::*, component::ComponentId}; + /// # + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct X(i32); + /// # #[derive(Component, PartialEq, Debug)] + /// # pub struct Y(i32); + /// # let mut world = World::new(); + /// let entity = world.spawn((X(42), Y(10))).id(); + /// + /// // Grab the component IDs for `X` and `Y` in whatever way you like. + /// let x_id = world.register_component::(); + /// let y_id = world.register_component::(); + /// + /// // Then, get the components by ID. You'll receive a `HashMap` of ptrs. + /// let mut entity_mut = world.entity_mut(entity); + /// let mut ptrs = entity_mut.get_mut_by_id(&HashSet::from_iter([x_id, y_id])) + /// # .unwrap(); + /// # let [mut x_ptr, mut y_ptr] = ptrs.get_many_mut([&x_id, &y_id]).unwrap(); + /// # assert_eq!((unsafe { x_ptr.as_mut().deref_mut::() }, unsafe { y_ptr.as_mut().deref_mut::() }), (&mut X(42), &mut Y(10))); + /// ``` #[inline] - pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + pub fn get_mut_by_id( + &mut self, + component_ids: F, + ) -> Result, EntityComponentError> { // SAFETY: // - `&mut self` ensures that no references exist to this entity's components. - // - `as_unsafe_world_cell` gives mutable permission for all components on this entity - unsafe { self.0.get_mut_by_id(component_id) } + // - We have exclusive access to all components of this entity. + unsafe { component_ids.fetch_mut(self.0) } } - /// Consumes `self` and gets a [`MutUntyped<'w>`] of the component of the given [`ComponentId`] - /// with world `'w` lifetime from the entity. + /// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped) + /// to component(s) with lifetime `'w` for the current entity, based on the + /// given [`ComponentId`]s. /// - /// **You should prefer to use the typed API [`EntityMut::into_mut`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityMut::into_mut`] where + /// possible and only use this in cases where the actual component types + /// are not known at compile time.** + /// + /// Unlike [`EntityMut::into_mut`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors + /// + /// - Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// - Returns [`EntityComponentError::AliasedMutability`] if a component + /// is requested multiple times. + /// + /// # Examples + /// + /// For examples on how to use this method, see [`EntityMut::get_mut_by_id`]. #[inline] - pub fn into_mut_by_id(self, component_id: ComponentId) -> Option> { + pub fn into_mut_by_id( + self, + component_ids: F, + ) -> Result, EntityComponentError> { // SAFETY: - // consuming `self` ensures that no references exist to this entity's components. - unsafe { self.0.get_mut_by_id(component_id) } + // - consuming `self` ensures that no references exist to this entity's components. + // - We have exclusive access to all components of this entity. + unsafe { component_ids.fetch_mut(self.0) } } } @@ -761,58 +1021,127 @@ impl<'w> EntityWorldMut<'w> { EntityRef::from(self).get_change_ticks_by_id(component_id) } - /// Gets the component of the given [`ComponentId`] from the entity. + /// Returns [untyped read-only reference(s)](Ptr) to component(s) for the + /// current entity, based on the given [`ComponentId`]s. /// - /// **You should prefer to use the typed API [`EntityWorldMut::get`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityWorldMut::get`] where + /// possible and only use this in cases where the actual component types + /// are not known at compile time.** + /// + /// Unlike [`EntityWorldMut::get`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). /// - /// Unlike [`EntityWorldMut::get`], this returns a raw pointer to the component, - /// which is only valid while the [`EntityWorldMut`] is alive. + /// # Errors + /// + /// Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// + /// # Examples + /// + /// For examples on how to use this method, see [`EntityRef::get_by_id`]. #[inline] - pub fn get_by_id(&self, component_id: ComponentId) -> Option> { - EntityRef::from(self).get_by_id(component_id) + pub fn get_by_id( + &self, + component_ids: F, + ) -> Result, EntityComponentError> { + EntityRef::from(self).get_by_id(component_ids) } - /// Consumes `self` and gets the component of the given [`ComponentId`] with - /// with world `'w` lifetime from the entity. + /// Consumes `self` and returns [untyped read-only reference(s)](Ptr) to + /// component(s) with lifetime `'w` for the current entity, based on the + /// given [`ComponentId`]s. + /// + /// **You should prefer to use the typed API [`EntityWorldMut::into_borrow`] + /// where possible and only use this in cases where the actual component + /// types are not known at compile time.** + /// + /// Unlike [`EntityWorldMut::into_borrow`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors + /// + /// Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. /// - /// **You should prefer to use the typed API [`EntityWorldMut::into_borrow`] where - /// possible and only use this in cases where the actual component types are not - /// known at compile time.** + /// # Examples + /// + /// For examples on how to use this method, see [`EntityRef::get_by_id`]. #[inline] - pub fn into_borrow_by_id(self, component_id: ComponentId) -> Option> { - // SAFETY: consuming `self` implies exclusive access - unsafe { self.into_unsafe_entity_cell().get_by_id(component_id) } + pub fn into_borrow_by_id( + self, + component_ids: F, + ) -> Result, EntityComponentError> { + // SAFETY: + // - We have read-only access to all components of this entity. + // - consuming `self` ensures that no references exist to this entity's components. + unsafe { component_ids.fetch_ref(self.into_unsafe_entity_cell()) } } - /// Gets a [`MutUntyped`] of the component of the given [`ComponentId`] from the entity. + /// Returns [untyped mutable reference(s)](MutUntyped) to component(s) for + /// the current entity, based on the given [`ComponentId`]s. /// - /// **You should prefer to use the typed API [`EntityWorldMut::get_mut`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityWorldMut::get_mut`] where + /// possible and only use this in cases where the actual component types + /// are not known at compile time.** + /// + /// Unlike [`EntityWorldMut::get_mut`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors /// - /// Unlike [`EntityWorldMut::get_mut`], this returns a raw pointer to the component, - /// which is only valid while the [`EntityWorldMut`] is alive. + /// - Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// - Returns [`EntityComponentError::AliasedMutability`] if a component + /// is requested multiple times. + /// + /// # Examples + /// + /// For examples on how to use this method, see [`EntityMut::get_mut_by_id`]. #[inline] - pub fn get_mut_by_id(&mut self, component_id: ComponentId) -> Option> { + pub fn get_mut_by_id( + &mut self, + component_ids: F, + ) -> Result, EntityComponentError> { // SAFETY: // - `&mut self` ensures that no references exist to this entity's components. - // - `as_unsafe_world_cell` gives mutable permission for all components on this entity - unsafe { self.as_unsafe_entity_cell().get_mut_by_id(component_id) } + // - We have exclusive access to all components of this entity. + unsafe { component_ids.fetch_mut(self.as_unsafe_entity_cell()) } } - /// Consumes `self` and gets a [`MutUntyped<'w>`] of the component with the world `'w` lifetime - /// of the given [`ComponentId`] from the entity. + /// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped) + /// to component(s) with lifetime `'w` for the current entity, based on the + /// given [`ComponentId`]s. /// - /// **You should prefer to use the typed API [`EntityWorldMut::into_mut`] where possible and only - /// use this in cases where the actual component types are not known at - /// compile time.** + /// **You should prefer to use the typed API [`EntityWorldMut::into_mut`] where + /// possible and only use this in cases where the actual component types + /// are not known at compile time.** + /// + /// Unlike [`EntityWorldMut::into_mut`], this returns untyped reference(s) to + /// component(s), and it's the job of the caller to ensure the correct + /// type(s) are dereferenced (if necessary). + /// + /// # Errors + /// + /// - Returns [`EntityComponentError::MissingComponent`] if the entity does + /// not have a component. + /// - Returns [`EntityComponentError::AliasedMutability`] if a component + /// is requested multiple times. + /// + /// # Examples + /// + /// For examples on how to use this method, see [`EntityMut::get_mut_by_id`]. #[inline] - pub fn into_mut_by_id(self, component_id: ComponentId) -> Option> { + pub fn into_mut_by_id( + self, + component_ids: F, + ) -> Result, EntityComponentError> { // SAFETY: - // consuming `self` ensures that no references exist to this entity's components. - unsafe { self.into_unsafe_entity_cell().get_mut_by_id(component_id) } + // - consuming `self` ensures that no references exist to this entity's components. + // - We have exclusive access to all components of this entity. + unsafe { component_ids.fetch_mut(self.into_unsafe_entity_cell()) } } /// Adds a [`Bundle`] of components to the entity. @@ -2841,17 +3170,269 @@ pub(crate) unsafe fn take_component<'a>( } } +/// Types that can be used to fetch components from an entity dynamically by +/// [`ComponentId`]s. +/// +/// Provided implementations are: +/// - [`ComponentId`]: Returns a single untyped reference. +/// - `[ComponentId; N]` and `&[ComponentId; N]`: Returns a same-sized array of untyped references. +/// - `&[ComponentId]`: Returns a [`Vec`] of untyped references. +/// - [`&HashSet`](HashSet): Returns a [`HashMap`] of IDs to untyped references. +/// +/// # Performance +/// +/// - The slice and array implementations perform an aliased mutability check in +/// [`DynamicComponentFetch::fetch_mut`] that is `O(N^2)`. +/// - The [`HashSet`] implementation performs no such check as the type itself +/// guarantees unique IDs. +/// - The single [`ComponentId`] 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. +/// - [`DynamicComponentFetch::fetch_ref`] returns only read-only references. +pub unsafe trait DynamicComponentFetch { + /// The read-only reference type returned by [`DynamicComponentFetch::fetch_ref`]. + type Ref<'w>; + + /// The mutable reference type returned by [`DynamicComponentFetch::fetch_mut`]. + type Mut<'w>; + + /// Returns untyped read-only reference(s) to the component(s) with the + /// given [`ComponentId`]s, as determined by `self`. + /// + /// # Safety + /// + /// It is the caller's responsibility to ensure that: + /// - The given [`UnsafeEntityCell`] has read-only access to the fetched components. + /// - No other mutable references to the fetched components exist at the same time. + /// + /// # Errors + /// + /// - Returns [`EntityComponentError::MissingComponent`] if a component is missing from the entity. + unsafe fn fetch_ref( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError>; + + /// Returns untyped mutable reference(s) to the component(s) with the + /// given [`ComponentId`]s, as determined by `self`. + /// + /// # Safety + /// + /// It is the caller's responsibility to ensure that: + /// - The given [`UnsafeEntityCell`] has mutable access to the fetched components. + /// - No other references to the fetched components exist at the same time. + /// + /// # Errors + /// + /// - Returns [`EntityComponentError::MissingComponent`] if a component is missing from the entity. + /// - Returns [`EntityComponentError::AliasedMutability`] if a component is requested multiple times. + unsafe fn fetch_mut( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError>; +} + +// SAFETY: +// - No aliased mutability is caused because a single reference is returned. +// - No mutable references are returned by `fetch_ref`. +unsafe impl DynamicComponentFetch for ComponentId { + type Ref<'w> = Ptr<'w>; + type Mut<'w> = MutUntyped<'w>; + + unsafe fn fetch_ref( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + // SAFETY: caller ensures that the cell has read access to the component. + unsafe { cell.get_by_id(self) }.ok_or(EntityComponentError::MissingComponent(self)) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + // SAFETY: caller ensures that the cell has mutable access to the component. + unsafe { cell.get_mut_by_id(self) }.ok_or(EntityComponentError::MissingComponent(self)) + } +} + +// SAFETY: +// - No aliased mutability is caused because the array is checked for duplicates. +// - No mutable references are returned by `fetch_ref`. +unsafe impl DynamicComponentFetch for [ComponentId; N] { + type Ref<'w> = [Ptr<'w>; N]; + type Mut<'w> = [MutUntyped<'w>; N]; + + unsafe fn fetch_ref( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + <&Self>::fetch_ref(&self, cell) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + <&Self>::fetch_mut(&self, cell) + } +} + +// SAFETY: +// - No aliased mutability is caused because the array is checked for duplicates. +// - No mutable references are returned by `fetch_ref`. +unsafe impl DynamicComponentFetch for &'_ [ComponentId; N] { + type Ref<'w> = [Ptr<'w>; N]; + type Mut<'w> = [MutUntyped<'w>; N]; + + unsafe fn fetch_ref( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + let mut ptrs = [const { MaybeUninit::uninit() }; N]; + for (ptr, &id) in core::iter::zip(&mut ptrs, self) { + *ptr = MaybeUninit::new( + // SAFETY: caller ensures that the cell has read access to the component. + unsafe { cell.get_by_id(id) }.ok_or(EntityComponentError::MissingComponent(id))?, + ); + } + + // SAFETY: Each ptr was initialized in the loop above. + let ptrs = ptrs.map(|ptr| unsafe { MaybeUninit::assume_init(ptr) }); + + Ok(ptrs) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + // Check for duplicate component IDs. + for i in 0..self.len() { + for j in 0..i { + if self[i] == self[j] { + return Err(EntityComponentError::AliasedMutability(self[i])); + } + } + } + + let mut ptrs = [const { MaybeUninit::uninit() }; N]; + for (ptr, &id) in core::iter::zip(&mut ptrs, self) { + *ptr = MaybeUninit::new( + // SAFETY: caller ensures that the cell has mutable access to the component. + unsafe { cell.get_mut_by_id(id) } + .ok_or(EntityComponentError::MissingComponent(id))?, + ); + } + + // SAFETY: Each ptr was initialized in the loop above. + let ptrs = ptrs.map(|ptr| unsafe { MaybeUninit::assume_init(ptr) }); + + Ok(ptrs) + } +} + +// SAFETY: +// - No aliased mutability is caused because the slice is checked for duplicates. +// - No mutable references are returned by `fetch_ref`. +unsafe impl DynamicComponentFetch for &'_ [ComponentId] { + type Ref<'w> = Vec>; + type Mut<'w> = Vec>; + + unsafe fn fetch_ref( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + let mut ptrs = Vec::with_capacity(self.len()); + for &id in self { + ptrs.push( + // SAFETY: caller ensures that the cell has read access to the component. + unsafe { cell.get_by_id(id) }.ok_or(EntityComponentError::MissingComponent(id))?, + ); + } + Ok(ptrs) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + // Check for duplicate component IDs. + for i in 0..self.len() { + for j in 0..i { + if self[i] == self[j] { + return Err(EntityComponentError::AliasedMutability(self[i])); + } + } + } + + let mut ptrs = Vec::with_capacity(self.len()); + for &id in self { + ptrs.push( + // SAFETY: caller ensures that the cell has mutable access to the component. + unsafe { cell.get_mut_by_id(id) } + .ok_or(EntityComponentError::MissingComponent(id))?, + ); + } + Ok(ptrs) + } +} + +// SAFETY: +// - No aliased mutability is caused because `HashSet` guarantees unique elements. +// - No mutable references are returned by `fetch_ref`. +unsafe impl DynamicComponentFetch for &'_ HashSet { + type Ref<'w> = HashMap>; + type Mut<'w> = HashMap>; + + unsafe fn fetch_ref( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + let mut ptrs = HashMap::with_capacity(self.len()); + for &id in self { + ptrs.insert( + id, + // SAFETY: caller ensures that the cell has read access to the component. + unsafe { cell.get_by_id(id) }.ok_or(EntityComponentError::MissingComponent(id))?, + ); + } + Ok(ptrs) + } + + unsafe fn fetch_mut( + self, + cell: UnsafeEntityCell<'_>, + ) -> Result, EntityComponentError> { + let mut ptrs = HashMap::with_capacity(self.len()); + for &id in self { + ptrs.insert( + id, + // SAFETY: caller ensures that the cell has mutable access to the component. + unsafe { cell.get_mut_by_id(id) } + .ok_or(EntityComponentError::MissingComponent(id))?, + ); + } + Ok(ptrs) + } +} + #[cfg(test)] mod tests { - use bevy_ptr::OwningPtr; + use bevy_ptr::{OwningPtr, Ptr}; use core::panic::AssertUnwindSafe; use crate::{ self as bevy_ecs, + change_detection::MutUntyped, component::ComponentId, prelude::*, system::{assert_is_system, RunSystemOnce as _}, - world::{FilteredEntityMut, FilteredEntityRef}, + world::{error::EntityComponentError, FilteredEntityMut, FilteredEntityRef}, }; use super::{EntityMutExcept, EntityRefExcept}; @@ -2935,7 +3516,7 @@ mod tests { let mut world = World::new(); let entity = world.spawn_empty().id(); let entity = world.entity(entity); - assert!(entity.get_by_id(invalid_component_id).is_none()); + assert!(entity.get_by_id(invalid_component_id).is_err()); } #[test] @@ -2944,8 +3525,8 @@ mod tests { let mut world = World::new(); let mut entity = world.spawn_empty(); - assert!(entity.get_by_id(invalid_component_id).is_none()); - assert!(entity.get_mut_by_id(invalid_component_id).is_none()); + assert!(entity.get_by_id(invalid_component_id).is_err()); + assert!(entity.get_mut_by_id(invalid_component_id).is_err()); } // regression test for https://github.com/bevyengine/bevy/pull/7387 @@ -3575,13 +4156,14 @@ mod tests { assert!(e.get_change_ticks_by_id(a_id).is_none()); } + #[derive(Component, PartialEq, Eq, Debug)] + struct X(usize); + + #[derive(Component, PartialEq, Eq, Debug)] + struct Y(usize); + #[test] fn get_components() { - #[derive(Component, PartialEq, Eq, Debug)] - struct X(usize); - - #[derive(Component, PartialEq, Eq, Debug)] - struct Y(usize); let mut world = World::default(); let e1 = world.spawn((X(7), Y(10))).id(); let e2 = world.spawn(X(8)).id(); @@ -3594,4 +4176,238 @@ mod tests { assert_eq!(None, world.entity(e2).get_components::<(&X, &Y)>()); assert_eq!(None, world.entity(e3).get_components::<(&X, &Y)>()); } + + #[test] + fn get_by_id_array() { + let mut world = World::default(); + let e1 = world.spawn((X(7), Y(10))).id(); + let e2 = world.spawn(X(8)).id(); + let e3 = world.spawn_empty().id(); + + let x_id = world.register_component::(); + let y_id = world.register_component::(); + + assert_eq!( + Ok((&X(7), &Y(10))), + world + .entity(e1) + .get_by_id([x_id, y_id]) + .map(|[x_ptr, y_ptr]| { + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.deref::() }, unsafe { y_ptr.deref::() }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(y_id)), + world + .entity(e2) + .get_by_id([x_id, y_id]) + .map(|[x_ptr, y_ptr]| { + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.deref::() }, unsafe { y_ptr.deref::() }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(x_id)), + world + .entity(e3) + .get_by_id([x_id, y_id]) + .map(|[x_ptr, y_ptr]| { + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.deref::() }, unsafe { y_ptr.deref::() }) + }) + ); + } + + #[test] + fn get_by_id_vec() { + let mut world = World::default(); + let e1 = world.spawn((X(7), Y(10))).id(); + let e2 = world.spawn(X(8)).id(); + let e3 = world.spawn_empty().id(); + + let x_id = world.register_component::(); + let y_id = world.register_component::(); + + assert_eq!( + Ok((&X(7), &Y(10))), + world + .entity(e1) + .get_by_id(&[x_id, y_id] as &[ComponentId]) + .map(|ptrs| { + let Ok([x_ptr, y_ptr]): Result<[Ptr; 2], _> = ptrs.try_into() else { + panic!("get_by_id(slice) didn't return 2 elements") + }; + + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.deref::() }, unsafe { y_ptr.deref::() }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(y_id)), + world + .entity(e2) + .get_by_id(&[x_id, y_id] as &[ComponentId]) + .map(|ptrs| { + let Ok([x_ptr, y_ptr]): Result<[Ptr; 2], _> = ptrs.try_into() else { + panic!("get_by_id(slice) didn't return 2 elements") + }; + + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.deref::() }, unsafe { y_ptr.deref::() }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(x_id)), + world + .entity(e3) + .get_by_id(&[x_id, y_id] as &[ComponentId]) + .map(|ptrs| { + let Ok([x_ptr, y_ptr]): Result<[Ptr; 2], _> = ptrs.try_into() else { + panic!("get_by_id(slice) didn't return 2 elements") + }; + + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.deref::() }, unsafe { y_ptr.deref::() }) + }) + ); + } + + #[test] + fn get_mut_by_id_array() { + let mut world = World::default(); + let e1 = world.spawn((X(7), Y(10))).id(); + let e2 = world.spawn(X(8)).id(); + let e3 = world.spawn_empty().id(); + + let x_id = world.register_component::(); + let y_id = world.register_component::(); + + assert_eq!( + Ok((&mut X(7), &mut Y(10))), + world + .entity_mut(e1) + .get_mut_by_id([x_id, y_id]) + .map(|[x_ptr, y_ptr]| { + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.into_inner().deref_mut::() }, unsafe { + y_ptr.into_inner().deref_mut::() + }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(y_id)), + world + .entity_mut(e2) + .get_mut_by_id([x_id, y_id]) + .map(|[x_ptr, y_ptr]| { + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.into_inner().deref_mut::() }, unsafe { + y_ptr.into_inner().deref_mut::() + }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(x_id)), + world + .entity_mut(e3) + .get_mut_by_id([x_id, y_id]) + .map(|[x_ptr, y_ptr]| { + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.into_inner().deref_mut::() }, unsafe { + y_ptr.into_inner().deref_mut::() + }) + }) + ); + + assert_eq!( + Err(EntityComponentError::AliasedMutability(x_id)), + world + .entity_mut(e1) + .get_mut_by_id([x_id, x_id]) + .map(|_| { unreachable!() }) + ); + assert_eq!( + Err(EntityComponentError::AliasedMutability(x_id)), + world + .entity_mut(e3) + .get_mut_by_id([x_id, x_id]) + .map(|_| { unreachable!() }) + ); + } + + #[test] + fn get_mut_by_id_vec() { + let mut world = World::default(); + let e1 = world.spawn((X(7), Y(10))).id(); + let e2 = world.spawn(X(8)).id(); + let e3 = world.spawn_empty().id(); + + let x_id = world.register_component::(); + let y_id = world.register_component::(); + + assert_eq!( + Ok((&mut X(7), &mut Y(10))), + world + .entity_mut(e1) + .get_mut_by_id(&[x_id, y_id] as &[ComponentId]) + .map(|ptrs| { + let Ok([x_ptr, y_ptr]): Result<[MutUntyped; 2], _> = ptrs.try_into() else { + panic!("get_mut_by_id(slice) didn't return 2 elements") + }; + + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.into_inner().deref_mut::() }, unsafe { + y_ptr.into_inner().deref_mut::() + }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(y_id)), + world + .entity_mut(e2) + .get_mut_by_id(&[x_id, y_id] as &[ComponentId]) + .map(|ptrs| { + let Ok([x_ptr, y_ptr]): Result<[MutUntyped; 2], _> = ptrs.try_into() else { + panic!("get_mut_by_id(slice) didn't return 2 elements") + }; + + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.into_inner().deref_mut::() }, unsafe { + y_ptr.into_inner().deref_mut::() + }) + }) + ); + assert_eq!( + Err(EntityComponentError::MissingComponent(x_id)), + world + .entity_mut(e3) + .get_mut_by_id(&[x_id, y_id] as &[ComponentId]) + .map(|ptrs| { + let Ok([x_ptr, y_ptr]): Result<[MutUntyped; 2], _> = ptrs.try_into() else { + panic!("get_mut_by_id(slice) didn't return 2 elements") + }; + + // SAFETY: components match the id they were fetched with + (unsafe { x_ptr.into_inner().deref_mut::() }, unsafe { + y_ptr.into_inner().deref_mut::() + }) + }) + ); + + assert_eq!( + Err(EntityComponentError::AliasedMutability(x_id)), + world + .entity_mut(e1) + .get_mut_by_id(&[x_id, x_id]) + .map(|_| { unreachable!() }) + ); + assert_eq!( + Err(EntityComponentError::AliasedMutability(x_id)), + world + .entity_mut(e3) + .get_mut_by_id(&[x_id, x_id]) + .map(|_| { unreachable!() }) + ); + } } diff --git a/crates/bevy_ecs/src/world/error.rs b/crates/bevy_ecs/src/world/error.rs index 326b0310ba15c..5fc4264e07cd3 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::schedule::InternedScheduleLabel; +use crate::{component::ComponentId, schedule::InternedScheduleLabel}; /// The error type returned by [`World::try_run_schedule`] if the provided schedule does not exist. /// @@ -10,3 +10,14 @@ use crate::schedule::InternedScheduleLabel; #[derive(Error, Debug)] #[error("The schedule with the label {0:?} was not found.")] pub struct TryRunScheduleError(pub InternedScheduleLabel); + +/// An error that occurs when dynamically retrieving components from an entity. +#[derive(Error, Debug, Clone, Copy, PartialEq, Eq)] +pub enum EntityComponentError { + /// The component with the given [`ComponentId`] does not exist on the entity. + #[error("The component with ID {0:?} does not exist on the entity.")] + MissingComponent(ComponentId), + /// The component with the given [`ComponentId`] was requested mutably more than once. + #[error("The component with ID {0:?} was requested mutably more than once.")] + AliasedMutability(ComponentId), +} diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 6a4caa3a00c49..5ed99314f7633 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -563,11 +563,20 @@ where .short_arc_3d_between(lower_center, start, lower_apex, self.color); }); - let upper_lines = upper_points.windows(2).map(|win| (win[0], win[1])); - let lower_lines = lower_points.windows(2).map(|win| (win[0], win[1])); - upper_lines.chain(lower_lines).for_each(|(start, end)| { - self.gizmos.line(start, end, self.color); - }); + let circle_rotation = self + .isometry + .rotation + .mul_quat(Quat::from_rotation_x(core::f32::consts::FRAC_PI_2)); + self.gizmos.circle( + Isometry3d::new(upper_center, circle_rotation), + self.radius, + self.color, + ); + self.gizmos.circle( + Isometry3d::new(lower_center, circle_rotation), + self.radius, + self.color, + ); let connection_lines = upper_points.into_iter().zip(lower_points).skip(1); connection_lines.for_each(|(start, end)| { diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index df8ff45b72d5b..05d4c596f8469 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -22,15 +22,14 @@ //! # use bevy_gltf::prelude::*; //! //! fn spawn_gltf(mut commands: Commands, asset_server: Res) { -//! commands.spawn(SceneBundle { +//! commands.spawn(( //! // This is equivalent to "models/FlightHelmet/FlightHelmet.gltf#Scene0" //! // The `#Scene0` label here is very important because it tells bevy to load the first scene in the glTF file. //! // If this isn't specified bevy doesn't know which part of the glTF file to load. -//! scene: asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf")), +//! SceneRoot(asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/FlightHelmet/FlightHelmet.gltf"))), //! // You can use the transform to give it a position -//! transform: Transform::from_xyz(2.0, 0.0, -5.0), -//! ..Default::default() -//! }); +//! Transform::from_xyz(2.0, 0.0, -5.0), +//! )); //! } //! ``` //! # Loading parts of a glTF asset @@ -72,18 +71,14 @@ //! }; //! *loaded = true; //! -//! commands.spawn(SceneBundle { -//! // Gets the first scene in the file -//! scene: gltf.scenes[0].clone(), -//! ..Default::default() -//! }); +//! // Spawns the first scene in the file +//! commands.spawn(SceneRoot(gltf.scenes[0].clone())); //! -//! commands.spawn(SceneBundle { -//! // Gets the scene named "Lenses_low" -//! scene: gltf.named_scenes["Lenses_low"].clone(), -//! transform: Transform::from_xyz(1.0, 2.0, 3.0), -//! ..Default::default() -//! }); +//! // Spawns the scene named "Lenses_low" +//! commands.spawn(( +//! SceneRoot(gltf.named_scenes["Lenses_low"].clone()), +//! Transform::from_xyz(1.0, 2.0, 3.0), +//! )); //! } //! ``` //! diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 4aebd12a86a00..9bb72e2356729 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -17,7 +17,8 @@ use bevy_ecs::{ use bevy_hierarchy::{BuildChildren, ChildBuild, WorldChildBuilder}; use bevy_math::{Affine2, Mat4, Vec3}; use bevy_pbr::{ - DirectionalLight, PbrBundle, PointLight, SpotLight, StandardMaterial, UvChannel, MAX_JOINTS, + DirectionalLight, MeshMaterial3d, PointLight, SpotLight, StandardMaterial, UvChannel, + MAX_JOINTS, }; use bevy_render::{ alpha::AlphaMode, @@ -25,7 +26,7 @@ use bevy_render::{ mesh::{ morph::{MeshMorphWeights, MorphAttributes, MorphTargetImage, MorphWeights}, skinning::{SkinnedMesh, SkinnedMeshInverseBindposes}, - Indices, Mesh, MeshVertexAttribute, VertexAttributeValues, + Indices, Mesh, Mesh3d, MeshVertexAttribute, VertexAttributeValues, }, prelude::SpatialBundle, primitives::Aabb, @@ -1456,12 +1457,13 @@ fn load_node( }; let bounds = primitive.bounding_box(); - let mut mesh_entity = parent.spawn(PbrBundle { + let mut mesh_entity = parent.spawn(( // TODO: handle missing label handle errors here? - mesh: load_context.get_label_handle(primitive_label.to_string()), - material: load_context.get_label_handle(&material_label), - ..Default::default() - }); + Mesh3d(load_context.get_label_handle(primitive_label.to_string())), + MeshMaterial3d::( + load_context.get_label_handle(&material_label), + ), + )); let target_count = primitive.morph_targets().len(); if target_count != 0 { diff --git a/crates/bevy_input/src/gamepad.rs b/crates/bevy_input/src/gamepad.rs index 2fee5a4db544c..152efe74c0ea7 100644 --- a/crates/bevy_input/src/gamepad.rs +++ b/crates/bevy_input/src/gamepad.rs @@ -1345,7 +1345,7 @@ pub fn gamepad_connection_system( let id = connection_event.gamepad; match &connection_event.connection { GamepadConnection::Connected(info) => { - let Some(gamepad) = commands.get_entity(id) else { + let Some(mut gamepad) = commands.get_entity(id) else { warn!("Gamepad {:} removed before handling connection event.", id); continue; }; @@ -1353,7 +1353,7 @@ pub fn gamepad_connection_system( info!("Gamepad {:?} connected.", id); } GamepadConnection::Disconnected => { - let Some(gamepad) = commands.get_entity(id) else { + let Some(mut gamepad) = commands.get_entity(id) else { warn!("Gamepad {:} removed before handling disconnection event. You can ignore this if you manually removed it.", id); continue; }; diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 45bab606ceb99..5128a772bbf2f 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -99,6 +99,10 @@ async-io = ["bevy_tasks/async-io"] wayland = ["bevy_winit/wayland"] x11 = ["bevy_winit/x11"] +# Android activity support (choose one) +android-native-activity = ["bevy_winit/android-native-activity"] +android-game-activity = ["bevy_winit/android-game-activity"] + # Transmission textures in `StandardMaterial`: pbr_transmission_textures = [ "bevy_pbr?/pbr_transmission_textures", diff --git a/crates/bevy_math/src/curve/easing.rs b/crates/bevy_math/src/curve/easing.rs new file mode 100644 index 0000000000000..fd163291d20c2 --- /dev/null +++ b/crates/bevy_math/src/curve/easing.rs @@ -0,0 +1,298 @@ +//! Module containing different [`Easing`] curves to control the transition between two values and +//! the [`EasingCurve`] struct to make use of them. + +use crate::{ + ops::{self, FloatPow}, + VectorSpace, +}; + +use super::{Curve, FunctionCurve, Interval}; + +/// A trait for [`Curves`] that map the [unit interval] to some other values. These kinds of curves +/// are used to create a transition between two values. Easing curves are most commonly known from +/// [CSS animations] but are also widely used in other fields. +/// +/// [unit interval]: `Interval::UNIT` +/// [`Curves`]: `Curve` +/// [CSS animations]: https://developer.mozilla.org/en-US/docs/Web/CSS/easing-function +pub trait Easing: Curve {} +impl> Easing for EasingCurve {} +impl Easing for LinearCurve {} +impl Easing for StepCurve {} +impl Easing for ElasticCurve {} + +/// A [`Curve`] that is defined by +/// +/// - an initial `start` sample value at `t = 0` +/// - a final `end` sample value at `t = 1` +/// - an [`EasingCurve`] to interpolate between the two values within the [unit interval]. +/// +/// [unit interval]: `Interval::UNIT` +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub struct EasingCurve +where + T: VectorSpace, + E: Curve, +{ + start: T, + end: T, + easing: E, +} + +impl Curve for EasingCurve +where + T: VectorSpace, + E: Curve, +{ + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + let domain = self.easing.domain(); + let t = domain.start().lerp(domain.end(), t); + self.start.lerp(self.end, self.easing.sample_unchecked(t)) + } +} + +impl EasingCurve +where + T: VectorSpace, + E: Curve, +{ + /// Create a new [`EasingCurve`] over the [unit interval] which transitions between a `start` + /// and an `end` value based on the provided [`Curve`] curve. + /// + /// If the input curve's domain is not the unit interval, then the [`EasingCurve`] will ensure + /// that this invariant is guaranteed by internally [reparametrizing] the curve to the unit + /// interval. + /// + /// [`Curve`]: `Curve` + /// [unit interval]: `Interval::UNIT` + /// [reparametrizing]: `Curve::reparametrize_linear` + pub fn new(start: T, end: T, easing: E) -> Result { + easing + .domain() + .is_bounded() + .then_some(Self { start, end, easing }) + .ok_or(EasingCurveError) + } +} + +impl EasingCurve f32>> { + /// A [`Curve`] mapping the [unit interval] to itself. + /// + /// Quadratic easing functions can have exactly one critical point. This is a point on the function + /// such that `f′(t) = 0`. This means that there won't be any sudden jumps at this point leading to + /// smooth transitions. A common choice is to place that point at `t = 0` or [`t = 1`]. + /// + /// It uses the function `f(t) = t²` + /// + /// [unit domain]: `Interval::UNIT` + /// [`t = 1`]: `Self::quadratic_ease_out` + pub fn quadratic_ease_in() -> Self { + Self { + start: 0.0, + end: 1.0, + easing: FunctionCurve::new(Interval::UNIT, FloatPow::squared), + } + } + + /// A [`Curve`] mapping the [unit interval] to itself. + /// + /// Quadratic easing functions can have exactly one critical point. This is a point on the function + /// such that `f′(t) = 0`. This means that there won't be any sudden jumps at this point leading to + /// smooth transitions. A common choice is to place that point at [`t = 0`] or`t = 1`. + /// + /// It uses the function `f(t) = 1 - (1 - t)²` + /// + /// [unit domain]: `Interval::UNIT` + /// [`t = 0`]: `Self::quadratic_ease_in` + pub fn quadratic_ease_out() -> Self { + fn f(t: f32) -> f32 { + 1.0 - (1.0 - t).squared() + } + Self { + start: 0.0, + end: 1.0, + easing: FunctionCurve::new(Interval::UNIT, f), + } + } + + /// A [`Curve`] mapping the [unit interval] to itself. + /// + /// Cubic easing functions can have up to two critical points. These are points on the function + /// such that `f′(t) = 0`. This means that there won't be any sudden jumps at these points leading to + /// smooth transitions. For this curve they are placed at `t = 0` and `t = 1` respectively and the + /// result is a well-known kind of [sigmoid function] called a [smoothstep function]. + /// + /// It uses the function `f(t) = t² * (3 - 2t)` + /// + /// [unit domain]: `Interval::UNIT` + /// [sigmoid function]: https://en.wikipedia.org/wiki/Sigmoid_function + /// [smoothstep function]: https://en.wikipedia.org/wiki/Smoothstep + pub fn smoothstep() -> Self { + fn f(t: f32) -> f32 { + t.squared() * (3.0 - 2.0 * t) + } + Self { + start: 0.0, + end: 1.0, + easing: FunctionCurve::new(Interval::UNIT, f), + } + } + + /// A [`Curve`] mapping the [unit interval] to itself. + /// + /// It uses the function `f(t) = t` + /// + /// [unit domain]: `Interval::UNIT` + pub fn identity() -> Self { + Self { + start: 0.0, + end: 1.0, + easing: FunctionCurve::new(Interval::UNIT, core::convert::identity), + } + } +} + +/// An error that occurs if the construction of [`EasingCurve`] fails +#[derive(Debug, thiserror::Error)] +#[error("Easing curves can only be constructed from curves with bounded domain")] +pub struct EasingCurveError; + +/// A [`Curve`] that is defined by a `start` and an `end` point, together with linear interpolation +/// between the values over the [unit interval]. It's basically an [`EasingCurve`] with the +/// identity as an easing function. +/// +/// [unit interval]: `Interval::UNIT` +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub struct LinearCurve { + start: T, + end: T, +} + +impl Curve for LinearCurve +where + T: VectorSpace, +{ + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> T { + self.start.lerp(self.end, t) + } +} + +impl LinearCurve +where + T: VectorSpace, +{ + /// Create a new [`LinearCurve`] over the [unit interval] from `start` to `end`. + /// + /// [unit interval]: `Interval::UNIT` + pub fn new(start: T, end: T) -> Self { + Self { start, end } + } +} + +/// A [`Curve`] mapping the [unit interval] to itself. +/// +/// This leads to a cruve with sudden jumps at the step points and segments with constant values +/// everywhere else. +/// +/// It uses the function `f(n,t) = round(t * n) / n` +/// +/// parametrized by `n`, the number of jumps +/// +/// - for `n == 0` this is equal to [`constant_curve(Interval::UNIT, 0.0)`] +/// - for `n == 1` this makes a single jump at `t = 0.5`, splitting the interval evenly +/// - 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` +/// [`constant_curve(Interval::UNIT, 0.0)`]: `crate::curve::constant_curve` +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub struct StepCurve { + num_steps: usize, +} + +impl Curve for StepCurve { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + if t != 0.0 || t != 1.0 { + (t * self.num_steps as f32).round() / self.num_steps.max(1) as f32 + } else { + t + } + } +} + +impl StepCurve { + /// Create a new [`StepCurve`] over the [unit interval] which makes the given amount of steps. + /// + /// [unit interval]: `Interval::UNIT` + pub fn new(num_steps: usize) -> Self { + Self { num_steps } + } +} + +/// A [`Curve`] over the [unit interval]. +/// +/// This class of easing functions is derived as an approximation of a [spring-mass-system] +/// solution. +/// +/// - For `ω → 0` the curve converges to the [smoothstep function] +/// - For `ω → ∞` the curve gets increasingly more bouncy +/// +/// It uses the function `f(omega,t) = 1 - (1 - t)²(2sin(omega * t) / omega + cos(omega * t))` +/// +/// parametrized by `omega` +/// +/// [unit domain]: `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)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "bevy_reflect", derive(bevy_reflect::Reflect))] +pub struct ElasticCurve { + omega: f32, +} + +impl Curve for ElasticCurve { + #[inline] + fn domain(&self) -> Interval { + Interval::UNIT + } + + #[inline] + fn sample_unchecked(&self, t: f32) -> f32 { + 1.0 - (1.0 - t).squared() + * (2.0 * ops::sin(self.omega * t) / self.omega + ops::cos(self.omega * t)) + } +} + +impl ElasticCurve { + /// Create a new [`ElasticCurve`] over the [unit interval] with the given parameter `omega`. + /// + /// [unit interval]: `Interval::UNIT` + pub fn new(omega: f32) -> Self { + Self { omega } + } +} diff --git a/crates/bevy_math/src/curve/interval.rs b/crates/bevy_math/src/curve/interval.rs index ae651b52cfb77..244fd1b4c424c 100644 --- a/crates/bevy_math/src/curve/interval.rs +++ b/crates/bevy_math/src/curve/interval.rs @@ -65,7 +65,7 @@ impl Interval { } } - /// The unit interval covering the range between `0.0` and `1.0`. + /// An interval of length 1.0, starting at 0.0 and ending at 1.0. pub const UNIT: Self = Self { start: 0.0, end: 1.0, diff --git a/crates/bevy_math/src/curve/mod.rs b/crates/bevy_math/src/curve/mod.rs index 1a24266a3e335..9a1bb946c6cc0 100644 --- a/crates/bevy_math/src/curve/mod.rs +++ b/crates/bevy_math/src/curve/mod.rs @@ -4,6 +4,7 @@ pub mod adaptors; pub mod cores; +pub mod easing; pub mod interval; pub mod iterable; pub mod sample_curves; @@ -712,10 +713,12 @@ where #[cfg(test)] mod tests { + use super::easing::*; use super::*; use crate::{ops, Quat}; use approx::{assert_abs_diff_eq, AbsDiffEq}; use core::f32::consts::TAU; + use glam::*; #[test] fn curve_can_be_made_into_an_object() { @@ -748,6 +751,97 @@ mod tests { assert!(curve.sample(-1.0).is_none()); } + #[test] + fn linear_curve() { + let start = Vec2::ZERO; + let end = Vec2::new(1.0, 2.0); + let curve = LinearCurve::new(start, end); + + let mid = (start + end) / 2.0; + + [(0.0, start), (0.5, mid), (1.0, end)] + .into_iter() + .for_each(|(t, x)| { + assert!(curve.sample_unchecked(t).abs_diff_eq(x, f32::EPSILON)); + }); + } + + #[test] + fn easing_curves_step() { + let start = Vec2::ZERO; + let end = Vec2::new(1.0, 2.0); + + let curve = EasingCurve::new(start, end, StepCurve::new(4)).unwrap(); + [ + (0.0, start), + (0.124, start), + (0.125, Vec2::new(0.25, 0.5)), + (0.374, Vec2::new(0.25, 0.5)), + (0.375, Vec2::new(0.5, 1.0)), + (0.624, Vec2::new(0.5, 1.0)), + (0.625, Vec2::new(0.75, 1.5)), + (0.874, Vec2::new(0.75, 1.5)), + (0.875, end), + (1.0, end), + ] + .into_iter() + .for_each(|(t, x)| { + assert!(curve.sample_unchecked(t).abs_diff_eq(x, f32::EPSILON)); + }); + } + + #[test] + fn easing_curves_quadratic() { + let start = Vec2::ZERO; + let end = Vec2::new(1.0, 2.0); + + let curve = EasingCurve::new(start, end, EasingCurve::quadratic_ease_in()).unwrap(); + [ + (0.0, start), + (0.25, Vec2::new(0.0625, 0.125)), + (0.5, Vec2::new(0.25, 0.5)), + (1.0, end), + ] + .into_iter() + .for_each(|(t, x)| { + assert!(curve.sample_unchecked(t).abs_diff_eq(x, f32::EPSILON),); + }); + } + + #[test] + fn easing_curve_non_unit_domain() { + let start = Vec2::ZERO; + let end = Vec2::new(1.0, 2.0); + + // even though the quadratic_ease_in input curve has the domain [0.0, 2.0], the easing + // curve correctly behaves as if its domain were [0.0, 1.0] + let curve = EasingCurve::new( + start, + end, + EasingCurve::quadratic_ease_in() + .reparametrize(Interval::new(0.0, 2.0).unwrap(), |t| t / 2.0), + ) + .unwrap(); + + [ + (-0.1, None), + (0.0, Some(start)), + (0.25, Some(Vec2::new(0.0625, 0.125))), + (0.5, Some(Vec2::new(0.25, 0.5))), + (1.0, Some(end)), + (1.1, None), + ] + .into_iter() + .for_each(|(t, x)| { + let sample = curve.sample(t); + match (sample, x) { + (None, None) => assert_eq!(sample, x), + (Some(s), Some(x)) => assert!(s.abs_diff_eq(x, f32::EPSILON)), + _ => unreachable!(), + }; + }); + } + #[test] fn mapping() { let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); diff --git a/crates/bevy_pbr/src/bundle.rs b/crates/bevy_pbr/src/bundle.rs index 71f732752546f..55cdb3609a892 100644 --- a/crates/bevy_pbr/src/bundle.rs +++ b/crates/bevy_pbr/src/bundle.rs @@ -1,10 +1,9 @@ #![expect(deprecated)] use crate::{ - CascadeShadowConfig, Cascades, DirectionalLight, Material, PointLight, SpotLight, - StandardMaterial, + CascadeShadowConfig, Cascades, DirectionalLight, Material, MeshMaterial3d, PointLight, + SpotLight, StandardMaterial, }; -use bevy_asset::Handle; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ bundle::Bundle, @@ -14,21 +13,29 @@ use bevy_ecs::{ }; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ - mesh::Mesh, + mesh::Mesh3d, primitives::{CascadesFrusta, CubemapFrusta, Frustum}, view::{InheritedVisibility, ViewVisibility, Visibility}, world_sync::SyncToRenderWorld, }; use bevy_transform::components::{GlobalTransform, Transform}; -/// A component bundle for PBR entities with a [`Mesh`] and a [`StandardMaterial`]. +/// A component bundle for PBR entities with a [`Mesh3d`] and a [`MeshMaterial3d`]. +#[deprecated( + since = "0.15.0", + note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically." +)] pub type PbrBundle = MaterialMeshBundle; -/// A component bundle for entities with a [`Mesh`] and a [`Material`]. +/// A component bundle for entities with a [`Mesh3d`] and a [`MeshMaterial3d`]. #[derive(Bundle, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically." +)] pub struct MaterialMeshBundle { - pub mesh: Handle, - pub material: Handle, + pub mesh: Mesh3d, + pub material: MeshMaterial3d, pub transform: Transform, pub global_transform: GlobalTransform, /// User indication of whether an entity is visible diff --git a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl index de191ce295b6b..843ed2bbf69ab 100644 --- a/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl +++ b/crates/bevy_pbr/src/deferred/deferred_lighting.wgsl @@ -10,7 +10,7 @@ #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture -#import bevy_pbr::gtao_utils::gtao_multibounce +#import bevy_pbr::ssao_utils::ssao_multibounce #endif struct FullscreenVertexOutput { @@ -64,7 +64,7 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; - let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); pbr_input.diffuse_occlusion = min(pbr_input.diffuse_occlusion, ssao_multibounce); // Neubelt and Pettineo 2013, "Crafting a Next-gen Material Pipeline for The Order: 1886" diff --git a/crates/bevy_pbr/src/lib.rs b/crates/bevy_pbr/src/lib.rs index 1b446b57a645b..68f75af7bef86 100644 --- a/crates/bevy_pbr/src/lib.rs +++ b/crates/bevy_pbr/src/lib.rs @@ -34,6 +34,7 @@ mod light; mod light_probe; mod lightmap; mod material; +mod mesh_material; mod parallax; mod pbr_material; mod prepass; @@ -53,6 +54,7 @@ pub use light::*; pub use light_probe::*; pub use lightmap::*; pub use material::*; +pub use mesh_material::*; pub use parallax::*; pub use pbr_material::*; pub use prepass::*; @@ -83,6 +85,7 @@ pub mod prelude { LightProbe, }, material::{Material, MaterialPlugin}, + mesh_material::MeshMaterial3d, parallax::ParallaxMappingMethod, pbr_material::StandardMaterial, ssao::ScreenSpaceAmbientOcclusionPlugin, @@ -411,13 +414,13 @@ impl Plugin for PbrPlugin { app.add_plugins(DeferredPbrLightingPlugin); } + // Initialize the default material. app.world_mut() .resource_mut::>() .insert( &Handle::::default(), StandardMaterial { - base_color: Color::srgb(1.0, 0.0, 0.5), - unlit: true, + base_color: Color::WHITE, ..Default::default() }, ); @@ -428,7 +431,14 @@ impl Plugin for PbrPlugin { // Extract the required data from the main world render_app - .add_systems(ExtractSchedule, (extract_clusters, extract_lights)) + .add_systems( + ExtractSchedule, + ( + extract_clusters, + extract_lights, + extract_default_materials.after(clear_material_instances::), + ), + ) .add_systems( Render, ( diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index e7477a5a23639..bdcd3c567e942 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -7,7 +7,7 @@ use bevy_render::{ camera::{Camera, CameraProjection}, extract_component::ExtractComponent, extract_resource::ExtractResource, - mesh::Mesh, + mesh::Mesh3d, primitives::{Aabb, CascadesFrusta, CubemapFrusta, Frustum, Sphere}, view::{ InheritedVisibility, NoFrustumCulling, RenderLayers, ViewVisibility, VisibilityRange, @@ -440,11 +440,11 @@ fn calculate_cascade( texel_size: cascade_texel_size, } } -/// Add this component to make a [`Mesh`] not cast shadows. +/// Add this component to make a [`Mesh3d`] not cast shadows. #[derive(Debug, Component, Reflect, Default)] #[reflect(Component, Default, Debug)] pub struct NotShadowCaster; -/// Add this component to make a [`Mesh`] not receive shadows. +/// Add this component to make a [`Mesh3d`] not receive shadows. /// /// **Note:** If you're using diffuse transmission, setting [`NotShadowReceiver`] will /// cause both “regular” shadows as well as diffusely transmitted shadows to be disabled, @@ -452,7 +452,7 @@ pub struct NotShadowCaster; #[derive(Debug, Component, Reflect, Default)] #[reflect(Component, Default, Debug)] pub struct NotShadowReceiver; -/// Add this component to make a [`Mesh`] using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0` +/// Add this component to make a [`Mesh3d`] using a PBR material with [`diffuse_transmission`](crate::pbr_material::StandardMaterial::diffuse_transmission)`> 0.0` /// receive shadows on its diffuse transmission lobe. (i.e. its “backside”) /// /// Not enabled by default, as it requires carefully setting up [`thickness`](crate::pbr_material::StandardMaterial::thickness) @@ -697,7 +697,7 @@ pub fn check_dir_light_mesh_visibility( ( Without, Without, - With>, + With, ), >, visible_entity_ranges: Option>, @@ -866,7 +866,7 @@ pub fn check_point_light_mesh_visibility( ( Without, Without, - With>, + With, ), >, visible_entity_ranges: Option>, diff --git a/crates/bevy_pbr/src/lightmap/mod.rs b/crates/bevy_pbr/src/lightmap/mod.rs index d1fd1cc143b8c..f1b03f97af3f8 100644 --- a/crates/bevy_pbr/src/lightmap/mod.rs +++ b/crates/bevy_pbr/src/lightmap/mod.rs @@ -6,10 +6,10 @@ //! with an addon like [The Lightmapper]. The tools in the [`bevy-baked-gi`] //! project support other lightmap baking methods. //! -//! When a [`Lightmap`] component is added to an entity with a [`Mesh`] and a -//! [`StandardMaterial`](crate::StandardMaterial), Bevy applies the lightmap when rendering. The brightness +//! When a [`Lightmap`] component is added to an entity with a [`Mesh3d`] and a +//! [`MeshMaterial3d`], Bevy applies the lightmap when rendering. The brightness //! of the lightmap may be controlled with the `lightmap_exposure` field on -//! `StandardMaterial`. +//! [`StandardMaterial`]. //! //! During the rendering extraction phase, we extract all lightmaps into the //! [`RenderLightmaps`] table, which lives in the render world. Mesh bindgroup @@ -25,7 +25,9 @@ //! appropriately. //! //! [The Lightmapper]: https://github.com/Naxela/The_Lightmapper -//! +//! [`Mesh3d`]: bevy_render::mesh::Mesh3d +//! [`MeshMaterial3d`]: crate::StandardMaterial +//! [`StandardMaterial`]: crate::StandardMaterial //! [`bevy-baked-gi`]: https://github.com/pcwalton/bevy-baked-gi use bevy_app::{App, Plugin}; @@ -61,10 +63,10 @@ pub struct LightmapPlugin; /// A component that applies baked indirect diffuse global illumination from a /// lightmap. /// -/// When assigned to an entity that contains a [`Mesh`] and a -/// [`StandardMaterial`](crate::StandardMaterial), if the mesh has a second UV -/// layer ([`ATTRIBUTE_UV_1`](bevy_render::mesh::Mesh::ATTRIBUTE_UV_1)), then -/// the lightmap will render using those UVs. +/// When assigned to an entity that contains a [`Mesh3d`](bevy_render::mesh::Mesh3d) and a +/// [`MeshMaterial3d`](crate::StandardMaterial), if the mesh +/// has a second UV layer ([`ATTRIBUTE_UV_1`](bevy_render::mesh::Mesh::ATTRIBUTE_UV_1)), +/// then the lightmap will render using those UVs. #[derive(Component, Clone, Reflect)] #[reflect(Component, Default)] pub struct Lightmap { diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index c08555744bb56..29a3a85b342dd 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -24,14 +24,15 @@ use bevy_reflect::std_traits::ReflectDefault; use bevy_reflect::Reflect; use bevy_render::{ camera::TemporalJitter, - extract_instances::{ExtractInstancesPlugin, ExtractedInstances}, + extract_instances::ExtractedInstances, extract_resource::ExtractResource, - mesh::{MeshVertexBufferLayoutRef, RenderMesh}, + mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_asset::{PrepareAssetError, RenderAsset, RenderAssetPlugin, RenderAssets}, render_phase::*, render_resource::*, renderer::RenderDevice, - view::{ExtractedView, Msaa, RenderVisibilityRanges, VisibleEntities, WithMesh}, + view::{ExtractedView, Msaa, RenderVisibilityRanges, ViewVisibility, VisibleEntities}, + Extract, }; use bevy_utils::tracing::error; use core::{ @@ -43,26 +44,28 @@ use core::{ use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight}; -/// Materials are used alongside [`MaterialPlugin`] and [`MaterialMeshBundle`] +/// Materials are used alongside [`MaterialPlugin`], [`Mesh3d`], and [`MeshMaterial3d`] /// to spawn entities that are rendered with a specific [`Material`] type. They serve as an easy to use high level -/// way to render [`Mesh`](bevy_render::mesh::Mesh) entities with custom shader logic. +/// way to render [`Mesh3d`] entities with custom shader logic. /// /// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. /// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. /// /// # Example /// -/// Here is a simple Material implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// Here is a simple [`Material`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, /// check out the [`AsBindGroup`] documentation. +/// /// ``` -/// # use bevy_pbr::{Material, MaterialMeshBundle}; +/// # use bevy_pbr::{Material, MeshMaterial3d}; /// # use bevy_ecs::prelude::*; /// # use bevy_reflect::TypePath; -/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image}; +/// # use bevy_render::{mesh::{Mesh, Mesh3d}, render_resource::{AsBindGroup, ShaderRef}, texture::Image}; /// # use bevy_color::LinearRgba; /// # use bevy_color::palettes::basic::RED; /// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; -/// +/// # use bevy_math::primitives::Capsule3d; +/// # /// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] /// pub struct CustomMaterial { /// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to @@ -84,17 +87,23 @@ use self::{irradiance_volume::IrradianceVolume, prelude::EnvironmentMapLight}; /// } /// } /// -/// // Spawn an entity using `CustomMaterial`. -/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { -/// commands.spawn(MaterialMeshBundle { -/// material: materials.add(CustomMaterial { +/// // Spawn an entity with a mesh using `CustomMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// asset_server: Res +/// ) { +/// commands.spawn(( +/// Mesh3d(meshes.add(Capsule3d::default())), +/// MeshMaterial3d(materials.add(CustomMaterial { /// color: RED.into(), /// color_texture: asset_server.load("some_image.png"), -/// }), -/// ..Default::default() -/// }); +/// })), +/// )); /// } /// ``` +/// /// In WGSL shaders, the material's binding would look like this: /// /// ```wgsl @@ -258,20 +267,25 @@ where M::Data: PartialEq + Eq + Hash + Clone, { fn build(&self, app: &mut App) { - app.init_asset::().add_plugins(( - ExtractInstancesPlugin::>::extract_visible(), - RenderAssetPlugin::>::default(), - )); + app.init_asset::() + .register_type::>() + .register_type::() + .add_plugins(RenderAssetPlugin::>::default()); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { render_app .init_resource::>() + .init_resource::>>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .add_render_command::>() .init_resource::>>() + .add_systems( + ExtractSchedule, + (clear_material_instances::, extract_mesh_materials::).chain(), + ) .add_systems( Render, queue_material_meshes:: @@ -527,6 +541,35 @@ pub const fn screen_space_specular_transmission_pipeline_key( } } +pub(super) fn clear_material_instances( + mut material_instances: ResMut>, +) { + material_instances.clear(); +} + +fn extract_mesh_materials( + mut material_instances: ResMut>, + query: Extract), With>>, +) { + for (entity, view_visibility, material) in &query { + if view_visibility.get() { + material_instances.insert(entity, material.id()); + } + } +} + +/// Extracts default materials for 3D meshes with no [`MeshMaterial3d`]. +pub(super) fn extract_default_materials( + mut material_instances: ResMut>, + query: Extract, Without)>>, +) { + for (entity, view_visibility) in &query { + if view_visibility.get() { + material_instances.insert(entity, AssetId::default()); + } + } +} + /// For each view, iterates over all the meshes visible from that view and adds /// them to [`BinnedRenderPhase`]s or [`SortedRenderPhase`]s as appropriate. #[allow(clippy::too_many_arguments)] @@ -686,7 +729,7 @@ pub fn queue_material_meshes( } let rangefinder = view.rangefinder3d(); - for visible_entity in visible_entities.iter::() { + for visible_entity in visible_entities.iter::>() { let Some(material_asset_id) = render_material_instances.get(visible_entity) else { continue; }; diff --git a/crates/bevy_pbr/src/mesh_material.rs b/crates/bevy_pbr/src/mesh_material.rs new file mode 100644 index 0000000000000..90ee6ea9dc8ad --- /dev/null +++ b/crates/bevy_pbr/src/mesh_material.rs @@ -0,0 +1,106 @@ +use crate::Material; +use bevy_asset::{AssetId, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; + +/// A [material](Material) for a [`Mesh3d`]. +/// +/// See [`Material`] for general information about 3D materials and how to implement your own materials. +/// +/// [`Mesh3d`]: bevy_render::mesh::Mesh3d +/// +/// # Example +/// +/// ``` +/// # use bevy_pbr::{Material, MeshMaterial3d, StandardMaterial}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::mesh::{Mesh, Mesh3d}; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::Assets; +/// # use bevy_math::primitives::Capsule3d; +/// # +/// // Spawn an entity with a mesh using `StandardMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// commands.spawn(( +/// Mesh3d(meshes.add(Capsule3d::default())), +/// MeshMaterial3d(materials.add(StandardMaterial { +/// base_color: RED.into(), +/// ..Default::default() +/// })), +/// )); +/// } +/// ``` +/// +/// ## Default Material +/// +/// Meshes without a [`MeshMaterial3d`] are rendered with a default [`StandardMaterial`]. +/// This material can be overridden by inserting a custom material for the default asset handle. +/// +/// ``` +/// # use bevy_pbr::{Material, MeshMaterial3d, StandardMaterial}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::mesh::{Mesh, Mesh3d}; +/// # use bevy_color::Color; +/// # use bevy_asset::{Assets, Handle}; +/// # use bevy_math::primitives::Capsule3d; +/// # +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// // Optional: Insert a custom default material. +/// materials.insert( +/// &Handle::::default(), +/// StandardMaterial::from(Color::srgb(1.0, 0.0, 1.0)), +/// ); +/// +/// // Spawn a capsule with no material. +/// // The mesh will be rendered with the default material. +/// commands.spawn(Mesh3d(meshes.add(Capsule3d::default()))); +/// } +/// ``` +/// +/// [`StandardMaterial`]: crate::StandardMaterial +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default)] +#[require(HasMaterial3d)] +pub struct MeshMaterial3d(pub Handle); + +impl Default for MeshMaterial3d { + fn default() -> Self { + Self(Handle::default()) + } +} + +impl From> for MeshMaterial3d { + fn from(handle: Handle) -> Self { + Self(handle) + } +} + +impl From> for AssetId { + fn from(material: MeshMaterial3d) -> Self { + material.id() + } +} + +impl From<&MeshMaterial3d> for AssetId { + fn from(material: &MeshMaterial3d) -> Self { + material.id() + } +} + +/// A component that marks an entity as having a [`MeshMaterial3d`]. +/// [`Mesh3d`] entities without this component are rendered with a [default material]. +/// +/// [`Mesh3d`]: bevy_render::mesh::Mesh3d +/// [default material]: crate::MeshMaterial3d#default-material +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default)] +pub struct HasMaterial3d; diff --git a/crates/bevy_pbr/src/meshlet/from_mesh.rs b/crates/bevy_pbr/src/meshlet/from_mesh.rs index 3c8df5de3bf88..668ff32ebafcb 100644 --- a/crates/bevy_pbr/src/meshlet/from_mesh.rs +++ b/crates/bevy_pbr/src/meshlet/from_mesh.rs @@ -30,7 +30,7 @@ impl MeshletMesh { let indices = validate_input_mesh(mesh)?; // Split the mesh into an initial list of meshlets (LOD 0) - let vertex_buffer = mesh.get_vertex_buffer_data(); + let vertex_buffer = mesh.create_packed_vertex_buffer_data(); let vertex_stride = mesh.get_vertex_size() as usize; let vertices = VertexDataAdapter::new(&vertex_buffer, vertex_stride, 0).unwrap(); let mut meshlets = compute_meshlets(&indices, &vertices); diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index 166ef0c2cc1c1..6aaeed96787eb 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -1,9 +1,8 @@ mod prepass_bindings; use bevy_render::{ - mesh::{MeshVertexBufferLayoutRef, RenderMesh}, + mesh::{Mesh3d, MeshVertexBufferLayoutRef, RenderMesh}, render_resource::binding_types::uniform_buffer, - view::WithMesh, world_sync::RenderEntity, }; pub use prepass_bindings::*; @@ -220,9 +219,9 @@ pub fn update_previous_view_data( pub struct PreviousGlobalTransform(pub Affine3A); #[cfg(not(feature = "meshlet"))] -type PreviousMeshFilter = With>; +type PreviousMeshFilter = With; #[cfg(feature = "meshlet")] -type PreviousMeshFilter = Or<(With>, With>)>; +type PreviousMeshFilter = Or<(With, With>)>; pub fn update_mesh_previous_global_transforms( mut commands: Commands, @@ -586,7 +585,7 @@ 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 entity = commands.get_or_spawn(entity); + let mut entity = commands.get_or_spawn(entity); if let Some(previous_view_data) = maybe_previous_view_data { entity.insert(previous_view_data.clone()); @@ -766,7 +765,7 @@ pub fn queue_prepass_material_meshes( view_key |= MeshPipelineKey::MOTION_VECTOR_PREPASS; } - for visible_entity in visible_entities.iter::() { + for visible_entity in visible_entities.iter::>() { let Some(material_asset_id) = render_material_instances.get(visible_entity) else { continue; }; diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 212737e058be3..ea0bfd25e7aed 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -435,9 +435,9 @@ pub(crate) fn add_light_view_entities( trigger: Trigger, mut commands: Commands, ) { - commands - .get_entity(trigger.entity()) - .map(|v| v.insert(LightViewEntities::default())); + if let Some(mut v) = commands.get_entity(trigger.entity()) { + v.insert(LightViewEntities::default()); + } } pub(crate) fn remove_light_view_entities( @@ -447,7 +447,7 @@ pub(crate) fn remove_light_view_entities( ) { if let Ok(entities) = query.get(trigger.entity()) { for e in entities.0.iter().copied() { - if let Some(v) = commands.get_entity(e) { + if let Some(mut v) = commands.get_entity(e) { v.despawn(); } } diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index c1c9a29129563..91f93c61b2560 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -567,7 +567,7 @@ pub struct RenderMeshInstanceGpuQueues(Parallel); impl RenderMeshInstanceShared { fn from_components( previous_transform: Option<&PreviousGlobalTransform>, - handle: &Handle, + mesh: &Mesh3d, not_shadow_caster: bool, no_automatic_batching: bool, ) -> Self { @@ -583,8 +583,7 @@ impl RenderMeshInstanceShared { ); RenderMeshInstanceShared { - mesh_asset_id: handle.id(), - + mesh_asset_id: mesh.id(), flags: mesh_instance_flags, material_bind_group_id: AtomicMaterialBindGroupId::default(), } @@ -870,7 +869,7 @@ pub fn extract_meshes_for_cpu_building( &ViewVisibility, &GlobalTransform, Option<&PreviousGlobalTransform>, - &Handle, + &Mesh3d, Has, Has, Has, @@ -887,7 +886,7 @@ pub fn extract_meshes_for_cpu_building( view_visibility, transform, previous_transform, - handle, + mesh, not_shadow_receiver, transmitted_receiver, not_shadow_caster, @@ -912,7 +911,7 @@ pub fn extract_meshes_for_cpu_building( let shared = RenderMeshInstanceShared::from_components( previous_transform, - handle, + mesh, not_shadow_caster, no_automatic_batching, ); @@ -969,7 +968,7 @@ pub fn extract_meshes_for_gpu_building( Option<&PreviousGlobalTransform>, Option<&Lightmap>, Option<&Aabb>, - &Handle, + &Mesh3d, Has, Has, Has, @@ -1003,7 +1002,7 @@ pub fn extract_meshes_for_gpu_building( previous_transform, lightmap, aabb, - handle, + mesh, not_shadow_receiver, transmitted_receiver, not_shadow_caster, @@ -1028,7 +1027,7 @@ pub fn extract_meshes_for_gpu_building( let shared = RenderMeshInstanceShared::from_components( previous_transform, - handle, + mesh, not_shadow_caster, no_automatic_batching, ); diff --git a/crates/bevy_pbr/src/render/mesh_view_bindings.rs b/crates/bevy_pbr/src/render/mesh_view_bindings.rs index f44999b8b6499..0973b6513609c 100644 --- a/crates/bevy_pbr/src/render/mesh_view_bindings.rs +++ b/crates/bevy_pbr/src/render/mesh_view_bindings.rs @@ -44,7 +44,7 @@ use crate::{ }, prepass, EnvironmentMapUniformBuffer, FogMeta, GlobalClusterableObjectMeta, GpuClusterableObjects, GpuFog, GpuLights, LightMeta, LightProbesBuffer, LightProbesUniform, - MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionTextures, + MeshPipeline, MeshPipelineKey, RenderViewLightProbes, ScreenSpaceAmbientOcclusionResources, ScreenSpaceReflectionsBuffer, ScreenSpaceReflectionsUniform, ShadowSamplers, ViewClusterBindings, ViewShadowBindings, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, }; @@ -462,7 +462,7 @@ pub fn prepare_mesh_view_bind_groups( &ViewShadowBindings, &ViewClusterBindings, &Msaa, - Option<&ScreenSpaceAmbientOcclusionTextures>, + Option<&ScreenSpaceAmbientOcclusionResources>, Option<&ViewPrepassTextures>, Option<&ViewTransmissionTexture>, &Tonemapping, @@ -507,7 +507,7 @@ pub fn prepare_mesh_view_bind_groups( shadow_bindings, cluster_bindings, msaa, - ssao_textures, + ssao_resources, prepass_textures, transmission_texture, tonemapping, @@ -519,7 +519,7 @@ pub fn prepare_mesh_view_bind_groups( .image_for_samplecount(1, TextureFormat::bevy_default()) .texture_view .clone(); - let ssao_view = ssao_textures + let ssao_view = ssao_resources .map(|t| &t.screen_space_ambient_occlusion_texture.default_view) .unwrap_or(&fallback_ssao); diff --git a/crates/bevy_pbr/src/render/pbr_fragment.wgsl b/crates/bevy_pbr/src/render/pbr_fragment.wgsl index 2dca49ecbca58..91e104ede525f 100644 --- a/crates/bevy_pbr/src/render/pbr_fragment.wgsl +++ b/crates/bevy_pbr/src/render/pbr_fragment.wgsl @@ -15,7 +15,7 @@ #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION #import bevy_pbr::mesh_view_bindings::screen_space_ambient_occlusion_texture -#import bevy_pbr::gtao_utils::gtao_multibounce +#import bevy_pbr::ssao_utils::ssao_multibounce #endif #ifdef MESHLET_MESH_MATERIAL_PASS @@ -344,7 +344,7 @@ fn pbr_input_from_standard_material( #endif #ifdef SCREEN_SPACE_AMBIENT_OCCLUSION let ssao = textureLoad(screen_space_ambient_occlusion_texture, vec2(in.position.xy), 0i).r; - let ssao_multibounce = gtao_multibounce(ssao, pbr_input.material.base_color.rgb); + let ssao_multibounce = ssao_multibounce(ssao, pbr_input.material.base_color.rgb); diffuse_occlusion = min(diffuse_occlusion, ssao_multibounce); // Use SSAO to estimate the specular occlusion. // Lagarde and Rousiers 2014, "Moving Frostbite to Physically Based Rendering" diff --git a/crates/bevy_pbr/src/ssao/mod.rs b/crates/bevy_pbr/src/ssao/mod.rs index 7495960391306..a37fb9c79985d 100644 --- a/crates/bevy_pbr/src/ssao/mod.rs +++ b/crates/bevy_pbr/src/ssao/mod.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] + use crate::NodePbr; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; @@ -40,9 +42,9 @@ use bevy_utils::{ use core::mem; const PREPROCESS_DEPTH_SHADER_HANDLE: Handle = Handle::weak_from_u128(102258915420479); -const GTAO_SHADER_HANDLE: Handle = Handle::weak_from_u128(253938746510568); +const SSAO_SHADER_HANDLE: Handle = Handle::weak_from_u128(253938746510568); const SPATIAL_DENOISE_SHADER_HANDLE: Handle = Handle::weak_from_u128(466162052558226); -const GTAO_UTILS_SHADER_HANDLE: Handle = Handle::weak_from_u128(366465052568786); +const SSAO_UTILS_SHADER_HANDLE: Handle = Handle::weak_from_u128(366465052568786); /// Plugin for screen space ambient occlusion. pub struct ScreenSpaceAmbientOcclusionPlugin; @@ -55,7 +57,7 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { "preprocess_depth.wgsl", Shader::from_wgsl ); - load_internal_asset!(app, GTAO_SHADER_HANDLE, "gtao.wgsl", Shader::from_wgsl); + load_internal_asset!(app, SSAO_SHADER_HANDLE, "ssao.wgsl", Shader::from_wgsl); load_internal_asset!( app, SPATIAL_DENOISE_SHADER_HANDLE, @@ -64,8 +66,8 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { ); load_internal_asset!( app, - GTAO_UTILS_SHADER_HANDLE, - "gtao_utils.wgsl", + SSAO_UTILS_SHADER_HANDLE, + "ssao_utils.wgsl", Shader::from_wgsl ); @@ -129,6 +131,10 @@ impl Plugin for ScreenSpaceAmbientOcclusionPlugin { /// Bundle to apply screen space ambient occlusion. #[derive(Bundle, Default, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `ScreenSpaceAmbientOcclusion` component instead. Inserting it will now also insert the other components required by it automatically." +)] pub struct ScreenSpaceAmbientOcclusionBundle { pub settings: ScreenSpaceAmbientOcclusion, pub depth_prepass: DepthPrepass, @@ -146,19 +152,34 @@ pub struct ScreenSpaceAmbientOcclusionBundle { /// /// # Usage Notes /// -/// Requires that you add [`ScreenSpaceAmbientOcclusionPlugin`] to your app, -/// and add the [`DepthPrepass`] and [`NormalPrepass`] components to your camera. +/// Requires that you add [`ScreenSpaceAmbientOcclusionPlugin`] to your app. /// /// It strongly recommended that you use SSAO in conjunction with /// TAA ([`bevy_core_pipeline::experimental::taa::TemporalAntiAliasing`]). /// Doing so greatly reduces SSAO noise. /// -/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU` or `DirectX12`. -#[derive(Component, ExtractComponent, Reflect, PartialEq, Eq, Hash, Clone, Default, Debug)] -#[reflect(Component, Debug, Default, Hash, PartialEq)] +/// SSAO is not supported on `WebGL2`, and is not currently supported on `WebGPU`. +#[derive(Component, ExtractComponent, Reflect, PartialEq, Clone, Debug)] +#[reflect(Component, Debug, Default, PartialEq)] +#[require(DepthPrepass, NormalPrepass)] #[doc(alias = "Ssao")] pub struct ScreenSpaceAmbientOcclusion { + /// Quality of the SSAO effect. pub quality_level: ScreenSpaceAmbientOcclusionQualityLevel, + /// A constant estimated thickness of objects. + /// + /// This value is used to decide how far behind an object a ray of light needs to be in order + /// to pass behind it. Any ray closer than that will be occluded. + pub constant_object_thickness: f32, +} + +impl Default for ScreenSpaceAmbientOcclusion { + fn default() -> Self { + Self { + quality_level: ScreenSpaceAmbientOcclusionQualityLevel::default(), + constant_object_thickness: 0.25, + } + } } #[deprecated(since = "0.15.0", note = "Renamed to `ScreenSpaceAmbientOcclusion`")] @@ -218,7 +239,7 @@ impl ViewNode for SsaoNode { Some(camera_size), Some(preprocess_depth_pipeline), Some(spatial_denoise_pipeline), - Some(gtao_pipeline), + Some(ssao_pipeline), ) = ( camera.physical_viewport_size, pipeline_cache.get_compute_pipeline(pipelines.preprocess_depth_pipeline), @@ -254,21 +275,21 @@ impl ViewNode for SsaoNode { } { - let mut gtao_pass = + let mut ssao_pass = render_context .command_encoder() .begin_compute_pass(&ComputePassDescriptor { - label: Some("ssao_gtao_pass"), + label: Some("ssao_ssao_pass"), timestamp_writes: None, }); - gtao_pass.set_pipeline(gtao_pipeline); - gtao_pass.set_bind_group(0, &bind_groups.gtao_bind_group, &[]); - gtao_pass.set_bind_group( + ssao_pass.set_pipeline(ssao_pipeline); + ssao_pass.set_bind_group(0, &bind_groups.ssao_bind_group, &[]); + ssao_pass.set_bind_group( 1, &bind_groups.common_bind_group, &[view_uniform_offset.offset], ); - gtao_pass.dispatch_workgroups( + ssao_pass.dispatch_workgroups( div_ceil(camera_size.x, 8), div_ceil(camera_size.y, 8), 1, @@ -309,11 +330,12 @@ struct SsaoPipelines { common_bind_group_layout: BindGroupLayout, preprocess_depth_bind_group_layout: BindGroupLayout, - gtao_bind_group_layout: BindGroupLayout, + ssao_bind_group_layout: BindGroupLayout, spatial_denoise_bind_group_layout: BindGroupLayout, hilbert_index_lut: TextureView, point_clamp_sampler: Sampler, + linear_clamp_sampler: Sampler, } impl FromWorld for SsaoPipelines { @@ -352,6 +374,14 @@ impl FromWorld for SsaoPipelines { address_mode_v: AddressMode::ClampToEdge, ..Default::default() }); + let linear_clamp_sampler = render_device.create_sampler(&SamplerDescriptor { + min_filter: FilterMode::Linear, + mag_filter: FilterMode::Linear, + mipmap_filter: FilterMode::Nearest, + address_mode_u: AddressMode::ClampToEdge, + address_mode_v: AddressMode::ClampToEdge, + ..Default::default() + }); let common_bind_group_layout = render_device.create_bind_group_layout( "ssao_common_bind_group_layout", @@ -359,6 +389,7 @@ impl FromWorld for SsaoPipelines { ShaderStages::COMPUTE, ( sampler(SamplerBindingType::NonFiltering), + sampler(SamplerBindingType::Filtering), uniform_buffer::(true), ), ), @@ -379,17 +410,18 @@ impl FromWorld for SsaoPipelines { ), ); - let gtao_bind_group_layout = render_device.create_bind_group_layout( - "ssao_gtao_bind_group_layout", + let ssao_bind_group_layout = render_device.create_bind_group_layout( + "ssao_ssao_bind_group_layout", &BindGroupLayoutEntries::sequential( ShaderStages::COMPUTE, ( - texture_2d(TextureSampleType::Float { filterable: false }), + texture_2d(TextureSampleType::Float { filterable: true }), texture_2d(TextureSampleType::Float { filterable: false }), texture_2d(TextureSampleType::Uint), texture_storage_2d(TextureFormat::R16Float, StorageTextureAccess::WriteOnly), texture_storage_2d(TextureFormat::R32Uint, StorageTextureAccess::WriteOnly), uniform_buffer::(false), + uniform_buffer::(false), ), ), ); @@ -438,18 +470,19 @@ impl FromWorld for SsaoPipelines { common_bind_group_layout, preprocess_depth_bind_group_layout, - gtao_bind_group_layout, + ssao_bind_group_layout, spatial_denoise_bind_group_layout, hilbert_index_lut, point_clamp_sampler, + linear_clamp_sampler, } } } #[derive(PartialEq, Eq, Hash, Clone)] struct SsaoPipelineKey { - ssao_settings: ScreenSpaceAmbientOcclusion, + quality_level: ScreenSpaceAmbientOcclusionQualityLevel, temporal_jitter: bool, } @@ -457,7 +490,7 @@ impl SpecializedComputePipeline for SsaoPipelines { type Key = SsaoPipelineKey; fn specialize(&self, key: Self::Key) -> ComputePipelineDescriptor { - let (slice_count, samples_per_slice_side) = key.ssao_settings.quality_level.sample_counts(); + let (slice_count, samples_per_slice_side) = key.quality_level.sample_counts(); let mut shader_defs = vec![ ShaderDefVal::Int("SLICE_COUNT".to_string(), slice_count as i32), @@ -472,15 +505,15 @@ impl SpecializedComputePipeline for SsaoPipelines { } ComputePipelineDescriptor { - label: Some("ssao_gtao_pipeline".into()), + label: Some("ssao_ssao_pipeline".into()), layout: vec![ - self.gtao_bind_group_layout.clone(), + self.ssao_bind_group_layout.clone(), self.common_bind_group_layout.clone(), ], push_constant_ranges: vec![], - shader: GTAO_SHADER_HANDLE, + shader: SSAO_SHADER_HANDLE, shader_defs, - entry_point: "gtao".into(), + entry_point: "ssao".into(), } } } @@ -511,20 +544,21 @@ fn extract_ssao_settings( } #[derive(Component)] -pub struct ScreenSpaceAmbientOcclusionTextures { +pub struct ScreenSpaceAmbientOcclusionResources { preprocessed_depth_texture: CachedTexture, ssao_noisy_texture: CachedTexture, // Pre-spatially denoised texture pub screen_space_ambient_occlusion_texture: CachedTexture, // Spatially denoised texture depth_differences_texture: CachedTexture, + thickness_buffer: Buffer, } fn prepare_ssao_textures( mut commands: Commands, mut texture_cache: ResMut, render_device: Res, - views: Query<(Entity, &ExtractedCamera), With>, + views: Query<(Entity, &ExtractedCamera, &ScreenSpaceAmbientOcclusion)>, ) { - for (entity, camera) in &views { + for (entity, camera, ssao_settings) in &views { let Some(physical_viewport_size) = camera.physical_viewport_size else { continue; }; @@ -590,13 +624,20 @@ fn prepare_ssao_textures( }, ); + let thickness_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { + label: Some("thickness_buffer"), + contents: &ssao_settings.constant_object_thickness.to_le_bytes(), + usage: BufferUsages::UNIFORM, + }); + commands .entity(entity) - .insert(ScreenSpaceAmbientOcclusionTextures { + .insert(ScreenSpaceAmbientOcclusionResources { preprocessed_depth_texture, ssao_noisy_texture, screen_space_ambient_occlusion_texture: ssao_texture, depth_differences_texture, + thickness_buffer, }); } } @@ -616,7 +657,7 @@ fn prepare_ssao_pipelines( &pipeline_cache, &pipeline, SsaoPipelineKey { - ssao_settings: ssao_settings.clone(), + quality_level: ssao_settings.quality_level, temporal_jitter, }, ); @@ -629,7 +670,7 @@ fn prepare_ssao_pipelines( struct SsaoBindGroups { common_bind_group: BindGroup, preprocess_depth_bind_group: BindGroup, - gtao_bind_group: BindGroup, + ssao_bind_group: BindGroup, spatial_denoise_bind_group: BindGroup, } @@ -641,7 +682,7 @@ fn prepare_ssao_bind_groups( global_uniforms: Res, views: Query<( Entity, - &ScreenSpaceAmbientOcclusionTextures, + &ScreenSpaceAmbientOcclusionResources, &ViewPrepassTextures, )>, ) { @@ -652,15 +693,19 @@ fn prepare_ssao_bind_groups( return; }; - for (entity, ssao_textures, prepass_textures) in &views { + for (entity, ssao_resources, prepass_textures) in &views { let common_bind_group = render_device.create_bind_group( "ssao_common_bind_group", &pipelines.common_bind_group_layout, - &BindGroupEntries::sequential((&pipelines.point_clamp_sampler, view_uniforms.clone())), + &BindGroupEntries::sequential(( + &pipelines.point_clamp_sampler, + &pipelines.linear_clamp_sampler, + view_uniforms.clone(), + )), ); let create_depth_view = |mip_level| { - ssao_textures + ssao_resources .preprocessed_depth_texture .texture .create_view(&TextureViewDescriptor { @@ -686,16 +731,17 @@ fn prepare_ssao_bind_groups( )), ); - let gtao_bind_group = render_device.create_bind_group( - "ssao_gtao_bind_group", - &pipelines.gtao_bind_group_layout, + let ssao_bind_group = render_device.create_bind_group( + "ssao_ssao_bind_group", + &pipelines.ssao_bind_group_layout, &BindGroupEntries::sequential(( - &ssao_textures.preprocessed_depth_texture.default_view, + &ssao_resources.preprocessed_depth_texture.default_view, prepass_textures.normal_view().unwrap(), &pipelines.hilbert_index_lut, - &ssao_textures.ssao_noisy_texture.default_view, - &ssao_textures.depth_differences_texture.default_view, + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, globals_uniforms.clone(), + ssao_resources.thickness_buffer.as_entire_binding(), )), ); @@ -703,9 +749,9 @@ fn prepare_ssao_bind_groups( "ssao_spatial_denoise_bind_group", &pipelines.spatial_denoise_bind_group_layout, &BindGroupEntries::sequential(( - &ssao_textures.ssao_noisy_texture.default_view, - &ssao_textures.depth_differences_texture.default_view, - &ssao_textures + &ssao_resources.ssao_noisy_texture.default_view, + &ssao_resources.depth_differences_texture.default_view, + &ssao_resources .screen_space_ambient_occlusion_texture .default_view, )), @@ -714,7 +760,7 @@ fn prepare_ssao_bind_groups( commands.entity(entity).insert(SsaoBindGroups { common_bind_group, preprocess_depth_bind_group, - gtao_bind_group, + ssao_bind_group, spatial_denoise_bind_group, }); } diff --git a/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl b/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl index 73dccaa02c09a..a386b09d9c23c 100644 --- a/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl +++ b/crates/bevy_pbr/src/ssao/preprocess_depth.wgsl @@ -14,7 +14,8 @@ @group(0) @binding(4) var preprocessed_depth_mip3: texture_storage_2d; @group(0) @binding(5) var preprocessed_depth_mip4: texture_storage_2d; @group(1) @binding(0) var point_clamp_sampler: sampler; -@group(1) @binding(1) var view: View; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; // Using 4 depths from the previous MIP, compute a weighted average for the depth of the current MIP diff --git a/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl b/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl index 2448db309fce7..1c04f9cfab2f7 100644 --- a/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl +++ b/crates/bevy_pbr/src/ssao/spatial_denoise.wgsl @@ -15,7 +15,8 @@ @group(0) @binding(1) var depth_differences: texture_2d; @group(0) @binding(2) var ambient_occlusion: texture_storage_2d; @group(1) @binding(0) var point_clamp_sampler: sampler; -@group(1) @binding(1) var view: View; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; @compute @workgroup_size(8, 8, 1) diff --git a/crates/bevy_pbr/src/ssao/gtao.wgsl b/crates/bevy_pbr/src/ssao/ssao.wgsl similarity index 75% rename from crates/bevy_pbr/src/ssao/gtao.wgsl rename to crates/bevy_pbr/src/ssao/ssao.wgsl index ada9f1d123a6d..1fbd73e8d98ac 100644 --- a/crates/bevy_pbr/src/ssao/gtao.wgsl +++ b/crates/bevy_pbr/src/ssao/ssao.wgsl @@ -1,11 +1,16 @@ -// Ground Truth-based Ambient Occlusion (GTAO) -// Paper: https://www.activision.com/cdn/research/Practical_Real_Time_Strategies_for_Accurate_Indirect_Occlusion_NEW%20VERSION_COLOR.pdf -// Presentation: https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf +// Visibility Bitmask Ambient Occlusion (VBAO) +// Paper: ttps://ar5iv.labs.arxiv.org/html/2301.11376 // Source code heavily based on XeGTAO v1.30 from Intel // https://github.com/GameTechDev/XeGTAO/blob/0d177ce06bfa642f64d8af4de1197ad1bcb862d4/Source/Rendering/Shaders/XeGTAO.hlsli -#import bevy_pbr::gtao_utils::fast_acos +// Source code based on the existing XeGTAO implementation and +// https://cdrinmatane.github.io/posts/ssaovb-code/ + +// Source code base on SSRT3 implementation +// https://github.com/cdrinmatane/SSRT3 + +#import bevy_pbr::ssao_utils::fast_acos #import bevy_render::{ view::View, @@ -19,8 +24,10 @@ @group(0) @binding(3) var ambient_occlusion: texture_storage_2d; @group(0) @binding(4) var depth_differences: texture_storage_2d; @group(0) @binding(5) var globals: Globals; +@group(0) @binding(6) var thickness: f32; @group(1) @binding(0) var point_clamp_sampler: sampler; -@group(1) @binding(1) var view: View; +@group(1) @binding(1) var linear_clamp_sampler: sampler; +@group(1) @binding(2) var view: View; fn load_noise(pixel_coordinates: vec2) -> vec2 { var index = textureLoad(hilbert_index_lut, pixel_coordinates % 64, 0).r; @@ -81,13 +88,46 @@ fn reconstruct_view_space_position(depth: f32, uv: vec2) -> vec3 { } fn load_and_reconstruct_view_space_position(uv: vec2, sample_mip_level: f32) -> vec3 { - let depth = textureSampleLevel(preprocessed_depth, point_clamp_sampler, uv, sample_mip_level).r; + let depth = textureSampleLevel(preprocessed_depth, linear_clamp_sampler, uv, sample_mip_level).r; return reconstruct_view_space_position(depth, uv); } +fn updateSectors( + min_horizon: f32, + max_horizon: f32, + samples_per_slice: f32, + bitmask: u32, +) -> u32 { + let start_horizon = u32(min_horizon * samples_per_slice); + let angle_horizon = u32(ceil((max_horizon - min_horizon) * samples_per_slice)); + + return insertBits(bitmask, 0xFFFFFFFFu, start_horizon, angle_horizon); +} + +fn processSample( + delta_position: vec3, + view_vec: vec3, + sampling_direction: f32, + n: vec2, + samples_per_slice: f32, + bitmask: ptr, +) { + let delta_position_back_face = delta_position - view_vec * thickness; + + var front_back_horizon = vec2( + fast_acos(dot(normalize(delta_position), view_vec)), + fast_acos(dot(normalize(delta_position_back_face), view_vec)), + ); + + front_back_horizon = saturate(fma(vec2(sampling_direction), -front_back_horizon, n)); + front_back_horizon = select(front_back_horizon.xy, front_back_horizon.yx, sampling_direction >= 0.0); + + *bitmask = updateSectors(front_back_horizon.x, front_back_horizon.y, samples_per_slice, *bitmask); +} + @compute @workgroup_size(8, 8, 1) -fn gtao(@builtin(global_invocation_id) global_id: vec3) { +fn ssao(@builtin(global_invocation_id) global_id: vec3) { let slice_count = f32(#SLICE_COUNT); let samples_per_slice_side = f32(#SAMPLES_PER_SLICE_SIDE); let effect_radius = 0.5 * 1.457; @@ -110,6 +150,7 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3) { let sample_scale = (-0.5 * effect_radius * view.clip_from_view[0][0]) / pixel_position.z; var visibility = 0.0; + var occluded_sample_count = 0u; for (var slice_t = 0.0; slice_t < slice_count; slice_t += 1.0) { let slice = slice_t + noise.x; let phi = (PI / slice_count) * slice; @@ -123,12 +164,10 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3) { let sign_norm = sign(dot(orthographic_direction, projected_normal)); let cos_norm = saturate(dot(projected_normal, view_vec) / projected_normal_length); - let n = sign_norm * fast_acos(cos_norm); + let n = vec2((HALF_PI - sign_norm * fast_acos(cos_norm)) * (1.0 / PI)); + + var bitmask = 0u; - let min_cos_horizon_1 = cos(n + HALF_PI); - let min_cos_horizon_2 = cos(n - HALF_PI); - var cos_horizon_1 = min_cos_horizon_1; - var cos_horizon_2 = min_cos_horizon_2; let sample_mul = vec2(omega.x, -omega.y) * sample_scale; for (var sample_t = 0.0; sample_t < samples_per_slice_side; sample_t += 1.0) { var sample_noise = (slice_t + sample_t * samples_per_slice_side) * 0.6180339887498948482; @@ -145,27 +184,16 @@ fn gtao(@builtin(global_invocation_id) global_id: vec3) { let sample_difference_1 = sample_position_1 - pixel_position; let sample_difference_2 = sample_position_2 - pixel_position; - let sample_distance_1 = length(sample_difference_1); - let sample_distance_2 = length(sample_difference_2); - var sample_cos_horizon_1 = dot(sample_difference_1 / sample_distance_1, view_vec); - var sample_cos_horizon_2 = dot(sample_difference_2 / sample_distance_2, view_vec); - - let weight_1 = saturate(sample_distance_1 * falloff_mul + falloff_add); - let weight_2 = saturate(sample_distance_2 * falloff_mul + falloff_add); - sample_cos_horizon_1 = mix(min_cos_horizon_1, sample_cos_horizon_1, weight_1); - sample_cos_horizon_2 = mix(min_cos_horizon_2, sample_cos_horizon_2, weight_2); - - cos_horizon_1 = max(cos_horizon_1, sample_cos_horizon_1); - cos_horizon_2 = max(cos_horizon_2, sample_cos_horizon_2); + + processSample(sample_difference_1, view_vec, -1.0, n, samples_per_slice_side * 2.0, &bitmask); + processSample(sample_difference_2, view_vec, 1.0, n, samples_per_slice_side * 2.0, &bitmask); } - let horizon_1 = fast_acos(cos_horizon_1); - let horizon_2 = -fast_acos(cos_horizon_2); - let v1 = (cos_norm + 2.0 * horizon_1 * sin(n) - cos(2.0 * horizon_1 - n)) / 4.0; - let v2 = (cos_norm + 2.0 * horizon_2 * sin(n) - cos(2.0 * horizon_2 - n)) / 4.0; - visibility += projected_normal_length * (v1 + v2); + occluded_sample_count += countOneBits(bitmask); } - visibility /= slice_count; + + visibility = 1.0 - f32(occluded_sample_count) / (slice_count * 2.0 * samples_per_slice_side); + visibility = clamp(visibility, 0.03, 1.0); textureStore(ambient_occlusion, pixel_coordinates, vec4(visibility, 0.0, 0.0, 0.0)); diff --git a/crates/bevy_pbr/src/ssao/gtao_utils.wgsl b/crates/bevy_pbr/src/ssao/ssao_utils.wgsl similarity index 87% rename from crates/bevy_pbr/src/ssao/gtao_utils.wgsl rename to crates/bevy_pbr/src/ssao/ssao_utils.wgsl index 32c46e1d1d95c..ecc5a4a54de2a 100644 --- a/crates/bevy_pbr/src/ssao/gtao_utils.wgsl +++ b/crates/bevy_pbr/src/ssao/ssao_utils.wgsl @@ -1,10 +1,10 @@ -#define_import_path bevy_pbr::gtao_utils +#define_import_path bevy_pbr::ssao_utils #import bevy_render::maths::{PI, HALF_PI} // Approximates single-bounce ambient occlusion to multi-bounce ambient occlusion // https://blog.selfshadow.com/publications/s2016-shading-course/activision/s2016_pbs_activision_occlusion.pdf#page=78 -fn gtao_multibounce(visibility: f32, base_color: vec3) -> vec3 { +fn ssao_multibounce(visibility: f32, base_color: vec3) -> vec3 { let a = 2.0404 * base_color - 0.3324; let b = -4.7951 * base_color + 0.6417; let c = 2.7552 * base_color + 0.6903; diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 6d9c8c6dd79e7..eef6d9857ec2d 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -1,5 +1,7 @@ //! Screen space reflections implemented via raymarching. +#![expect(deprecated)] + use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Handle}; use bevy_core_pipeline::{ @@ -58,6 +60,10 @@ pub struct ScreenSpaceReflectionsPlugin; /// A convenient bundle to add screen space reflections to a camera, along with /// the depth and deferred prepasses required to enable them. #[derive(Bundle, Default)] +#[deprecated( + since = "0.15.0", + note = "Use the `ScreenSpaceReflections` components instead. Inserting it will now also insert the other components required by it automatically." +)] pub struct ScreenSpaceReflectionsBundle { /// The component that enables SSR. pub settings: ScreenSpaceReflections, @@ -70,8 +76,8 @@ pub struct ScreenSpaceReflectionsBundle { /// Add this component to a camera to enable *screen-space reflections* (SSR). /// /// Screen-space reflections currently require deferred rendering in order to -/// appear. Therefore, you'll generally need to add a [`DepthPrepass`] and a -/// [`DeferredPrepass`] to the camera as well. +/// appear. Therefore, they also need the [`DepthPrepass`] and [`DeferredPrepass`] +/// components, which are inserted automatically. /// /// SSR currently performs no roughness filtering for glossy reflections, so /// only very smooth surfaces will reflect objects in screen space. You can @@ -92,6 +98,7 @@ pub struct ScreenSpaceReflectionsBundle { /// which is required for screen-space raymarching. #[derive(Clone, Copy, Component, Reflect)] #[reflect(Component, Default)] +#[require(DepthPrepass, DeferredPrepass)] #[doc(alias = "Ssr")] pub struct ScreenSpaceReflections { /// The maximum PBR roughness level that will enable screen space diff --git a/crates/bevy_pbr/src/volumetric_fog/mod.rs b/crates/bevy_pbr/src/volumetric_fog/mod.rs index 8a45fdc4b0107..1636f8734bb8a 100644 --- a/crates/bevy_pbr/src/volumetric_fog/mod.rs +++ b/crates/bevy_pbr/src/volumetric_fog/mod.rs @@ -29,6 +29,8 @@ //! //! [Henyey-Greenstein phase function]: https://www.pbr-book.org/4ed/Volume_Scattering/Phase_Functions#TheHenyeyndashGreensteinPhaseFunction +#![expect(deprecated)] + use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Assets, Handle}; use bevy_color::Color; @@ -121,6 +123,10 @@ pub type VolumetricFogSettings = VolumetricFog; /// A convenient [`Bundle`] that contains all components necessary to generate a /// fog volume. #[derive(Bundle, Clone, Debug, Default)] +#[deprecated( + since = "0.15.0", + note = "Use the `FogVolume` component instead. Inserting it will now also insert the other components required by it automatically." +)] pub struct FogVolumeBundle { /// The actual fog volume. pub fog_volume: FogVolume, @@ -139,6 +145,7 @@ pub struct FogVolumeBundle { #[derive(Clone, Component, Debug, Reflect)] #[reflect(Component, Default, Debug)] +#[require(Transform, Visibility)] pub struct FogVolume { /// The color of the fog. /// diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index 86e8bd49e914e..413933135c85a 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -1,11 +1,13 @@ -use crate::{Material, MaterialPipeline, MaterialPipelineKey, MaterialPlugin}; +use crate::{Material, MaterialPipeline, MaterialPipelineKey, MaterialPlugin, MeshMaterial3d}; use bevy_app::{Plugin, Startup, Update}; use bevy_asset::{load_internal_asset, Asset, Assets, Handle}; use bevy_color::{Color, LinearRgba}; use bevy_ecs::prelude::*; use bevy_reflect::{std_traits::ReflectDefault, Reflect, TypePath}; use bevy_render::{ - extract_resource::ExtractResource, mesh::MeshVertexBufferLayoutRef, prelude::*, + extract_resource::ExtractResource, + mesh::{Mesh3d, MeshVertexBufferLayoutRef}, + prelude::*, render_resource::*, }; @@ -129,12 +131,12 @@ fn global_color_changed( fn wireframe_color_changed( mut materials: ResMut>, mut colors_changed: Query< - (&mut Handle, &WireframeColor), + (&mut MeshMaterial3d, &WireframeColor), (With, Changed), >, ) { for (mut handle, wireframe_color) in &mut colors_changed { - *handle = materials.add(WireframeMaterial { + handle.0 = materials.add(WireframeMaterial { color: wireframe_color.color.into(), }); } @@ -147,27 +149,27 @@ fn apply_wireframe_material( mut materials: ResMut>, wireframes: Query< (Entity, Option<&WireframeColor>), - (With, Without>), + (With, Without>), >, - no_wireframes: Query, With>)>, + no_wireframes: Query, With>)>, mut removed_wireframes: RemovedComponents, global_material: Res, ) { for e in removed_wireframes.read().chain(no_wireframes.iter()) { - if let Some(commands) = commands.get_entity(e) { - commands.remove::>(); + if let Some(mut commands) = commands.get_entity(e) { + commands.remove::>(); } } let mut material_to_spawn = vec![]; for (e, maybe_color) in &wireframes { let material = get_wireframe_material(maybe_color, &mut materials, &global_material); - material_to_spawn.push((e, material)); + material_to_spawn.push((e, MeshMaterial3d(material))); } commands.insert_or_spawn_batch(material_to_spawn); } -type WireframeFilter = (With>, Without, Without); +type WireframeFilter = (With, Without, Without); /// Applies or removes a wireframe material on any mesh without a [`Wireframe`] or [`NoWireframe`] component. fn apply_global_wireframe_material( @@ -175,9 +177,12 @@ fn apply_global_wireframe_material( config: Res, meshes_without_material: Query< (Entity, Option<&WireframeColor>), - (WireframeFilter, Without>), + (WireframeFilter, Without>), + >, + meshes_with_global_material: Query< + Entity, + (WireframeFilter, With>), >, - meshes_with_global_material: Query>)>, global_material: Res, mut materials: ResMut>, ) { @@ -187,12 +192,14 @@ fn apply_global_wireframe_material( let material = get_wireframe_material(maybe_color, &mut materials, &global_material); // We only add the material handle but not the Wireframe component // This makes it easy to detect which mesh is using the global material and which ones are user specified - material_to_spawn.push((e, material)); + material_to_spawn.push((e, MeshMaterial3d(material))); } commands.insert_or_spawn_batch(material_to_spawn); } else { for e in &meshes_with_global_material { - commands.entity(e).remove::>(); + commands + .entity(e) + .remove::>(); } } } diff --git a/crates/bevy_picking/src/focus.rs b/crates/bevy_picking/src/focus.rs index 6d9a5d1c95799..fdb5b862be980 100644 --- a/crates/bevy_picking/src/focus.rs +++ b/crates/bevy_picking/src/focus.rs @@ -244,7 +244,7 @@ pub fn update_interactions( for (hovered_entity, new_interaction) in new_interaction_state.drain() { if let Ok(mut interaction) = interact.get_mut(hovered_entity) { *interaction = new_interaction; - } else if let Some(entity_commands) = commands.get_entity(hovered_entity) { + } else if let Some(mut entity_commands) = commands.get_entity(hovered_entity) { entity_commands.try_insert(new_interaction); } } diff --git a/crates/bevy_reflect/src/impls/std.rs b/crates/bevy_reflect/src/impls/std.rs index 734486715c195..60704d602accb 100644 --- a/crates/bevy_reflect/src/impls/std.rs +++ b/crates/bevy_reflect/src/impls/std.rs @@ -201,7 +201,7 @@ impl_reflect_opaque!(::core::num::NonZeroI8( )); impl_reflect_opaque!(::core::num::Wrapping()); impl_reflect_opaque!(::core::num::Saturating()); -impl_reflect_opaque!(::alloc::sync::Arc); +impl_reflect_opaque!(::alloc::sync::Arc); // `Serialize` and `Deserialize` only for platforms supported by serde: // https://github.com/serde-rs/serde/blob/3ffb86fc70efd3d329519e2dddfa306cc04f167c/serde/src/de/impls.rs#L1732 diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index fcecd5ed8de7d..a54fb68c19974 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -520,7 +520,7 @@ //! [the language feature for dyn upcasting coercion]: https://github.com/rust-lang/rust/issues/65991 //! [derive macro]: derive@crate::Reflect //! [`'static` lifetime]: https://doc.rust-lang.org/rust-by-example/scope/lifetime/static_lifetime.html#trait-bound -//! [`Function`]: func::Function +//! [`Function`]: crate::func::Function //! [derive macro documentation]: derive@crate::Reflect //! [deriving `Reflect`]: derive@crate::Reflect //! [type data]: TypeData diff --git a/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs new file mode 100644 index 0000000000000..ace4dc65b8073 --- /dev/null +++ b/crates/bevy_reflect/src/serde/de/deserialize_with_registry.rs @@ -0,0 +1,83 @@ +use crate::serde::de::error_utils::make_custom_error; +use crate::{FromType, PartialReflect, TypeRegistry}; +use serde::Deserializer; + +/// Trait used to provide finer control when deserializing a reflected type with one of +/// the reflection deserializers. +/// +/// This trait is the reflection equivalent of `serde`'s [`Deserialize`] trait. +/// The main difference is that this trait provides access to the [`TypeRegistry`], +/// which means that we can use the registry and all its stored type information +/// to deserialize our type. +/// +/// This can be useful when writing a custom reflection deserializer where we may +/// want to handle parts of the deserialization process, but temporarily pass control +/// to the standard reflection deserializer for other parts. +/// +/// For the serialization equivalent of this trait, see [`SerializeWithRegistry`]. +/// +/// # Rationale +/// +/// Without this trait and its associated [type data], such a deserializer would have to +/// write out all of the deserialization logic itself, possibly including +/// unnecessary code duplication and trivial implementations. +/// +/// This is because a normal [`Deserialize`] implementation has no knowledge of the +/// [`TypeRegistry`] and therefore cannot create a reflection-based deserializer for +/// nested items. +/// +/// # Implementors +/// +/// In order for this to work with the reflection deserializers like [`TypedReflectDeserializer`] +/// and [`ReflectDeserializer`], implementors should be sure to register the +/// [`ReflectDeserializeWithRegistry`] type data. +/// This can be done [via the registry] or by adding `#[reflect(DeserializeWithRegistry)]` to +/// the type definition. +/// +/// [`Deserialize`]: ::serde::Deserialize +/// [`SerializeWithRegistry`]: crate::serde::SerializeWithRegistry +/// [type data]: ReflectDeserializeWithRegistry +/// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer +/// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer +/// [via the registry]: TypeRegistry::register_type_data +pub trait DeserializeWithRegistry<'de>: PartialReflect + Sized { + fn deserialize(deserializer: D, registry: &TypeRegistry) -> Result + where + D: Deserializer<'de>; +} + +/// Type data used to deserialize a [`PartialReflect`] type with a custom [`DeserializeWithRegistry`] implementation. +#[derive(Clone)] +pub struct ReflectDeserializeWithRegistry { + deserialize: fn( + deserializer: &mut dyn erased_serde::Deserializer, + registry: &TypeRegistry, + ) -> Result, erased_serde::Error>, +} + +impl ReflectDeserializeWithRegistry { + /// Deserialize a [`PartialReflect`] type with this type data's custom [`DeserializeWithRegistry`] implementation. + pub fn deserialize<'de, D>( + &self, + deserializer: D, + registry: &TypeRegistry, + ) -> Result, D::Error> + where + D: Deserializer<'de>, + { + let mut erased = ::erase(deserializer); + (self.deserialize)(&mut erased, registry).map_err(make_custom_error) + } +} + +impl DeserializeWithRegistry<'de>> FromType + for ReflectDeserializeWithRegistry +{ + fn from_type() -> Self { + Self { + deserialize: |deserializer, registry| { + Ok(Box::new(T::deserialize(deserializer, registry)?)) + }, + } + } +} diff --git a/crates/bevy_reflect/src/serde/de/deserializer.rs b/crates/bevy_reflect/src/serde/de/deserializer.rs index 6eddc2deed076..1379fb3768d2f 100644 --- a/crates/bevy_reflect/src/serde/de/deserializer.rs +++ b/crates/bevy_reflect/src/serde/de/deserializer.rs @@ -1,5 +1,6 @@ #[cfg(feature = "debug_stack")] use crate::serde::de::error_utils::TYPE_INFO_STACK; +use crate::serde::ReflectDeserializeWithRegistry; use crate::{ serde::{ de::{ @@ -9,7 +10,7 @@ use crate::{ }, TypeRegistrationDeserializer, }, - PartialReflect, ReflectDeserialize, TypeInfo, TypeRegistration, TypeRegistry, + PartialReflect, ReflectDeserialize, TypeInfo, TypePath, TypeRegistration, TypeRegistry, }; use core::{fmt, fmt::Formatter}; use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor}; @@ -231,6 +232,7 @@ pub struct TypedReflectDeserializer<'a> { } impl<'a> TypedReflectDeserializer<'a> { + /// Creates a new [`TypedReflectDeserializer`] for the given type registration. pub fn new(registration: &'a TypeRegistration, registry: &'a TypeRegistry) -> Self { #[cfg(feature = "debug_stack")] TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new()); @@ -241,6 +243,22 @@ impl<'a> TypedReflectDeserializer<'a> { } } + /// Creates a new [`TypedReflectDeserializer`] for the given type `T`. + /// + /// # Panics + /// + /// Panics if `T` is not registered in the given [`TypeRegistry`]. + pub fn of(registry: &'a TypeRegistry) -> Self { + let registration = registry + .get(core::any::TypeId::of::()) + .unwrap_or_else(|| panic!("no registration found for type `{}`", T::type_path())); + + Self { + registration, + registry, + } + } + /// An internal constructor for creating a deserializer without resetting the type info stack. pub(super) fn new_internal( registration: &'a TypeRegistration, @@ -269,6 +287,13 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { return Ok(value.into_partial_reflect()); } + if let Some(deserialize_reflect) = + self.registration.data::() + { + let value = deserialize_reflect.deserialize(deserializer, self.registry)?; + return Ok(value); + } + match self.registration.type_info() { TypeInfo::Struct(struct_info) => { let mut dynamic_struct = deserializer.deserialize_struct( diff --git a/crates/bevy_reflect/src/serde/de/mod.rs b/crates/bevy_reflect/src/serde/de/mod.rs index 482ce9fb6378c..318cb8d42671f 100644 --- a/crates/bevy_reflect/src/serde/de/mod.rs +++ b/crates/bevy_reflect/src/serde/de/mod.rs @@ -1,7 +1,9 @@ +pub use deserialize_with_registry::*; pub use deserializer::*; pub use registrations::*; mod arrays; +mod deserialize_with_registry; mod deserializer; mod enums; mod error_utils; diff --git a/crates/bevy_reflect/src/serde/mod.rs b/crates/bevy_reflect/src/serde/mod.rs index 5a307c7d18443..b0f0d7e5c6910 100644 --- a/crates/bevy_reflect/src/serde/mod.rs +++ b/crates/bevy_reflect/src/serde/mod.rs @@ -8,11 +8,10 @@ pub use type_data::*; #[cfg(test)] mod tests { + use super::*; use crate::{ - self as bevy_reflect, - serde::{ReflectDeserializer, ReflectSerializer}, - type_registry::TypeRegistry, - DynamicStruct, DynamicTupleStruct, FromReflect, PartialReflect, Reflect, Struct, + self as bevy_reflect, type_registry::TypeRegistry, DynamicStruct, DynamicTupleStruct, + FromReflect, PartialReflect, Reflect, Struct, }; use serde::de::DeserializeSeed; @@ -183,4 +182,221 @@ mod tests { .reflect_partial_eq(result.as_partial_reflect()) .unwrap()); } + + mod type_data { + use super::*; + use crate::from_reflect::FromReflect; + use crate::serde::{DeserializeWithRegistry, ReflectDeserializeWithRegistry}; + use crate::serde::{ReflectSerializeWithRegistry, SerializeWithRegistry}; + use crate::{ReflectFromReflect, TypePath}; + use alloc::sync::Arc; + use bevy_reflect_derive::reflect_trait; + use core::fmt::{Debug, Formatter}; + use serde::de::{SeqAccess, Visitor}; + use serde::ser::SerializeSeq; + use serde::{Deserializer, Serializer}; + + #[reflect_trait] + trait Enemy: Reflect + Debug { + #[allow(dead_code, reason = "this method is purely for testing purposes")] + fn hp(&self) -> u8; + } + + // This is needed to support Arc + impl TypePath for dyn Enemy { + fn type_path() -> &'static str { + "dyn bevy_reflect::serde::tests::type_data::Enemy" + } + + fn short_type_path() -> &'static str { + "dyn Enemy" + } + } + + #[derive(Reflect, Debug)] + #[reflect(Enemy)] + struct Skeleton(u8); + + impl Enemy for Skeleton { + fn hp(&self) -> u8 { + self.0 + } + } + + #[derive(Reflect, Debug)] + #[reflect(Enemy)] + struct Zombie { + health: u8, + walk_speed: f32, + } + + impl Enemy for Zombie { + fn hp(&self) -> u8 { + self.health + } + } + + #[derive(Reflect, Debug)] + struct Level { + name: String, + enemies: EnemyList, + } + + #[derive(Reflect, Debug)] + #[reflect(SerializeWithRegistry, DeserializeWithRegistry)] + // Note that we have to use `Arc` instead of `Box` here due to the + // former being the only one between the two to implement `Reflect`. + struct EnemyList(Vec>); + + impl SerializeWithRegistry for EnemyList { + fn serialize( + &self, + serializer: S, + registry: &TypeRegistry, + ) -> Result + where + S: Serializer, + { + let mut state = serializer.serialize_seq(Some(self.0.len()))?; + for enemy in &self.0 { + state.serialize_element(&ReflectSerializer::new( + (**enemy).as_partial_reflect(), + registry, + ))?; + } + state.end() + } + } + + impl<'de> DeserializeWithRegistry<'de> for EnemyList { + fn deserialize(deserializer: D, registry: &TypeRegistry) -> Result + where + D: Deserializer<'de>, + { + struct EnemyListVisitor<'a> { + registry: &'a TypeRegistry, + } + + impl<'a, 'de> Visitor<'de> for EnemyListVisitor<'a> { + type Value = Vec>; + + fn expecting(&self, formatter: &mut Formatter) -> core::fmt::Result { + write!(formatter, "a list of enemies") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: SeqAccess<'de>, + { + let mut enemies = Vec::new(); + while let Some(enemy) = + seq.next_element_seed(ReflectDeserializer::new(self.registry))? + { + let registration = self + .registry + .get_with_type_path( + enemy.get_represented_type_info().unwrap().type_path(), + ) + .unwrap(); + + // 1. Convert any possible dynamic values to concrete ones + let enemy = registration + .data::() + .unwrap() + .from_reflect(&*enemy) + .unwrap(); + + // 2. Convert the concrete value to a boxed trait object + let enemy = registration + .data::() + .unwrap() + .get_boxed(enemy) + .unwrap(); + + enemies.push(enemy.into()); + } + + Ok(enemies) + } + } + + deserializer + .deserialize_seq(EnemyListVisitor { registry }) + .map(EnemyList) + } + } + + fn create_registry() -> TypeRegistry { + let mut registry = TypeRegistry::default(); + registry.register::(); + registry.register::(); + registry.register::(); + registry.register::(); + registry + } + + #[test] + fn should_serialize_with_serialize_with_registry() { + let registry = create_registry(); + + let level = Level { + name: String::from("Level 1"), + enemies: EnemyList(vec![ + Arc::new(Skeleton(10)), + Arc::new(Zombie { + health: 20, + walk_speed: 0.5, + }), + ]), + }; + + let serializer = ReflectSerializer::new(&level, ®istry); + let serialized = ron::ser::to_string(&serializer).unwrap(); + + let expected = r#"{"bevy_reflect::serde::tests::type_data::Level":(name:"Level 1",enemies:[{"bevy_reflect::serde::tests::type_data::Skeleton":(10)},{"bevy_reflect::serde::tests::type_data::Zombie":(health:20,walk_speed:0.5)}])}"#; + + assert_eq!(expected, serialized); + } + + #[test] + fn should_deserialize_with_deserialize_with_registry() { + let registry = create_registry(); + + let input = r#"{"bevy_reflect::serde::tests::type_data::Level":(name:"Level 1",enemies:[{"bevy_reflect::serde::tests::type_data::Skeleton":(10)},{"bevy_reflect::serde::tests::type_data::Zombie":(health:20,walk_speed:0.5)}])}"#; + + let mut deserializer = ron::de::Deserializer::from_str(input).unwrap(); + let reflect_deserializer = ReflectDeserializer::new(®istry); + let value = reflect_deserializer.deserialize(&mut deserializer).unwrap(); + + let output = Level::from_reflect(&*value).unwrap(); + + let expected = Level { + name: String::from("Level 1"), + enemies: EnemyList(vec![ + Arc::new(Skeleton(10)), + Arc::new(Zombie { + health: 20, + walk_speed: 0.5, + }), + ]), + }; + + // Poor man's comparison since we can't derive PartialEq for Arc + assert_eq!(format!("{:?}", expected), format!("{:?}", output)); + + let unexpected = Level { + name: String::from("Level 1"), + enemies: EnemyList(vec![ + Arc::new(Skeleton(20)), + Arc::new(Zombie { + health: 20, + walk_speed: 5.0, + }), + ]), + }; + + // Poor man's comparison since we can't derive PartialEq for Arc + assert_ne!(format!("{:?}", unexpected), format!("{:?}", output)); + } + } } diff --git a/crates/bevy_reflect/src/serde/ser/custom_serialization.rs b/crates/bevy_reflect/src/serde/ser/custom_serialization.rs new file mode 100644 index 0000000000000..18d2abed9c010 --- /dev/null +++ b/crates/bevy_reflect/src/serde/ser/custom_serialization.rs @@ -0,0 +1,62 @@ +use crate::serde::ser::error_utils::make_custom_error; +#[cfg(feature = "debug_stack")] +use crate::serde::ser::error_utils::TYPE_INFO_STACK; +use crate::serde::ReflectSerializeWithRegistry; +use crate::{PartialReflect, ReflectSerialize, TypeRegistry}; +use core::borrow::Borrow; +use serde::{Serialize, Serializer}; + +/// Attempts to serialize a [`PartialReflect`] value with custom [`ReflectSerialize`] +/// or [`ReflectSerializeWithRegistry`] type data. +/// +/// On success, returns the result of the serialization. +/// On failure, returns the original serializer and the error that occurred. +pub(super) fn try_custom_serialize( + value: &dyn PartialReflect, + type_registry: &TypeRegistry, + serializer: S, +) -> Result, (S, S::Error)> { + let Some(value) = value.try_as_reflect() else { + return Err(( + serializer, + make_custom_error(format_args!( + "type `{}` does not implement `Reflect`", + value.reflect_type_path() + )), + )); + }; + + let info = value.reflect_type_info(); + + let Some(registration) = type_registry.get(info.type_id()) else { + return Err(( + serializer, + make_custom_error(format_args!( + "type `{}` is not registered in the type registry", + info.type_path(), + )), + )); + }; + + if let Some(reflect_serialize) = registration.data::() { + #[cfg(feature = "debug_stack")] + TYPE_INFO_STACK.with_borrow_mut(crate::type_info_stack::TypeInfoStack::pop); + + Ok(reflect_serialize + .get_serializable(value) + .borrow() + .serialize(serializer)) + } else if let Some(reflect_serialize_with_registry) = + registration.data::() + { + #[cfg(feature = "debug_stack")] + TYPE_INFO_STACK.with_borrow_mut(crate::type_info_stack::TypeInfoStack::pop); + + Ok(reflect_serialize_with_registry.serialize(value, serializer, type_registry)) + } else { + Err((serializer, make_custom_error(format_args!( + "type `{}` did not register the `ReflectSerialize` or `ReflectSerializeWithRegistry` type data. For certain types, this may need to be registered manually using `register_type_data`", + info.type_path(), + )))) + } +} diff --git a/crates/bevy_reflect/src/serde/ser/mod.rs b/crates/bevy_reflect/src/serde/ser/mod.rs index 9ce71a839be35..0a0d006b989fc 100644 --- a/crates/bevy_reflect/src/serde/ser/mod.rs +++ b/crates/bevy_reflect/src/serde/ser/mod.rs @@ -1,12 +1,15 @@ pub use serializable::*; +pub use serialize_with_registry::*; pub use serializer::*; mod arrays; +mod custom_serialization; mod enums; mod error_utils; mod lists; mod maps; mod serializable; +mod serialize_with_registry; mod serializer; mod sets; mod structs; @@ -459,7 +462,7 @@ mod tests { assert_eq!( error, ron::Error::Message( - "type `core::ops::RangeInclusive` did not register the `ReflectSerialize` type data. For certain types, this may need to be registered manually using `register_type_data` (stack: `core::ops::RangeInclusive`)".to_string() + "type `core::ops::RangeInclusive` did not register the `ReflectSerialize` or `ReflectSerializeWithRegistry` type data. For certain types, this may need to be registered manually using `register_type_data` (stack: `core::ops::RangeInclusive`)".to_string() ) ); #[cfg(not(feature = "debug_stack"))] diff --git a/crates/bevy_reflect/src/serde/ser/serializable.rs b/crates/bevy_reflect/src/serde/ser/serializable.rs index 3ca19a3912568..b7420280a8818 100644 --- a/crates/bevy_reflect/src/serde/ser/serializable.rs +++ b/crates/bevy_reflect/src/serde/ser/serializable.rs @@ -1,8 +1,4 @@ -use crate::{ - serde::ser::error_utils::make_custom_error, PartialReflect, ReflectSerialize, TypeRegistry, -}; use core::ops::Deref; -use serde::ser::Error; /// A type-erased serializable value. pub enum Serializable<'a> { @@ -10,47 +6,6 @@ pub enum Serializable<'a> { Borrowed(&'a dyn erased_serde::Serialize), } -impl<'a> Serializable<'a> { - /// Attempts to create a [`Serializable`] from a [`PartialReflect`] value. - /// - /// Returns an error if any of the following conditions are met: - /// - The underlying type of `value` does not implement [`Reflect`]. - /// - The underlying type of `value` does not represent any type (via [`PartialReflect::get_represented_type_info`]). - /// - The represented type of `value` is not registered in the `type_registry`. - /// - The represented type of `value` did not register the [`ReflectSerialize`] type data. - /// - /// [`Reflect`]: crate::Reflect - pub fn try_from_reflect_value( - value: &'a dyn PartialReflect, - type_registry: &TypeRegistry, - ) -> Result, E> { - let value = value.try_as_reflect().ok_or_else(|| { - make_custom_error(format_args!( - "type `{}` does not implement `Reflect`", - value.reflect_type_path() - )) - })?; - - let info = value.reflect_type_info(); - - let registration = type_registry.get(info.type_id()).ok_or_else(|| { - make_custom_error(format_args!( - "type `{}` is not registered in the type registry", - info.type_path(), - )) - })?; - - let reflect_serialize = registration.data::().ok_or_else(|| { - make_custom_error(format_args!( - "type `{}` did not register the `ReflectSerialize` type data. For certain types, this may need to be registered manually using `register_type_data`", - info.type_path(), - )) - })?; - - Ok(reflect_serialize.get_serializable(value)) - } -} - impl<'a> Deref for Serializable<'a> { type Target = dyn erased_serde::Serialize + 'a; diff --git a/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs new file mode 100644 index 0000000000000..5d6c82efcdcbe --- /dev/null +++ b/crates/bevy_reflect/src/serde/ser/serialize_with_registry.rs @@ -0,0 +1,100 @@ +use crate::{FromType, Reflect, TypeRegistry}; +use serde::{Serialize, Serializer}; + +/// Trait used to provide finer control when serializing a reflected type with one of +/// the reflection serializers. +/// +/// This trait is the reflection equivalent of `serde`'s [`Serialize`] trait. +/// The main difference is that this trait provides access to the [`TypeRegistry`], +/// which means that we can use the registry and all its stored type information +/// to serialize our type. +/// +/// This can be useful when writing a custom reflection serializer where we may +/// want to handle parts of the serialization process, but temporarily pass control +/// to the standard reflection serializer for other parts. +/// +/// For the deserialization equivalent of this trait, see [`DeserializeWithRegistry`]. +/// +/// # Rationale +/// +/// Without this trait and its associated [type data], such a serializer would have to +/// write out all of the serialization logic itself, possibly including +/// unnecessary code duplication and trivial implementations. +/// +/// This is because a normal [`Serialize`] implementation has no knowledge of the +/// [`TypeRegistry`] and therefore cannot create a reflection-based serializer for +/// nested items. +/// +/// # Implementors +/// +/// In order for this to work with the reflection serializers like [`TypedReflectSerializer`] +/// and [`ReflectSerializer`], implementors should be sure to register the +/// [`ReflectSerializeWithRegistry`] type data. +/// This can be done [via the registry] or by adding `#[reflect(SerializeWithRegistry)]` to +/// the type definition. +/// +/// [`DeserializeWithRegistry`]: crate::serde::DeserializeWithRegistry +/// [type data]: ReflectSerializeWithRegistry +/// [`TypedReflectSerializer`]: crate::serde::TypedReflectSerializer +/// [`ReflectSerializer`]: crate::serde::ReflectSerializer +/// [via the registry]: TypeRegistry::register_type_data +pub trait SerializeWithRegistry { + fn serialize(&self, serializer: S, registry: &TypeRegistry) -> Result + where + S: Serializer; +} + +/// Type data used to serialize a [`Reflect`] type with a custom [`SerializeWithRegistry`] implementation. +#[derive(Clone)] +pub struct ReflectSerializeWithRegistry { + serialize: for<'a> fn( + value: &'a dyn Reflect, + registry: &'a TypeRegistry, + ) -> Box, +} + +impl ReflectSerializeWithRegistry { + /// Serialize a [`Reflect`] type with this type data's custom [`SerializeWithRegistry`] implementation. + pub fn serialize( + &self, + value: &dyn Reflect, + serializer: S, + registry: &TypeRegistry, + ) -> Result + where + S: Serializer, + { + ((self.serialize)(value, registry)).serialize(serializer) + } +} + +impl FromType for ReflectSerializeWithRegistry { + fn from_type() -> Self { + Self { + serialize: |value: &dyn Reflect, registry| { + let value = value.downcast_ref::().unwrap_or_else(|| { + panic!( + "Expected value to be of type {:?} but received {:?}", + core::any::type_name::(), + value.reflect_type_path() + ) + }); + Box::new(SerializableWithRegistry { value, registry }) + }, + } + } +} + +struct SerializableWithRegistry<'a, T: SerializeWithRegistry> { + value: &'a T, + registry: &'a TypeRegistry, +} + +impl<'a, T: SerializeWithRegistry> Serialize for SerializableWithRegistry<'a, T> { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + self.value.serialize(serializer, self.registry) + } +} diff --git a/crates/bevy_reflect/src/serde/ser/serializer.rs b/crates/bevy_reflect/src/serde/ser/serializer.rs index 55fad6d8e6281..a803399829376 100644 --- a/crates/bevy_reflect/src/serde/ser/serializer.rs +++ b/crates/bevy_reflect/src/serde/ser/serializer.rs @@ -1,14 +1,11 @@ #[cfg(feature = "debug_stack")] use crate::serde::ser::error_utils::TYPE_INFO_STACK; use crate::{ - serde::{ - ser::{ - arrays::ArraySerializer, enums::EnumSerializer, error_utils::make_custom_error, - lists::ListSerializer, maps::MapSerializer, sets::SetSerializer, - structs::StructSerializer, tuple_structs::TupleStructSerializer, - tuples::TupleSerializer, - }, - Serializable, + serde::ser::{ + arrays::ArraySerializer, custom_serialization::try_custom_serialize, enums::EnumSerializer, + error_utils::make_custom_error, lists::ListSerializer, maps::MapSerializer, + sets::SetSerializer, structs::StructSerializer, tuple_structs::TupleStructSerializer, + tuples::TupleSerializer, }, PartialReflect, ReflectRef, TypeRegistry, }; @@ -158,16 +155,12 @@ impl<'a> Serialize for TypedReflectSerializer<'a> { TYPE_INFO_STACK.with_borrow_mut(|stack| stack.push(info)); } } - // Handle both Value case and types that have a custom `Serialize` - let serializable = - Serializable::try_from_reflect_value::(self.value, self.registry); - if let Ok(serializable) = serializable { - #[cfg(feature = "debug_stack")] - TYPE_INFO_STACK.with_borrow_mut(crate::type_info_stack::TypeInfoStack::pop); - - return serializable.serialize(serializer); - } + let (serializer, error) = match try_custom_serialize(self.value, self.registry, serializer) + { + Ok(result) => return result, + Err(value) => value, + }; let output = match self.value.reflect_ref() { ReflectRef::Struct(value) => { @@ -196,7 +189,7 @@ impl<'a> Serialize for TypedReflectSerializer<'a> { } #[cfg(feature = "functions")] ReflectRef::Function(_) => Err(make_custom_error("functions cannot be serialized")), - ReflectRef::Opaque(_) => Err(serializable.err().unwrap()), + ReflectRef::Opaque(_) => Err(error), }; #[cfg(feature = "debug_stack")] diff --git a/crates/bevy_reflect/src/tuple.rs b/crates/bevy_reflect/src/tuple.rs index a0bb522c574e2..5eb3d3817e56c 100644 --- a/crates/bevy_reflect/src/tuple.rs +++ b/crates/bevy_reflect/src/tuple.rs @@ -676,6 +676,7 @@ impl_reflect_tuple! {0: A, 1: B, 2: C, 3: D, 4: E, 5: F, 6: G, 7: H, 8: I, 9: J, macro_rules! impl_type_path_tuple { ($(#[$meta:meta])*) => { + $(#[$meta])* impl TypePath for () { fn type_path() -> &'static str { "()" @@ -737,29 +738,50 @@ all_tuples!( #[cfg(feature = "functions")] const _: () = { macro_rules! impl_get_ownership_tuple { - ($($name: ident),*) => { + ($(#[$meta:meta])* $($name: ident),*) => { + $(#[$meta])* $crate::func::args::impl_get_ownership!(($($name,)*); <$($name),*>); }; } - all_tuples!(impl_get_ownership_tuple, 0, 12, P); + all_tuples!( + #[doc(fake_variadic)] + impl_get_ownership_tuple, + 0, + 12, + P + ); macro_rules! impl_from_arg_tuple { - ($($name: ident),*) => { + ($(#[$meta:meta])* $($name: ident),*) => { + $(#[$meta])* $crate::func::args::impl_from_arg!(($($name,)*); <$($name: FromReflect + MaybeTyped + TypePath + GetTypeRegistration),*>); }; } - all_tuples!(impl_from_arg_tuple, 0, 12, P); + all_tuples!( + #[doc(fake_variadic)] + impl_from_arg_tuple, + 0, + 12, + P + ); macro_rules! impl_into_return_tuple { - ($($name: ident),+) => { + ($(#[$meta:meta])* $($name: ident),+) => { + $(#[$meta])* $crate::func::impl_into_return!(($($name,)*); <$($name: FromReflect + MaybeTyped + TypePath + GetTypeRegistration),*>); }; } // The unit type (i.e. `()`) is special-cased, so we skip implementing it here. - all_tuples!(impl_into_return_tuple, 1, 12, P); + all_tuples!( + #[doc(fake_variadic)] + impl_into_return_tuple, + 1, + 12, + P + ); }; #[cfg(test)] diff --git a/crates/bevy_remote/src/builtin_methods.rs b/crates/bevy_remote/src/builtin_methods.rs index 67a26e15f6df8..168999d6226bc 100644 --- a/crates/bevy_remote/src/builtin_methods.rs +++ b/crates/bevy_remote/src/builtin_methods.rs @@ -18,7 +18,7 @@ use bevy_reflect::{ }; use bevy_utils::HashMap; use serde::{de::DeserializeSeed as _, Deserialize, Serialize}; -use serde_json::Value; +use serde_json::{Map, Value}; use crate::{error_codes, BrpError, BrpResult}; @@ -64,6 +64,11 @@ pub struct BrpGetParams { /// /// [full paths]: bevy_reflect::TypePath::type_path pub components: Vec, + + /// An optional flag to fail when encountering an invalid component rather + /// than skipping it. Defaults to false. + #[serde(default)] + pub strict: bool, } /// `bevy/query`: Performs a query over components in the ECS, returning entities @@ -228,7 +233,20 @@ pub struct BrpSpawnResponse { } /// The response to a `bevy/get` request. -pub type BrpGetResponse = HashMap; +#[derive(Debug, Serialize, Deserialize, Clone)] +#[serde(untagged)] +pub enum BrpGetResponse { + /// The non-strict response that reports errors separately without failing the entire request. + Lenient { + /// A map of successful components with their values. + components: HashMap, + /// A map of unsuccessful components with their errors. + errors: HashMap, + }, + /// The strict response that will fail if any components are not present or aren't + /// reflect-able. + Strict(HashMap), +} /// The response to a `bevy/list` request. pub type BrpListResponse = Vec; @@ -273,44 +291,80 @@ fn parse_some Deserialize<'de>>(value: Option) -> Result>, world: &World) -> BrpResult { - let BrpGetParams { entity, components } = parse_some(params)?; + let BrpGetParams { + entity, + components, + strict, + } = parse_some(params)?; let app_type_registry = world.resource::(); let type_registry = app_type_registry.read(); let entity_ref = get_entity(world, entity)?; - let mut response = BrpGetResponse::default(); + let mut response = if strict { + BrpGetResponse::Strict(Default::default()) + } else { + BrpGetResponse::Lenient { + components: Default::default(), + errors: Default::default(), + } + }; for component_path in components { - let reflect_component = get_reflect_component(&type_registry, &component_path) - .map_err(BrpError::component_error)?; + match handle_get_component(&component_path, entity, entity_ref, &type_registry) { + Ok(serialized_object) => match response { + BrpGetResponse::Strict(ref mut components) + | BrpGetResponse::Lenient { + ref mut components, .. + } => { + components.extend(serialized_object.into_iter()); + } + }, + Err(err) => match response { + BrpGetResponse::Strict(_) => return Err(err), + BrpGetResponse::Lenient { ref mut errors, .. } => { + let err_value = serde_json::to_value(err).map_err(BrpError::internal)?; + errors.insert(component_path, err_value); + } + }, + } + } - // Retrieve the reflected value for the given specified component on the given entity. - let Some(reflected) = reflect_component.reflect(entity_ref) else { - return Err(BrpError::component_not_present(&component_path, entity)); - }; + serde_json::to_value(response).map_err(BrpError::internal) +} - // Each component value serializes to a map with a single entry. - let reflect_serializer = - ReflectSerializer::new(reflected.as_partial_reflect(), &type_registry); - let Value::Object(serialized_object) = - serde_json::to_value(&reflect_serializer).map_err(|err| BrpError { - code: error_codes::COMPONENT_ERROR, - message: err.to_string(), - data: None, - })? - else { - return Err(BrpError { - code: error_codes::COMPONENT_ERROR, - message: format!("Component `{}` could not be serialized", component_path), - data: None, - }); - }; +/// Handle a single component for [`process_remote_get_request`]. +fn handle_get_component( + component_path: &str, + entity: Entity, + entity_ref: EntityRef, + type_registry: &TypeRegistry, +) -> Result, BrpError> { + let reflect_component = + get_reflect_component(type_registry, component_path).map_err(BrpError::component_error)?; - response.extend(serialized_object.into_iter()); - } + // Retrieve the reflected value for the given specified component on the given entity. + let Some(reflected) = reflect_component.reflect(entity_ref) else { + return Err(BrpError::component_not_present(component_path, entity)); + }; - serde_json::to_value(response).map_err(BrpError::internal) + // Each component value serializes to a map with a single entry. + let reflect_serializer = ReflectSerializer::new(reflected.as_partial_reflect(), type_registry); + let Value::Object(serialized_object) = + serde_json::to_value(&reflect_serializer).map_err(|err| BrpError { + code: error_codes::COMPONENT_ERROR, + message: err.to_string(), + data: None, + })? + else { + return Err(BrpError { + code: error_codes::COMPONENT_ERROR, + message: format!("Component `{}` could not be serialized", component_path), + data: None, + }); + }; + + Ok(serialized_object) } /// Handles a `bevy/query` request coming from a client. diff --git a/crates/bevy_remote/src/lib.rs b/crates/bevy_remote/src/lib.rs index 8f11a18adb041..57bc05035346b 100644 --- a/crates/bevy_remote/src/lib.rs +++ b/crates/bevy_remote/src/lib.rs @@ -109,6 +109,17 @@ //! `params`: //! - `entity`: The ID of the entity whose components will be fetched. //! - `components`: An array of [fully-qualified type names] of components to fetch. +//! - `strict` (optional): A flag to enable strict mode which will fail if any one of the +//! components is not present or can not be reflected. Defaults to false. +//! +//! If `strict` is false: +//! +//! `result`: +//! - `components`: A map associating each type name to its value on the requested entity. +//! - `errors`: A map associating each type name with an error if it was not on the entity +//! or could not be reflected. +//! +//! If `strict` is true: //! //! `result`: A map associating each type name to its value on the requested entity. //! @@ -388,6 +399,11 @@ impl RemoteMethods { ) -> Option { self.0.insert(method_name.into(), handler) } + + /// Retrieves a handler by method name. + pub fn get(&self, method_name: &str) -> Option<&RemoteMethod> { + self.0.get(method_name) + } } /// A single request from a Bevy Remote Protocol client to the server, diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 1cd6238a5073c..702a3aea4b7c5 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -67,6 +67,54 @@ impl Default for Viewport { } } +/// Settings to define a camera sub view. +/// +/// When [`Camera::sub_camera_view`] is `Some`, only the sub-section of the +/// image defined by `size` and `offset` (relative to the `full_size` of the +/// whole image) is projected to the cameras viewport. +/// +/// Take the example of the following multi-monitor setup: +/// ```css +/// ┌───┬───┐ +/// │ A │ B │ +/// ├───┼───┤ +/// │ C │ D │ +/// └───┴───┘ +/// ``` +/// If each monitor is 1920x1080, the whole image will have a resolution of +/// 3840x2160. For each monitor we can use a single camera with a viewport of +/// the same size as the monitor it corresponds to. To ensure that the image is +/// cohesive, we can use a different sub view on each camera: +/// - Camera A: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 0,0 +/// - Camera B: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 1920,0 +/// - Camera C: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 0,1080 +/// - Camera D: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = +/// 1920,1080 +/// +/// However since only the ratio between the values is important, they could all +/// be divided by 120 and still produce the same image. Camera D would for +/// example have the following values: +/// `full_size` = 32x18, `size` = 16x9, `offset` = 16,9 +#[derive(Debug, Clone, Copy, Reflect, PartialEq)] +pub struct SubCameraView { + /// Size of the entire camera view + pub full_size: UVec2, + /// Offset of the sub camera + pub offset: Vec2, + /// Size of the sub camera + pub size: UVec2, +} + +impl Default for SubCameraView { + fn default() -> Self { + Self { + full_size: UVec2::new(1, 1), + offset: Vec2::new(0., 0.), + size: UVec2::new(1, 1), + } + } +} + /// Information about the current [`RenderTarget`]. #[derive(Default, Debug, Clone)] pub struct RenderTargetInfo { @@ -86,6 +134,7 @@ pub struct ComputedCameraValues { target_info: Option, // size of the `Viewport` old_viewport_size: Option, + old_sub_camera_view: Option, } /// How much energy a `Camera3d` absorbs from incoming light. @@ -256,6 +305,8 @@ pub struct Camera { pub msaa_writeback: bool, /// The clear color operation to perform on the render target. pub clear_color: ClearColorConfig, + /// If set, this camera will be a sub camera of a large view, defined by a [`SubCameraView`]. + pub sub_camera_view: Option, } impl Default for Camera { @@ -270,6 +321,7 @@ impl Default for Camera { hdr: false, msaa_writeback: true, clear_color: Default::default(), + sub_camera_view: None, } } } @@ -843,6 +895,7 @@ pub fn camera_system( || camera.is_added() || camera_projection.is_changed() || camera.computed.old_viewport_size != viewport_size + || camera.computed.old_sub_camera_view != camera.sub_camera_view { let new_computed_target_info = normalized_target.get_render_target_info( &windows, @@ -890,7 +943,10 @@ pub fn camera_system( camera.computed.target_info = new_computed_target_info; if let Some(size) = camera.logical_viewport_size() { camera_projection.update(size.x, size.y); - camera.computed.clip_from_view = camera_projection.get_clip_from_view(); + camera.computed.clip_from_view = match &camera.sub_camera_view { + Some(sub_view) => camera_projection.get_clip_from_view_for_sub(sub_view), + None => camera_projection.get_clip_from_view(), + } } } } @@ -898,6 +954,10 @@ pub fn camera_system( if camera.computed.old_viewport_size != viewport_size { camera.computed.old_viewport_size = viewport_size; } + + if camera.computed.old_sub_camera_view != camera.sub_camera_view { + camera.computed.old_sub_camera_view = camera.sub_camera_view; + } } } @@ -991,7 +1051,7 @@ pub fn extract_cameras( } let mut commands = commands.entity(render_entity.id()); - commands = commands.insert(( + commands.insert(( ExtractedCamera { target: camera.target.normalize(primary_window), viewport: camera.viewport.clone(), @@ -1027,15 +1087,15 @@ pub fn extract_cameras( )); if let Some(temporal_jitter) = temporal_jitter { - commands = commands.insert(temporal_jitter.clone()); + commands.insert(temporal_jitter.clone()); } if let Some(render_layers) = render_layers { - commands = commands.insert(render_layers.clone()); + commands.insert(render_layers.clone()); } if let Some(perspective) = projection { - commands = commands.insert(perspective.clone()); + commands.insert(perspective.clone()); } if gpu_culling { if *gpu_preprocessing_support == GpuPreprocessingSupport::Culling { diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 91c9b9cc8d8ec..832ba6e56f0bf 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -6,7 +6,7 @@ use core::{ use crate::{primitives::Frustum, view::VisibilitySystems}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_ecs::prelude::*; -use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A}; +use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{ std_traits::ReflectDefault, GetTypeRegistration, Reflect, ReflectDeserialize, ReflectSerialize, }; @@ -76,6 +76,7 @@ pub struct CameraUpdateSystem; /// [`Camera`]: crate::camera::Camera pub trait CameraProjection { fn get_clip_from_view(&self) -> Mat4; + fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4; fn update(&mut self, width: f32, height: f32); fn far(&self) -> f32; fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8]; @@ -124,6 +125,13 @@ impl CameraProjection for Projection { } } + fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { + match self { + Projection::Perspective(projection) => projection.get_clip_from_view_for_sub(sub_view), + Projection::Orthographic(projection) => projection.get_clip_from_view_for_sub(sub_view), + } + } + fn update(&mut self, width: f32, height: f32) { match self { Projection::Perspective(projection) => projection.update(width, height), @@ -189,6 +197,45 @@ impl CameraProjection for PerspectiveProjection { Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near) } + fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { + let full_width = sub_view.full_size.x as f32; + let full_height = sub_view.full_size.y as f32; + let sub_width = sub_view.size.x as f32; + let sub_height = sub_view.size.y as f32; + let offset_x = sub_view.offset.x; + // Y-axis increases from top to bottom + let offset_y = full_height - (sub_view.offset.y + sub_height); + + // Original frustum parameters + let top = self.near * ops::tan(0.5 * self.fov); + let bottom = -top; + let right = top * self.aspect_ratio; + let left = -right; + + // Calculate scaling factors + let width = right - left; + let height = top - bottom; + + // Calculate the new frustum parameters + let left_prime = left + (width * offset_x) / full_width; + let right_prime = left + (width * (offset_x + sub_width)) / full_width; + let bottom_prime = bottom + (height * offset_y) / full_height; + let top_prime = bottom + (height * (offset_y + sub_height)) / full_height; + + // Compute the new projection matrix + let x = (2.0 * self.near) / (right_prime - left_prime); + let y = (2.0 * self.near) / (top_prime - bottom_prime); + let a = (right_prime + left_prime) / (right_prime - left_prime); + let b = (top_prime + bottom_prime) / (top_prime - bottom_prime); + + Mat4::from_cols( + Vec4::new(x, 0.0, 0.0, 0.0), + Vec4::new(0.0, y, 0.0, 0.0), + Vec4::new(a, b, 0.0, -1.0), + Vec4::new(0.0, 0.0, self.near, 0.0), + ) + } + fn update(&mut self, width: f32, height: f32) { self.aspect_ratio = AspectRatio::try_new(width, height) .expect("Failed to update PerspectiveProjection: width and height must be positive, non-zero values") @@ -395,6 +442,42 @@ impl CameraProjection for OrthographicProjection { ) } + fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { + let full_width = sub_view.full_size.x as f32; + let full_height = sub_view.full_size.y as f32; + let offset_x = sub_view.offset.x; + let offset_y = sub_view.offset.y; + let sub_width = sub_view.size.x as f32; + let sub_height = sub_view.size.y as f32; + + // Orthographic projection parameters + let top = self.area.max.y; + let bottom = self.area.min.y; + let right = self.area.max.x; + let left = self.area.min.x; + + // Calculate scaling factors + let scale_w = (right - left) / full_width; + let scale_h = (top - bottom) / full_height; + + // Calculate the new orthographic bounds + let left_prime = left + scale_w * offset_x; + let right_prime = left_prime + scale_w * sub_width; + let top_prime = top - scale_h * offset_y; + let bottom_prime = top_prime - scale_h * sub_height; + + Mat4::orthographic_rh( + left_prime, + right_prime, + bottom_prime, + top_prime, + // NOTE: near and far are swapped to invert the depth range from [0,1] to [1,0] + // This is for interoperability with pipelines using infinite reverse perspective projections. + self.far, + self.near, + ) + } + fn update(&mut self, width: f32, height: f32) { let (projection_width, projection_height) = match self.scaling_mode { ScalingMode::WindowSize(pixel_scale) => (width / pixel_scale, height / pixel_scale), diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 51bd01c612d30..40a8c1ebb0d5e 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -54,7 +54,10 @@ pub mod prelude { Camera, ClearColor, ClearColorConfig, OrthographicProjection, PerspectiveProjection, Projection, }, - mesh::{morph::MorphWeights, primitives::MeshBuilder, primitives::Meshable, Mesh}, + mesh::{ + morph::MorphWeights, primitives::MeshBuilder, primitives::Meshable, Mesh, Mesh2d, + Mesh3d, + }, render_resource::Shader, spatial_bundle::SpatialBundle, texture::{image_texture_conversion::IntoDynamicImageError, Image, ImagePlugin}, diff --git a/crates/bevy_render/src/mesh/allocator.rs b/crates/bevy_render/src/mesh/allocator.rs index e20cb1774453e..c9d36c5855b51 100644 --- a/crates/bevy_render/src/mesh/allocator.rs +++ b/crates/bevy_render/src/mesh/allocator.rs @@ -427,7 +427,7 @@ impl MeshAllocator { if self.general_vertex_slabs_supported { self.allocate( mesh_id, - mesh.get_vertex_buffer_data().len() as u64, + mesh.get_vertex_size() * mesh.count_vertices() as u64, vertex_element_layout, &mut slabs_to_grow, mesh_allocator_settings, @@ -474,12 +474,11 @@ impl MeshAllocator { let Some(&slab_id) = self.mesh_id_to_vertex_slab.get(mesh_id) else { return; }; - let vertex_data = mesh.get_vertex_buffer_data(); + let vertex_data = mesh.create_packed_vertex_buffer_data(); // Call the generic function. self.copy_element_data( mesh_id, - mesh, &vertex_data, BufferUsages::VERTEX, slab_id, @@ -507,7 +506,6 @@ impl MeshAllocator { // Call the generic function. self.copy_element_data( mesh_id, - mesh, index_data, BufferUsages::INDEX, slab_id, @@ -521,7 +519,6 @@ impl MeshAllocator { fn copy_element_data( &mut self, mesh_id: &AssetId, - mesh: &Mesh, data: &[u8], buffer_usages: BufferUsages, slab_id: SlabId, @@ -567,7 +564,7 @@ impl MeshAllocator { slab_id, buffer_usages_to_str(buffer_usages) )), - contents: &mesh.get_vertex_buffer_data(), + contents: data, usage: buffer_usages | BufferUsages::COPY_DST, }, )); diff --git a/crates/bevy_render/src/mesh/components.rs b/crates/bevy_render/src/mesh/components.rs new file mode 100644 index 0000000000000..89ff93e76bfa8 --- /dev/null +++ b/crates/bevy_render/src/mesh/components.rs @@ -0,0 +1,115 @@ +use crate::{mesh::Mesh, view::Visibility}; +use bevy_asset::{AssetId, Handle}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{component::Component, reflect::ReflectComponent}; +use bevy_reflect::{std_traits::ReflectDefault, Reflect}; +use bevy_transform::components::Transform; + +/// A component for rendering 2D meshes, typically with a [`MeshMaterial2d`] using a [`ColorMaterial`]. +/// +/// Meshes without a [`MeshMaterial2d`] will be rendered with a [default material]. +/// +/// [`MeshMaterial2d`]: +/// [`ColorMaterial`]: +/// [default material]: +/// +/// # Example +/// +/// ```ignore +/// # use bevy_sprite::{ColorMaterial, Mesh2d, MeshMaterial2d}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::mesh::Mesh; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::Assets; +/// # use bevy_math::primitives::Circle; +/// # +/// // Spawn an entity with a mesh using `ColorMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// commands.spawn(( +/// Mesh2d(meshes.add(Circle::new(50.0))), +/// MeshMaterial2d(materials.add(ColorMaterial::from_color(RED))), +/// )); +/// } +/// ``` +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default)] +#[require(Transform, Visibility)] +pub struct Mesh2d(pub Handle); + +impl From> for Mesh2d { + fn from(handle: Handle) -> Self { + Self(handle) + } +} + +impl From for AssetId { + fn from(mesh: Mesh2d) -> Self { + mesh.id() + } +} + +impl From<&Mesh2d> for AssetId { + fn from(mesh: &Mesh2d) -> Self { + mesh.id() + } +} + +/// A component for rendering 3D meshes, typically with a [`MeshMaterial3d`] using a [`StandardMaterial`]. +/// +/// Meshes without a [`MeshMaterial3d`] will be rendered with a [default material]. +/// +/// [`MeshMaterial3d`]: +/// [`StandardMaterial`]: +/// [default material]: +/// +/// # Example +/// +/// ```ignore +/// # use bevy_pbr::{Material, MeshMaterial3d, StandardMaterial}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::mesh::{Mesh, Mesh3d}; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::Assets; +/// # use bevy_math::primitives::Capsule3d; +/// # +/// // Spawn an entity with a mesh using `StandardMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// commands.spawn(( +/// Mesh3d(meshes.add(Capsule3d::default())), +/// MeshMaterial3d(materials.add(StandardMaterial { +/// base_color: RED.into(), +/// ..Default::default() +/// })), +/// )); +/// } +/// ``` +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default)] +#[require(Transform, Visibility)] +pub struct Mesh3d(pub Handle); + +impl From> for Mesh3d { + fn from(handle: Handle) -> Self { + Self(handle) + } +} + +impl From for AssetId { + fn from(mesh: Mesh3d) -> Self { + mesh.id() + } +} + +impl From<&Mesh3d> for AssetId { + fn from(mesh: &Mesh3d) -> Self { + mesh.id() + } +} diff --git a/crates/bevy_render/src/mesh/mesh/mod.rs b/crates/bevy_render/src/mesh/mesh/mod.rs index 46faf1908eff5..1f40cb8b41a4c 100644 --- a/crates/bevy_render/src/mesh/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mesh/mod.rs @@ -36,11 +36,10 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// /// Meshes can be automatically generated by a bevy `AssetLoader` (generally by loading a `Gltf` file), /// or by converting a [primitive](bevy_math::primitives) using [`into`](Into). -/// It is also possible to create one manually. -/// They can be edited after creation. +/// It is also possible to create one manually. They can be edited after creation. /// -/// Meshes can be rendered with a `Material`, like `StandardMaterial` in `PbrBundle` -/// or `ColorMaterial` in `ColorMesh2dBundle`. +/// Meshes can be rendered with a [`Mesh2d`](super::Mesh2d) and `MeshMaterial2d` +/// or [`Mesh3d`](super::Mesh3d) and `MeshMaterial3d` for 2D and 3D respectively. /// /// A [`Mesh`] in Bevy is equivalent to a "primitive" in the glTF format, for a /// glTF Mesh representation, see `GltfMesh`. @@ -49,6 +48,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// /// The following function will construct a flat mesh, to be rendered with a /// `StandardMaterial` or `ColorMaterial`: +/// /// ``` /// # use bevy_render::mesh::{Mesh, Indices}; /// # use bevy_render::render_resource::PrimitiveTopology; @@ -84,7 +84,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// ``` /// /// You can see how it looks like [here](https://github.com/bevyengine/bevy/blob/main/assets/docs/Mesh.png), -/// used in a `PbrBundle` with a square bevy logo texture, with added axis, points, +/// used in a `Mesh3d` with a square bevy logo texture, with added axis, points, /// lines and text for clarity. /// /// ## Other examples @@ -92,7 +92,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// For further visualization, explanation, and examples, see the built-in Bevy examples, /// and the [implementation of the built-in shapes](https://github.com/bevyengine/bevy/tree/main/crates/bevy_render/src/mesh/primitives). /// In particular, [generate_custom_mesh](https://github.com/bevyengine/bevy/blob/main/examples/3d/generate_custom_mesh.rs) -/// teaches you to access modify a Mesh's attributes after creating it. +/// teaches you to access and modify the attributes of a [`Mesh`] after creating it. /// /// ## Common points of confusion /// @@ -458,13 +458,8 @@ impl Mesh { /// /// If the vertex attributes have different lengths, they are all truncated to /// the length of the smallest. - pub fn get_vertex_buffer_data(&self) -> Vec { - let mut vertex_size = 0; - for attribute_data in self.attributes.values() { - let vertex_format = attribute_data.attribute.format; - vertex_size += vertex_format.get_size() as usize; - } - + pub fn create_packed_vertex_buffer_data(&self) -> Vec { + 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 diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index 6897e1d176f74..3b6572ea100f9 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -2,12 +2,14 @@ mod mesh; pub mod allocator; +mod components; pub mod morph; pub mod primitives; use alloc::sync::Arc; use allocator::MeshAllocatorPlugin; use bevy_utils::HashSet; +pub use components::{Mesh2d, Mesh3d}; use core::hash::{Hash, Hasher}; pub use mesh::*; pub use primitives::*; @@ -25,6 +27,7 @@ impl Plugin for MeshPlugin { app.init_asset::() .init_asset::() .register_asset_reflect::() + .register_type::() .register_type::() .register_type::>() // 'Mesh' must be prepared after 'Image' as meshes rely on the morph target image being ready diff --git a/crates/bevy_render/src/mesh/morph.rs b/crates/bevy_render/src/mesh/morph.rs index db28ed5afea0e..3096842e73b11 100644 --- a/crates/bevy_render/src/mesh/morph.rs +++ b/crates/bevy_render/src/mesh/morph.rs @@ -14,6 +14,8 @@ use bytemuck::{Pod, Zeroable}; use core::iter; use thiserror::Error; +use super::Mesh3d; + const MAX_TEXTURE_WIDTH: u32 = 2048; // NOTE: "component" refers to the element count of math objects, // Vec3 has 3 components, Mat2 has 4 components. @@ -114,7 +116,7 @@ impl MorphTargetImage { } } -/// Controls the [morph targets] for all child [`Handle`] entities. In most cases, [`MorphWeights`] should be considered +/// Controls the [morph targets] for all child [`Mesh3d`] entities. In most cases, [`MorphWeights`] should be considered /// the "source of truth" when writing morph targets for meshes. However you can choose to write child [`MeshMorphWeights`] /// if your situation requires more granularity. Just note that if you set [`MorphWeights`], it will overwrite child /// [`MeshMorphWeights`] values. @@ -122,9 +124,9 @@ impl MorphTargetImage { /// This exists because Bevy's [`Mesh`] corresponds to a _single_ surface / material, whereas morph targets /// as defined in the GLTF spec exist on "multi-primitive meshes" (where each primitive is its own surface with its own material). /// Therefore in Bevy [`MorphWeights`] an a parent entity are the "canonical weights" from a GLTF perspective, which then -/// synchronized to child [`Handle`] / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective). +/// synchronized to child [`Mesh3d`] / [`MeshMorphWeights`] (which correspond to "primitives" / "surfaces" from a GLTF perspective). /// -/// Add this to the parent of one or more [`Entities`](`Entity`) with a [`Handle`] with a [`MeshMorphWeights`]. +/// Add this to the parent of one or more [`Entities`](`Entity`) with a [`Mesh3d`] with a [`MeshMorphWeights`]. /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation #[derive(Reflect, Default, Debug, Clone, Component)] @@ -148,7 +150,7 @@ impl MorphWeights { first_mesh, }) } - /// The first child [`Handle`] primitive controlled by these weights. + /// The first child [`Mesh3d`] primitive controlled by these weights. /// This can be used to look up metadata information such as [`Mesh::morph_target_names`]. pub fn first_mesh(&self) -> Option<&Handle> { self.first_mesh.as_ref() @@ -168,7 +170,7 @@ impl MorphWeights { /// /// See [`MorphWeights`] for more details on Bevy's morph target implementation. /// -/// Add this to an [`Entity`] with a [`Handle`] with a [`MorphAttributes`] set +/// Add this to an [`Entity`] with a [`Mesh3d`] with a [`MorphAttributes`] set /// to control individual weights of each morph target. /// /// [morph targets]: https://en.wikipedia.org/wiki/Morph_target_animation @@ -198,8 +200,8 @@ impl MeshMorphWeights { /// /// Only direct children are updated, to fulfill the expectations of glTF spec. pub fn inherit_weights( - morph_nodes: Query<(&Children, &MorphWeights), (Without>, Changed)>, - mut morph_primitives: Query<&mut MeshMorphWeights, With>>, + morph_nodes: Query<(&Children, &MorphWeights), (Without, Changed)>, + mut morph_primitives: Query<&mut MeshMorphWeights, With>, ) { for (children, parent_weights) in &morph_nodes { let mut iter = morph_primitives.iter_many_mut(children); diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index c2909b795b295..66925005ef956 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -17,19 +17,17 @@ use bevy_reflect::prelude::*; /// with the camera's [`Frustum`]. /// /// It will be added automatically by the systems in [`CalculateBounds`] to entities that: -/// - could be subject to frustum culling, for example with a [`Handle`] +/// - could be subject to frustum culling, for example with a [`Mesh3d`] /// or `Sprite` component, /// - don't have the [`NoFrustumCulling`] component. /// /// It won't be updated automatically if the space occupied by the entity changes, -/// for example if the vertex positions of a [`Mesh`] inside a `Handle` are -/// updated. +/// for example if the vertex positions of a [`Mesh3d`] are updated. /// /// [`Camera`]: crate::camera::Camera /// [`NoFrustumCulling`]: crate::view::visibility::NoFrustumCulling /// [`CalculateBounds`]: crate::view::visibility::VisibilitySystems::CalculateBounds -/// [`Mesh`]: crate::mesh::Mesh -/// [`Handle`]: crate::mesh::Mesh +/// [`Mesh3d`]: crate::mesh::Mesh #[derive(Component, Clone, Copy, Debug, Default, Reflect, PartialEq)] #[reflect(Component, Default, Debug, PartialEq)] pub struct Aabb { diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 68e784dd09a7c..8f9a8748d69d0 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -9,7 +9,7 @@ pub use range::*; pub use render_layers::*; use bevy_app::{Plugin, PostUpdate}; -use bevy_asset::{Assets, Handle}; +use bevy_asset::Assets; use bevy_derive::Deref; use bevy_ecs::{prelude::*, query::QueryFilter}; use bevy_hierarchy::{Children, Parent}; @@ -19,7 +19,7 @@ use bevy_utils::{Parallel, TypeIdMap}; use crate::{ camera::{Camera, CameraProjection}, - mesh::Mesh, + mesh::{Mesh, Mesh3d}, primitives::{Aabb, Frustum, Sphere}, }; @@ -271,10 +271,6 @@ impl VisibleEntities { } } -/// A convenient alias for `With>`, for use with -/// [`VisibleEntities`]. -pub type WithMesh = With>; - #[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)] pub enum VisibilitySystems { /// Label for the [`calculate_bounds`], `calculate_bounds_2d` and `calculate_bounds_text2d` systems, @@ -312,20 +308,20 @@ impl Plugin for VisibilityPlugin { ( calculate_bounds.in_set(CalculateBounds), (visibility_propagate_system, reset_view_visibility).in_set(VisibilityPropagate), - check_visibility::.in_set(CheckVisibility), + check_visibility::>.in_set(CheckVisibility), ), ); } } /// Computes and adds an [`Aabb`] component to entities with a -/// [`Handle`](Mesh) component and without a [`NoFrustumCulling`] component. +/// [`Mesh3d`] component and without a [`NoFrustumCulling`] component. /// /// This system is used in system set [`VisibilitySystems::CalculateBounds`]. pub fn calculate_bounds( mut commands: Commands, meshes: Res>, - without_aabb: Query<(Entity, &Handle), (Without, Without)>, + without_aabb: Query<(Entity, &Mesh3d), (Without, Without)>, ) { for (entity, mesh_handle) in &without_aabb { if let Some(mesh) = meshes.get(mesh_handle) { diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index cbf93d2b2674a..74e089ed9ffc1 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -24,13 +24,14 @@ use wgpu::{BufferBindingType, BufferUsages}; use crate::{ camera::Camera, + mesh::Mesh3d, primitives::Aabb, render_resource::BufferVec, renderer::{RenderDevice, RenderQueue}, Extract, ExtractSchedule, Render, RenderApp, RenderSet, }; -use super::{check_visibility, VisibilitySystems, WithMesh}; +use super::{check_visibility, VisibilitySystems}; /// We need at least 4 storage buffer bindings available to enable the /// visibility range buffer. @@ -57,7 +58,7 @@ impl Plugin for VisibilityRangePlugin { PostUpdate, check_visibility_ranges .in_set(VisibilitySystems::CheckVisibility) - .before(check_visibility::), + .before(check_visibility::>), ); let Some(render_app) = app.get_sub_app_mut(RenderApp) else { diff --git a/crates/bevy_scene/src/bundle.rs b/crates/bevy_scene/src/bundle.rs index ac656f15af93f..b0f8b38a7734f 100644 --- a/crates/bevy_scene/src/bundle.rs +++ b/crates/bevy_scene/src/bundle.rs @@ -1,4 +1,5 @@ -use bevy_asset::Handle; +#![expect(deprecated)] + use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ bundle::Bundle, @@ -11,21 +12,25 @@ use bevy_ecs::{ use bevy_render::prelude::{InheritedVisibility, ViewVisibility, Visibility}; use bevy_transform::components::{GlobalTransform, Transform}; -use crate::{DynamicScene, InstanceId, Scene, SceneSpawner}; +use crate::{DynamicSceneRoot, InstanceId, SceneRoot, SceneSpawner}; /// [`InstanceId`] of a spawned scene. It can be used with the [`SceneSpawner`] to /// interact with the spawned scene. #[derive(Component, Deref, DerefMut)] pub struct SceneInstance(pub(crate) InstanceId); -/// A component bundle for a [`Scene`] root. +/// A component bundle for a [`Scene`](crate::Scene) root. /// /// The scene from `scene` will be spawned as a child of the entity with this component. /// Once it's spawned, the entity will have a [`SceneInstance`] component. #[derive(Default, Bundle, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `SceneRoot` component instead. Inserting `SceneRoot` will also insert the other components required by scenes automatically." +)] pub struct SceneBundle { /// Handle to the scene to spawn. - pub scene: Handle, + pub scene: SceneRoot, /// Transform of the scene root entity. pub transform: Transform, /// Global transform of the scene root entity. @@ -42,14 +47,18 @@ pub struct SceneBundle { pub view_visibility: ViewVisibility, } -/// A component bundle for a [`DynamicScene`] root. +/// A component bundle for a [`DynamicScene`](crate::DynamicScene) root. /// /// The dynamic scene from `scene` will be spawn as a child of the entity with this component. /// Once it's spawned, the entity will have a [`SceneInstance`] component. #[derive(Default, Bundle, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `DynamicSceneRoot` component instead. Inserting `DynamicSceneRoot` will also insert the other components required by scenes automatically." +)] pub struct DynamicSceneBundle { /// Handle to the scene to spawn. - pub scene: Handle, + pub scene: DynamicSceneRoot, /// Transform of the scene root entity. pub transform: Transform, /// Global transform of the scene root entity. @@ -66,21 +75,21 @@ pub struct DynamicSceneBundle { pub view_visibility: ViewVisibility, } -/// System that will spawn scenes from [`SceneBundle`]. +/// System that will spawn scenes from the [`SceneRoot`] and [`DynamicSceneRoot`] components. pub fn scene_spawner( mut commands: Commands, mut scene_to_spawn: Query< - (Entity, &Handle, Option<&mut SceneInstance>), - (Changed>, Without>), + (Entity, &SceneRoot, Option<&mut SceneInstance>), + (Changed, Without), >, mut dynamic_scene_to_spawn: Query< - (Entity, &Handle, Option<&mut SceneInstance>), - (Changed>, Without>), + (Entity, &DynamicSceneRoot, Option<&mut SceneInstance>), + (Changed, Without), >, mut scene_spawner: ResMut, ) { for (entity, scene, instance) in &mut scene_to_spawn { - let new_instance = scene_spawner.spawn_as_child(scene.clone(), entity); + let new_instance = scene_spawner.spawn_as_child(scene.0.clone(), entity); if let Some(mut old_instance) = instance { scene_spawner.despawn_instance(**old_instance); *old_instance = SceneInstance(new_instance); @@ -89,7 +98,7 @@ pub fn scene_spawner( } } for (entity, dynamic_scene, instance) in &mut dynamic_scene_to_spawn { - let new_instance = scene_spawner.spawn_dynamic_as_child(dynamic_scene.clone(), entity); + let new_instance = scene_spawner.spawn_dynamic_as_child(dynamic_scene.0.clone(), entity); if let Some(mut old_instance) = instance { scene_spawner.despawn_instance(**old_instance); *old_instance = SceneInstance(new_instance); @@ -101,7 +110,7 @@ pub fn scene_spawner( #[cfg(test)] mod tests { - use crate::{DynamicScene, DynamicSceneBundle, ScenePlugin, SceneSpawner}; + use crate::{DynamicScene, DynamicSceneRoot, ScenePlugin, SceneSpawner}; use bevy_app::{App, ScheduleRunnerPlugin}; use bevy_asset::{AssetPlugin, Assets}; use bevy_ecs::{ @@ -111,7 +120,6 @@ mod tests { }; use bevy_hierarchy::{Children, HierarchyPlugin}; use bevy_reflect::Reflect; - use bevy_utils::default; #[derive(Component, Reflect, Default)] #[reflect(Component)] @@ -143,13 +151,10 @@ mod tests { .resource_mut::>() .add(scene); - // spawn the scene as a child of `entity` using the `DynamicSceneBundle` + // spawn the scene as a child of `entity` using `DynamicSceneRoot` let entity = app .world_mut() - .spawn(DynamicSceneBundle { - scene: scene_handle.clone(), - ..default() - }) + .spawn(DynamicSceneRoot(scene_handle.clone())) .id(); // run the app's schedule once, so that the scene gets spawned diff --git a/crates/bevy_scene/src/components.rs b/crates/bevy_scene/src/components.rs new file mode 100644 index 0000000000000..74f0f545d1d82 --- /dev/null +++ b/crates/bevy_scene/src/components.rs @@ -0,0 +1,36 @@ +use bevy_asset::Handle; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::component::Component; +use bevy_reflect::Reflect; +use bevy_transform::components::Transform; + +#[cfg(feature = "bevy_render")] +use bevy_render::view::visibility::Visibility; + +use crate::{DynamicScene, Scene}; + +/// Adding this component will spawn the scene as a child of that entity. +/// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[require(Transform)] +#[cfg_attr(feature = "bevy_render", require(Visibility))] +pub struct SceneRoot(pub Handle); + +impl From> for SceneRoot { + fn from(handle: Handle) -> Self { + Self(handle) + } +} + +/// Adding this component will spawn the scene as a child of that entity. +/// Once it's spawned, the entity will have a [`SceneInstance`](crate::SceneInstance) component. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[require(Transform)] +#[cfg_attr(feature = "bevy_render", require(Visibility))] +pub struct DynamicSceneRoot(pub Handle); + +impl From> for DynamicSceneRoot { + fn from(handle: Handle) -> Self { + Self(handle) + } +} diff --git a/crates/bevy_scene/src/dynamic_scene.rs b/crates/bevy_scene/src/dynamic_scene.rs index 6649ef8ac521f..123ebf93aecb2 100644 --- a/crates/bevy_scene/src/dynamic_scene.rs +++ b/crates/bevy_scene/src/dynamic_scene.rs @@ -1,16 +1,15 @@ use crate::{ron, DynamicSceneBuilder, Scene, SceneSpawnError}; +use bevy_asset::Asset; +use bevy_ecs::reflect::ReflectResource; use bevy_ecs::{ - entity::{Entity, EntityHashMap}, + entity::{Entity, EntityHashMap, SceneEntityMapper}, reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities}, world::World, }; use bevy_reflect::{PartialReflect, TypePath, TypeRegistry}; -use bevy_utils::TypeIdMap; #[cfg(feature = "serialize")] use crate::serde::SceneSerializer; -use bevy_asset::Asset; -use bevy_ecs::reflect::{ReflectMapEntitiesResource, ReflectResource}; #[cfg(feature = "serialize")] use serde::Serialize; @@ -19,10 +18,7 @@ use serde::Serialize; /// Each dynamic entity in the collection contains its own run-time defined set of components. /// To spawn a dynamic scene, you can use either: /// * [`SceneSpawner::spawn_dynamic`](crate::SceneSpawner::spawn_dynamic) -/// * adding the [`DynamicSceneBundle`](crate::DynamicSceneBundle) to an entity -/// * adding the [`Handle`](bevy_asset::Handle) to an entity (the scene will only be -/// visible if the entity already has [`Transform`](bevy_transform::components::Transform) and -/// [`GlobalTransform`](bevy_transform::components::GlobalTransform) components) +/// * adding the [`DynamicSceneRoot`](crate::components::DynamicSceneRoot) component to an entity. /// * using the [`DynamicSceneBuilder`] to construct a `DynamicScene` from `World`. #[derive(Asset, TypePath, Default)] pub struct DynamicScene { @@ -70,23 +66,26 @@ impl DynamicScene { ) -> Result<(), SceneSpawnError> { let type_registry = type_registry.read(); - // For each component types that reference other entities, we keep track - // of which entities in the scene use that component. - // This is so we can update the scene-internal references to references - // of the actual entities in the world. - let mut scene_mappings: TypeIdMap> = Default::default(); - + // First ensure that every entity in the scene has a corresponding world + // entity in the entity map. for scene_entity in &self.entities { // Fetch the entity with the given entity id from the `entity_map` // or spawn a new entity with a transiently unique id if there is // no corresponding entry. - let entity = *entity_map + entity_map .entry(scene_entity.entity) .or_insert_with(|| world.spawn_empty().id()); - let entity_mut = &mut world.entity_mut(entity); + } + + for scene_entity in &self.entities { + // Fetch the entity with the given entity id from the `entity_map`. + let entity = *entity_map + .get(&scene_entity.entity) + .expect("should have previously spawned an empty entity"); // Apply/ add each component to the given entity. for component in &scene_entity.components { + let mut component = component.clone_value(); let type_info = component.get_represented_type_info().ok_or_else(|| { SceneSpawnError::NoRepresentedType { type_path: component.reflect_type_path().to_string(), @@ -104,39 +103,26 @@ impl DynamicScene { } })?; - // If this component references entities in the scene, track it - // so we can update it to the entity in the world. - if registration.data::().is_some() { - scene_mappings - .entry(registration.type_id()) - .or_default() - .push(entity); + // If this component references entities in the scene, update + // them to the entities in the world. + if let Some(map_entities) = registration.data::() { + SceneEntityMapper::world_scope(entity_map, world, |_, mapper| { + map_entities.map_entities(component.as_partial_reflect_mut(), mapper); + }); } - // If the entity already has the given component attached, - // just apply the (possibly) new value, otherwise add the - // component to the entity. reflect_component.apply_or_insert( - entity_mut, + &mut world.entity_mut(entity), component.as_partial_reflect(), &type_registry, ); } } - // Updates references to entities in the scene to entities in the world - for (type_id, entities) in scene_mappings.into_iter() { - let registration = type_registry.get(type_id).expect( - "we should be getting TypeId from this TypeRegistration in the first place", - ); - if let Some(map_entities_reflect) = registration.data::() { - map_entities_reflect.map_entities(world, entity_map, &entities); - } - } - // Insert resources after all entities have been added to the world. // This ensures the entities are available for the resources to reference during mapping. for resource in &self.resources { + let mut resource = resource.clone_value(); let type_info = resource.get_represented_type_info().ok_or_else(|| { SceneSpawnError::NoRepresentedType { type_path: resource.reflect_type_path().to_string(), @@ -153,14 +139,17 @@ impl DynamicScene { } })?; + // If this component references entities in the scene, update + // them to the entities in the world. + if let Some(map_entities) = registration.data::() { + SceneEntityMapper::world_scope(entity_map, world, |_, mapper| { + map_entities.map_entities(resource.as_partial_reflect_mut(), mapper); + }); + } + // If the world already contains an instance of the given resource // just apply the (possibly) new value, otherwise insert the resource - reflect_resource.apply_or_insert(world, &**resource, &type_registry); - - // Map entities in the resource if it implements [`MapEntities`]. - if let Some(map_entities_reflect) = registration.data::() { - map_entities_reflect.map_entities(world, entity_map); - } + reflect_resource.apply_or_insert(world, resource.as_partial_reflect(), &type_registry); } Ok(()) @@ -210,11 +199,10 @@ where mod tests { use bevy_ecs::{ component::Component, - entity::{Entity, EntityHashMap, EntityMapper, MapEntities, VisitEntities}, - reflect::{ - AppTypeRegistry, ReflectComponent, ReflectMapEntities, ReflectMapEntitiesResource, - ReflectResource, + entity::{ + Entity, EntityHashMap, EntityMapper, MapEntities, VisitEntities, VisitEntitiesMut, }, + reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities, ReflectResource}, system::Resource, world::{Command, World}, }; @@ -224,20 +212,13 @@ mod tests { use crate::dynamic_scene::DynamicScene; use crate::dynamic_scene_builder::DynamicSceneBuilder; - #[derive(Resource, Reflect, Debug, VisitEntities)] - #[reflect(Resource, MapEntitiesResource)] + #[derive(Resource, Reflect, Debug, VisitEntities, VisitEntitiesMut)] + #[reflect(Resource, MapEntities)] struct TestResource { entity_a: Entity, entity_b: Entity, } - impl MapEntities for TestResource { - fn map_entities(&mut self, entity_mapper: &mut M) { - self.entity_a = entity_mapper.map_entity(self.entity_a); - self.entity_b = entity_mapper.map_entity(self.entity_b); - } - } - #[test] fn resource_entity_map_maps_entities() { let type_registry = AppTypeRegistry::default(); diff --git a/crates/bevy_scene/src/lib.rs b/crates/bevy_scene/src/lib.rs index ffbc7af17e790..8a21b2040d78e 100644 --- a/crates/bevy_scene/src/lib.rs +++ b/crates/bevy_scene/src/lib.rs @@ -14,6 +14,7 @@ extern crate alloc; mod bundle; +mod components; mod dynamic_scene; mod dynamic_scene_builder; mod scene; @@ -29,6 +30,7 @@ pub use bevy_asset::ron; use bevy_ecs::schedule::IntoSystemConfigs; pub use bundle::*; +pub use components::*; pub use dynamic_scene::*; pub use dynamic_scene_builder::*; pub use scene::*; @@ -39,16 +41,17 @@ pub use scene_spawner::*; /// The scene 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::{ - DynamicScene, DynamicSceneBuilder, DynamicSceneBundle, Scene, SceneBundle, SceneFilter, - SceneSpawner, + DynamicScene, DynamicSceneBuilder, DynamicSceneBundle, DynamicSceneRoot, Scene, + SceneBundle, SceneFilter, SceneRoot, SceneSpawner, }; } use bevy_app::prelude::*; -use bevy_asset::{AssetApp, Handle}; +use bevy_asset::AssetApp; /// Plugin that provides scene functionality to an [`App`]. #[derive(Default)] @@ -61,13 +64,15 @@ impl Plugin for ScenePlugin { .init_asset::() .init_asset_loader::() .init_resource::() + .register_type::() + .register_type::() .add_systems(SpawnScene, (scene_spawner, scene_spawner_system).chain()); - // Register component hooks for DynamicScene + // Register component hooks for DynamicSceneRoot app.world_mut() - .register_component_hooks::>() + .register_component_hooks::() .on_remove(|mut world, entity, _| { - let Some(handle) = world.get::>(entity) else { + let Some(handle) = world.get::(entity) else { return; }; let id = handle.id(); @@ -82,9 +87,9 @@ impl Plugin for ScenePlugin { } }); - // Register component hooks for Scene + // Register component hooks for SceneRoot app.world_mut() - .register_component_hooks::>() + .register_component_hooks::() .on_remove(|mut world, entity, _| { if let Some(&SceneInstance(scene_instance)) = world.get::(entity) { let Some(mut scene_spawner) = world.get_resource_mut::() else { diff --git a/crates/bevy_scene/src/scene.rs b/crates/bevy_scene/src/scene.rs index 5d0cf57639201..961f056a193dd 100644 --- a/crates/bevy_scene/src/scene.rs +++ b/crates/bevy_scene/src/scene.rs @@ -1,18 +1,15 @@ use crate::{DynamicScene, SceneSpawnError}; use bevy_asset::Asset; use bevy_ecs::{ - entity::{Entity, EntityHashMap}, + entity::{Entity, EntityHashMap, SceneEntityMapper}, reflect::{AppTypeRegistry, ReflectComponent, ReflectMapEntities, ReflectResource}, world::World, }; -use bevy_reflect::TypePath; +use bevy_reflect::{PartialReflect, TypePath}; /// To spawn a scene, you can use either: /// * [`SceneSpawner::spawn`](crate::SceneSpawner::spawn) -/// * adding the [`SceneBundle`](crate::SceneBundle) to an entity -/// * adding the [`Handle`](bevy_asset::Handle) to an entity (the scene will only be -/// visible if the entity already has [`Transform`](bevy_transform::components::Transform) and -/// [`GlobalTransform`](bevy_transform::components::GlobalTransform) components) +/// * adding the [`SceneRoot`](crate::components::SceneRoot) component to an entity. #[derive(Asset, TypePath, Debug)] pub struct Scene { /// The world of the scene, containing its entities and resources. @@ -90,11 +87,22 @@ impl Scene { reflect_resource.copy(&self.world, world, &type_registry); } + // Ensure that all scene entities have been allocated in the destination + // world before handling components that may contain references that need mapping. for archetype in self.world.archetypes().iter() { for scene_entity in archetype.entities() { - let entity = entity_map + entity_map .entry(scene_entity.id()) .or_insert_with(|| world.spawn_empty().id()); + } + } + + for archetype in self.world.archetypes().iter() { + for scene_entity in archetype.entities() { + let entity = *entity_map + .get(&scene_entity.id()) + .expect("should have previously spawned an entity"); + for component_id in archetype.components() { let component_info = self .world @@ -102,35 +110,41 @@ impl Scene { .get_info(component_id) .expect("component_ids in archetypes should have ComponentInfo"); - let reflect_component = type_registry + let registration = type_registry .get(component_info.type_id().unwrap()) .ok_or_else(|| SceneSpawnError::UnregisteredType { std_type_name: component_info.name().to_string(), - }) - .and_then(|registration| { - registration.data::().ok_or_else(|| { - SceneSpawnError::UnregisteredComponent { - type_path: registration.type_info().type_path().to_string(), - } - }) })?; - reflect_component.copy( - &self.world, - world, - scene_entity.id(), - *entity, + let reflect_component = + registration.data::().ok_or_else(|| { + SceneSpawnError::UnregisteredComponent { + type_path: registration.type_info().type_path().to_string(), + } + })?; + + let Some(mut component) = reflect_component + .reflect(self.world.entity(scene_entity.id())) + .map(PartialReflect::clone_value) + else { + continue; + }; + + // If this component references entities in the scene, + // update them to the entities in the world. + if let Some(map_entities) = registration.data::() { + SceneEntityMapper::world_scope(entity_map, world, |_, mapper| { + map_entities.map_entities(component.as_partial_reflect_mut(), mapper); + }); + } + reflect_component.apply_or_insert( + &mut world.entity_mut(entity), + component.as_partial_reflect(), &type_registry, ); } } } - for registration in type_registry.iter() { - if let Some(map_entities_reflect) = registration.data::() { - map_entities_reflect.map_all_entities(world, entity_map); - } - } - Ok(()) } } diff --git a/crates/bevy_scene/src/scene_spawner.rs b/crates/bevy_scene/src/scene_spawner.rs index cd138502251bf..b354b52b09175 100644 --- a/crates/bevy_scene/src/scene_spawner.rs +++ b/crates/bevy_scene/src/scene_spawner.rs @@ -482,7 +482,7 @@ mod tests { }; use bevy_reflect::Reflect; - use crate::{DynamicSceneBuilder, ScenePlugin}; + use crate::{DynamicSceneBuilder, DynamicSceneRoot, ScenePlugin}; use super::*; @@ -725,7 +725,8 @@ mod tests { // Spawn scene. for _ in 0..count { - app.world_mut().spawn((ComponentA, scene.clone())); + app.world_mut() + .spawn((ComponentA, DynamicSceneRoot(scene.clone()))); } app.update(); diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index a5d645e001873..fd6562997f546 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -25,6 +25,7 @@ mod texture_slice; /// The sprite 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::{ @@ -32,7 +33,7 @@ pub mod prelude { sprite::{ImageScaleMode, Sprite}, texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources}, texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer}, - ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder, + ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder, }; } @@ -52,7 +53,7 @@ use bevy_core_pipeline::core_2d::Transparent2d; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_render::{ extract_component::{ExtractComponent, ExtractComponentPlugin}, - mesh::Mesh, + mesh::{Mesh, Mesh2d}, primitives::Aabb, render_phase::AddRenderCommand, render_resource::{Shader, SpecializedRenderPipelines}, @@ -84,10 +85,6 @@ pub enum SpriteSystem { #[reflect(Component, Default, Debug)] pub struct SpriteSource; -/// A convenient alias for `With>`, for use with -/// [`bevy_render::view::VisibleEntities`]. -pub type WithMesh2d = With; - /// A convenient alias for `Or, With>`, for use with /// [`bevy_render::view::VisibleEntities`]. pub type WithSprite = Or<(With, With)>; @@ -113,7 +110,7 @@ impl Plugin for SpritePlugin { .register_type::() .register_type::() .register_type::() - .register_type::() + .register_type::() .register_type::() .add_plugins(( Mesh2dRenderPlugin, @@ -130,7 +127,7 @@ impl Plugin for SpritePlugin { ) .in_set(SpriteSystem::ComputeSlices), ( - check_visibility::, + check_visibility::>, check_visibility::, ) .in_set(VisibilitySystems::CheckVisibility), @@ -176,7 +173,7 @@ impl Plugin for SpritePlugin { } /// System calculating and inserting an [`Aabb`] component to entities with either: -/// - a `Mesh2dHandle` component, +/// - a `Mesh2d` component, /// - a `Sprite` and `Handle` components, /// and without a [`NoFrustumCulling`] component. /// @@ -186,7 +183,7 @@ pub fn calculate_bounds_2d( meshes: Res>, images: Res>, atlases: Res>, - meshes_without_aabb: Query<(Entity, &Mesh2dHandle), (Without, Without)>, + meshes_without_aabb: Query<(Entity, &Mesh2d), (Without, Without)>, sprites_to_recalculate_aabb: Query< (Entity, &Sprite, &Handle, Option<&TextureAtlas>), ( diff --git a/crates/bevy_sprite/src/mesh2d/color_material.rs b/crates/bevy_sprite/src/mesh2d/color_material.rs index 2df333539a2b5..f2e5aab14e5ea 100644 --- a/crates/bevy_sprite/src/mesh2d/color_material.rs +++ b/crates/bevy_sprite/src/mesh2d/color_material.rs @@ -1,13 +1,20 @@ -use crate::{AlphaMode2d, Material2d, Material2dPlugin, MaterialMesh2dBundle}; +#![expect(deprecated)] + +use crate::{ + clear_material_2d_instances, extract_default_materials_2d, AlphaMode2d, Material2d, + Material2dPlugin, MaterialMesh2dBundle, +}; use bevy_app::{App, Plugin}; use bevy_asset::{load_internal_asset, Asset, AssetApp, Assets, Handle}; use bevy_color::{Alpha, Color, ColorToComponents, LinearRgba}; +use bevy_ecs::schedule::IntoSystemConfigs; use bevy_math::Vec4; use bevy_reflect::prelude::*; use bevy_render::{ render_asset::RenderAssets, render_resource::*, texture::{GpuImage, Image}, + ExtractSchedule, RenderApp, }; pub const COLOR_MATERIAL_SHADER_HANDLE: Handle = @@ -28,19 +35,30 @@ impl Plugin for ColorMaterialPlugin { app.add_plugins(Material2dPlugin::::default()) .register_asset_reflect::(); + // Initialize the default material. app.world_mut() .resource_mut::>() .insert( &Handle::::default(), ColorMaterial { - color: Color::srgb(1.0, 0.0, 1.0), + color: Color::WHITE, ..Default::default() }, ); + + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + + // Extract default materials for entities with no material. + render_app.add_systems( + ExtractSchedule, + extract_default_materials_2d.after(clear_material_2d_instances::), + ); } } -/// A [2d material](Material2d) that renders [2d meshes](crate::Mesh2dHandle) with a texture tinted by a uniform color +/// A [2d material](Material2d) that renders [2d meshes](crate::Mesh2d) with a texture tinted by a uniform color #[derive(Asset, AsBindGroup, Reflect, Debug, Clone)] #[reflect(Default, Debug)] #[uniform(0, ColorMaterialUniform)] @@ -158,5 +176,9 @@ impl Material2d for ColorMaterial { } } -/// A component bundle for entities with a [`Mesh2dHandle`](crate::Mesh2dHandle) and a [`ColorMaterial`]. +/// A component bundle for entities with a [`Mesh2d`](crate::Mesh2d) and a [`ColorMaterial`]. +#[deprecated( + since = "0.15.0", + note = "Use the `Mesh3d` and `MeshMaterial3d` components instead. Inserting them will now also insert the other components required by them automatically." +)] pub type ColorMesh2dBundle = MaterialMesh2dBundle; diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index be3010792a0f4..d773c386ebddc 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -1,3 +1,5 @@ +#![expect(deprecated)] + use bevy_app::{App, Plugin}; use bevy_asset::{Asset, AssetApp, AssetId, AssetServer, Handle}; use bevy_core_pipeline::{ @@ -36,29 +38,34 @@ use bevy_utils::tracing::error; use core::{hash::Hash, marker::PhantomData}; use crate::{ - DrawMesh2d, Mesh2dHandle, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, - SetMesh2dBindGroup, SetMesh2dViewBindGroup, WithMesh2d, + DrawMesh2d, Mesh2d, Mesh2dPipeline, Mesh2dPipelineKey, RenderMesh2dInstances, + SetMesh2dBindGroup, SetMesh2dViewBindGroup, }; -/// Materials are used alongside [`Material2dPlugin`] and [`MaterialMesh2dBundle`] +use super::ColorMaterial; + +/// Materials are used alongside [`Material2dPlugin`], [`Mesh2d`], and [`MeshMaterial2d`] /// to spawn entities that are rendered with a specific [`Material2d`] type. They serve as an easy to use high level -/// way to render [`Mesh2dHandle`] entities with custom shader logic. +/// way to render [`Mesh2d`] entities with custom shader logic. /// -/// Material2ds must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// Materials must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. /// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. /// /// # Example /// -/// Here is a simple Material2d implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// Here is a simple [`Material2d`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, /// check out the [`AsBindGroup`] documentation. +/// /// ``` -/// # use bevy_sprite::{Material2d, MaterialMesh2dBundle}; +/// # use bevy_sprite::{Material2d, MeshMaterial2d}; /// # use bevy_ecs::prelude::*; /// # use bevy_reflect::TypePath; -/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image}; +/// # use bevy_render::{mesh::{Mesh, Mesh2d}, render_resource::{AsBindGroup, ShaderRef}, texture::Image}; /// # use bevy_color::LinearRgba; +/// # use bevy_color::palettes::basic::RED; /// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; -/// +/// # use bevy_math::primitives::Circle; +/// # /// #[derive(AsBindGroup, Debug, Clone, Asset, TypePath)] /// pub struct CustomMaterial { /// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to @@ -80,17 +87,23 @@ use crate::{ /// } /// } /// -/// // Spawn an entity using `CustomMaterial`. -/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { -/// commands.spawn(MaterialMesh2dBundle { -/// material: materials.add(CustomMaterial { -/// color: LinearRgba::RED, +/// // Spawn an entity with a mesh using `CustomMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// asset_server: Res, +/// ) { +/// commands.spawn(( +/// Mesh2d(meshes.add(Circle::new(50.0))), +/// MeshMaterial2d(materials.add(CustomMaterial { +/// color: RED.into(), /// color_texture: asset_server.load("some_image.png"), -/// }), -/// ..Default::default() -/// }); +/// })), +/// )); /// } /// ``` +/// /// In WGSL shaders, the material's binding would look like this: /// /// ```wgsl @@ -137,8 +150,104 @@ pub trait Material2d: AsBindGroup + Asset + Clone + Sized { } } +/// A [material](Material2d) for a [`Mesh2d`]. +/// +/// See [`Material2d`] for general information about 2D materials and how to implement your own materials. +/// +/// # Example +/// +/// ``` +/// # use bevy_sprite::{ColorMaterial, MeshMaterial2d}; +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::mesh::{Mesh, Mesh2d}; +/// # use bevy_color::palettes::basic::RED; +/// # use bevy_asset::Assets; +/// # use bevy_math::primitives::Circle; +/// # +/// // Spawn an entity with a mesh using `ColorMaterial`. +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// commands.spawn(( +/// Mesh2d(meshes.add(Circle::new(50.0))), +/// MeshMaterial2d(materials.add(ColorMaterial::from_color(RED))), +/// )); +/// } +/// ``` +/// +/// [`MeshMaterial2d`]: crate::MeshMaterial2d +/// [`ColorMaterial`]: crate::ColorMaterial +/// +/// ## Default Material +/// +/// Meshes without a [`MeshMaterial2d`] are rendered with a default [`ColorMaterial`]. +/// This material can be overridden by inserting a custom material for the default asset handle. +/// +/// ``` +/// # use bevy_sprite::ColorMaterial; +/// # use bevy_ecs::prelude::*; +/// # use bevy_render::mesh::{Mesh, Mesh2d}; +/// # use bevy_color::Color; +/// # use bevy_asset::{Assets, Handle}; +/// # use bevy_math::primitives::Circle; +/// # +/// fn setup( +/// mut commands: Commands, +/// mut meshes: ResMut>, +/// mut materials: ResMut>, +/// ) { +/// // Optional: Insert a custom default material. +/// materials.insert( +/// &Handle::::default(), +/// ColorMaterial::from(Color::srgb(1.0, 0.0, 1.0)), +/// ); +/// +/// // Spawn a circle with no material. +/// // The mesh will be rendered with the default material. +/// commands.spawn(Mesh2d(meshes.add(Circle::new(50.0)))); +/// } +/// ``` +#[derive(Component, Clone, Debug, Deref, DerefMut, Reflect, PartialEq, Eq)] +#[reflect(Component, Default)] +#[require(HasMaterial2d)] +pub struct MeshMaterial2d(pub Handle); + +impl Default for MeshMaterial2d { + fn default() -> Self { + Self(Handle::default()) + } +} + +impl From> for MeshMaterial2d { + fn from(handle: Handle) -> Self { + Self(handle) + } +} + +impl From> for AssetId { + fn from(material: MeshMaterial2d) -> Self { + material.id() + } +} + +impl From<&MeshMaterial2d> for AssetId { + fn from(material: &MeshMaterial2d) -> Self { + material.id() + } +} + +/// A component that marks an entity as having a [`MeshMaterial2d`]. +/// [`Mesh2d`] entities without this component are rendered with a [default material]. +/// +/// [default material]: crate::MeshMaterial2d#default-material +#[derive(Component, Clone, Debug, Default, Reflect)] +#[reflect(Component, Default)] +pub struct HasMaterial2d; + /// Sets how a 2d material's base color alpha channel is used for transparency. -/// Currently, this only works with [`Mesh2d`](crate::mesh2d::Mesh2d). Sprites are always transparent. +/// Currently, this only works with [`Mesh2d`]. Sprites are always transparent. /// /// This is very similar to [`AlphaMode`](bevy_render::alpha::AlphaMode) but this only applies to 2d meshes. /// We use a separate type because 2d doesn't support all the transparency modes that 3d does. @@ -179,6 +288,8 @@ where { fn build(&self, app: &mut App) { app.init_asset::() + .register_type::>() + .register_type::() .add_plugins(RenderAssetPlugin::>::default()); if let Some(render_app) = app.get_sub_app_mut(RenderApp) { @@ -188,7 +299,14 @@ where .add_render_command::>() .init_resource::>() .init_resource::>>() - .add_systems(ExtractSchedule, extract_material_meshes_2d::) + .add_systems( + ExtractSchedule, + ( + clear_material_2d_instances::, + extract_mesh_materials_2d::, + ) + .chain(), + ) .add_systems( Render, queue_material2d_meshes:: @@ -214,14 +332,33 @@ impl Default for RenderMaterial2dInstances { } } -fn extract_material_meshes_2d( +pub(crate) fn clear_material_2d_instances( mut material_instances: ResMut>, - query: Extract)>>, ) { material_instances.clear(); - for (entity, view_visibility, handle) in &query { +} + +fn extract_mesh_materials_2d( + mut material_instances: ResMut>, + query: Extract), With>>, +) { + for (entity, view_visibility, material) in &query { + if view_visibility.get() { + material_instances.insert(entity, material.id()); + } + } +} + +/// Extracts default materials for 2D meshes with no [`MeshMaterial2d`]. +pub(crate) fn extract_default_materials_2d( + mut material_instances: ResMut>, + query: Extract, Without)>>, +) { + let default_material: AssetId = Handle::::default().id(); + + for (entity, view_visibility) in &query { if view_visibility.get() { - material_instances.insert(entity, handle.id()); + material_instances.insert(entity, default_material); } } } @@ -340,7 +477,7 @@ impl FromWorld for Material2dPipeline { } } -type DrawMaterial2d = ( +pub(super) type DrawMaterial2d = ( SetItemPipeline, SetMesh2dViewBindGroup<0>, SetMesh2dBindGroup<1>, @@ -460,7 +597,7 @@ pub fn queue_material2d_meshes( view_key |= Mesh2dPipelineKey::DEBAND_DITHER; } } - for visible_entity in visible_entities.iter::() { + for visible_entity in visible_entities.iter::>() { let Some(material_asset_id) = render_material_instances.get(visible_entity) else { continue; }; @@ -609,11 +746,15 @@ impl RenderAsset for PreparedMaterial2d { } } -/// A component bundle for entities with a [`Mesh2dHandle`] and a [`Material2d`]. +/// A component bundle for entities with a [`Mesh2d`] and a [`MeshMaterial2d`]. #[derive(Bundle, Clone)] +#[deprecated( + since = "0.15.0", + note = "Use the `Mesh2d` and `MeshMaterial2d` components instead. Inserting them will now also insert the other components required by them automatically." +)] pub struct MaterialMesh2dBundle { - pub mesh: Mesh2dHandle, - pub material: Handle, + pub mesh: Mesh2d, + pub material: MeshMaterial2d, pub transform: Transform, pub global_transform: GlobalTransform, /// User indication of whether an entity is visible diff --git a/crates/bevy_sprite/src/mesh2d/mesh.rs b/crates/bevy_sprite/src/mesh2d/mesh.rs index a0e6229a6af1f..b0b5f39bc9af8 100644 --- a/crates/bevy_sprite/src/mesh2d/mesh.rs +++ b/crates/bevy_sprite/src/mesh2d/mesh.rs @@ -15,7 +15,6 @@ use bevy_ecs::{ system::{lifetimeless::*, SystemParamItem, SystemState}, }; use bevy_math::{Affine3, Vec4}; -use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ batching::{ gpu_preprocessing::IndirectParameters, @@ -27,7 +26,8 @@ use bevy_render::{ }, globals::{GlobalsBuffer, GlobalsUniform}, mesh::{ - allocator::MeshAllocator, Mesh, MeshVertexBufferLayoutRef, RenderMesh, RenderMeshBufferInfo, + allocator::MeshAllocator, Mesh, Mesh2d, MeshVertexBufferLayoutRef, RenderMesh, + RenderMeshBufferInfo, }, render_asset::RenderAssets, render_phase::{PhaseItem, RenderCommand, RenderCommandResult, TrackedRenderPass}, @@ -48,19 +48,6 @@ use nonmax::NonMaxU32; use crate::Material2dBindGroupId; -/// Component for rendering with meshes in the 2d pipeline, usually with a [2d material](crate::Material2d) such as [`ColorMaterial`](crate::ColorMaterial). -/// -/// It wraps a [`Handle`] to differentiate from the 3d pipelines which use the handles directly as components -#[derive(Default, Clone, Component, Debug, Reflect, PartialEq, Eq, Deref, DerefMut)] -#[reflect(Default, Component, Debug, PartialEq)] -pub struct Mesh2dHandle(pub Handle); - -impl From> for Mesh2dHandle { - fn from(handle: Handle) -> Self { - Self(handle) - } -} - #[derive(Default)] pub struct Mesh2dRenderPlugin; @@ -218,7 +205,7 @@ pub struct RenderMesh2dInstance { pub struct RenderMesh2dInstances(EntityHashMap); #[derive(Component)] -pub struct Mesh2d; +pub struct Mesh2dMarker; pub fn extract_mesh2d( mut render_mesh_instances: ResMut, @@ -227,7 +214,7 @@ pub fn extract_mesh2d( Entity, &ViewVisibility, &GlobalTransform, - &Mesh2dHandle, + &Mesh2d, Has, )>, >, diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index a8822c800e5ec..6f2659fbaaf91 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -1,4 +1,4 @@ -use crate::{Material2d, Material2dKey, Material2dPlugin, Mesh2dHandle}; +use crate::{Material2d, Material2dKey, Material2dPlugin, Mesh2d}; use bevy_app::{Plugin, Startup, Update}; use bevy_asset::{load_internal_asset, Asset, Assets, Handle}; use bevy_color::{Color, LinearRgba}; @@ -9,6 +9,8 @@ use bevy_render::{ render_resource::*, }; +use super::MeshMaterial2d; + pub const WIREFRAME_2D_SHADER_HANDLE: Handle = Handle::weak_from_u128(6920362697190520314); /// A [`Plugin`] that draws wireframes for 2D meshes. @@ -126,12 +128,12 @@ fn global_color_changed( fn wireframe_color_changed( mut materials: ResMut>, mut colors_changed: Query< - (&mut Handle, &Wireframe2dColor), + (&mut MeshMaterial2d, &Wireframe2dColor), (With, Changed), >, ) { for (mut handle, wireframe_color) in &mut colors_changed { - *handle = materials.add(Wireframe2dMaterial { + handle.0 = materials.add(Wireframe2dMaterial { color: wireframe_color.color.into(), }); } @@ -144,15 +146,24 @@ fn apply_wireframe_material( mut materials: ResMut>, wireframes: Query< (Entity, Option<&Wireframe2dColor>), - (With, Without>), + ( + With, + Without>, + ), + >, + no_wireframes: Query< + Entity, + ( + With, + With>, + ), >, - no_wireframes: Query, With>)>, mut removed_wireframes: RemovedComponents, global_material: Res, ) { for e in removed_wireframes.read().chain(no_wireframes.iter()) { - if let Some(commands) = commands.get_entity(e) { - commands.remove::>(); + if let Some(mut commands) = commands.get_entity(e) { + commands.remove::>(); } } @@ -166,16 +177,12 @@ fn apply_wireframe_material( // If there's no color specified we can use the global material since it's already set to use the default_color global_material.handle.clone() }; - wireframes_to_spawn.push((e, material)); + wireframes_to_spawn.push((e, MeshMaterial2d(material))); } commands.insert_or_spawn_batch(wireframes_to_spawn); } -type Wireframe2dFilter = ( - With, - Without, - Without, -); +type Wireframe2dFilter = (With, Without, Without); /// Applies or removes a wireframe material on any mesh without a [`Wireframe2d`] or [`NoWireframe2d`] component. fn apply_global_wireframe_material( @@ -183,11 +190,14 @@ fn apply_global_wireframe_material( config: Res, meshes_without_material: Query< Entity, - (Wireframe2dFilter, Without>), + ( + Wireframe2dFilter, + Without>, + ), >, meshes_with_global_material: Query< Entity, - (Wireframe2dFilter, With>), + (Wireframe2dFilter, With>), >, global_material: Res, ) { @@ -196,12 +206,14 @@ fn apply_global_wireframe_material( for e in &meshes_without_material { // We only add the material handle but not the Wireframe component // This makes it easy to detect which mesh is using the global material and which ones are user specified - material_to_spawn.push((e, global_material.handle.clone())); + material_to_spawn.push((e, MeshMaterial2d(global_material.handle.clone()))); } commands.insert_or_spawn_batch(material_to_spawn); } else { for e in &meshes_with_global_material { - commands.entity(e).remove::>(); + commands + .entity(e) + .remove::>(); } } } diff --git a/crates/bevy_text/src/pipeline.rs b/crates/bevy_text/src/pipeline.rs index c969b9dc2d470..d435d25db26aa 100644 --- a/crates/bevy_text/src/pipeline.rs +++ b/crates/bevy_text/src/pipeline.rs @@ -16,8 +16,8 @@ use bevy_utils::HashMap; use cosmic_text::{Attrs, Buffer, Family, Metrics, Shaping, Wrap}; use crate::{ - error::TextError, BreakLineOn, CosmicBuffer, Font, FontAtlasSets, FontSmoothing, JustifyText, - PositionedGlyph, TextBounds, TextSection, YAxisOrientation, + error::TextError, CosmicBuffer, Font, FontAtlasSets, FontSmoothing, JustifyText, LineBreak, + PositionedGlyph, TextBounds, TextSection, TextStyle, YAxisOrientation, }; /// A wrapper resource around a [`cosmic_text::FontSystem`] @@ -51,17 +51,26 @@ impl Default for SwashCache { } } +/// Information about a font collected as part of preparing for text layout. +#[derive(Clone)] +struct FontFaceInfo { + stretch: cosmic_text::fontdb::Stretch, + style: cosmic_text::fontdb::Style, + weight: cosmic_text::fontdb::Weight, + family_name: Arc, +} + /// The `TextPipeline` is used to layout and render [`Text`](crate::Text). /// /// See the [crate-level documentation](crate) for more information. #[derive(Default, Resource)] pub struct TextPipeline { /// Identifies a font [`ID`](cosmic_text::fontdb::ID) by its [`Font`] [`Asset`](bevy_asset::Asset). - map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, String)>, + map_handle_to_font_id: HashMap, (cosmic_text::fontdb::ID, Arc)>, /// Buffered vec for collecting spans. /// /// See [this dark magic](https://users.rust-lang.org/t/how-to-cache-a-vectors-capacity/94478/10). - spans_buffer: Vec<(&'static str, Attrs<'static>)>, + spans_buffer: Vec<(usize, &'static str, &'static TextStyle, FontFaceInfo)>, } impl TextPipeline { @@ -69,11 +78,11 @@ impl TextPipeline { /// /// Negative or 0.0 font sizes will not be laid out. #[allow(clippy::too_many_arguments)] - pub fn update_buffer( + pub fn update_buffer<'a>( &mut self, fonts: &Assets, - sections: &[TextSection], - linebreak_behavior: BreakLineOn, + text_spans: impl Iterator, + linebreak: LineBreak, bounds: TextBounds, scale_factor: f64, buffer: &mut CosmicBuffer, @@ -82,16 +91,45 @@ impl TextPipeline { ) -> Result<(), TextError> { let font_system = &mut font_system.0; - // return early if the fonts are not loaded yet - let mut font_size = 0.; - for section in sections { - if section.style.font_size > font_size { - font_size = section.style.font_size; + // Collect span information into a vec. This is necessary because font loading requires mut access + // to FontSystem, which the cosmic-text Buffer also needs. + let mut font_size: f32 = 0.; + let mut spans: Vec<(usize, &str, &TextStyle, FontFaceInfo)> = + core::mem::take(&mut self.spans_buffer) + .into_iter() + .map(|_| -> (usize, &str, &TextStyle, FontFaceInfo) { unreachable!() }) + .collect(); + + for (span_index, (span, style)) in text_spans.enumerate() { + // Return early if a font is not loaded yet. + if !fonts.contains(style.font.id()) { + spans.clear(); + self.spans_buffer = spans + .into_iter() + .map( + |_| -> (usize, &'static str, &'static TextStyle, FontFaceInfo) { + unreachable!() + }, + ) + .collect(); + + return Err(TextError::NoSuchFont); + } + + // Get max font size for use in cosmic Metrics. + font_size = font_size.max(style.font_size); + + // Load Bevy fonts into cosmic-text's font system. + let face_info = + load_font_to_fontdb(style, font_system, &mut self.map_handle_to_font_id, fonts); + + // Save spans that aren't zero-sized. + if scale_factor <= 0.0 || style.font_size <= 0.0 { + continue; } - fonts - .get(section.style.font.id()) - .ok_or(TextError::NoSuchFont)?; + spans.push((span_index, span, style, face_info)); } + let line_height = font_size * 1.2; let mut metrics = Metrics::new(font_size, line_height).scale(scale_factor as f32); // Metrics of 0.0 cause `Buffer::set_metrics` to panic. We hack around this by 'falling @@ -100,55 +138,30 @@ impl TextPipeline { metrics.font_size = metrics.font_size.max(0.000001); metrics.line_height = metrics.line_height.max(0.000001); - // Load Bevy fonts into cosmic-text's font system. - // This is done as as separate pre-pass to avoid borrow checker issues - for section in sections.iter() { - load_font_to_fontdb(section, font_system, &mut self.map_handle_to_font_id, fonts); - } - // Map text sections to cosmic-text spans, and ignore sections with negative or zero fontsizes, // since they cannot be rendered by cosmic-text. // // The section index is stored in the metadata of the spans, and could be used // to look up the section the span came from and is not used internally // in cosmic-text. - let mut spans: Vec<(&str, Attrs)> = core::mem::take(&mut self.spans_buffer) - .into_iter() - .map(|_| -> (&str, Attrs) { unreachable!() }) - .collect(); - // `metrics.font_size` hack continued: ignore all spans when scale_factor is zero. - if scale_factor > 0.0 { - spans.extend( - sections - .iter() - .enumerate() - .filter(|(_section_index, section)| section.style.font_size > 0.0) - .map(|(section_index, section)| { - ( - §ion.value[..], - get_attrs( - section, - section_index, - font_system, - &self.map_handle_to_font_id, - scale_factor, - ), - ) - }), - ); - } - let spans_iter = spans.iter().copied(); + let spans_iter = spans.iter().map(|(span_index, span, style, font_info)| { + ( + *span, + get_attrs(*span_index, style, font_info, scale_factor), + ) + }); + // Update the buffer. buffer.set_metrics(font_system, metrics); buffer.set_size(font_system, bounds.width, bounds.height); buffer.set_wrap( font_system, - match linebreak_behavior { - BreakLineOn::WordBoundary => Wrap::Word, - BreakLineOn::AnyCharacter => Wrap::Glyph, - BreakLineOn::WordOrCharacter => Wrap::WordOrGlyph, - BreakLineOn::NoWrap => Wrap::None, + match linebreak { + LineBreak::WordBoundary => Wrap::Word, + LineBreak::AnyCharacter => Wrap::Glyph, + LineBreak::WordOrCharacter => Wrap::WordOrGlyph, + LineBreak::NoWrap => Wrap::None, }, ); @@ -165,7 +178,7 @@ impl TextPipeline { spans.clear(); self.spans_buffer = spans .into_iter() - .map(|_| -> (&'static str, Attrs<'static>) { unreachable!() }) + .map(|_| -> (usize, &'static str, &'static TextStyle, FontFaceInfo) { unreachable!() }) .collect(); Ok(()) @@ -183,7 +196,7 @@ impl TextPipeline { sections: &[TextSection], scale_factor: f64, text_alignment: JustifyText, - linebreak_behavior: BreakLineOn, + linebreak: LineBreak, font_smoothing: FontSmoothing, bounds: TextBounds, font_atlas_sets: &mut FontAtlasSets, @@ -203,8 +216,10 @@ impl TextPipeline { self.update_buffer( fonts, - sections, - linebreak_behavior, + sections + .iter() + .map(|section| (section.value.as_str(), §ion.style)), + linebreak, bounds, scale_factor, buffer, @@ -301,7 +316,7 @@ impl TextPipeline { fonts: &Assets, sections: &[TextSection], scale_factor: f64, - linebreak_behavior: BreakLineOn, + linebreak: LineBreak, buffer: &mut CosmicBuffer, text_alignment: JustifyText, font_system: &mut CosmicFontSystem, @@ -310,8 +325,10 @@ impl TextPipeline { self.update_buffer( fonts, - sections, - linebreak_behavior, + sections + .iter() + .map(|section| (section.value.as_str(), §ion.style)), + linebreak, MIN_WIDTH_CONTENT_BOUNDS, scale_factor, buffer, @@ -384,13 +401,13 @@ impl TextMeasureInfo { } fn load_font_to_fontdb( - section: &TextSection, + style: &TextStyle, font_system: &mut cosmic_text::FontSystem, - map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, String)>, + map_handle_to_font_id: &mut HashMap, (cosmic_text::fontdb::ID, Arc)>, fonts: &Assets, -) { - let font_handle = section.style.font.clone(); - map_handle_to_font_id +) -> FontFaceInfo { + let font_handle = style.font.clone(); + let (face_id, family_name) = map_handle_to_font_id .entry(font_handle.id()) .or_insert_with(|| { let font = fonts.get(font_handle.id()).expect( @@ -404,34 +421,35 @@ fn load_font_to_fontdb( // TODO: it is assumed this is the right font face let face_id = *ids.last().unwrap(); let face = font_system.db().face(face_id).unwrap(); - let family_name = face.families[0].0.to_owned(); + let family_name = Arc::from(face.families[0].0.as_str()); (face_id, family_name) }); + let face = font_system.db().face(*face_id).unwrap(); + + FontFaceInfo { + stretch: face.stretch, + style: face.style, + weight: face.weight, + family_name: family_name.clone(), + } } -/// Translates [`TextSection`] to [`Attrs`], -/// loading fonts into the [`Database`](cosmic_text::fontdb::Database) if required. +/// Translates [`TextStyle`] to [`Attrs`]. fn get_attrs<'a>( - section: &TextSection, - section_index: usize, - font_system: &mut cosmic_text::FontSystem, - map_handle_to_font_id: &'a HashMap, (cosmic_text::fontdb::ID, String)>, + span_index: usize, + style: &TextStyle, + face_info: &'a FontFaceInfo, scale_factor: f64, ) -> Attrs<'a> { - let (face_id, family_name) = map_handle_to_font_id - .get(§ion.style.font.id()) - .expect("Already loaded with load_font_to_fontdb"); - let face = font_system.db().face(*face_id).unwrap(); - let attrs = Attrs::new() - .metadata(section_index) - .family(Family::Name(family_name)) - .stretch(face.stretch) - .style(face.style) - .weight(face.weight) - .metrics(Metrics::relative(section.style.font_size, 1.2).scale(scale_factor as f32)) - .color(cosmic_text::Color(section.style.color.to_linear().as_u32())); + .metadata(span_index) + .family(Family::Name(&face_info.family_name)) + .stretch(face_info.stretch) + .style(face_info.style) + .weight(face_info.weight) + .metrics(Metrics::relative(style.font_size, 1.2).scale(scale_factor as f32)) + .color(cosmic_text::Color(style.color.to_linear().as_u32())); attrs } diff --git a/crates/bevy_text/src/text.rs b/crates/bevy_text/src/text.rs index a785bb51afac2..5f6215ea0ad6f 100644 --- a/crates/bevy_text/src/text.rs +++ b/crates/bevy_text/src/text.rs @@ -35,7 +35,7 @@ pub struct Text { /// Should not affect its position within a container. pub justify: JustifyText, /// How the text should linebreak when running out of the bounds determined by `max_size` - pub linebreak_behavior: BreakLineOn, + pub linebreak: LineBreak, /// The antialiasing method to use when rendering text. pub font_smoothing: FontSmoothing, } @@ -123,7 +123,7 @@ impl Text { /// Returns this [`Text`] with soft wrapping disabled. /// Hard wrapping, where text contains an explicit linebreak such as the escape sequence `\n`, will still occur. pub const fn with_no_wrap(mut self) -> Self { - self.linebreak_behavior = BreakLineOn::NoWrap; + self.linebreak = LineBreak::NoWrap; self } @@ -253,7 +253,7 @@ impl Default for TextStyle { /// Determines how lines will be broken when preventing text from running out of bounds. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Reflect, Serialize, Deserialize)] #[reflect(Serialize, Deserialize)] -pub enum BreakLineOn { +pub enum LineBreak { /// Uses the [Unicode Line Breaking Algorithm](https://www.unicode.org/reports/tr14/). /// Lines will be broken up at the nearest suitable word boundary, usually a space. /// This behavior suits most cases, as it keeps words intact across linebreaks. diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 5b84c0c4a8f84..ed744c54ff177 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -1,6 +1,6 @@ use crate::pipeline::CosmicFontSystem; use crate::{ - BreakLineOn, CosmicBuffer, Font, FontAtlasSets, PositionedGlyph, SwashCache, Text, TextBounds, + CosmicBuffer, Font, FontAtlasSets, LineBreak, PositionedGlyph, SwashCache, Text, TextBounds, TextError, TextLayoutInfo, TextPipeline, YAxisOrientation, }; use bevy_asset::Assets; @@ -176,7 +176,7 @@ pub fn update_text2d_layout( for (entity, text, bounds, text_layout_info, mut buffer) in &mut text_query { if factor_changed || text.is_changed() || bounds.is_changed() || queue.remove(&entity) { let text_bounds = TextBounds { - width: if text.linebreak_behavior == BreakLineOn::NoWrap { + width: if text.linebreak == LineBreak::NoWrap { None } else { bounds.width.map(|width| scale_value(width, scale_factor)) @@ -193,7 +193,7 @@ pub fn update_text2d_layout( &text.sections, scale_factor.into(), text.justify, - text.linebreak_behavior, + text.linebreak, text.font_smoothing, text_bounds, &mut font_atlas_sets, diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index 53cfb272b2219..fb17415fe2eb4 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -1,6 +1,6 @@ use crate::{ prelude::{Button, Label}, - Node, UiImage, + Node, UiChildren, UiImage, }; use bevy_a11y::{ accesskit::{NodeBuilder, Rect, Role}, @@ -14,15 +14,14 @@ use bevy_ecs::{ system::{Commands, Query}, world::Ref, }; -use bevy_hierarchy::Children; use bevy_render::{camera::CameraUpdateSystem, prelude::Camera}; use bevy_text::Text; use bevy_transform::prelude::GlobalTransform; -fn calc_name(texts: &Query<&Text>, children: &Children) -> Option> { +fn calc_name(texts: &Query<&Text>, children: impl Iterator) -> Option> { let mut name = None; for child in children { - if let Ok(text) = texts.get(*child) { + if let Ok(text) = texts.get(child) { let values = text .sections .iter() @@ -59,11 +58,12 @@ fn calc_bounds( fn button_changed( mut commands: Commands, - mut query: Query<(Entity, &Children, Option<&mut AccessibilityNode>), Changed