Merge branch 'main' into mod-picking-ui-example

This commit is contained in:
Rich Churcher 2024-11-17 17:26:36 +13:00
commit 49c6000e61
80 changed files with 1683 additions and 708 deletions

View file

@ -230,10 +230,10 @@ jobs:
- name: Taplo info - name: Taplo info
if: failure() if: failure()
run: | run: |
echo 'To fix toml fmt, please run `taplo fmt`' echo 'To fix toml fmt, please run `taplo fmt`.'
echo 'To check for a diff, run `taplo fmt --check --diff' echo 'To check for a diff, run `taplo fmt --check --diff`.'
echo 'You can find taplo here: https://taplo.tamasfe.dev/' echo 'You can find taplo here: https://taplo.tamasfe.dev/'
echo 'Or if you use VSCode, use the `Even Better Toml` extension with 2 spaces' echo 'Or if you use VSCode, use the `Even Better Toml` extension.'
echo 'You can find the extension here: https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml' echo 'You can find the extension here: https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml'
typos: typos:
@ -242,7 +242,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Check for typos - name: Check for typos
uses: crate-ci/typos@v1.27.0 uses: crate-ci/typos@v1.27.3
- name: Typos info - name: Typos info
if: failure() if: failure()
run: | run: |

View file

@ -31,7 +31,7 @@ jobs:
with: with:
path: | path: |
target target
key: ${{ runner.os }}-ios-install-${{ matrix.toolchain }}-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-ios-install-${{ hashFiles('**/Cargo.lock') }}
# TODO: remove x86 target once it always run on arm GitHub runners # TODO: remove x86 target once it always run on arm GitHub runners
- name: Add iOS targets - name: Add iOS targets

View file

@ -59,28 +59,10 @@ jobs:
# See tools/ci/src/main.rs for the commands this runs # See tools/ci/src/main.rs for the commands this runs
run: cargo run -p ci -- compile run: cargo run -p ci -- compile
check-doc:
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@beta
- name: Install Linux dependencies
uses: ./.github/actions/install-linux-deps
with:
wayland: true
xkb: true
- name: Build and check docs
# See tools/ci/src/main.rs for the commands this runs
run: cargo run -p ci -- doc
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: "-C debuginfo=0 --cfg docsrs_dep"
open-issue: open-issue:
name: Warn that weekly CI fails name: Warn that weekly CI fails
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [test, lint, check-compiles, check-doc] needs: [test, lint, check-compiles]
permissions: permissions:
issues: write issues: write
# Use always() so the job doesn't get canceled if any other jobs fail # Use always() so the job doesn't get canceled if any other jobs fail

12
.gitignore vendored
View file

@ -1,9 +1,9 @@
# Rust build artifacts # Rust build artifacts
/target target
crates/*/target crates/**/target
benches/**/target
tools/**/target
**/*.rs.bk **/*.rs.bk
/benches/target
/tools/compile_fail_utils/target
# Cargo # Cargo
Cargo.lock Cargo.lock
@ -11,8 +11,8 @@ Cargo.lock
.cargo/config.toml .cargo/config.toml
# IDE files # IDE files
/.idea .idea
/.vscode .vscode
.zed .zed
dxcompiler.dll dxcompiler.dll
dxil.dll dxil.dll

View file

@ -125,6 +125,7 @@ default = [
"bevy_text", "bevy_text",
"bevy_ui", "bevy_ui",
"bevy_ui_picking_backend", "bevy_ui_picking_backend",
"bevy_window",
"bevy_winit", "bevy_winit",
"custom_cursor", "custom_cursor",
"default_font", "default_font",
@ -218,6 +219,9 @@ bevy_ui = [
"bevy_ui_picking_backend", "bevy_ui_picking_backend",
] ]
# Windowing layer
bevy_window = ["bevy_internal/bevy_window"]
# winit window and input backend # winit window and input backend
bevy_winit = ["bevy_internal/bevy_winit"] bevy_winit = ["bevy_internal/bevy_winit"]
@ -405,7 +409,7 @@ pbr_multi_layer_material_textures = [
pbr_anisotropy_texture = ["bevy_internal/pbr_anisotropy_texture"] pbr_anisotropy_texture = ["bevy_internal/pbr_anisotropy_texture"]
# Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs # Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs
pbr_pcss = ["bevy_internal/pbr_pcss"] experimental_pbr_pcss = ["bevy_internal/experimental_pbr_pcss"]
# Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU. # Enable some limitations to be able to use WebGL2. Please refer to the [WebGL2 and WebGPU](https://github.com/bevyengine/bevy/tree/latest/examples#webgl2-and-webgpu) section of the examples README for more information on how to run Wasm builds with WebGPU.
webgl2 = ["bevy_internal/webgl"] webgl2 = ["bevy_internal/webgl"]
@ -2706,6 +2710,17 @@ description = "Test rendering of many UI elements"
category = "Stress Tests" category = "Stress Tests"
wasm = true wasm = true
[[example]]
name = "many_cameras_lights"
path = "examples/stress_tests/many_cameras_lights.rs"
doc-scrape-examples = true
[package.metadata.example.many_cameras_lights]
name = "Many Cameras & Lights"
description = "Test rendering of many cameras and lights"
category = "Stress Tests"
wasm = true
[[example]] [[example]]
name = "many_cubes" name = "many_cubes"
path = "examples/stress_tests/many_cubes.rs" path = "examples/stress_tests/many_cubes.rs"
@ -3791,7 +3806,7 @@ wasm = true
name = "pcss" name = "pcss"
path = "examples/3d/pcss.rs" path = "examples/3d/pcss.rs"
doc-scrape-examples = true doc-scrape-examples = true
required-features = ["pbr_pcss"] required-features = ["experimental_pbr_pcss"]
[package.metadata.example.pcss] [package.metadata.example.pcss]
name = "Percentage-closer soft shadows" name = "Percentage-closer soft shadows"

View file

@ -1,6 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use bevy_math::{prelude::*, *}; use bevy_math::prelude::*;
fn easing(c: &mut Criterion) { fn easing(c: &mut Criterion) {
let cubic_bezier = CubicSegment::new_bezier(vec2(0.25, 0.1), vec2(0.25, 1.0)); let cubic_bezier = CubicSegment::new_bezier(vec2(0.25, 0.1), vec2(0.25, 1.0));

View file

@ -7,9 +7,9 @@
//! `Curve<Vec3>` that we want to use to animate something. That could be defined in //! `Curve<Vec3>` that we want to use to animate something. That could be defined in
//! a number of different ways, but let's imagine that we've defined it [using a function]: //! a number of different ways, but let's imagine that we've defined it [using a function]:
//! //!
//! # use bevy_math::curve::{Curve, Interval, function_curve}; //! # use bevy_math::curve::{Curve, Interval, FunctionCurve};
//! # use bevy_math::vec3; //! # use bevy_math::vec3;
//! let wobble_curve = function_curve( //! let wobble_curve = FunctionCurve::new(
//! Interval::UNIT, //! Interval::UNIT,
//! |t| { vec3(t.cos(), 0.0, 0.0) }, //! |t| { vec3(t.cos(), 0.0, 0.0) },
//! ); //! );
@ -25,10 +25,10 @@
//! the adaptor [`TranslationCurve`], which wraps any `Curve<Vec3>` and turns it into an //! the adaptor [`TranslationCurve`], which wraps any `Curve<Vec3>` and turns it into an
//! [`AnimationCurve`] that will use the given curve to animate the entity's translation: //! [`AnimationCurve`] that will use the given curve to animate the entity's translation:
//! //!
//! # use bevy_math::curve::{Curve, Interval, function_curve}; //! # use bevy_math::curve::{Curve, Interval, FunctionCurve};
//! # use bevy_math::vec3; //! # use bevy_math::vec3;
//! # use bevy_animation::animation_curves::*; //! # use bevy_animation::animation_curves::*;
//! # let wobble_curve = function_curve( //! # let wobble_curve = FunctionCurve::new(
//! # Interval::UNIT, //! # Interval::UNIT,
//! # |t| vec3(t.cos(), 0.0, 0.0) //! # |t| vec3(t.cos(), 0.0, 0.0)
//! # ); //! # );
@ -37,11 +37,11 @@
//! And finally, this `AnimationCurve` needs to be added to an [`AnimationClip`] in order to //! And finally, this `AnimationCurve` needs to be added to an [`AnimationClip`] in order to
//! actually animate something. This is what that looks like: //! actually animate something. This is what that looks like:
//! //!
//! # use bevy_math::curve::{Curve, Interval, function_curve}; //! # use bevy_math::curve::{Curve, Interval, FunctionCurve};
//! # use bevy_animation::{AnimationClip, AnimationTargetId, animation_curves::*}; //! # use bevy_animation::{AnimationClip, AnimationTargetId, animation_curves::*};
//! # use bevy_core::Name; //! # use bevy_core::Name;
//! # use bevy_math::vec3; //! # use bevy_math::vec3;
//! # let wobble_curve = function_curve( //! # let wobble_curve = FunctionCurve::new(
//! # Interval::UNIT, //! # Interval::UNIT,
//! # |t| { vec3(t.cos(), 0.0, 0.0) }, //! # |t| { vec3(t.cos(), 0.0, 0.0) },
//! # ); //! # );
@ -71,7 +71,7 @@
//! Animation of arbitrary components can be accomplished using [`AnimatableProperty`] in //! Animation of arbitrary components can be accomplished using [`AnimatableProperty`] in
//! conjunction with [`AnimatableCurve`]. See the documentation [there] for details. //! conjunction with [`AnimatableCurve`]. See the documentation [there] for details.
//! //!
//! [using a function]: bevy_math::curve::function_curve //! [using a function]: bevy_math::curve::FunctionCurve
//! [translation component of a `Transform`]: bevy_transform::prelude::Transform::translation //! [translation component of a `Transform`]: bevy_transform::prelude::Transform::translation
//! [`AnimationClip`]: crate::AnimationClip //! [`AnimationClip`]: crate::AnimationClip
//! [there]: AnimatableProperty //! [there]: AnimatableProperty

View file

@ -1,9 +1,12 @@
use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder}; use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder};
use bevy_utils::tracing::trace; use bevy_utils::tracing::trace;
use alloc::sync::Arc;
use core::fmt::Debug;
/// Defines a simple way to determine how many threads to use given the number of remaining cores /// Defines a simple way to determine how many threads to use given the number of remaining cores
/// and number of total cores /// and number of total cores
#[derive(Clone, Debug)] #[derive(Clone)]
pub struct TaskPoolThreadAssignmentPolicy { pub struct TaskPoolThreadAssignmentPolicy {
/// Force using at least this many threads /// Force using at least this many threads
pub min_threads: usize, pub min_threads: usize,
@ -12,6 +15,22 @@ pub struct TaskPoolThreadAssignmentPolicy {
/// Target using this percentage of total cores, clamped by `min_threads` and `max_threads`. It is /// Target using this percentage of total cores, clamped by `min_threads` and `max_threads`. It is
/// permitted to use 1.0 to try to use all remaining threads /// permitted to use 1.0 to try to use all remaining threads
pub percent: f32, pub percent: f32,
/// Callback that is invoked once for every created thread as it starts.
/// This configuration will be ignored under wasm platform.
pub on_thread_spawn: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
/// Callback that is invoked once for every created thread as it terminates
/// This configuration will be ignored under wasm platform.
pub on_thread_destroy: Option<Arc<dyn Fn() + Send + Sync + 'static>>,
}
impl Debug for TaskPoolThreadAssignmentPolicy {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("TaskPoolThreadAssignmentPolicy")
.field("min_threads", &self.min_threads)
.field("max_threads", &self.max_threads)
.field("percent", &self.percent)
.finish()
}
} }
impl TaskPoolThreadAssignmentPolicy { impl TaskPoolThreadAssignmentPolicy {
@ -61,6 +80,8 @@ impl Default for TaskPoolOptions {
min_threads: 1, min_threads: 1,
max_threads: 4, max_threads: 4,
percent: 0.25, percent: 0.25,
on_thread_spawn: None,
on_thread_destroy: None,
}, },
// Use 25% of cores for async compute, at least 1, no more than 4 // Use 25% of cores for async compute, at least 1, no more than 4
@ -68,6 +89,8 @@ impl Default for TaskPoolOptions {
min_threads: 1, min_threads: 1,
max_threads: 4, max_threads: 4,
percent: 0.25, percent: 0.25,
on_thread_spawn: None,
on_thread_destroy: None,
}, },
// Use all remaining cores for compute (at least 1) // Use all remaining cores for compute (at least 1)
@ -75,6 +98,8 @@ impl Default for TaskPoolOptions {
min_threads: 1, min_threads: 1,
max_threads: usize::MAX, max_threads: usize::MAX,
percent: 1.0, // This 1.0 here means "whatever is left over" percent: 1.0, // This 1.0 here means "whatever is left over"
on_thread_spawn: None,
on_thread_destroy: None,
}, },
} }
} }
@ -108,10 +133,21 @@ impl TaskPoolOptions {
remaining_threads = remaining_threads.saturating_sub(io_threads); remaining_threads = remaining_threads.saturating_sub(io_threads);
IoTaskPool::get_or_init(|| { IoTaskPool::get_or_init(|| {
TaskPoolBuilder::default() let mut builder = TaskPoolBuilder::default()
.num_threads(io_threads) .num_threads(io_threads)
.thread_name("IO Task Pool".to_string()) .thread_name("IO Task Pool".to_string());
.build()
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(f) = self.io.on_thread_spawn.clone() {
builder = builder.on_thread_spawn(move || f());
}
if let Some(f) = self.io.on_thread_destroy.clone() {
builder = builder.on_thread_destroy(move || f());
}
}
builder.build()
}); });
} }
@ -125,10 +161,21 @@ impl TaskPoolOptions {
remaining_threads = remaining_threads.saturating_sub(async_compute_threads); remaining_threads = remaining_threads.saturating_sub(async_compute_threads);
AsyncComputeTaskPool::get_or_init(|| { AsyncComputeTaskPool::get_or_init(|| {
TaskPoolBuilder::default() let mut builder = TaskPoolBuilder::default()
.num_threads(async_compute_threads) .num_threads(async_compute_threads)
.thread_name("Async Compute Task Pool".to_string()) .thread_name("Async Compute Task Pool".to_string());
.build()
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(f) = self.async_compute.on_thread_spawn.clone() {
builder = builder.on_thread_spawn(move || f());
}
if let Some(f) = self.async_compute.on_thread_destroy.clone() {
builder = builder.on_thread_destroy(move || f());
}
}
builder.build()
}); });
} }
@ -142,10 +189,21 @@ impl TaskPoolOptions {
trace!("Compute Threads: {}", compute_threads); trace!("Compute Threads: {}", compute_threads);
ComputeTaskPool::get_or_init(|| { ComputeTaskPool::get_or_init(|| {
TaskPoolBuilder::default() let mut builder = TaskPoolBuilder::default()
.num_threads(compute_threads) .num_threads(compute_threads)
.thread_name("Compute Task Pool".to_string()) .thread_name("Compute Task Pool".to_string());
.build()
#[cfg(not(target_arch = "wasm32"))]
{
if let Some(f) = self.compute.on_thread_spawn.clone() {
builder = builder.on_thread_spawn(move || f());
}
if let Some(f) = self.compute.on_thread_destroy.clone() {
builder = builder.on_thread_destroy(move || f());
}
}
builder.build()
}); });
} }
} }

View file

@ -29,7 +29,8 @@
//! * Compatibility with SSAA and MSAA. //! * Compatibility with SSAA and MSAA.
//! //!
//! [SMAA]: https://www.iryoku.com/smaa/ //! [SMAA]: https://www.iryoku.com/smaa/
#[cfg(not(feature = "smaa_luts"))]
use crate::tonemapping::lut_placeholder;
use crate::{ use crate::{
core_2d::graph::{Core2d, Node2d}, core_2d::graph::{Core2d, Node2d},
core_3d::graph::{Core3d, Node3d}, core_3d::graph::{Core3d, Node3d},

View file

@ -33,9 +33,7 @@ bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.15.0-dev" } bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev", features = [ bevy_ui = { path = "../bevy_ui", version = "0.15.0-dev" }
"bevy_text",
] }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }
bevy_state = { path = "../bevy_state", version = "0.15.0-dev" } bevy_state = { path = "../bevy_state", version = "0.15.0-dev" }

View file

@ -17,7 +17,7 @@ use core::time::Duration;
/// (`ci_testing_config.ron` by default) and executes its specified actions. For a reference of the /// (`ci_testing_config.ron` by default) and executes its specified actions. For a reference of the
/// allowed configuration, see [`CiTestingConfig`]. /// allowed configuration, see [`CiTestingConfig`].
/// ///
/// This plugin is included within `DefaultPlugins`, `HeadlessPlugins` and `MinimalPlugins` /// This plugin is included within `DefaultPlugins` and `MinimalPlugins`
/// when the `bevy_ci_testing` feature is enabled. /// when the `bevy_ci_testing` feature is enabled.
/// It is recommended to only used this plugin during testing (manual or /// It is recommended to only used this plugin during testing (manual or
/// automatic), and disable it during regular development and for production builds. /// automatic), and disable it during regular development and for production builds.

View file

@ -82,7 +82,7 @@ pub fn derive_component(input: TokenStream) -> TokenStream {
for require in requires { for require in requires {
let ident = &require.path; let ident = &require.path;
register_recursive_requires.push(quote! { register_recursive_requires.push(quote! {
<#ident as Component>::register_required_components( <#ident as #bevy_ecs_path::component::Component>::register_required_components(
requiree, requiree,
components, components,
storages, storages,

View file

@ -772,7 +772,7 @@ impl<T: SparseSetIndex> Access<T> {
/// `Access`, it's not recommended. Prefer to manage your own lists of /// `Access`, it's not recommended. Prefer to manage your own lists of
/// accessible components if your application needs to do that. /// accessible components if your application needs to do that.
#[doc(hidden)] #[doc(hidden)]
#[deprecated] // TODO: this should be deprecated and removed, see https://github.com/bevyengine/bevy/issues/16339
pub fn component_reads_and_writes(&self) -> (impl Iterator<Item = T> + '_, bool) { pub fn component_reads_and_writes(&self) -> (impl Iterator<Item = T> + '_, bool) {
( (
self.component_read_and_writes self.component_read_and_writes

View file

@ -4,6 +4,7 @@ use core::{marker::PhantomData, panic::Location};
use super::{ use super::{
Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource, RunSystemCachedWith, Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource, RunSystemCachedWith,
UnregisterSystem,
}; };
use crate::{ use crate::{
self as bevy_ecs, self as bevy_ecs,
@ -890,6 +891,17 @@ impl<'w, 's> Commands<'w, 's> {
SystemId::from_entity(entity) SystemId::from_entity(entity)
} }
/// Removes a system previously registered with [`Commands::register_system`] or [`World::register_system`].
///
/// See [`World::unregister_system`] for more information.
pub fn unregister_system<I, O>(&mut self, system_id: SystemId<I, O>)
where
I: SystemInput + Send + 'static,
O: Send + 'static,
{
self.queue(UnregisterSystem::new(system_id));
}
/// Similar to [`Self::run_system`], but caching the [`SystemId`] in a /// Similar to [`Self::run_system`], but caching the [`SystemId`] in a
/// [`CachedSystemId`](crate::system::CachedSystemId) resource. /// [`CachedSystemId`](crate::system::CachedSystemId) resource.
/// ///

View file

@ -30,7 +30,7 @@ pub struct SystemIdMarker;
/// A system that has been removed from the registry. /// A system that has been removed from the registry.
/// It contains the system and whether or not it has been initialized. /// It contains the system and whether or not it has been initialized.
/// ///
/// This struct is returned by [`World::remove_system`]. /// This struct is returned by [`World::unregister_system`].
pub struct RemovedSystem<I = (), O = ()> { pub struct RemovedSystem<I = (), O = ()> {
initialized: bool, initialized: bool,
system: BoxedSystem<I, O>, system: BoxedSystem<I, O>,
@ -172,7 +172,7 @@ impl World {
/// ///
/// If no system corresponds to the given [`SystemId`], this method returns an error. /// If no system corresponds to the given [`SystemId`], this method returns an error.
/// Systems are also not allowed to remove themselves, this returns an error too. /// Systems are also not allowed to remove themselves, this returns an error too.
pub fn remove_system<I, O>( pub fn unregister_system<I, O>(
&mut self, &mut self,
id: SystemId<I, O>, id: SystemId<I, O>,
) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>> ) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>>
@ -412,7 +412,7 @@ impl World {
/// Removes a cached system and its [`CachedSystemId`] resource. /// Removes a cached system and its [`CachedSystemId`] resource.
/// ///
/// See [`World::register_system_cached`] for more information. /// See [`World::register_system_cached`] for more information.
pub fn remove_system_cached<I, O, M, S>( pub fn unregister_system_cached<I, O, M, S>(
&mut self, &mut self,
_system: S, _system: S,
) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>> ) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>>
@ -424,7 +424,7 @@ impl World {
let id = self let id = self
.remove_resource::<CachedSystemId<S::System>>() .remove_resource::<CachedSystemId<S::System>>()
.ok_or(RegisteredSystemError::SystemNotCached)?; .ok_or(RegisteredSystemError::SystemNotCached)?;
self.remove_system(id.0) self.unregister_system(id.0)
} }
/// Runs a cached system, registering it if necessary. /// Runs a cached system, registering it if necessary.
@ -544,6 +544,32 @@ where
} }
} }
/// The [`Command`] type for unregistering one-shot systems from [`Commands`](crate::system::Commands).
pub struct UnregisterSystem<I: SystemInput + 'static, O: 'static> {
system_id: SystemId<I, O>,
}
impl<I, O> UnregisterSystem<I, O>
where
I: SystemInput + 'static,
O: 'static,
{
/// Creates a new [`Command`] struct, which can be added to [`Commands`](crate::system::Commands).
pub fn new(system_id: SystemId<I, O>) -> Self {
Self { system_id }
}
}
impl<I, O> Command for UnregisterSystem<I, O>
where
I: SystemInput + 'static,
O: 'static,
{
fn apply(self, world: &mut World) {
let _ = world.unregister_system(self.system_id);
}
}
/// The [`Command`] type for running a cached one-shot system from /// The [`Command`] type for running a cached one-shot system from
/// [`Commands`](crate::system::Commands). /// [`Commands`](crate::system::Commands).
/// ///
@ -834,7 +860,7 @@ mod tests {
let new = world.register_system_cached(four); let new = world.register_system_cached(four);
assert_eq!(old, new); assert_eq!(old, new);
let result = world.remove_system_cached(four); let result = world.unregister_system_cached(four);
assert!(result.is_ok()); assert!(result.is_ok());
let new = world.register_system_cached(four); let new = world.register_system_cached(four);
assert_ne!(old, new); assert_ne!(old, new);

View file

@ -730,6 +730,34 @@ impl<'w> EntityMut<'w> {
unsafe { component_ids.fetch_mut(self.0) } unsafe { component_ids.fetch_mut(self.0) }
} }
/// Returns [untyped mutable reference](MutUntyped) to component for
/// the current entity, based on the given [`ComponentId`].
///
/// Unlike [`EntityMut::get_mut_by_id`], this method borrows &self instead of
/// &mut self, allowing the caller to access multiple components simultaneously.
///
/// # Errors
///
/// - Returns [`EntityComponentError::MissingComponent`] if the entity does
/// not have a component.
/// - Returns [`EntityComponentError::AliasedMutability`] if a component
/// is requested multiple times.
///
/// # Safety
/// It is the callers responsibility to ensure that
/// - the [`UnsafeEntityCell`] has permission to access the component mutably
/// - no other references to the component exist at the same time
#[inline]
pub unsafe fn get_mut_by_id_unchecked<F: DynamicComponentFetch>(
&self,
component_ids: F,
) -> Result<F::Mut<'_>, EntityComponentError> {
// SAFETY:
// - The caller must ensure simultaneous access is limited
// - to components that are mutually independent.
unsafe { component_ids.fetch_mut(self.0) }
}
/// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped) /// Consumes `self` and returns [untyped mutable reference(s)](MutUntyped)
/// to component(s) with lifetime `'w` for the current entity, based on the /// to component(s) with lifetime `'w` for the current entity, based on the
/// given [`ComponentId`]s. /// given [`ComponentId`]s.
@ -4425,4 +4453,27 @@ mod tests {
.map(|_| { unreachable!() }) .map(|_| { unreachable!() })
); );
} }
#[test]
fn get_mut_by_id_unchecked() {
let mut world = World::default();
let e1 = world.spawn((X(7), Y(10))).id();
let x_id = world.register_component::<X>();
let y_id = world.register_component::<Y>();
let e1_mut = &world.get_entity_mut([e1]).unwrap()[0];
// SAFETY: The entity e1 contains component X.
let x_ptr = unsafe { e1_mut.get_mut_by_id_unchecked(x_id) }.unwrap();
// SAFETY: The entity e1 contains component Y, with components X and Y being mutually independent.
let y_ptr = unsafe { e1_mut.get_mut_by_id_unchecked(y_id) }.unwrap();
// SAFETY: components match the id they were fetched with
let x_component = unsafe { x_ptr.into_inner().deref_mut::<X>() };
x_component.0 += 1;
// SAFETY: components match the id they were fetched with
let y_component = unsafe { y_ptr.into_inner().deref_mut::<Y>() };
y_component.0 -= 1;
assert_eq!((&mut X(8), &mut Y(9)), (x_component, y_component));
}
} }

View file

@ -33,7 +33,7 @@ where
/// # use bevy_color::palettes::basic::{RED}; /// # use bevy_color::palettes::basic::{RED};
/// fn system(mut gizmos: Gizmos) { /// fn system(mut gizmos: Gizmos) {
/// let domain = Interval::UNIT; /// let domain = Interval::UNIT;
/// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); /// let curve = FunctionCurve::new(domain, |t| Vec2::from(t.sin_cos()));
/// gizmos.curve_2d(curve, (0..=100).map(|n| n as f32 / 100.0), RED); /// gizmos.curve_2d(curve, (0..=100).map(|n| n as f32 / 100.0), RED);
/// } /// }
/// # bevy_ecs::system::assert_is_system(system); /// # bevy_ecs::system::assert_is_system(system);
@ -67,7 +67,7 @@ where
/// # use bevy_color::palettes::basic::{RED}; /// # use bevy_color::palettes::basic::{RED};
/// fn system(mut gizmos: Gizmos) { /// fn system(mut gizmos: Gizmos) {
/// let domain = Interval::UNIT; /// let domain = Interval::UNIT;
/// let curve = function_curve(domain, |t| { /// let curve = FunctionCurve::new(domain, |t| {
/// let (x,y) = t.sin_cos(); /// let (x,y) = t.sin_cos();
/// Vec3::new(x, y, t) /// Vec3::new(x, y, t)
/// }); /// });
@ -104,7 +104,7 @@ where
/// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}};
/// fn system(mut gizmos: Gizmos) { /// fn system(mut gizmos: Gizmos) {
/// let domain = Interval::UNIT; /// let domain = Interval::UNIT;
/// let curve = function_curve(domain, |t| Vec2::from(t.sin_cos())); /// let curve = FunctionCurve::new(domain, |t| Vec2::from(t.sin_cos()));
/// gizmos.curve_gradient_2d( /// gizmos.curve_gradient_2d(
/// curve, /// curve,
/// (0..=100).map(|n| n as f32 / 100.0) /// (0..=100).map(|n| n as f32 / 100.0)
@ -147,7 +147,7 @@ where
/// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}}; /// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}};
/// fn system(mut gizmos: Gizmos) { /// fn system(mut gizmos: Gizmos) {
/// let domain = Interval::UNIT; /// let domain = Interval::UNIT;
/// let curve = function_curve(domain, |t| { /// let curve = FunctionCurve::new(domain, |t| {
/// let (x,y) = t.sin_cos(); /// let (x,y) = t.sin_cos();
/// Vec3::new(x, y, t) /// Vec3::new(x, y, t)
/// }); /// });

View file

@ -275,7 +275,7 @@ async fn load_gltf<'a, 'b, 'c>(
#[cfg(feature = "bevy_animation")] #[cfg(feature = "bevy_animation")]
let (animations, named_animations, animation_roots) = { let (animations, named_animations, animation_roots) = {
use bevy_animation::{animation_curves::*, gltf_curves::*, VariableCurve}; use bevy_animation::{animation_curves::*, gltf_curves::*, VariableCurve};
use bevy_math::curve::{constant_curve, Interval, UnevenSampleAutoCurve}; use bevy_math::curve::{ConstantCurve, Interval, UnevenSampleAutoCurve};
use bevy_math::{Quat, Vec4}; use bevy_math::{Quat, Vec4};
use gltf::animation::util::ReadOutputs; use gltf::animation::util::ReadOutputs;
let mut animations = vec![]; let mut animations = vec![];
@ -313,7 +313,7 @@ async fn load_gltf<'a, 'b, 'c>(
let translations: Vec<Vec3> = tr.map(Vec3::from).collect(); let translations: Vec<Vec3> = tr.map(Vec3::from).collect();
if keyframe_timestamps.len() == 1 { if keyframe_timestamps.len() == 1 {
#[allow(clippy::unnecessary_map_on_constructor)] #[allow(clippy::unnecessary_map_on_constructor)]
Some(constant_curve(Interval::EVERYWHERE, translations[0])) Some(ConstantCurve::new(Interval::EVERYWHERE, translations[0]))
.map(TranslationCurve) .map(TranslationCurve)
.map(VariableCurve::new) .map(VariableCurve::new)
} else { } else {
@ -348,7 +348,7 @@ async fn load_gltf<'a, 'b, 'c>(
rots.into_f32().map(Quat::from_array).collect(); rots.into_f32().map(Quat::from_array).collect();
if keyframe_timestamps.len() == 1 { if keyframe_timestamps.len() == 1 {
#[allow(clippy::unnecessary_map_on_constructor)] #[allow(clippy::unnecessary_map_on_constructor)]
Some(constant_curve(Interval::EVERYWHERE, rotations[0])) Some(ConstantCurve::new(Interval::EVERYWHERE, rotations[0]))
.map(RotationCurve) .map(RotationCurve)
.map(VariableCurve::new) .map(VariableCurve::new)
} else { } else {
@ -385,7 +385,7 @@ async fn load_gltf<'a, 'b, 'c>(
let scales: Vec<Vec3> = scale.map(Vec3::from).collect(); let scales: Vec<Vec3> = scale.map(Vec3::from).collect();
if keyframe_timestamps.len() == 1 { if keyframe_timestamps.len() == 1 {
#[allow(clippy::unnecessary_map_on_constructor)] #[allow(clippy::unnecessary_map_on_constructor)]
Some(constant_curve(Interval::EVERYWHERE, scales[0])) Some(ConstantCurve::new(Interval::EVERYWHERE, scales[0]))
.map(ScaleCurve) .map(ScaleCurve)
.map(VariableCurve::new) .map(VariableCurve::new)
} else { } else {
@ -419,7 +419,7 @@ async fn load_gltf<'a, 'b, 'c>(
let weights: Vec<f32> = weights.into_f32().collect(); let weights: Vec<f32> = weights.into_f32().collect();
if keyframe_timestamps.len() == 1 { if keyframe_timestamps.len() == 1 {
#[allow(clippy::unnecessary_map_on_constructor)] #[allow(clippy::unnecessary_map_on_constructor)]
Some(constant_curve(Interval::EVERYWHERE, weights)) Some(ConstantCurve::new(Interval::EVERYWHERE, weights))
.map(WeightsCurve) .map(WeightsCurve)
.map(VariableCurve::new) .map(VariableCurve::new)
} else { } else {

View file

@ -97,7 +97,7 @@ serialize = [
"bevy_time/serialize", "bevy_time/serialize",
"bevy_transform/serialize", "bevy_transform/serialize",
"bevy_ui?/serialize", "bevy_ui?/serialize",
"bevy_window/serialize", "bevy_window?/serialize",
"bevy_winit?/serialize", "bevy_winit?/serialize",
] ]
multi_threaded = [ multi_threaded = [
@ -135,7 +135,7 @@ pbr_anisotropy_texture = [
] ]
# Percentage-closer soft shadows # Percentage-closer soft shadows
pbr_pcss = ["bevy_pbr?/pbr_pcss"] experimental_pbr_pcss = ["bevy_pbr?/experimental_pbr_pcss"]
# Optimise for WebGL2 # Optimise for WebGL2
webgl = [ webgl = [
@ -162,6 +162,7 @@ animation = ["bevy_animation", "bevy_gltf?/bevy_animation"]
bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"] bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"]
bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr"] bevy_pbr = ["dep:bevy_pbr", "bevy_gizmos?/bevy_pbr"]
bevy_window = ["dep:bevy_window", "dep:bevy_a11y"]
# Used to disable code that is unsupported when Bevy is dynamically linked # Used to disable code that is unsupported when Bevy is dynamically linked
dynamic_linking = ["bevy_diagnostic/dynamic_linking"] dynamic_linking = ["bevy_diagnostic/dynamic_linking"]
@ -173,7 +174,7 @@ android_shared_stdcxx = ["bevy_audio/android_shared_stdcxx"]
# screen readers and forks.) # screen readers and forks.)
accesskit_unix = ["bevy_winit/accesskit_unix"] accesskit_unix = ["bevy_winit/accesskit_unix"]
bevy_text = ["dep:bevy_text", "bevy_ui?/bevy_text"] bevy_text = ["dep:bevy_text"]
bevy_render = [ bevy_render = [
"dep:bevy_render", "dep:bevy_render",
@ -248,7 +249,7 @@ ghost_nodes = ["bevy_ui/ghost_nodes"]
[dependencies] [dependencies]
# bevy # bevy
bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev" } bevy_a11y = { path = "../bevy_a11y", version = "0.15.0-dev", optional = true }
bevy_app = { path = "../bevy_app", version = "0.15.0-dev" } bevy_app = { path = "../bevy_app", version = "0.15.0-dev" }
bevy_core = { path = "../bevy_core", version = "0.15.0-dev" } bevy_core = { path = "../bevy_core", version = "0.15.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" } bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
@ -266,7 +267,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
bevy_time = { path = "../bevy_time", version = "0.15.0-dev" } bevy_time = { path = "../bevy_time", version = "0.15.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" } bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev", optional = true }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" } bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" }
# bevy (optional) # bevy (optional)
bevy_animation = { path = "../bevy_animation", optional = true, version = "0.15.0-dev" } bevy_animation = { path = "../bevy_animation", optional = true, version = "0.15.0-dev" }

View file

@ -13,7 +13,11 @@ plugin_group! {
bevy_hierarchy:::HierarchyPlugin, bevy_hierarchy:::HierarchyPlugin,
bevy_diagnostic:::DiagnosticsPlugin, bevy_diagnostic:::DiagnosticsPlugin,
bevy_input:::InputPlugin, bevy_input:::InputPlugin,
#[custom(cfg(not(feature = "bevy_window")))]
bevy_app:::ScheduleRunnerPlugin,
#[cfg(feature = "bevy_window")]
bevy_window:::WindowPlugin, bevy_window:::WindowPlugin,
#[cfg(feature = "bevy_window")]
bevy_a11y:::AccessibilityPlugin, bevy_a11y:::AccessibilityPlugin,
#[custom(cfg(not(target_arch = "wasm32")))] #[custom(cfg(not(target_arch = "wasm32")))]
bevy_app:::TerminalCtrlCHandlerPlugin, bevy_app:::TerminalCtrlCHandlerPlugin,
@ -72,55 +76,6 @@ plugin_group! {
/// ///
/// [`DefaultPlugins`] contains all the plugins typically required to build /// [`DefaultPlugins`] contains all the plugins typically required to build
/// a *Bevy* application which includes a *window* and presentation components. /// a *Bevy* application which includes a *window* and presentation components.
/// For *headless* cases without a *window* or presentation, see [`HeadlessPlugins`].
/// For the absolute minimum number of plugins needed to run a Bevy application, see [`MinimalPlugins`].
}
plugin_group! {
/// This plugin group will add all the default plugins for a headless (no *window* or rendering) *Bevy* application:
pub struct HeadlessPlugins {
bevy_app:::PanicHandlerPlugin,
bevy_log:::LogPlugin,
bevy_core:::TaskPoolPlugin,
bevy_core:::TypeRegistrationPlugin,
bevy_core:::FrameCountPlugin,
bevy_time:::TimePlugin,
bevy_transform:::TransformPlugin,
bevy_hierarchy:::HierarchyPlugin,
bevy_diagnostic:::DiagnosticsPlugin,
bevy_app:::ScheduleRunnerPlugin,
#[custom(cfg(not(target_arch = "wasm32")))]
bevy_app:::TerminalCtrlCHandlerPlugin,
#[cfg(feature = "bevy_asset")]
bevy_asset:::AssetPlugin,
#[cfg(feature = "bevy_scene")]
bevy_scene:::ScenePlugin,
#[cfg(feature = "bevy_animation")]
bevy_animation:::AnimationPlugin,
#[cfg(feature = "bevy_state")]
bevy_state::app:::StatesPlugin,
#[cfg(feature = "bevy_ci_testing")]
bevy_dev_tools::ci_testing:::CiTestingPlugin,
#[doc(hidden)]
:IgnoreAmbiguitiesPlugin,
}
/// This group of plugins is intended for use for *headless* programs, for example: dedicated game servers.
/// See the [*Bevy* *headless* example](https://github.com/bevyengine/bevy/blob/main/examples/app/headless.rs)
///
/// [`HeadlessPlugins`] obeys *Cargo* *feature* flags. Users may exert control over this plugin group
/// by disabling `default-features` in their `Cargo.toml` and enabling only those features
/// that they wish to use.
///
/// [`HeadlessPlugins`] contains all the plugins typically required to build
/// a *Bevy* application. In contrast with [`DefaultPlugins`], it leaves out *window* and presentation components.
/// This allows applications built using this plugin group to run on devices that do not have a screen or rendering
/// capabilities.
/// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin)
/// to provide functionality that would otherwise be driven by a windowed application's
/// *event loop* or *message loop*.
///
/// Windowed applications that wish to use a reduced set of plugins should consider the
/// [`DefaultPlugins`] plugin group which can be controlled with *Cargo* *feature* flags.
/// For the absolute minimum number of plugins needed to run a Bevy application, see [`MinimalPlugins`]. /// For the absolute minimum number of plugins needed to run a Bevy application, see [`MinimalPlugins`].
} }
@ -162,8 +117,6 @@ plugin_group! {
} }
/// This plugin group represents the absolute minimum, bare-bones, bevy application. /// This plugin group represents the absolute minimum, bare-bones, bevy application.
/// Use this if you want to have absolute control over the plugins used. /// Use this if you want to have absolute control over the plugins used.
/// If you are looking to make a *headless* application - without a *window* or rendering,
/// it is usually best to use [`HeadlessPlugins`].
/// ///
/// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin) /// It includes a [schedule runner (`ScheduleRunnerPlugin`)](crate::app::ScheduleRunnerPlugin)
/// to provide functionality that would otherwise be driven by a windowed application's /// to provide functionality that would otherwise be driven by a windowed application's

View file

@ -13,6 +13,7 @@ pub mod prelude;
mod default_plugins; mod default_plugins;
pub use default_plugins::*; pub use default_plugins::*;
#[cfg(feature = "bevy_window")]
pub use bevy_a11y as a11y; pub use bevy_a11y as a11y;
#[cfg(feature = "bevy_animation")] #[cfg(feature = "bevy_animation")]
pub use bevy_animation as animation; pub use bevy_animation as animation;
@ -66,6 +67,7 @@ pub use bevy_transform as transform;
#[cfg(feature = "bevy_ui")] #[cfg(feature = "bevy_ui")]
pub use bevy_ui as ui; pub use bevy_ui as ui;
pub use bevy_utils as utils; pub use bevy_utils as utils;
#[cfg(feature = "bevy_window")]
pub use bevy_window as window; pub use bevy_window as window;
#[cfg(feature = "bevy_winit")] #[cfg(feature = "bevy_winit")]
pub use bevy_winit as winit; pub use bevy_winit as winit;

View file

@ -2,10 +2,13 @@
pub use crate::{ pub use crate::{
app::prelude::*, core::prelude::*, ecs::prelude::*, hierarchy::prelude::*, input::prelude::*, app::prelude::*, core::prelude::*, ecs::prelude::*, hierarchy::prelude::*, input::prelude::*,
log::prelude::*, math::prelude::*, reflect::prelude::*, time::prelude::*, log::prelude::*, math::prelude::*, reflect::prelude::*, time::prelude::*,
transform::prelude::*, utils::prelude::*, window::prelude::*, DefaultPlugins, HeadlessPlugins, transform::prelude::*, utils::prelude::*, DefaultPlugins, MinimalPlugins,
MinimalPlugins,
}; };
#[doc(hidden)]
#[cfg(feature = "bevy_window")]
pub use crate::window::prelude::*;
#[doc(hidden)] #[doc(hidden)]
#[cfg(feature = "bevy_image")] #[cfg(feature = "bevy_image")]
pub use crate::image::prelude::*; pub use crate::image::prelude::*;

View file

@ -1,7 +1,5 @@
//! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives). //! Contains [`Bounded3d`] implementations for [geometric primitives](crate::primitives).
use glam::Vec3A;
use crate::{ use crate::{
bounding::{Bounded2d, BoundingCircle}, bounding::{Bounded2d, BoundingCircle},
ops, ops,
@ -9,7 +7,7 @@ use crate::{
BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d,
Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d, Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d,
}, },
Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Isometry2d, Isometry3d, Mat3, Vec2, Vec3, Vec3A,
}; };
use super::{Aabb3d, Bounded3d, BoundingSphere}; use super::{Aabb3d, Bounded3d, BoundingSphere};

View file

@ -3,9 +3,10 @@
//! //!
//! [easing functions]: EaseFunction //! [easing functions]: EaseFunction
use crate::{Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace}; use crate::{
curve::{FunctionCurve, Interval},
use super::{function_curve, Curve, Interval}; Curve, Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace,
};
// TODO: Think about merging `Ease` with `StableInterpolate` // TODO: Think about merging `Ease` with `StableInterpolate`
@ -28,13 +29,13 @@ pub trait Ease: Sized {
impl<V: VectorSpace> Ease for V { impl<V: VectorSpace> Ease for V {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
function_curve(Interval::EVERYWHERE, move |t| V::lerp(start, end, t)) FunctionCurve::new(Interval::EVERYWHERE, move |t| V::lerp(start, end, t))
} }
} }
impl Ease for Rot2 { impl Ease for Rot2 {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
function_curve(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t)) FunctionCurve::new(Interval::EVERYWHERE, move |t| Rot2::slerp(start, end, t))
} }
} }
@ -44,7 +45,7 @@ impl Ease for Quat {
let end_adjusted = if dot < 0.0 { -end } else { end }; let end_adjusted = if dot < 0.0 { -end } else { end };
let difference = end_adjusted * start.inverse(); let difference = end_adjusted * start.inverse();
let (axis, angle) = difference.to_axis_angle(); let (axis, angle) = difference.to_axis_angle();
function_curve(Interval::EVERYWHERE, move |s| { FunctionCurve::new(Interval::EVERYWHERE, move |s| {
Quat::from_axis_angle(axis, angle * s) * start Quat::from_axis_angle(axis, angle * s) * start
}) })
} }
@ -52,7 +53,7 @@ impl Ease for Quat {
impl Ease for Dir2 { impl Ease for Dir2 {
fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> { fn interpolating_curve_unbounded(start: Self, end: Self) -> impl Curve<Self> {
function_curve(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t)) FunctionCurve::new(Interval::EVERYWHERE, move |t| Dir2::slerp(start, end, t))
} }
} }
@ -71,20 +72,6 @@ impl Ease for Dir3A {
} }
} }
/// Given a `start` and `end` value, create a curve parametrized over [the unit interval]
/// that connects them, using the given [ease function] to determine the form of the
/// curve in between.
///
/// [the unit interval]: Interval::UNIT
/// [ease function]: EaseFunction
pub fn easing_curve<T: Ease + Clone>(start: T, end: T, ease_fn: EaseFunction) -> EasingCurve<T> {
EasingCurve {
start,
end,
ease_fn,
}
}
/// A [`Curve`] that is defined by /// A [`Curve`] that is defined by
/// ///
/// - an initial `start` sample value at `t = 0` /// - an initial `start` sample value at `t = 0`
@ -104,6 +91,22 @@ pub struct EasingCurve<T> {
ease_fn: EaseFunction, ease_fn: EaseFunction,
} }
impl<T> EasingCurve<T> {
/// Given a `start` and `end` value, create a curve parametrized over [the unit interval]
/// that connects them, using the given [ease function] to determine the form of the
/// curve in between.
///
/// [the unit interval]: Interval::UNIT
/// [ease function]: EaseFunction
pub fn new(start: T, end: T, ease_fn: EaseFunction) -> Self {
Self {
start,
end,
ease_fn,
}
}
}
impl<T> Curve<T> for EasingCurve<T> impl<T> Curve<T> for EasingCurve<T>
where where
T: Ease + Clone, T: Ease + Clone,

View file

@ -56,13 +56,13 @@
//! # use bevy_math::vec3; //! # use bevy_math::vec3;
//! # use bevy_math::curve::*; //! # use bevy_math::curve::*;
//! // A sinusoid: //! // A sinusoid:
//! let sine_curve = function_curve(Interval::EVERYWHERE, f32::sin); //! let sine_curve = FunctionCurve::new(Interval::EVERYWHERE, f32::sin);
//! //!
//! // A sawtooth wave: //! // A sawtooth wave:
//! let sawtooth_curve = function_curve(Interval::EVERYWHERE, |t| t % 1.0); //! let sawtooth_curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t % 1.0);
//! //!
//! // A helix: //! // A helix:
//! let helix_curve = function_curve(Interval::EVERYWHERE, |theta| vec3(theta.sin(), theta, theta.cos())); //! let helix_curve = FunctionCurve::new(Interval::EVERYWHERE, |theta| vec3(theta.sin(), theta, theta.cos()));
//! ``` //! ```
//! //!
//! Sample-interpolated curves commonly arises in both rasterization and in animation, and this library //! Sample-interpolated curves commonly arises in both rasterization and in animation, and this library
@ -127,7 +127,7 @@
//! # use bevy_math::{vec2, prelude::*}; //! # use bevy_math::{vec2, prelude::*};
//! # use std::f32::consts::TAU; //! # use std::f32::consts::TAU;
//! // Our original curve, which may look something like this: //! // Our original curve, which may look something like this:
//! let ellipse_curve = function_curve( //! let ellipse_curve = FunctionCurve::new(
//! interval(0.0, TAU).unwrap(), //! interval(0.0, TAU).unwrap(),
//! |t| vec2(t.cos(), t.sin() * 2.0) //! |t| vec2(t.cos(), t.sin() * 2.0)
//! ); //! );
@ -141,7 +141,7 @@
//! ```rust //! ```rust
//! # use bevy_math::{vec2, prelude::*}; //! # use bevy_math::{vec2, prelude::*};
//! # use std::f32::consts::TAU; //! # use std::f32::consts::TAU;
//! # let ellipse_curve = function_curve(interval(0.0, TAU).unwrap(), |t| vec2(t.cos(), t.sin() * 2.0)); //! # let ellipse_curve = FunctionCurve::new(interval(0.0, TAU).unwrap(), |t| vec2(t.cos(), t.sin() * 2.0));
//! # let ellipse_motion_curve = ellipse_curve.map(|pos| pos.extend(0.0)); //! # let ellipse_motion_curve = ellipse_curve.map(|pos| pos.extend(0.0));
//! // Change the domain to `[0, 1]` instead of `[0, TAU]`: //! // Change the domain to `[0, 1]` instead of `[0, TAU]`:
//! let final_curve = ellipse_motion_curve.reparametrize_linear(Interval::UNIT).unwrap(); //! let final_curve = ellipse_motion_curve.reparametrize_linear(Interval::UNIT).unwrap();
@ -155,7 +155,7 @@
//! // A line segment curve connecting two points in the plane: //! // A line segment curve connecting two points in the plane:
//! let start = vec2(-1.0, 1.0); //! let start = vec2(-1.0, 1.0);
//! let end = vec2(1.0, 1.0); //! let end = vec2(1.0, 1.0);
//! let segment = function_curve(Interval::UNIT, |t| start.lerp(end, t)); //! let segment = FunctionCurve::new(Interval::UNIT, |t| start.lerp(end, t));
//! //!
//! // Let's make a curve that goes back and forth along this line segment forever. //! // Let's make a curve that goes back and forth along this line segment forever.
//! // //! //
@ -177,13 +177,13 @@
//! # use bevy_math::{vec2, prelude::*}; //! # use bevy_math::{vec2, prelude::*};
//! # use std::f32::consts::PI; //! # use std::f32::consts::PI;
//! // A line segment connecting `(-1, 0)` to `(0, 0)`: //! // A line segment connecting `(-1, 0)` to `(0, 0)`:
//! let line_curve = function_curve( //! let line_curve = FunctionCurve::new(
//! Interval::UNIT, //! Interval::UNIT,
//! |t| vec2(-1.0, 0.0).lerp(vec2(0.0, 0.0), t) //! |t| vec2(-1.0, 0.0).lerp(vec2(0.0, 0.0), t)
//! ); //! );
//! //!
//! // A half-circle curve starting at `(0, 0)`: //! // A half-circle curve starting at `(0, 0)`:
//! let half_circle_curve = function_curve( //! let half_circle_curve = FunctionCurve::new(
//! interval(0.0, PI).unwrap(), //! interval(0.0, PI).unwrap(),
//! |t| vec2(t.cos() * -1.0 + 1.0, t.sin()) //! |t| vec2(t.cos() * -1.0 + 1.0, t.sin())
//! ); //! );
@ -198,10 +198,10 @@
//! ```rust //! ```rust
//! # use bevy_math::{vec2, prelude::*}; //! # use bevy_math::{vec2, prelude::*};
//! // Some entity's position in 2D: //! // Some entity's position in 2D:
//! let position_curve = function_curve(Interval::UNIT, |t| vec2(t.cos(), t.sin())); //! let position_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t.cos(), t.sin()));
//! //!
//! // The same entity's orientation, described as a rotation. (In this case it will be spinning.) //! // The same entity's orientation, described as a rotation. (In this case it will be spinning.)
//! let orientation_curve = function_curve(Interval::UNIT, |t| Rot2::radians(5.0 * t)); //! let orientation_curve = FunctionCurve::new(Interval::UNIT, |t| Rot2::radians(5.0 * t));
//! //!
//! // Both in one curve with `(Vec2, Rot2)` output: //! // Both in one curve with `(Vec2, Rot2)` output:
//! let position_and_orientation = position_curve.zip(orientation_curve).unwrap(); //! let position_and_orientation = position_curve.zip(orientation_curve).unwrap();
@ -220,7 +220,7 @@
//! ```rust //! ```rust
//! # use bevy_math::{vec2, prelude::*}; //! # use bevy_math::{vec2, prelude::*};
//! // A curve that is not easily transported because it relies on evaluating a function: //! // A curve that is not easily transported because it relies on evaluating a function:
//! let interesting_curve = function_curve(Interval::UNIT, |t| vec2(t * 3.0, t.exp())); //! let interesting_curve = FunctionCurve::new(Interval::UNIT, |t| vec2(t * 3.0, t.exp()));
//! //!
//! // A rasterized form of the preceding curve which is just a `SampleAutoCurve`. Inside, this //! // A rasterized form of the preceding curve which is just a `SampleAutoCurve`. Inside, this
//! // just stores an `Interval` along with a buffer of sample data, so it's easy to serialize //! // just stores an `Interval` along with a buffer of sample data, so it's easy to serialize
@ -250,7 +250,7 @@
//! Here is a demonstration: //! Here is a demonstration:
//! ```rust //! ```rust
//! # use bevy_math::prelude::*; //! # use bevy_math::prelude::*;
//! # let some_magic_constructor = || easing_curve(0.0, 1.0, EaseFunction::ElasticInOut).graph(); //! # let some_magic_constructor = || EasingCurve::new(0.0, 1.0, EaseFunction::ElasticInOut).graph();
//! //`my_curve` is obtained somehow. It is a `Curve<(f32, f32)>`. //! //`my_curve` is obtained somehow. It is a `Curve<(f32, f32)>`.
//! let my_curve = some_magic_constructor(); //! let my_curve = some_magic_constructor();
//! //!
@ -272,7 +272,7 @@
//! [changing parametrizations]: Curve::reparametrize //! [changing parametrizations]: Curve::reparametrize
//! [mapping output]: Curve::map //! [mapping output]: Curve::map
//! [rasterization]: Curve::resample //! [rasterization]: Curve::resample
//! [functions]: function_curve //! [functions]: FunctionCurve
//! [sample interpolation]: SampleCurve //! [sample interpolation]: SampleCurve
//! [splines]: crate::cubic_splines //! [splines]: crate::cubic_splines
//! [easings]: easing //! [easings]: easing
@ -282,7 +282,7 @@
//! [`zip`]: Curve::zip //! [`zip`]: Curve::zip
//! [`resample`]: Curve::resample //! [`resample`]: Curve::resample
//! //!
//! [^footnote]: In fact, universal as well, in some sense: if `curve` is any curve, then `function_curve //! [^footnote]: In fact, universal as well, in some sense: if `curve` is any curve, then `FunctionCurve::new
//! (curve.domain(), |t| curve.sample_unchecked(t))` is an equivalent function curve. //! (curve.domain(), |t| curve.sample_unchecked(t))` is an equivalent function curve.
pub mod adaptors; pub mod adaptors;
@ -414,7 +414,7 @@ pub trait Curve<T> {
/// factor rather than multiplying: /// factor rather than multiplying:
/// ``` /// ```
/// # use bevy_math::curve::*; /// # use bevy_math::curve::*;
/// let my_curve = constant_curve(Interval::UNIT, 1.0); /// let my_curve = ConstantCurve::new(Interval::UNIT, 1.0);
/// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0); /// let scaled_curve = my_curve.reparametrize(interval(0.0, 2.0).unwrap(), |t| t / 2.0);
/// ``` /// ```
/// This kind of linear remapping is provided by the convenience method /// This kind of linear remapping is provided by the convenience method
@ -425,12 +425,12 @@ pub trait Curve<T> {
/// // Reverse a curve: /// // Reverse a curve:
/// # use bevy_math::curve::*; /// # use bevy_math::curve::*;
/// # use bevy_math::vec2; /// # use bevy_math::vec2;
/// let my_curve = constant_curve(Interval::UNIT, 1.0); /// let my_curve = ConstantCurve::new(Interval::UNIT, 1.0);
/// let domain = my_curve.domain(); /// let domain = my_curve.domain();
/// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - (t - domain.start())); /// let reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - (t - domain.start()));
/// ///
/// // Take a segment of a curve: /// // Take a segment of a curve:
/// # let my_curve = constant_curve(Interval::UNIT, 1.0); /// # let my_curve = ConstantCurve::new(Interval::UNIT, 1.0);
/// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t); /// let curve_segment = my_curve.reparametrize(interval(0.0, 0.5).unwrap(), |t| 0.5 + t);
/// ``` /// ```
#[must_use] #[must_use]
@ -712,7 +712,7 @@ pub trait Curve<T> {
/// ``` /// ```
/// # use bevy_math::*; /// # use bevy_math::*;
/// # use bevy_math::curve::*; /// # use bevy_math::curve::*;
/// let quarter_rotation = function_curve(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t)); /// let quarter_rotation = FunctionCurve::new(interval(0.0, 90.0).unwrap(), |t| Rot2::degrees(t));
/// // A curve which only stores three data points and uses `nlerp` to interpolate them: /// // A curve which only stores three data points and uses `nlerp` to interpolate them:
/// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t)); /// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
/// ``` /// ```
@ -863,7 +863,7 @@ pub trait Curve<T> {
/// # Example /// # Example
/// ``` /// ```
/// # use bevy_math::curve::*; /// # use bevy_math::curve::*;
/// let my_curve = function_curve(Interval::UNIT, |t| t * t + 1.0); /// let my_curve = FunctionCurve::new(Interval::UNIT, |t| t * t + 1.0);
/// ///
/// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes /// // Borrow `my_curve` long enough to resample a mapped version. Note that `map` takes
/// // ownership of its input. /// // ownership of its input.
@ -976,27 +976,8 @@ pub enum ResamplingError {
UnboundedDomain, UnboundedDomain,
} }
/// Create a [`Curve`] that constantly takes the given `value` over the given `domain`.
pub fn constant_curve<T: Clone>(domain: Interval, value: T) -> ConstantCurve<T> {
ConstantCurve { domain, value }
}
/// Convert the given function `f` into a [`Curve`] with the given `domain`, sampled by
/// evaluating the function.
pub fn function_curve<T, F>(domain: Interval, f: F) -> FunctionCurve<T, F>
where
F: Fn(f32) -> T,
{
FunctionCurve {
domain,
f,
_phantom: PhantomData,
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::easing::*;
use super::*; use super::*;
use crate::{ops, Quat}; use crate::{ops, Quat};
use approx::{assert_abs_diff_eq, AbsDiffEq}; use approx::{assert_abs_diff_eq, AbsDiffEq};
@ -1005,7 +986,7 @@ mod tests {
#[test] #[test]
fn curve_can_be_made_into_an_object() { fn curve_can_be_made_into_an_object() {
let curve = constant_curve(Interval::UNIT, 42.0); let curve = ConstantCurve::new(Interval::UNIT, 42.0);
let curve: &dyn Curve<f64> = &curve; let curve: &dyn Curve<f64> = &curve;
assert_eq!(curve.sample(1.0), Some(42.0)); assert_eq!(curve.sample(1.0), Some(42.0));
@ -1014,21 +995,21 @@ mod tests {
#[test] #[test]
fn constant_curves() { fn constant_curves() {
let curve = constant_curve(Interval::EVERYWHERE, 5.0); let curve = ConstantCurve::new(Interval::EVERYWHERE, 5.0);
assert!(curve.sample_unchecked(-35.0) == 5.0); assert!(curve.sample_unchecked(-35.0) == 5.0);
let curve = constant_curve(Interval::UNIT, true); let curve = ConstantCurve::new(Interval::UNIT, true);
assert!(curve.sample_unchecked(2.0)); assert!(curve.sample_unchecked(2.0));
assert!(curve.sample(2.0).is_none()); assert!(curve.sample(2.0).is_none());
} }
#[test] #[test]
fn function_curves() { fn function_curves() {
let curve = function_curve(Interval::EVERYWHERE, |t| t * t); let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * t);
assert!(curve.sample_unchecked(2.0).abs_diff_eq(&4.0, f32::EPSILON)); assert!(curve.sample_unchecked(2.0).abs_diff_eq(&4.0, f32::EPSILON));
assert!(curve.sample_unchecked(-3.0).abs_diff_eq(&9.0, f32::EPSILON)); assert!(curve.sample_unchecked(-3.0).abs_diff_eq(&9.0, f32::EPSILON));
let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::log2); let curve = FunctionCurve::new(interval(0.0, f32::INFINITY).unwrap(), ops::log2);
assert_eq!(curve.sample_unchecked(3.5), ops::log2(3.5)); assert_eq!(curve.sample_unchecked(3.5), ops::log2(3.5));
assert!(curve.sample_unchecked(-1.0).is_nan()); assert!(curve.sample_unchecked(-1.0).is_nan());
assert!(curve.sample(-1.0).is_none()); assert!(curve.sample(-1.0).is_none());
@ -1038,7 +1019,7 @@ mod tests {
fn linear_curve() { fn linear_curve() {
let start = Vec2::ZERO; let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0); let end = Vec2::new(1.0, 2.0);
let curve = easing_curve(start, end, EaseFunction::Linear); let curve = EasingCurve::new(start, end, EaseFunction::Linear);
let mid = (start + end) / 2.0; let mid = (start + end) / 2.0;
@ -1054,7 +1035,7 @@ mod tests {
let start = Vec2::ZERO; let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0); let end = Vec2::new(1.0, 2.0);
let curve = easing_curve(start, end, EaseFunction::Steps(4)); let curve = EasingCurve::new(start, end, EaseFunction::Steps(4));
[ [
(0.0, start), (0.0, start),
(0.124, start), (0.124, start),
@ -1078,7 +1059,7 @@ mod tests {
let start = Vec2::ZERO; let start = Vec2::ZERO;
let end = Vec2::new(1.0, 2.0); let end = Vec2::new(1.0, 2.0);
let curve = easing_curve(start, end, EaseFunction::QuadraticIn); let curve = EasingCurve::new(start, end, EaseFunction::QuadraticIn);
[ [
(0.0, start), (0.0, start),
(0.25, Vec2::new(0.0625, 0.125)), (0.25, Vec2::new(0.0625, 0.125)),
@ -1093,7 +1074,7 @@ mod tests {
#[test] #[test]
fn mapping() { fn mapping() {
let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * 3.0 + 1.0);
let mapped_curve = curve.map(|x| x / 7.0); let mapped_curve = curve.map(|x| x / 7.0);
assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0); assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0);
assert_eq!( assert_eq!(
@ -1102,7 +1083,7 @@ mod tests {
); );
assert_eq!(mapped_curve.domain(), Interval::EVERYWHERE); assert_eq!(mapped_curve.domain(), Interval::EVERYWHERE);
let curve = function_curve(Interval::UNIT, |t| t * TAU); let curve = FunctionCurve::new(Interval::UNIT, |t| t * TAU);
let mapped_curve = curve.map(Quat::from_rotation_z); let mapped_curve = curve.map(Quat::from_rotation_z);
assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY); assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY);
assert!(mapped_curve.sample_unchecked(1.0).is_near_identity()); assert!(mapped_curve.sample_unchecked(1.0).is_near_identity());
@ -1111,7 +1092,7 @@ mod tests {
#[test] #[test]
fn reverse() { fn reverse() {
let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let rev_curve = curve.reverse().unwrap(); let rev_curve = curve.reverse().unwrap();
assert_eq!(rev_curve.sample(-0.1), None); assert_eq!(rev_curve.sample(-0.1), None);
assert_eq!(rev_curve.sample(0.0), Some(1.0 * 3.0 + 1.0)); assert_eq!(rev_curve.sample(0.0), Some(1.0 * 3.0 + 1.0));
@ -1119,7 +1100,7 @@ mod tests {
assert_eq!(rev_curve.sample(1.0), Some(0.0 * 3.0 + 1.0)); assert_eq!(rev_curve.sample(1.0), Some(0.0 * 3.0 + 1.0));
assert_eq!(rev_curve.sample(1.1), None); assert_eq!(rev_curve.sample(1.1), None);
let curve = function_curve(Interval::new(-2.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let curve = FunctionCurve::new(Interval::new(-2.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let rev_curve = curve.reverse().unwrap(); let rev_curve = curve.reverse().unwrap();
assert_eq!(rev_curve.sample(-2.1), None); assert_eq!(rev_curve.sample(-2.1), None);
assert_eq!(rev_curve.sample(-2.0), Some(1.0 * 3.0 + 1.0)); assert_eq!(rev_curve.sample(-2.0), Some(1.0 * 3.0 + 1.0));
@ -1130,7 +1111,7 @@ mod tests {
#[test] #[test]
fn repeat() { fn repeat() {
let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let repeat_curve = curve.by_ref().repeat(1).unwrap(); let repeat_curve = curve.by_ref().repeat(1).unwrap();
assert_eq!(repeat_curve.sample(-0.1), None); assert_eq!(repeat_curve.sample(-0.1), None);
assert_eq!(repeat_curve.sample(0.0), Some(0.0 * 3.0 + 1.0)); assert_eq!(repeat_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
@ -1159,7 +1140,7 @@ mod tests {
#[test] #[test]
fn ping_pong() { fn ping_pong() {
let curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let ping_pong_curve = curve.ping_pong().unwrap(); let ping_pong_curve = curve.ping_pong().unwrap();
assert_eq!(ping_pong_curve.sample(-0.1), None); assert_eq!(ping_pong_curve.sample(-0.1), None);
assert_eq!(ping_pong_curve.sample(0.0), Some(0.0 * 3.0 + 1.0)); assert_eq!(ping_pong_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
@ -1169,7 +1150,7 @@ mod tests {
assert_eq!(ping_pong_curve.sample(2.0), Some(0.0 * 3.0 + 1.0)); assert_eq!(ping_pong_curve.sample(2.0), Some(0.0 * 3.0 + 1.0));
assert_eq!(ping_pong_curve.sample(2.1), None); assert_eq!(ping_pong_curve.sample(2.1), None);
let curve = function_curve(Interval::new(-2.0, 2.0).unwrap(), |t| t * 3.0 + 1.0); let curve = FunctionCurve::new(Interval::new(-2.0, 2.0).unwrap(), |t| t * 3.0 + 1.0);
let ping_pong_curve = curve.ping_pong().unwrap(); let ping_pong_curve = curve.ping_pong().unwrap();
assert_eq!(ping_pong_curve.sample(-2.1), None); assert_eq!(ping_pong_curve.sample(-2.1), None);
assert_eq!(ping_pong_curve.sample(-2.0), Some(-2.0 * 3.0 + 1.0)); assert_eq!(ping_pong_curve.sample(-2.0), Some(-2.0 * 3.0 + 1.0));
@ -1182,8 +1163,8 @@ mod tests {
#[test] #[test]
fn continue_chain() { fn continue_chain() {
let first = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let first = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let second = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * t); let second = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * t);
let c0_chain_curve = first.chain_continue(second).unwrap(); let c0_chain_curve = first.chain_continue(second).unwrap();
assert_eq!(c0_chain_curve.sample(-0.1), None); assert_eq!(c0_chain_curve.sample(-0.1), None);
assert_eq!(c0_chain_curve.sample(0.0), Some(0.0 * 3.0 + 1.0)); assert_eq!(c0_chain_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
@ -1196,7 +1177,7 @@ mod tests {
#[test] #[test]
fn reparameterization() { fn reparameterization() {
let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2); let curve = FunctionCurve::new(interval(1.0, f32::INFINITY).unwrap(), ops::log2);
let reparametrized_curve = curve let reparametrized_curve = curve
.by_ref() .by_ref()
.reparametrize(interval(0.0, f32::INFINITY).unwrap(), ops::exp2); .reparametrize(interval(0.0, f32::INFINITY).unwrap(), ops::exp2);
@ -1216,7 +1197,7 @@ mod tests {
#[test] #[test]
fn multiple_maps() { fn multiple_maps() {
// Make sure these actually happen in the right order. // Make sure these actually happen in the right order.
let curve = function_curve(Interval::UNIT, ops::exp2); let curve = FunctionCurve::new(Interval::UNIT, ops::exp2);
let first_mapped = curve.map(ops::log2); let first_mapped = curve.map(ops::log2);
let second_mapped = first_mapped.map(|x| x * -2.0); let second_mapped = first_mapped.map(|x| x * -2.0);
assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0); assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0);
@ -1227,7 +1208,7 @@ mod tests {
#[test] #[test]
fn multiple_reparams() { fn multiple_reparams() {
// Make sure these happen in the right order too. // Make sure these happen in the right order too.
let curve = function_curve(Interval::UNIT, ops::exp2); let curve = FunctionCurve::new(Interval::UNIT, ops::exp2);
let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), ops::log2); let first_reparam = curve.reparametrize(interval(1.0, 2.0).unwrap(), ops::log2);
let second_reparam = first_reparam.reparametrize(Interval::UNIT, |t| t + 1.0); let second_reparam = first_reparam.reparametrize(Interval::UNIT, |t| t + 1.0);
assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0); assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0);
@ -1237,7 +1218,7 @@ mod tests {
#[test] #[test]
fn resampling() { fn resampling() {
let curve = function_curve(interval(1.0, 4.0).unwrap(), ops::log2); let curve = FunctionCurve::new(interval(1.0, 4.0).unwrap(), ops::log2);
// Need at least one segment to sample. // Need at least one segment to sample.
let nice_try = curve.by_ref().resample_auto(0); let nice_try = curve.by_ref().resample_auto(0);
@ -1257,7 +1238,7 @@ mod tests {
} }
// Another example. // Another example.
let curve = function_curve(interval(0.0, TAU).unwrap(), ops::cos); let curve = FunctionCurve::new(interval(0.0, TAU).unwrap(), ops::cos);
let resampled_curve = curve.by_ref().resample_auto(1000).unwrap(); let resampled_curve = curve.by_ref().resample_auto(1000).unwrap();
for test_pt in curve.domain().spaced_points(1001).unwrap() { for test_pt in curve.domain().spaced_points(1001).unwrap() {
let expected = curve.sample_unchecked(test_pt); let expected = curve.sample_unchecked(test_pt);
@ -1271,7 +1252,7 @@ mod tests {
#[test] #[test]
fn uneven_resampling() { fn uneven_resampling() {
let curve = function_curve(interval(0.0, f32::INFINITY).unwrap(), ops::exp); let curve = FunctionCurve::new(interval(0.0, f32::INFINITY).unwrap(), ops::exp);
// Need at least two points to resample. // Need at least two points to resample.
let nice_try = curve.by_ref().resample_uneven_auto([1.0; 1]); let nice_try = curve.by_ref().resample_uneven_auto([1.0; 1]);
@ -1290,7 +1271,7 @@ mod tests {
assert_abs_diff_eq!(resampled_curve.domain().end(), 9.9, epsilon = 1e-6); assert_abs_diff_eq!(resampled_curve.domain().end(), 9.9, epsilon = 1e-6);
// Another example. // Another example.
let curve = function_curve(interval(1.0, f32::INFINITY).unwrap(), ops::log2); let curve = FunctionCurve::new(interval(1.0, f32::INFINITY).unwrap(), ops::log2);
let sample_points = (0..10).map(|idx| ops::exp2(idx as f32)); let sample_points = (0..10).map(|idx| ops::exp2(idx as f32));
let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap(); let resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap();
for idx in 0..10 { for idx in 0..10 {
@ -1306,7 +1287,7 @@ mod tests {
fn sample_iterators() { fn sample_iterators() {
let times = [-0.5, 0.0, 0.5, 1.0, 1.5]; let times = [-0.5, 0.0, 0.5, 1.0, 1.5];
let curve = function_curve(Interval::EVERYWHERE, |t| t * 3.0 + 1.0); let curve = FunctionCurve::new(Interval::EVERYWHERE, |t| t * 3.0 + 1.0);
let samples = curve.sample_iter_unchecked(times).collect::<Vec<_>>(); let samples = curve.sample_iter_unchecked(times).collect::<Vec<_>>();
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap(); let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();
@ -1316,7 +1297,7 @@ mod tests {
assert_eq!(y3, 1.0 * 3.0 + 1.0); assert_eq!(y3, 1.0 * 3.0 + 1.0);
assert_eq!(y4, 1.5 * 3.0 + 1.0); assert_eq!(y4, 1.5 * 3.0 + 1.0);
let finite_curve = function_curve(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0); let finite_curve = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let samples = finite_curve.sample_iter(times).collect::<Vec<_>>(); let samples = finite_curve.sample_iter(times).collect::<Vec<_>>();
let [y0, y1, y2, y3, y4] = samples.try_into().unwrap(); let [y0, y1, y2, y3, y4] = samples.try_into().unwrap();

View file

@ -58,17 +58,19 @@ pub use sampling::{FromRng, ShapeSample};
pub mod prelude { pub mod prelude {
#[doc(hidden)] #[doc(hidden)]
pub use crate::{ pub use crate::{
bvec2, bvec3, bvec3a, bvec4, bvec4a,
cubic_splines::{ cubic_splines::{
CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator, CubicBSpline, CubicBezier, CubicCardinalSpline, CubicCurve, CubicGenerator,
CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator, CubicHermite, CubicNurbs, CubicNurbsError, CubicSegment, CyclicCubicGenerator,
RationalCurve, RationalGenerator, RationalSegment, RationalCurve, RationalGenerator, RationalSegment,
}, },
direction::{Dir2, Dir3, Dir3A}, direction::{Dir2, Dir3, Dir3A},
ops, ivec2, ivec3, ivec4, mat2, mat3, mat3a, mat4, ops,
primitives::*, primitives::*,
BVec2, BVec3, BVec4, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, quat, uvec2, uvec3, uvec4, vec2, vec3, vec3a, vec4, BVec2, BVec3, BVec3A, BVec4, BVec4A,
Isometry3d, Mat2, Mat3, Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, EulerRot, FloatExt, IRect, IVec2, IVec3, IVec4, Isometry2d, Isometry3d, Mat2, Mat3, Mat3A,
UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles, Mat4, Quat, Ray2d, Ray3d, Rect, Rot2, StableInterpolate, URect, UVec2, UVec3, UVec4, Vec2,
Vec2Swizzles, Vec3, Vec3A, Vec3Swizzles, Vec4, Vec4Swizzles,
}; };
#[doc(hidden)] #[doc(hidden)]

View file

@ -14,7 +14,7 @@ webgpu = []
pbr_transmission_textures = [] pbr_transmission_textures = []
pbr_multi_layer_material_textures = [] pbr_multi_layer_material_textures = []
pbr_anisotropy_texture = [] pbr_anisotropy_texture = []
pbr_pcss = [] experimental_pbr_pcss = []
shader_format_glsl = ["bevy_render/shader_format_glsl"] shader_format_glsl = ["bevy_render/shader_format_glsl"]
trace = ["bevy_render/trace"] trace = ["bevy_render/trace"]
ios_simulator = ["bevy_render/ios_simulator"] ios_simulator = ["bevy_render/ios_simulator"]

View file

@ -95,6 +95,7 @@ pub struct DirectionalLight {
/// ///
/// Note that soft shadows are significantly more expensive to render than /// Note that soft shadows are significantly more expensive to render than
/// hard shadows. /// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadow_size: Option<f32>, pub soft_shadow_size: Option<f32>,
/// A value that adjusts the tradeoff between self-shadowing artifacts and /// A value that adjusts the tradeoff between self-shadowing artifacts and
@ -120,9 +121,10 @@ impl Default for DirectionalLight {
color: Color::WHITE, color: Color::WHITE,
illuminance: light_consts::lux::AMBIENT_DAYLIGHT, illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
shadows_enabled: false, shadows_enabled: false,
soft_shadow_size: None,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadow_size: None,
} }
} }
} }

View file

@ -1,6 +1,9 @@
use core::ops::DerefMut; use core::ops::DerefMut;
use bevy_ecs::{entity::EntityHashMap, prelude::*}; use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
};
use bevy_math::{ops, Mat4, Vec3A, Vec4}; use bevy_math::{ops, Mat4, Vec3A, Vec4};
use bevy_reflect::prelude::*; use bevy_reflect::prelude::*;
use bevy_render::{ use bevy_render::{
@ -836,6 +839,7 @@ pub fn check_dir_light_mesh_visibility(
}); });
} }
#[allow(clippy::too_many_arguments)]
pub fn check_point_light_mesh_visibility( pub fn check_point_light_mesh_visibility(
visible_point_lights: Query<&VisibleClusterableObjects>, visible_point_lights: Query<&VisibleClusterableObjects>,
mut point_lights: Query<( mut point_lights: Query<(
@ -872,10 +876,17 @@ pub fn check_point_light_mesh_visibility(
visible_entity_ranges: Option<Res<VisibleEntityRanges>>, visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>, mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>, mut spot_visible_entities_queue: Local<Parallel<Vec<Entity>>>,
mut checked_lights: Local<EntityHashSet>,
) { ) {
checked_lights.clear();
let visible_entity_ranges = visible_entity_ranges.as_deref(); let visible_entity_ranges = visible_entity_ranges.as_deref();
for visible_lights in &visible_point_lights { for visible_lights in &visible_point_lights {
for light_entity in visible_lights.entities.iter().copied() { for light_entity in visible_lights.entities.iter().copied() {
if !checked_lights.insert(light_entity) {
continue;
}
// Point lights // Point lights
if let Ok(( if let Ok((
point_light, point_light,

View file

@ -61,6 +61,7 @@ pub struct PointLight {
/// ///
/// Note that soft shadows are significantly more expensive to render than /// Note that soft shadows are significantly more expensive to render than
/// hard shadows. /// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadows_enabled: bool, pub soft_shadows_enabled: bool,
/// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions /// A bias used when sampling shadow maps to avoid "shadow-acne", or false shadow occlusions
@ -95,10 +96,11 @@ impl Default for PointLight {
range: 20.0, range: 20.0,
radius: 0.0, radius: 0.0,
shadows_enabled: false, shadows_enabled: false,
soft_shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z, shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadows_enabled: false,
} }
} }
} }

View file

@ -57,6 +57,7 @@ pub struct SpotLight {
/// ///
/// Note that soft shadows are significantly more expensive to render than /// Note that soft shadows are significantly more expensive to render than
/// hard shadows. /// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadows_enabled: bool, pub soft_shadows_enabled: bool,
/// A value that adjusts the tradeoff between self-shadowing artifacts and /// A value that adjusts the tradeoff between self-shadowing artifacts and
@ -115,12 +116,13 @@ impl Default for SpotLight {
range: 20.0, range: 20.0,
radius: 0.0, radius: 0.0,
shadows_enabled: false, shadows_enabled: false,
soft_shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS, shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS, shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z, shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
inner_angle: 0.0, inner_angle: 0.0,
outer_angle: core::f32::consts::FRAC_PI_4, outer_angle: core::f32::consts::FRAC_PI_4,
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadows_enabled: false,
} }
} }
} }

View file

@ -33,7 +33,9 @@ pub(crate) use self::{
}, },
}; };
pub use self::asset::{MeshletMesh, MeshletMeshLoader, MeshletMeshSaver}; pub use self::asset::{
MeshletMesh, MeshletMeshLoader, MeshletMeshSaver, MESHLET_MESH_ASSET_VERSION,
};
#[cfg(feature = "meshlet_processor")] #[cfg(feature = "meshlet_processor")]
pub use self::from_mesh::{ pub use self::from_mesh::{
MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR, MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR,

View file

@ -9,6 +9,7 @@ use bevy_ecs::{
system::lifetimeless::Read, system::lifetimeless::Read,
}; };
use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_render::camera::SortedCameras;
use bevy_render::sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity}; use bevy_render::sync_world::{MainEntity, RenderEntity, TemporaryRenderEntity};
use bevy_render::{ use bevy_render::{
diagnostic::RecordDiagnostics, diagnostic::RecordDiagnostics,
@ -29,6 +30,7 @@ use bevy_utils::tracing::info_span;
use bevy_utils::{ use bevy_utils::{
default, default,
tracing::{error, warn}, tracing::{error, warn},
HashMap,
}; };
use core::{hash::Hash, ops::Range}; use core::{hash::Hash, ops::Range};
@ -41,12 +43,12 @@ pub struct ExtractedPointLight {
pub radius: f32, pub radius: f32,
pub transform: GlobalTransform, pub transform: GlobalTransform,
pub shadows_enabled: bool, pub shadows_enabled: bool,
pub soft_shadows_enabled: bool,
pub shadow_depth_bias: f32, pub shadow_depth_bias: f32,
pub shadow_normal_bias: f32, pub shadow_normal_bias: f32,
pub shadow_map_near_z: f32, pub shadow_map_near_z: f32,
pub spot_light_angles: Option<(f32, f32)>, pub spot_light_angles: Option<(f32, f32)>,
pub volumetric: bool, pub volumetric: bool,
pub soft_shadows_enabled: bool,
} }
#[derive(Component, Debug)] #[derive(Component, Debug)]
@ -56,13 +58,13 @@ pub struct ExtractedDirectionalLight {
pub transform: GlobalTransform, pub transform: GlobalTransform,
pub shadows_enabled: bool, pub shadows_enabled: bool,
pub volumetric: bool, pub volumetric: bool,
pub soft_shadow_size: Option<f32>,
pub shadow_depth_bias: f32, pub shadow_depth_bias: f32,
pub shadow_normal_bias: f32, pub shadow_normal_bias: f32,
pub cascade_shadow_config: CascadeShadowConfig, pub cascade_shadow_config: CascadeShadowConfig,
pub cascades: EntityHashMap<Vec<Cascade>>, pub cascades: EntityHashMap<Vec<Cascade>>,
pub frusta: EntityHashMap<Vec<Frustum>>, pub frusta: EntityHashMap<Vec<Frustum>>,
pub render_layers: RenderLayers, pub render_layers: RenderLayers,
pub soft_shadow_size: Option<f32>,
} }
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl! // NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
@ -147,10 +149,10 @@ pub const MAX_CASCADES_PER_LIGHT: usize = 1;
#[derive(Resource, Clone)] #[derive(Resource, Clone)]
pub struct ShadowSamplers { pub struct ShadowSamplers {
pub point_light_comparison_sampler: Sampler, pub point_light_comparison_sampler: Sampler,
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
pub point_light_linear_sampler: Sampler, pub point_light_linear_sampler: Sampler,
pub directional_light_comparison_sampler: Sampler, pub directional_light_comparison_sampler: Sampler,
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
pub directional_light_linear_sampler: Sampler, pub directional_light_linear_sampler: Sampler,
} }
@ -174,7 +176,7 @@ impl FromWorld for ShadowSamplers {
compare: Some(CompareFunction::GreaterEqual), compare: Some(CompareFunction::GreaterEqual),
..base_sampler_descriptor ..base_sampler_descriptor
}), }),
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor), point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor),
directional_light_comparison_sampler: render_device.create_sampler( directional_light_comparison_sampler: render_device.create_sampler(
&SamplerDescriptor { &SamplerDescriptor {
@ -182,7 +184,7 @@ impl FromWorld for ShadowSamplers {
..base_sampler_descriptor ..base_sampler_descriptor
}, },
), ),
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
directional_light_linear_sampler: render_device directional_light_linear_sampler: render_device
.create_sampler(&base_sampler_descriptor), .create_sampler(&base_sampler_descriptor),
} }
@ -291,7 +293,6 @@ pub fn extract_lights(
radius: point_light.radius, radius: point_light.radius,
transform: *transform, transform: *transform,
shadows_enabled: point_light.shadows_enabled, shadows_enabled: point_light.shadows_enabled,
soft_shadows_enabled: point_light.soft_shadows_enabled,
shadow_depth_bias: point_light.shadow_depth_bias, shadow_depth_bias: point_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset // The factor of SQRT_2 is for the worst-case diagonal offset
shadow_normal_bias: point_light.shadow_normal_bias shadow_normal_bias: point_light.shadow_normal_bias
@ -300,6 +301,10 @@ pub fn extract_lights(
shadow_map_near_z: point_light.shadow_map_near_z, shadow_map_near_z: point_light.shadow_map_near_z,
spot_light_angles: None, spot_light_angles: None,
volumetric: volumetric_light.is_some(), volumetric: volumetric_light.is_some(),
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadows_enabled: point_light.soft_shadows_enabled,
#[cfg(not(feature = "experimental_pbr_pcss"))]
soft_shadows_enabled: false,
}; };
point_lights_values.push(( point_lights_values.push((
render_entity, render_entity,
@ -350,7 +355,6 @@ pub fn extract_lights(
radius: spot_light.radius, radius: spot_light.radius,
transform: *transform, transform: *transform,
shadows_enabled: spot_light.shadows_enabled, shadows_enabled: spot_light.shadows_enabled,
soft_shadows_enabled: spot_light.soft_shadows_enabled,
shadow_depth_bias: spot_light.shadow_depth_bias, shadow_depth_bias: spot_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset // The factor of SQRT_2 is for the worst-case diagonal offset
shadow_normal_bias: spot_light.shadow_normal_bias shadow_normal_bias: spot_light.shadow_normal_bias
@ -359,6 +363,10 @@ pub fn extract_lights(
shadow_map_near_z: spot_light.shadow_map_near_z, shadow_map_near_z: spot_light.shadow_map_near_z,
spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)), spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
volumetric: volumetric_light.is_some(), volumetric: volumetric_light.is_some(),
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadows_enabled: spot_light.soft_shadows_enabled,
#[cfg(not(feature = "experimental_pbr_pcss"))]
soft_shadows_enabled: false,
}, },
render_visible_entities, render_visible_entities,
*frustum, *frustum,
@ -430,7 +438,10 @@ pub fn extract_lights(
illuminance: directional_light.illuminance, illuminance: directional_light.illuminance,
transform: *transform, transform: *transform,
volumetric: volumetric_light.is_some(), volumetric: volumetric_light.is_some(),
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadow_size: directional_light.soft_shadow_size, soft_shadow_size: directional_light.soft_shadow_size,
#[cfg(not(feature = "experimental_pbr_pcss"))]
soft_shadow_size: None,
shadows_enabled: directional_light.shadows_enabled, shadows_enabled: directional_light.shadows_enabled,
shadow_depth_bias: directional_light.shadow_depth_bias, shadow_depth_bias: directional_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset // The factor of SQRT_2 is for the worst-case diagonal offset
@ -677,10 +688,11 @@ pub fn prepare_lights(
point_light_shadow_map: Res<PointLightShadowMap>, point_light_shadow_map: Res<PointLightShadowMap>,
directional_light_shadow_map: Res<DirectionalLightShadowMap>, directional_light_shadow_map: Res<DirectionalLightShadowMap>,
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>, mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
(mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): ( (
Local<bool>, mut max_directional_lights_warning_emitted,
Local<bool>, mut max_cascades_per_light_warning_emitted,
), mut live_shadow_mapping_lights,
): (Local<bool>, Local<bool>, Local<EntityHashSet>),
point_lights: Query<( point_lights: Query<(
Entity, Entity,
&ExtractedPointLight, &ExtractedPointLight,
@ -688,7 +700,7 @@ pub fn prepare_lights(
)>, )>,
directional_lights: Query<(Entity, &ExtractedDirectionalLight)>, directional_lights: Query<(Entity, &ExtractedDirectionalLight)>,
mut light_view_entities: Query<&mut LightViewEntities>, mut light_view_entities: Query<&mut LightViewEntities>,
mut live_shadow_mapping_lights: Local<EntityHashSet>, sorted_cameras: Res<SortedCameras>,
) { ) {
let views_iter = views.iter(); let views_iter = views.iter();
let views_count = views_iter.len(); let views_count = views_iter.len();
@ -911,17 +923,17 @@ pub fn prepare_lights(
.extend(1.0 / (light.range * light.range)), .extend(1.0 / (light.range * light.range)),
position_radius: light.transform.translation().extend(light.radius), position_radius: light.transform.translation().extend(light.radius),
flags: flags.bits(), flags: flags.bits(),
soft_shadow_size: if light.soft_shadows_enabled {
light.radius
} else {
0.0
},
shadow_depth_bias: light.shadow_depth_bias, shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias, shadow_normal_bias: light.shadow_normal_bias,
shadow_map_near_z: light.shadow_map_near_z, shadow_map_near_z: light.shadow_map_near_z,
spot_light_tan_angle, spot_light_tan_angle,
pad_a: 0.0, pad_a: 0.0,
pad_b: 0.0, pad_b: 0.0,
soft_shadow_size: if light.soft_shadows_enabled {
light.radius
} else {
0.0
},
}); });
global_light_meta.entity_to_index.insert(entity, index); global_light_meta.entity_to_index.insert(entity, index);
} }
@ -984,50 +996,108 @@ pub fn prepare_lights(
live_shadow_mapping_lights.clear(); live_shadow_mapping_lights.clear();
let mut point_light_depth_attachments = HashMap::<u32, DepthAttachment>::default();
let mut directional_light_depth_attachments = HashMap::<u32, DepthAttachment>::default();
let point_light_depth_texture = texture_cache.get(
&render_device,
TextureDescriptor {
size: Extent3d {
width: point_light_shadow_map.size as u32,
height: point_light_shadow_map.size as u32,
depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: CORE_3D_DEPTH_FORMAT,
label: Some("point_light_shadow_map_texture"),
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
let point_light_depth_texture_view =
point_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("point_light_shadow_map_array_texture_view"),
format: None,
// NOTE: iOS Simulator is missing CubeArray support so we use Cube instead.
// See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added.
#[cfg(all(
not(feature = "ios_simulator"),
any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
)
))]
dimension: Some(TextureViewDimension::CubeArray),
#[cfg(any(
feature = "ios_simulator",
all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu"))
))]
dimension: Some(TextureViewDimension::Cube),
aspect: TextureAspect::DepthOnly,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
});
let directional_light_depth_texture = texture_cache.get(
&render_device,
TextureDescriptor {
size: Extent3d {
width: (directional_light_shadow_map.size as u32)
.min(render_device.limits().max_texture_dimension_2d),
height: (directional_light_shadow_map.size as u32)
.min(render_device.limits().max_texture_dimension_2d),
depth_or_array_layers: (num_directional_cascades_enabled
+ spot_light_shadow_maps_count)
.max(1) as u32,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: CORE_3D_DEPTH_FORMAT,
label: Some("directional_light_shadow_map_texture"),
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
let directional_light_depth_texture_view =
directional_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("directional_light_shadow_map_array_texture_view"),
format: None,
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
dimension: Some(TextureViewDimension::D2Array),
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
dimension: Some(TextureViewDimension::D2),
aspect: TextureAspect::DepthOnly,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
});
let mut live_views = EntityHashSet::with_capacity_and_hasher(views_count, EntityHash); let mut live_views = EntityHashSet::with_capacity_and_hasher(views_count, EntityHash);
// set up light data for each view // set up light data for each view
for (entity, extracted_view, clusters, maybe_layers) in views.iter() { for (entity, extracted_view, clusters, maybe_layers) in sorted_cameras
.0
.iter()
.filter_map(|sorted_camera| views.get(sorted_camera.entity).ok())
{
live_views.insert(entity); live_views.insert(entity);
let point_light_depth_texture = texture_cache.get(
&render_device,
TextureDescriptor {
size: Extent3d {
width: point_light_shadow_map.size as u32,
height: point_light_shadow_map.size as u32,
depth_or_array_layers: point_light_shadow_maps_count.max(1) as u32 * 6,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: CORE_3D_DEPTH_FORMAT,
label: Some("point_light_shadow_map_texture"),
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
let directional_light_depth_texture = texture_cache.get(
&render_device,
TextureDescriptor {
size: Extent3d {
width: (directional_light_shadow_map.size as u32)
.min(render_device.limits().max_texture_dimension_2d),
height: (directional_light_shadow_map.size as u32)
.min(render_device.limits().max_texture_dimension_2d),
depth_or_array_layers: (num_directional_cascades_enabled
+ spot_light_shadow_maps_count)
.max(1) as u32,
},
mip_level_count: 1,
sample_count: 1,
dimension: TextureDimension::D2,
format: CORE_3D_DEPTH_FORMAT,
label: Some("directional_light_shadow_map_texture"),
usage: TextureUsages::RENDER_ATTACHMENT | TextureUsages::TEXTURE_BINDING,
view_formats: &[],
},
);
let mut view_lights = Vec::new(); let mut view_lights = Vec::new();
let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0; let is_orthographic = extracted_view.clip_from_view.w_axis.w == 1.0;
@ -1102,23 +1172,35 @@ pub fn prepare_lights(
.zip(light_view_entities.iter().copied()) .zip(light_view_entities.iter().copied())
.enumerate() .enumerate()
{ {
let depth_texture_view = let mut first = false;
point_light_depth_texture let base_array_layer = (light_index * 6 + face_index) as u32;
.texture
.create_view(&TextureViewDescriptor { let depth_attachment = point_light_depth_attachments
label: Some("point_light_shadow_map_texture_view"), .entry(base_array_layer)
format: None, .or_insert_with(|| {
dimension: Some(TextureViewDimension::D2), first = true;
aspect: TextureAspect::All,
base_mip_level: 0, let depth_texture_view =
mip_level_count: None, point_light_depth_texture
base_array_layer: (light_index * 6 + face_index) as u32, .texture
array_layer_count: Some(1u32), .create_view(&TextureViewDescriptor {
}); label: Some("point_light_shadow_map_texture_view"),
format: None,
dimension: Some(TextureViewDimension::D2),
aspect: TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer,
array_layer_count: Some(1u32),
});
DepthAttachment::new(depth_texture_view, Some(0.0))
})
.clone();
commands.entity(view_light_entity).insert(( commands.entity(view_light_entity).insert((
ShadowView { ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), depth_attachment,
pass_name: format!( pass_name: format!(
"shadow pass point light {} {}", "shadow pass point light {} {}",
light_index, light_index,
@ -1147,8 +1229,11 @@ pub fn prepare_lights(
view_lights.push(view_light_entity); view_lights.push(view_light_entity);
shadow_render_phases.insert_or_clear(view_light_entity); if first {
live_shadow_mapping_lights.insert(view_light_entity); // Subsequent views with the same light entity will reuse the same shadow map
shadow_render_phases.insert_or_clear(view_light_entity);
live_shadow_mapping_lights.insert(view_light_entity);
}
} }
} }
@ -1177,19 +1262,30 @@ pub fn prepare_lights(
[point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1; [point_light_count..point_light_count + spot_light_shadow_maps_count] are spot lights").1;
let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z); let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z);
let depth_texture_view = let mut first = false;
directional_light_depth_texture let base_array_layer = (num_directional_cascades_enabled + light_index) as u32;
.texture
.create_view(&TextureViewDescriptor { let depth_attachment = directional_light_depth_attachments
label: Some("spot_light_shadow_map_texture_view"), .entry(base_array_layer)
format: None, .or_insert_with(|| {
dimension: Some(TextureViewDimension::D2), first = true;
aspect: TextureAspect::All,
base_mip_level: 0, let depth_texture_view = directional_light_depth_texture.texture.create_view(
mip_level_count: None, &TextureViewDescriptor {
base_array_layer: (num_directional_cascades_enabled + light_index) as u32, label: Some("spot_light_shadow_map_texture_view"),
array_layer_count: Some(1u32), format: None,
}); dimension: Some(TextureViewDimension::D2),
aspect: TextureAspect::All,
base_mip_level: 0,
mip_level_count: None,
base_array_layer,
array_layer_count: Some(1u32),
},
);
DepthAttachment::new(depth_texture_view, Some(0.0))
})
.clone();
let light_view_entities = light_view_entities let light_view_entities = light_view_entities
.entry(entity) .entry(entity)
@ -1199,7 +1295,7 @@ pub fn prepare_lights(
commands.entity(view_light_entity).insert(( commands.entity(view_light_entity).insert((
ShadowView { ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), depth_attachment,
pass_name: format!("shadow pass spot light {light_index}"), pass_name: format!("shadow pass spot light {light_index}"),
}, },
ExtractedView { ExtractedView {
@ -1221,8 +1317,11 @@ pub fn prepare_lights(
view_lights.push(view_light_entity); view_lights.push(view_light_entity);
shadow_render_phases.insert_or_clear(view_light_entity); if first {
live_shadow_mapping_lights.insert(view_light_entity); // Subsequent views with the same light entity will reuse the same shadow map
shadow_render_phases.insert_or_clear(view_light_entity);
live_shadow_mapping_lights.insert(view_light_entity);
}
} }
// directional lights // directional lights
@ -1307,6 +1406,12 @@ pub fn prepare_lights(
base_array_layer: directional_depth_texture_array_index, base_array_layer: directional_depth_texture_array_index,
array_layer_count: Some(1u32), array_layer_count: Some(1u32),
}); });
// NOTE: For point and spotlights, we reuse the same depth attachment for all views.
// However, for directional lights, we want a new depth attachment for each view,
// so that the view is cleared for each view.
let depth_attachment = DepthAttachment::new(depth_texture_view, Some(0.0));
directional_depth_texture_array_index += 1; directional_depth_texture_array_index += 1;
let mut frustum = *frustum; let mut frustum = *frustum;
@ -1316,7 +1421,7 @@ pub fn prepare_lights(
commands.entity(view_light_entity).insert(( commands.entity(view_light_entity).insert((
ShadowView { ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)), depth_attachment,
pass_name: format!( pass_name: format!(
"shadow pass directional light {light_index} cascade {cascade_index}" "shadow pass directional light {light_index} cascade {cascade_index}"
), ),
@ -1342,65 +1447,19 @@ pub fn prepare_lights(
)); ));
view_lights.push(view_light_entity); view_lights.push(view_light_entity);
// Subsequent views with the same light entity will **NOT** reuse the same shadow map
// (Because the cascades are unique to each view)
shadow_render_phases.insert_or_clear(view_light_entity); shadow_render_phases.insert_or_clear(view_light_entity);
live_shadow_mapping_lights.insert(view_light_entity); live_shadow_mapping_lights.insert(view_light_entity);
} }
} }
let point_light_depth_texture_view =
point_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("point_light_shadow_map_array_texture_view"),
format: None,
// NOTE: iOS Simulator is missing CubeArray support so we use Cube instead.
// See https://github.com/bevyengine/bevy/pull/12052 - remove if support is added.
#[cfg(all(
not(feature = "ios_simulator"),
any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
)
))]
dimension: Some(TextureViewDimension::CubeArray),
#[cfg(any(
feature = "ios_simulator",
all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu"))
))]
dimension: Some(TextureViewDimension::Cube),
aspect: TextureAspect::DepthOnly,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
});
let directional_light_depth_texture_view = directional_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("directional_light_shadow_map_array_texture_view"),
format: None,
#[cfg(any(
not(feature = "webgl"),
not(target_arch = "wasm32"),
feature = "webgpu"
))]
dimension: Some(TextureViewDimension::D2Array),
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
dimension: Some(TextureViewDimension::D2),
aspect: TextureAspect::DepthOnly,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
});
commands.entity(entity).insert(( commands.entity(entity).insert((
ViewShadowBindings { ViewShadowBindings {
point_light_depth_texture: point_light_depth_texture.texture, point_light_depth_texture: point_light_depth_texture.texture.clone(),
point_light_depth_texture_view, point_light_depth_texture_view: point_light_depth_texture_view.clone(),
directional_light_depth_texture: directional_light_depth_texture.texture, directional_light_depth_texture: directional_light_depth_texture.texture.clone(),
directional_light_depth_texture_view, directional_light_depth_texture_view: directional_light_depth_texture_view.clone(),
}, },
ViewLightEntities { ViewLightEntities {
lights: view_lights, lights: view_lights,

View file

@ -1859,7 +1859,7 @@ impl SpecializedMeshPipeline for MeshPipeline {
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
shader_defs.push("WEBGL2".into()); shader_defs.push("WEBGL2".into());
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
shader_defs.push("PCSS_SAMPLERS_AVAILABLE".into()); shader_defs.push("PCSS_SAMPLERS_AVAILABLE".into());
if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) { if key.contains(MeshPipelineKey::TONEMAP_IN_SHADER) {

View file

@ -228,7 +228,7 @@ fn layout_entries(
// Point Shadow Texture Array Comparison Sampler // Point Shadow Texture Array Comparison Sampler
(3, sampler(SamplerBindingType::Comparison)), (3, sampler(SamplerBindingType::Comparison)),
// Point Shadow Texture Array Linear Sampler // Point Shadow Texture Array Linear Sampler
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
(4, sampler(SamplerBindingType::Filtering)), (4, sampler(SamplerBindingType::Filtering)),
// Directional Shadow Texture Array // Directional Shadow Texture Array
( (
@ -245,7 +245,7 @@ fn layout_entries(
// Directional Shadow Texture Array Comparison Sampler // Directional Shadow Texture Array Comparison Sampler
(6, sampler(SamplerBindingType::Comparison)), (6, sampler(SamplerBindingType::Comparison)),
// Directional Shadow Texture Array Linear Sampler // Directional Shadow Texture Array Linear Sampler
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
(7, sampler(SamplerBindingType::Filtering)), (7, sampler(SamplerBindingType::Filtering)),
// PointLights // PointLights
( (
@ -580,11 +580,11 @@ pub fn prepare_mesh_view_bind_groups(
(1, light_binding.clone()), (1, light_binding.clone()),
(2, &shadow_bindings.point_light_depth_texture_view), (2, &shadow_bindings.point_light_depth_texture_view),
(3, &shadow_samplers.point_light_comparison_sampler), (3, &shadow_samplers.point_light_comparison_sampler),
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
(4, &shadow_samplers.point_light_linear_sampler), (4, &shadow_samplers.point_light_linear_sampler),
(5, &shadow_bindings.directional_light_depth_texture_view), (5, &shadow_bindings.directional_light_depth_texture_view),
(6, &shadow_samplers.directional_light_comparison_sampler), (6, &shadow_samplers.directional_light_comparison_sampler),
#[cfg(feature = "pbr_pcss")] #[cfg(feature = "experimental_pbr_pcss")]
(7, &shadow_samplers.directional_light_linear_sampler), (7, &shadow_samplers.directional_light_linear_sampler),
(8, clusterable_objects_binding.clone()), (8, clusterable_objects_binding.clone()),
( (

View file

@ -6,10 +6,10 @@
//! detail. //! detail.
//! //!
//! Because `bevy_picking` is very loosely coupled with its backends, you can mix and match as //! Because `bevy_picking` is very loosely coupled with its backends, you can mix and match as
//! many backends as you want. For example, You could use the `rapier` backend to raycast against //! many backends as you want. For example, you could use the `rapier` backend to raycast against
//! physics objects, a picking shader backend to pick non-physics meshes, and the `bevy_ui` backend //! physics objects, a picking shader backend to pick non-physics meshes, and the `bevy_ui` backend
//! for your UI. The [`PointerHits`]s produced by these various backends will be combined, sorted, //! for your UI. The [`PointerHits`] instances produced by these various backends will be combined,
//! and used as a homogeneous input for the picking systems that consume these events. //! sorted, and used as a homogeneous input for the picking systems that consume these events.
//! //!
//! ## Implementation //! ## Implementation
//! //!
@ -22,7 +22,7 @@
//! //!
//! - Backends do not need to consider the [`PickingBehavior`](crate::PickingBehavior) component, though they may //! - Backends do not need to consider the [`PickingBehavior`](crate::PickingBehavior) component, though they may
//! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy //! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy
//! may want to early exit if it intersects an entity that blocks lower entities from being //! may want to exit early if it intersects an entity that blocks lower entities from being
//! picked. //! picked.
//! //!
//! ### Raycasting Backends //! ### Raycasting Backends
@ -49,9 +49,8 @@ pub mod prelude {
/// An event produced by a picking backend after it has run its hit tests, describing the entities /// An event produced by a picking backend after it has run its hit tests, describing the entities
/// under a pointer. /// under a pointer.
/// ///
/// Some backends may only support providing the topmost entity; this is a valid limitation of some /// Some backends may only support providing the topmost entity; this is a valid limitation. For
/// backends. For example, a picking shader might only have data on the topmost rendered output from /// example, a picking shader might only have data on the topmost rendered output from its buffer.
/// its buffer.
/// ///
/// Note that systems reading these events in [`PreUpdate`](bevy_app) will not report ordering /// Note that systems reading these events in [`PreUpdate`](bevy_app) will not report ordering
/// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered /// ambiguities with picking backends. Take care to ensure such systems are explicitly ordered

View file

@ -321,28 +321,51 @@ pub struct PickingEventWriters<'w> {
/// Dispatches interaction events to the target entities. /// Dispatches interaction events to the target entities.
/// ///
/// Within a single frame, events are dispatched in the following order: /// Within a single frame, events are dispatched in the following order:
/// + The sequence [`DragEnter`], [`Over`]. /// + [`Out`] → [`DragLeave`].
/// + [`DragEnter`] → [`Over`].
/// + Any number of any of the following: /// + Any number of any of the following:
/// + For each movement: The sequence [`DragStart`], [`Drag`], [`DragOver`], [`Move`]. /// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`].
/// + For each button press: Either [`Down`], or the sequence [`Click`], [`Up`], [`DragDrop`], [`DragEnd`], [`DragLeave`]. /// + For each button press: [`Down`] or [`Click`] → [`Up`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`].
/// + For each pointer cancellation: Simply [`Cancel`]. /// + For each pointer cancellation: [`Cancel`].
/// + Finally the sequence [`Out`], [`DragLeave`].
/// ///
/// Only the last event in a given sequence is garenteed to be present. /// Additionally, across multiple frames, the following are also strictly
/// ordered by the interaction state machine:
/// + When a pointer moves over the target:
/// [`Over`], [`Move`], [`Out`].
/// + When a pointer presses buttons on the target:
/// [`Down`], [`Click`], [`Up`].
/// + When a pointer drags the target:
/// [`DragStart`], [`Drag`], [`DragEnd`].
/// + When a pointer drags something over the target:
/// [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`].
/// + When a pointer is canceled:
/// No other events will follow the [`Cancel`] event for that pointer.
/// ///
/// Additionally, across multiple frames, the following are also strictly ordered by the interaction state machine: /// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`].
/// + When a pointer moves over the target: [`Over`], [`Move`], [`Out`]. /// The rest rely on additional data from the [`PointerInput`] event stream. To
/// + When a pointer presses buttons on the target: [`Down`], [`Up`], [`Click`]. /// receive these events for a custom pointer, you must add [`PointerInput`]
/// + When a pointer drags the target: [`DragStart`], [`Drag`], [`DragEnd`]. /// events.
/// + When a pointer drags something over the target: [`DragEnter`], [`DragOver`], [`DragDrop`], [`DragLeave`].
/// + When a pointer is canceled: No other events will follow the [`Cancel`] event for that pointer.
/// ///
/// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`]. The rest rely on additional data from the /// When the pointer goes from hovering entity A to entity B, entity A will
/// [`PointerInput`] event stream. To receive these events for a custom pointer, you must add [`PointerInput`] events. /// receive [`Out`] and then entity B will receive [`Over`]. No entity will ever
/// receive both an [`Over`] and and a [`Out`] event during the same frame.
/// ///
/// Note: Though it is common for the [`PointerInput`] stream may contain multiple pointer movements and presses each frame, /// When we account for event bubbling, this is no longer true. When focus shifts
/// the hover state is determined only by the pointer's *final position*. Since the hover state ultimately determines which /// between children, parent entities may receive redundant [`Out`] → [`Over`] pairs.
/// entities receive events, this may mean that an entity can receive events which occurred before it was actually hovered. /// In the context of UI, this is especially problematic. Additional hierarchy-aware
/// events will be added in a future release.
///
/// Both [`Click`] and [`Up`] target the entity hovered in the *previous frame*,
/// rather than the current frame. This is because touch pointers hover nothing
/// on the frame they are released. The end effect is that these two events can
/// be received sequentally after an [`Out`] event (but always on the same frame
/// as the [`Out`] event).
///
/// Note: Though it is common for the [`PointerInput`] stream may contain
/// multiple pointer movements and presses each frame, the hover state is
/// determined only by the pointer's *final position*. Since the hover state
/// ultimately determines which entities receive events, this may mean that an
/// entity can receive events from before or after it was actually hovered.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn pointer_events( pub fn pointer_events(
// Input // Input
@ -366,6 +389,57 @@ pub fn pointer_events(
.and_then(|pointer| pointer.location.clone()) .and_then(|pointer| pointer.location.clone())
}; };
// If the entity was hovered by a specific pointer last frame...
for (pointer_id, hovered_entity, hit) in previous_hover_map
.iter()
.flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone())))
{
// ...but is now not being hovered by that same pointer...
if !hover_map
.get(&pointer_id)
.iter()
.any(|e| e.contains_key(&hovered_entity))
{
let Some(location) = pointer_location(pointer_id) else {
debug!(
"Unable to get location for pointer {:?} during pointer out",
pointer_id
);
continue;
};
// Always send Out events
let out_event = Pointer::new(
pointer_id,
location.clone(),
hovered_entity,
Out { hit: hit.clone() },
);
commands.trigger_targets(out_event.clone(), hovered_entity);
event_writers.out_events.send(out_event);
// Possibly send DragLeave events
for button in PointerButton::iter() {
let state = pointer_state.get_mut(pointer_id, button);
state.dragging_over.remove(&hovered_entity);
for drag_target in state.dragging.keys() {
let drag_leave_event = Pointer::new(
pointer_id,
location.clone(),
hovered_entity,
DragLeave {
button,
dragged: *drag_target,
hit: hit.clone(),
},
);
commands.trigger_targets(drag_leave_event.clone(), hovered_entity);
event_writers.drag_leave_events.send(drag_leave_event);
}
}
}
}
// If the entity is hovered... // If the entity is hovered...
for (pointer_id, hovered_entity, hit) in hover_map for (pointer_id, hovered_entity, hit) in hover_map
.iter() .iter()
@ -659,55 +733,4 @@ pub fn pointer_events(
} }
} }
} }
// If the entity was hovered by a specific pointer last frame...
for (pointer_id, hovered_entity, hit) in previous_hover_map
.iter()
.flat_map(|(id, hashmap)| hashmap.iter().map(|data| (*id, *data.0, data.1.clone())))
{
// ...but is now not being hovered by that same pointer...
if !hover_map
.get(&pointer_id)
.iter()
.any(|e| e.contains_key(&hovered_entity))
{
let Some(location) = pointer_location(pointer_id) else {
debug!(
"Unable to get location for pointer {:?} during pointer out",
pointer_id
);
continue;
};
// Always send Out events
let out_event = Pointer::new(
pointer_id,
location.clone(),
hovered_entity,
Out { hit: hit.clone() },
);
commands.trigger_targets(out_event.clone(), hovered_entity);
event_writers.out_events.send(out_event);
// Possibly send DragLeave events
for button in PointerButton::iter() {
let state = pointer_state.get_mut(pointer_id, button);
state.dragging_over.remove(&hovered_entity);
for drag_target in state.dragging.keys() {
let drag_leave_event = Pointer::new(
pointer_id,
location.clone(),
hovered_entity,
DragLeave {
button,
dragged: *drag_target,
hit: hit.clone(),
},
);
commands.trigger_targets(drag_leave_event.clone(), hovered_entity);
event_writers.drag_leave_events.send(drag_leave_event);
}
}
}
}
} }

View file

@ -9,7 +9,7 @@
//! driven by lower-level input devices and consumed by higher-level interaction systems. //! driven by lower-level input devices and consumed by higher-level interaction systems.
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_math::{Rect, Vec2}; use bevy_math::Vec2;
use bevy_reflect::prelude::*; use bevy_reflect::prelude::*;
use bevy_render::camera::{Camera, NormalizedRenderTarget}; use bevy_render::camera::{Camera, NormalizedRenderTarget};
use bevy_utils::HashMap; use bevy_utils::HashMap;
@ -233,13 +233,9 @@ impl Location {
return false; return false;
} }
let position = Vec2::new(self.position.x, self.position.y);
camera camera
.logical_viewport_rect() .logical_viewport_rect()
.map(|Rect { min, max }| { .map(|rect| rect.contains(self.position))
(position - min).min_element() >= 0.0 && (position - max).max_element() <= 0.0
})
.unwrap_or(false) .unwrap_or(false)
} }
} }

View file

@ -5,31 +5,25 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{Error, SeqAccess, Visitor}; use serde::de::{Error, SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Array`] values. /// A [`Visitor`] for deserializing [`Array`] values.
/// ///
/// [`Array`]: crate::Array /// [`Array`]: crate::Array
pub(super) struct ArrayVisitor<'a> { pub(super) struct ArrayVisitor<'a, P> {
array_info: &'static ArrayInfo, pub array_info: &'static ArrayInfo,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> ArrayVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for ArrayVisitor<'_, P> {
pub fn new(array_info: &'static ArrayInfo, registry: &'a TypeRegistry) -> Self {
Self {
array_info,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for ArrayVisitor<'a> {
type Value = DynamicArray; type Value = DynamicArray;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("reflected array value") formatter.write_str("reflected array value")
} }
fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error> fn visit_seq<V>(mut self, mut seq: V) -> Result<Self::Value, V::Error>
where where
V: SeqAccess<'de>, V: SeqAccess<'de>,
{ {
@ -38,6 +32,7 @@ impl<'a, 'de> Visitor<'de> for ArrayVisitor<'a> {
while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal( while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal(
registration, registration,
self.registry, self.registry,
self.processor.as_deref_mut(),
))? { ))? {
vec.push(value); vec.push(value);
} }

View file

@ -15,6 +15,8 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor}; use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A general purpose deserializer for reflected types. /// A general purpose deserializer for reflected types.
/// ///
/// This is the deserializer counterpart to [`ReflectSerializer`]. /// This is the deserializer counterpart to [`ReflectSerializer`].
@ -42,6 +44,10 @@ use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor};
/// This means that if the actual type is needed, these dynamic representations will need to /// This means that if the actual type is needed, these dynamic representations will need to
/// be converted to the concrete type using [`FromReflect`] or [`ReflectFromReflect`]. /// be converted to the concrete type using [`FromReflect`] or [`ReflectFromReflect`].
/// ///
/// If you want to override deserialization for a specific [`TypeRegistration`],
/// you can pass in a reference to a [`ReflectDeserializerProcessor`] which will
/// take priority over all other deserialization methods - see [`with_processor`].
///
/// # Example /// # Example
/// ///
/// ``` /// ```
@ -94,28 +100,57 @@ use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor};
/// [`Box<DynamicList>`]: crate::DynamicList /// [`Box<DynamicList>`]: crate::DynamicList
/// [`FromReflect`]: crate::FromReflect /// [`FromReflect`]: crate::FromReflect
/// [`ReflectFromReflect`]: crate::ReflectFromReflect /// [`ReflectFromReflect`]: crate::ReflectFromReflect
pub struct ReflectDeserializer<'a> { /// [`with_processor`]: Self::with_processor
pub struct ReflectDeserializer<'a, P: ReflectDeserializerProcessor = ()> {
registry: &'a TypeRegistry, registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
} }
impl<'a> ReflectDeserializer<'a> { impl<'a> ReflectDeserializer<'a, ()> {
/// Creates a deserializer with no processor.
///
/// If you want to add custom logic for deserializing certain types, use
/// [`with_processor`].
///
/// [`with_processor`]: Self::with_processor
pub fn new(registry: &'a TypeRegistry) -> Self { pub fn new(registry: &'a TypeRegistry) -> Self {
Self { registry } Self {
registry,
processor: None,
}
} }
} }
impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> { impl<'a, P: ReflectDeserializerProcessor> ReflectDeserializer<'a, P> {
/// Creates a deserializer with a processor.
///
/// If you do not need any custom logic for handling certain types, use
/// [`new`].
///
/// [`new`]: Self::new
pub fn with_processor(registry: &'a TypeRegistry, processor: &'a mut P) -> Self {
Self {
registry,
processor: Some(processor),
}
}
}
impl<'de, P: ReflectDeserializerProcessor> DeserializeSeed<'de> for ReflectDeserializer<'_, P> {
type Value = Box<dyn PartialReflect>; type Value = Box<dyn PartialReflect>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
struct UntypedReflectDeserializerVisitor<'a> { struct UntypedReflectDeserializerVisitor<'a, P> {
registry: &'a TypeRegistry, registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
} }
impl<'a, 'de> Visitor<'de> for UntypedReflectDeserializerVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de>
for UntypedReflectDeserializerVisitor<'_, P>
{
type Value = Box<dyn PartialReflect>; type Value = Box<dyn PartialReflect>;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -131,10 +166,11 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> {
.next_key_seed(TypeRegistrationDeserializer::new(self.registry))? .next_key_seed(TypeRegistrationDeserializer::new(self.registry))?
.ok_or_else(|| Error::invalid_length(0, &"a single entry"))?; .ok_or_else(|| Error::invalid_length(0, &"a single entry"))?;
let value = map.next_value_seed(TypedReflectDeserializer { let value = map.next_value_seed(TypedReflectDeserializer::new_internal(
registration, registration,
registry: self.registry, self.registry,
})?; self.processor,
))?;
if map.next_key::<IgnoredAny>()?.is_some() { if map.next_key::<IgnoredAny>()?.is_some() {
return Err(Error::invalid_length(2, &"a single entry")); return Err(Error::invalid_length(2, &"a single entry"));
@ -146,6 +182,7 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> {
deserializer.deserialize_map(UntypedReflectDeserializerVisitor { deserializer.deserialize_map(UntypedReflectDeserializerVisitor {
registry: self.registry, registry: self.registry,
processor: self.processor,
}) })
} }
} }
@ -175,6 +212,10 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> {
/// This means that if the actual type is needed, these dynamic representations will need to /// This means that if the actual type is needed, these dynamic representations will need to
/// be converted to the concrete type using [`FromReflect`] or [`ReflectFromReflect`]. /// be converted to the concrete type using [`FromReflect`] or [`ReflectFromReflect`].
/// ///
/// If you want to override deserialization for a specific [`TypeRegistration`],
/// you can pass in a reference to a [`ReflectDeserializerProcessor`] which will
/// take priority over all other deserialization methods - see [`with_processor`].
///
/// # Example /// # Example
/// ///
/// ``` /// ```
@ -226,13 +267,20 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> {
/// [`Box<DynamicList>`]: crate::DynamicList /// [`Box<DynamicList>`]: crate::DynamicList
/// [`FromReflect`]: crate::FromReflect /// [`FromReflect`]: crate::FromReflect
/// [`ReflectFromReflect`]: crate::ReflectFromReflect /// [`ReflectFromReflect`]: crate::ReflectFromReflect
pub struct TypedReflectDeserializer<'a> { /// [`with_processor`]: Self::with_processor
pub struct TypedReflectDeserializer<'a, P: ReflectDeserializerProcessor = ()> {
registration: &'a TypeRegistration, registration: &'a TypeRegistration,
registry: &'a TypeRegistry, registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
} }
impl<'a> TypedReflectDeserializer<'a> { impl<'a> TypedReflectDeserializer<'a, ()> {
/// Creates a new [`TypedReflectDeserializer`] for the given type registration. /// Creates a typed deserializer with no processor.
///
/// If you want to add custom logic for deserializing certain types, use
/// [`with_processor`].
///
/// [`with_processor`]: Self::with_processor
pub fn new(registration: &'a TypeRegistration, registry: &'a TypeRegistry) -> Self { pub fn new(registration: &'a TypeRegistration, registry: &'a TypeRegistry) -> Self {
#[cfg(feature = "debug_stack")] #[cfg(feature = "debug_stack")]
TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new()); TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new());
@ -240,10 +288,12 @@ impl<'a> TypedReflectDeserializer<'a> {
Self { Self {
registration, registration,
registry, registry,
processor: None,
} }
} }
/// Creates a new [`TypedReflectDeserializer`] for the given type `T`. /// Creates a new [`TypedReflectDeserializer`] for the given type `T`
/// without a processor.
/// ///
/// # Panics /// # Panics
/// ///
@ -256,6 +306,30 @@ impl<'a> TypedReflectDeserializer<'a> {
Self { Self {
registration, registration,
registry, registry,
processor: None,
}
}
}
impl<'a, P: ReflectDeserializerProcessor> TypedReflectDeserializer<'a, P> {
/// Creates a typed deserializer with a processor.
///
/// If you do not need any custom logic for handling certain types, use
/// [`new`].
///
/// [`new`]: Self::new
pub fn with_processor(
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
processor: &'a mut P,
) -> Self {
#[cfg(feature = "debug_stack")]
TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new());
Self {
registration,
registry,
processor: Some(processor),
} }
} }
@ -263,22 +337,42 @@ impl<'a> TypedReflectDeserializer<'a> {
pub(super) fn new_internal( pub(super) fn new_internal(
registration: &'a TypeRegistration, registration: &'a TypeRegistration,
registry: &'a TypeRegistry, registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
) -> Self { ) -> Self {
Self { Self {
registration, registration,
registry, registry,
processor,
} }
} }
} }
impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> { impl<'de, P: ReflectDeserializerProcessor> DeserializeSeed<'de>
for TypedReflectDeserializer<'_, P>
{
type Value = Box<dyn PartialReflect>; type Value = Box<dyn PartialReflect>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error> fn deserialize<D>(mut self, deserializer: D) -> Result<Self::Value, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
{ {
let deserialize_internal = || -> Result<Self::Value, D::Error> { let deserialize_internal = || -> Result<Self::Value, D::Error> {
// First, check if our processor wants to deserialize this type
// This takes priority over any other deserialization operations
let deserializer = if let Some(processor) = self.processor.as_deref_mut() {
match processor.try_deserialize(self.registration, self.registry, deserializer) {
Ok(Ok(value)) => {
return Ok(value);
}
Err(err) => {
return Err(make_custom_error(err));
}
Ok(Err(deserializer)) => deserializer,
}
} else {
deserializer
};
let type_path = self.registration.type_info().type_path(); let type_path = self.registration.type_info().type_path();
// Handle both Value case and types that have a custom `ReflectDeserialize` // Handle both Value case and types that have a custom `ReflectDeserialize`
@ -299,7 +393,12 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> {
let mut dynamic_struct = deserializer.deserialize_struct( let mut dynamic_struct = deserializer.deserialize_struct(
struct_info.type_path_table().ident().unwrap(), struct_info.type_path_table().ident().unwrap(),
struct_info.field_names(), struct_info.field_names(),
StructVisitor::new(struct_info, self.registration, self.registry), StructVisitor {
struct_info,
registration: self.registration,
registry: self.registry,
processor: self.processor,
},
)?; )?;
dynamic_struct.set_represented_type(Some(self.registration.type_info())); dynamic_struct.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_struct)) Ok(Box::new(dynamic_struct))
@ -310,57 +409,76 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> {
{ {
deserializer.deserialize_newtype_struct( deserializer.deserialize_newtype_struct(
tuple_struct_info.type_path_table().ident().unwrap(), tuple_struct_info.type_path_table().ident().unwrap(),
TupleStructVisitor::new( TupleStructVisitor {
tuple_struct_info, tuple_struct_info,
self.registration, registration: self.registration,
self.registry, registry: self.registry,
), processor: self.processor,
},
)? )?
} else { } else {
deserializer.deserialize_tuple_struct( deserializer.deserialize_tuple_struct(
tuple_struct_info.type_path_table().ident().unwrap(), tuple_struct_info.type_path_table().ident().unwrap(),
tuple_struct_info.field_len(), tuple_struct_info.field_len(),
TupleStructVisitor::new( TupleStructVisitor {
tuple_struct_info, tuple_struct_info,
self.registration, registration: self.registration,
self.registry, registry: self.registry,
), processor: self.processor,
},
)? )?
}; };
dynamic_tuple_struct.set_represented_type(Some(self.registration.type_info())); dynamic_tuple_struct.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_tuple_struct)) Ok(Box::new(dynamic_tuple_struct))
} }
TypeInfo::List(list_info) => { TypeInfo::List(list_info) => {
let mut dynamic_list = let mut dynamic_list = deserializer.deserialize_seq(ListVisitor {
deserializer.deserialize_seq(ListVisitor::new(list_info, self.registry))?; list_info,
registry: self.registry,
processor: self.processor,
})?;
dynamic_list.set_represented_type(Some(self.registration.type_info())); dynamic_list.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_list)) Ok(Box::new(dynamic_list))
} }
TypeInfo::Array(array_info) => { TypeInfo::Array(array_info) => {
let mut dynamic_array = deserializer.deserialize_tuple( let mut dynamic_array = deserializer.deserialize_tuple(
array_info.capacity(), array_info.capacity(),
ArrayVisitor::new(array_info, self.registry), ArrayVisitor {
array_info,
registry: self.registry,
processor: self.processor,
},
)?; )?;
dynamic_array.set_represented_type(Some(self.registration.type_info())); dynamic_array.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_array)) Ok(Box::new(dynamic_array))
} }
TypeInfo::Map(map_info) => { TypeInfo::Map(map_info) => {
let mut dynamic_map = let mut dynamic_map = deserializer.deserialize_map(MapVisitor {
deserializer.deserialize_map(MapVisitor::new(map_info, self.registry))?; map_info,
registry: self.registry,
processor: self.processor,
})?;
dynamic_map.set_represented_type(Some(self.registration.type_info())); dynamic_map.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_map)) Ok(Box::new(dynamic_map))
} }
TypeInfo::Set(set_info) => { TypeInfo::Set(set_info) => {
let mut dynamic_set = let mut dynamic_set = deserializer.deserialize_seq(SetVisitor {
deserializer.deserialize_seq(SetVisitor::new(set_info, self.registry))?; set_info,
registry: self.registry,
processor: self.processor,
})?;
dynamic_set.set_represented_type(Some(self.registration.type_info())); dynamic_set.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_set)) Ok(Box::new(dynamic_set))
} }
TypeInfo::Tuple(tuple_info) => { TypeInfo::Tuple(tuple_info) => {
let mut dynamic_tuple = deserializer.deserialize_tuple( let mut dynamic_tuple = deserializer.deserialize_tuple(
tuple_info.field_len(), tuple_info.field_len(),
TupleVisitor::new(tuple_info, self.registration, self.registry), TupleVisitor {
tuple_info,
registration: self.registration,
registry: self.registry,
processor: self.processor,
},
)?; )?;
dynamic_tuple.set_represented_type(Some(self.registration.type_info())); dynamic_tuple.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_tuple)) Ok(Box::new(dynamic_tuple))
@ -370,13 +488,21 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> {
== Some("core::option") == Some("core::option")
&& enum_info.type_path_table().ident() == Some("Option") && enum_info.type_path_table().ident() == Some("Option")
{ {
deserializer deserializer.deserialize_option(OptionVisitor {
.deserialize_option(OptionVisitor::new(enum_info, self.registry))? enum_info,
registry: self.registry,
processor: self.processor,
})?
} else { } else {
deserializer.deserialize_enum( deserializer.deserialize_enum(
enum_info.type_path_table().ident().unwrap(), enum_info.type_path_table().ident().unwrap(),
enum_info.variant_names(), enum_info.variant_names(),
EnumVisitor::new(enum_info, self.registration, self.registry), EnumVisitor {
enum_info,
registration: self.registration,
registry: self.registry,
processor: self.processor,
},
)? )?
}; };
dynamic_enum.set_represented_type(Some(self.registration.type_info())); dynamic_enum.set_represented_type(Some(self.registration.type_info()));

View file

@ -15,30 +15,19 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor}; use serde::de::{DeserializeSeed, EnumAccess, Error, MapAccess, SeqAccess, VariantAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Enum`] values. /// A [`Visitor`] for deserializing [`Enum`] values.
/// ///
/// [`Enum`]: crate::Enum /// [`Enum`]: crate::Enum
pub(super) struct EnumVisitor<'a> { pub(super) struct EnumVisitor<'a, P> {
enum_info: &'static EnumInfo, pub enum_info: &'static EnumInfo,
registration: &'a TypeRegistration, pub registration: &'a TypeRegistration,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> EnumVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for EnumVisitor<'_, P> {
pub fn new(
enum_info: &'static EnumInfo,
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
) -> Self {
Self {
enum_info,
registration,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> {
type Value = DynamicEnum; type Value = DynamicEnum;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -63,6 +52,7 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> {
struct_info, struct_info,
registration: self.registration, registration: self.registration,
registry: self.registry, registry: self.registry,
processor: self.processor,
}, },
)? )?
.into(), .into(),
@ -71,9 +61,12 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> {
*TupleLikeInfo::field_at(tuple_info, 0)?.ty(), *TupleLikeInfo::field_at(tuple_info, 0)?.ty(),
self.registry, self.registry,
)?; )?;
let value = variant.newtype_variant_seed( let value =
TypedReflectDeserializer::new_internal(registration, self.registry), variant.newtype_variant_seed(TypedReflectDeserializer::new_internal(
)?; registration,
self.registry,
self.processor,
))?;
let mut dynamic_tuple = DynamicTuple::default(); let mut dynamic_tuple = DynamicTuple::default();
dynamic_tuple.insert_boxed(value); dynamic_tuple.insert_boxed(value);
dynamic_tuple.into() dynamic_tuple.into()
@ -85,6 +78,7 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> {
tuple_info, tuple_info,
registration: self.registration, registration: self.registration,
registry: self.registry, registry: self.registry,
processor: self.processor,
}, },
)? )?
.into(), .into(),
@ -151,13 +145,14 @@ impl<'de> DeserializeSeed<'de> for VariantDeserializer {
} }
} }
struct StructVariantVisitor<'a> { struct StructVariantVisitor<'a, P> {
struct_info: &'static StructVariantInfo, struct_info: &'static StructVariantInfo,
registration: &'a TypeRegistration, registration: &'a TypeRegistration,
registry: &'a TypeRegistry, registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
} }
impl<'a, 'de> Visitor<'de> for StructVariantVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for StructVariantVisitor<'_, P> {
type Value = DynamicStruct; type Value = DynamicStruct;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -168,24 +163,37 @@ impl<'a, 'de> Visitor<'de> for StructVariantVisitor<'a> {
where where
A: SeqAccess<'de>, A: SeqAccess<'de>,
{ {
visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) visit_struct_seq(
&mut seq,
self.struct_info,
self.registration,
self.registry,
self.processor,
)
} }
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where where
V: MapAccess<'de>, V: MapAccess<'de>,
{ {
visit_struct(&mut map, self.struct_info, self.registration, self.registry) visit_struct(
&mut map,
self.struct_info,
self.registration,
self.registry,
self.processor,
)
} }
} }
struct TupleVariantVisitor<'a> { struct TupleVariantVisitor<'a, P> {
tuple_info: &'static TupleVariantInfo, tuple_info: &'static TupleVariantInfo,
registration: &'a TypeRegistration, registration: &'a TypeRegistration,
registry: &'a TypeRegistry, registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
} }
impl<'a, 'de> Visitor<'de> for TupleVariantVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleVariantVisitor<'_, P> {
type Value = DynamicTuple; type Value = DynamicTuple;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -196,6 +204,12 @@ impl<'a, 'de> Visitor<'de> for TupleVariantVisitor<'a> {
where where
V: SeqAccess<'de>, V: SeqAccess<'de>,
{ {
visit_tuple(&mut seq, self.tuple_info, self.registration, self.registry) visit_tuple(
&mut seq,
self.tuple_info,
self.registration,
self.registry,
self.processor,
)
} }
} }

View file

@ -5,31 +5,25 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{SeqAccess, Visitor}; use serde::de::{SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`List`] values. /// A [`Visitor`] for deserializing [`List`] values.
/// ///
/// [`List`]: crate::List /// [`List`]: crate::List
pub(super) struct ListVisitor<'a> { pub(super) struct ListVisitor<'a, P> {
list_info: &'static ListInfo, pub list_info: &'static ListInfo,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> ListVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for ListVisitor<'_, P> {
pub fn new(list_info: &'static ListInfo, registry: &'a TypeRegistry) -> Self {
Self {
list_info,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for ListVisitor<'a> {
type Value = DynamicList; type Value = DynamicList;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("reflected list value") formatter.write_str("reflected list value")
} }
fn visit_seq<V>(self, mut seq: V) -> Result<Self::Value, V::Error> fn visit_seq<V>(mut self, mut seq: V) -> Result<Self::Value, V::Error>
where where
V: SeqAccess<'de>, V: SeqAccess<'de>,
{ {
@ -38,6 +32,7 @@ impl<'a, 'de> Visitor<'de> for ListVisitor<'a> {
while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal( while let Some(value) = seq.next_element_seed(TypedReflectDeserializer::new_internal(
registration, registration,
self.registry, self.registry,
self.processor.as_deref_mut(),
))? { ))? {
list.push_box(value); list.push_box(value);
} }

View file

@ -5,28 +5,25 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{MapAccess, Visitor}; use serde::de::{MapAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Map`] values. /// A [`Visitor`] for deserializing [`Map`] values.
/// ///
/// [`Map`]: crate::Map /// [`Map`]: crate::Map
pub(super) struct MapVisitor<'a> { pub(super) struct MapVisitor<'a, P> {
map_info: &'static MapInfo, pub map_info: &'static MapInfo,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> MapVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for MapVisitor<'_, P> {
pub fn new(map_info: &'static MapInfo, registry: &'a TypeRegistry) -> Self {
Self { map_info, registry }
}
}
impl<'a, 'de> Visitor<'de> for MapVisitor<'a> {
type Value = DynamicMap; type Value = DynamicMap;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("reflected map value") formatter.write_str("reflected map value")
} }
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> fn visit_map<V>(mut self, mut map: V) -> Result<Self::Value, V::Error>
where where
V: MapAccess<'de>, V: MapAccess<'de>,
{ {
@ -36,10 +33,12 @@ impl<'a, 'de> Visitor<'de> for MapVisitor<'a> {
while let Some(key) = map.next_key_seed(TypedReflectDeserializer::new_internal( while let Some(key) = map.next_key_seed(TypedReflectDeserializer::new_internal(
key_registration, key_registration,
self.registry, self.registry,
self.processor.as_deref_mut(),
))? { ))? {
let value = map.next_value_seed(TypedReflectDeserializer::new_internal( let value = map.next_value_seed(TypedReflectDeserializer::new_internal(
value_registration, value_registration,
self.registry, self.registry,
self.processor.as_deref_mut(),
))?; ))?;
dynamic_map.insert_boxed(key, value); dynamic_map.insert_boxed(key, value);
} }

View file

@ -1,5 +1,6 @@
pub use deserialize_with_registry::*; pub use deserialize_with_registry::*;
pub use deserializer::*; pub use deserializer::*;
pub use processor::*;
pub use registrations::*; pub use registrations::*;
mod arrays; mod arrays;
@ -11,6 +12,7 @@ mod helpers;
mod lists; mod lists;
mod maps; mod maps;
mod options; mod options;
mod processor;
mod registration_utils; mod registration_utils;
mod registrations; mod registrations;
mod sets; mod sets;
@ -24,12 +26,15 @@ mod tuples;
mod tests { mod tests {
use bincode::Options; use bincode::Options;
use core::{any::TypeId, f32::consts::PI, ops::RangeInclusive}; use core::{any::TypeId, f32::consts::PI, ops::RangeInclusive};
use serde::de::IgnoredAny;
use serde::Deserializer;
use serde::{de::DeserializeSeed, Deserialize}; use serde::{de::DeserializeSeed, Deserialize};
use bevy_utils::{HashMap, HashSet}; use bevy_utils::{HashMap, HashSet};
use crate as bevy_reflect; use crate::serde::ReflectDeserializerProcessor;
use crate::{self as bevy_reflect, TypeRegistration};
use crate::{ use crate::{
serde::{ReflectDeserializer, ReflectSerializer, TypedReflectDeserializer}, serde::{ReflectDeserializer, ReflectSerializer, TypedReflectDeserializer},
DynamicEnum, FromReflect, PartialReflect, Reflect, ReflectDeserialize, TypeRegistry, DynamicEnum, FromReflect, PartialReflect, Reflect, ReflectDeserialize, TypeRegistry,
@ -275,15 +280,14 @@ mod tests {
let mut registry = get_registry(); let mut registry = get_registry();
registry.register::<Foo>(); registry.register::<Foo>();
let registration = registry.get(TypeId::of::<Foo>()).unwrap(); let registration = registry.get(TypeId::of::<Foo>()).unwrap();
let reflect_deserializer = TypedReflectDeserializer::new_internal(registration, &registry); let reflect_deserializer = TypedReflectDeserializer::new(registration, &registry);
let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap();
let dynamic_output = reflect_deserializer let dynamic_output = reflect_deserializer
.deserialize(&mut ron_deserializer) .deserialize(&mut ron_deserializer)
.unwrap(); .unwrap();
let output = let output =
<Foo as FromReflect>::from_reflect(dynamic_output.as_ref().as_partial_reflect()) <Foo as FromReflect>::from_reflect(dynamic_output.as_partial_reflect()).unwrap();
.unwrap();
assert_eq!(expected, output); assert_eq!(expected, output);
} }
@ -517,6 +521,243 @@ mod tests {
assert_eq!(error, ron::Error::Message("type `core::ops::RangeInclusive<f32>` did not register the `ReflectDeserialize` type data. For certain types, this may need to be registered manually using `register_type_data`".to_string())); assert_eq!(error, ron::Error::Message("type `core::ops::RangeInclusive<f32>` did not register the `ReflectDeserialize` type data. For certain types, this may need to be registered manually using `register_type_data`".to_string()));
} }
#[test]
fn should_use_processor_for_custom_deserialization() {
#[derive(Reflect, Debug, PartialEq)]
struct Foo {
bar: i32,
qux: i64,
}
struct FooProcessor;
impl ReflectDeserializerProcessor for FooProcessor {
fn try_deserialize<'de, D>(
&mut self,
registration: &TypeRegistration,
_: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: Deserializer<'de>,
{
if registration.type_id() == TypeId::of::<i64>() {
let _ = deserializer.deserialize_ignored_any(IgnoredAny);
Ok(Ok(Box::new(456_i64)))
} else {
Ok(Err(deserializer))
}
}
}
let expected = Foo { bar: 123, qux: 456 };
let input = r#"(
bar: 123,
qux: 123,
)"#;
let mut registry = get_registry();
registry.register::<Foo>();
let registration = registry.get(TypeId::of::<Foo>()).unwrap();
let mut processor = FooProcessor;
let reflect_deserializer =
TypedReflectDeserializer::with_processor(registration, &registry, &mut processor);
let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap();
let dynamic_output = reflect_deserializer
.deserialize(&mut ron_deserializer)
.unwrap();
let output =
<Foo as FromReflect>::from_reflect(dynamic_output.as_partial_reflect()).unwrap();
assert_eq!(expected, output);
}
#[test]
fn should_use_processor_for_multiple_registrations() {
#[derive(Reflect, Debug, PartialEq)]
struct Foo {
bar: i32,
qux: i64,
}
struct FooProcessor;
impl ReflectDeserializerProcessor for FooProcessor {
fn try_deserialize<'de, D>(
&mut self,
registration: &TypeRegistration,
_: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: Deserializer<'de>,
{
if registration.type_id() == TypeId::of::<i32>() {
let _ = deserializer.deserialize_ignored_any(IgnoredAny);
Ok(Ok(Box::new(123_i32)))
} else if registration.type_id() == TypeId::of::<i64>() {
let _ = deserializer.deserialize_ignored_any(IgnoredAny);
Ok(Ok(Box::new(456_i64)))
} else {
Ok(Err(deserializer))
}
}
}
let expected = Foo { bar: 123, qux: 456 };
let input = r#"(
bar: 0,
qux: 0,
)"#;
let mut registry = get_registry();
registry.register::<Foo>();
let registration = registry.get(TypeId::of::<Foo>()).unwrap();
let mut processor = FooProcessor;
let reflect_deserializer =
TypedReflectDeserializer::with_processor(registration, &registry, &mut processor);
let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap();
let dynamic_output = reflect_deserializer
.deserialize(&mut ron_deserializer)
.unwrap();
let output =
<Foo as FromReflect>::from_reflect(dynamic_output.as_partial_reflect()).unwrap();
assert_eq!(expected, output);
}
#[test]
fn should_propagate_processor_deserialize_error() {
struct ErroringProcessor;
impl ReflectDeserializerProcessor for ErroringProcessor {
fn try_deserialize<'de, D>(
&mut self,
registration: &TypeRegistration,
_: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: Deserializer<'de>,
{
if registration.type_id() == TypeId::of::<i32>() {
Err(serde::de::Error::custom("my custom deserialize error"))
} else {
Ok(Err(deserializer))
}
}
}
let registry = get_registry();
let input = r#"{"i32":123}"#;
let mut deserializer = ron::de::Deserializer::from_str(input).unwrap();
let mut processor = ErroringProcessor;
let reflect_deserializer = ReflectDeserializer::with_processor(&registry, &mut processor);
let error = reflect_deserializer
.deserialize(&mut deserializer)
.unwrap_err();
#[cfg(feature = "debug_stack")]
assert_eq!(
error,
ron::Error::Message("my custom deserialize error (stack: `i32`)".to_string())
);
#[cfg(not(feature = "debug_stack"))]
assert_eq!(
error,
ron::Error::Message("my custom deserialize error".to_string())
);
}
#[test]
fn should_access_local_scope_in_processor() {
struct ValueCountingProcessor<'a> {
values_found: &'a mut usize,
}
impl ReflectDeserializerProcessor for ValueCountingProcessor<'_> {
fn try_deserialize<'de, D>(
&mut self,
_: &TypeRegistration,
_: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: Deserializer<'de>,
{
let _ = deserializer.deserialize_ignored_any(IgnoredAny)?;
*self.values_found += 1;
Ok(Ok(Box::new(123_i32)))
}
}
let registry = get_registry();
let input = r#"{"i32":0}"#;
let mut values_found = 0_usize;
let mut deserializer_processor = ValueCountingProcessor {
values_found: &mut values_found,
};
let mut deserializer = ron::de::Deserializer::from_str(input).unwrap();
let reflect_deserializer =
ReflectDeserializer::with_processor(&registry, &mut deserializer_processor);
reflect_deserializer.deserialize(&mut deserializer).unwrap();
assert_eq!(1, values_found);
}
#[test]
fn should_fail_from_reflect_if_processor_returns_wrong_typed_value() {
#[derive(Reflect, Debug, PartialEq)]
struct Foo {
bar: i32,
qux: i64,
}
struct WrongTypeProcessor;
impl ReflectDeserializerProcessor for WrongTypeProcessor {
fn try_deserialize<'de, D>(
&mut self,
registration: &TypeRegistration,
_registry: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: Deserializer<'de>,
{
if registration.type_id() == TypeId::of::<i32>() {
let _ = deserializer.deserialize_ignored_any(IgnoredAny);
Ok(Ok(Box::new(42_i64)))
} else {
Ok(Err(deserializer))
}
}
}
let input = r#"(
bar: 123,
qux: 123,
)"#;
let mut registry = get_registry();
registry.register::<Foo>();
let registration = registry.get(TypeId::of::<Foo>()).unwrap();
let mut processor = WrongTypeProcessor;
let reflect_deserializer =
TypedReflectDeserializer::with_processor(registration, &registry, &mut processor);
let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap();
let dynamic_output = reflect_deserializer
.deserialize(&mut ron_deserializer)
.unwrap();
assert!(<Foo as FromReflect>::from_reflect(dynamic_output.as_partial_reflect()).is_none());
}
#[cfg(feature = "functions")] #[cfg(feature = "functions")]
mod functions { mod functions {
use super::*; use super::*;

View file

@ -8,22 +8,16 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{DeserializeSeed, Error, Visitor}; use serde::de::{DeserializeSeed, Error, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Option`] values. /// A [`Visitor`] for deserializing [`Option`] values.
pub(super) struct OptionVisitor<'a> { pub(super) struct OptionVisitor<'a, P> {
enum_info: &'static EnumInfo, pub enum_info: &'static EnumInfo,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> OptionVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for OptionVisitor<'_, P> {
pub fn new(enum_info: &'static EnumInfo, registry: &'a TypeRegistry) -> Self {
Self {
enum_info,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for OptionVisitor<'a> {
type Value = DynamicEnum; type Value = DynamicEnum;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -49,7 +43,11 @@ impl<'a, 'de> Visitor<'de> for OptionVisitor<'a> {
VariantInfo::Tuple(tuple_info) if tuple_info.field_len() == 1 => { VariantInfo::Tuple(tuple_info) if tuple_info.field_len() == 1 => {
let field = tuple_info.field_at(0).unwrap(); let field = tuple_info.field_at(0).unwrap();
let registration = try_get_registration(*field.ty(), self.registry)?; let registration = try_get_registration(*field.ty(), self.registry)?;
let de = TypedReflectDeserializer::new_internal(registration, self.registry); let de = TypedReflectDeserializer::new_internal(
registration,
self.registry,
self.processor,
);
let mut value = DynamicTuple::default(); let mut value = DynamicTuple::default();
value.insert_boxed(de.deserialize(deserializer)?); value.insert_boxed(de.deserialize(deserializer)?);
let mut option = DynamicEnum::default(); let mut option = DynamicEnum::default();

View file

@ -0,0 +1,213 @@
use crate::{PartialReflect, TypeRegistration, TypeRegistry};
/// Allows overriding the default deserialization behavior of
/// [`ReflectDeserializer`] and [`TypedReflectDeserializer`] for specific
/// [`TypeRegistration`]s.
///
/// When deserializing a reflected value, you may want to override the default
/// behavior and use your own logic for deserialization. This logic may also
/// be context-dependent, and only apply for a single use of your
/// [`ReflectDeserializer`]. To achieve this, you can create a processor and
/// pass it in to your deserializer.
///
/// Whenever the deserializer attempts to deserialize a value, it will first
/// call [`try_deserialize`] on your processor, which may take ownership of the
/// deserializer and give back a [`Box<dyn PartialReflect>`], or return
/// ownership of the deserializer back, and continue with the default logic.
///
/// # Compared to [`DeserializeWithRegistry`]
///
/// [`DeserializeWithRegistry`] allows you to define how your type will be
/// deserialized by a [`TypedReflectDeserializer`], given the extra context of
/// the [`TypeRegistry`]. If your type can be deserialized entirely from that,
/// then you should prefer implementing that trait instead of using a processor.
///
/// However, you may need more context-dependent data which is only present in
/// the scope where you create the [`TypedReflectDeserializer`]. For example, in
/// an asset loader, the `&mut LoadContext` you get is only valid from within
/// the `load` function. This is where a processor is useful, as the processor
/// can capture local variables.
///
/// A [`ReflectDeserializerProcessor`] always takes priority over a
/// [`DeserializeWithRegistry`] implementation, so this is also useful for
/// overriding deserialization behavior if you need to do something custom.
///
/// # Examples
///
/// Deserializing a reflected value in an asset loader, and replacing asset
/// handles with a loaded equivalent:
///
/// ```
/// # use bevy_reflect::serde::{ReflectDeserializer, ReflectDeserializerProcessor};
/// # use bevy_reflect::{PartialReflect, Reflect, TypeData, TypeRegistration, TypeRegistry};
/// # use serde::de::{DeserializeSeed, Deserializer, Visitor};
/// # use std::marker::PhantomData;
/// #
/// # #[derive(Debug, Clone, Reflect)]
/// # struct LoadedUntypedAsset;
/// # #[derive(Debug, Clone, Reflect)]
/// # struct Handle<T: Reflect>(T);
/// # #[derive(Debug, Clone, Reflect)]
/// # struct Mesh;
/// #
/// # struct LoadContext;
/// # impl LoadContext {
/// # fn load(&mut self) -> &mut Self { unimplemented!() }
/// # fn with_asset_type_id(&mut self, (): ()) -> &mut Self { unimplemented!() }
/// # fn untyped(&mut self) -> &mut Self { unimplemented!() }
/// # fn load_asset(&mut self, (): ()) -> Handle<LoadedUntypedAsset> { unimplemented!() }
/// # }
/// #
/// # struct ReflectHandle;
/// # impl TypeData for ReflectHandle {
/// # fn clone_type_data(&self) -> Box<dyn TypeData> {
/// # unimplemented!()
/// # }
/// # }
/// # impl ReflectHandle {
/// # fn asset_type_id(&self) {
/// # unimplemented!()
/// # }
/// # }
/// #
/// # struct AssetPathVisitor;
/// # impl<'de> Visitor<'de> for AssetPathVisitor {
/// # type Value = ();
/// # fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result { unimplemented!() }
/// # }
/// # type AssetError = Box<dyn core::error::Error>;
/// #[derive(Debug, Clone, Reflect)]
/// struct MyAsset {
/// name: String,
/// mesh: Handle<Mesh>,
/// }
///
/// fn load(
/// asset_bytes: &[u8],
/// type_registry: &TypeRegistry,
/// load_context: &mut LoadContext,
/// ) -> Result<MyAsset, AssetError> {
/// struct HandleProcessor<'a> {
/// load_context: &'a mut LoadContext,
/// }
///
/// impl ReflectDeserializerProcessor for HandleProcessor<'_> {
/// fn try_deserialize<'de, D>(
/// &mut self,
/// registration: &TypeRegistration,
/// _registry: &TypeRegistry,
/// deserializer: D,
/// ) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
/// where
/// D: Deserializer<'de>,
/// {
/// let Some(reflect_handle) = registration.data::<ReflectHandle>() else {
/// // we don't want to deserialize this - give the deserializer back
/// return Ok(Err(deserializer));
/// };
///
/// let asset_type_id = reflect_handle.asset_type_id();
/// let asset_path = deserializer.deserialize_str(AssetPathVisitor)?;
///
/// let handle: Handle<LoadedUntypedAsset> = self.load_context
/// .load()
/// .with_asset_type_id(asset_type_id)
/// .untyped()
/// .load_asset(asset_path);
/// # let _: Result<_, ()> = {
/// Ok(Box::new(handle))
/// # };
/// # unimplemented!()
/// }
/// }
///
/// let mut ron_deserializer = ron::Deserializer::from_bytes(asset_bytes)?;
/// let mut processor = HandleProcessor { load_context };
/// let reflect_deserializer =
/// ReflectDeserializer::with_processor(type_registry, &mut processor);
/// let asset = reflect_deserializer.deserialize(&mut ron_deserializer)?;
/// # unimplemented!()
/// }
/// ```
///
/// [`ReflectDeserializer`]: crate::serde::ReflectDeserializer
/// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer
/// [`try_deserialize`]: Self::try_deserialize
/// [`DeserializeWithRegistry`]: crate::serde::DeserializeWithRegistry
pub trait ReflectDeserializerProcessor {
/// Attempts to deserialize the value which a [`TypedReflectDeserializer`]
/// is currently looking at, and knows the type of.
///
/// If you've read the `registration` and want to override the default
/// deserialization, return `Ok(Ok(value))` with the boxed reflected value
/// that you want to assign this value to. The type inside the box must
/// be the same one as the `registration` is for, otherwise future
/// reflection operations (such as using [`FromReflect`] to convert the
/// resulting [`Box<dyn PartialReflect>`] into a concrete type) will fail.
///
/// If you don't want to override the deserialization, return ownership of
/// the deserializer back via `Ok(Err(deserializer))`.
///
/// Note that, if you do want to return a value, you *must* read from the
/// deserializer passed to this function (you are free to ignore the result
/// though). Otherwise, the deserializer will be in an inconsistent state,
/// and future value parsing will fail.
///
/// # Examples
///
/// Correct way to return a constant value (not using any output from the
/// deserializer):
///
/// ```
/// # use bevy_reflect::{TypeRegistration, PartialReflect, TypeRegistry};
/// # use bevy_reflect::serde::ReflectDeserializerProcessor;
/// # use core::any::TypeId;
/// use serde::de::IgnoredAny;
///
/// struct ConstantI32Processor;
///
/// impl ReflectDeserializerProcessor for ConstantI32Processor {
/// fn try_deserialize<'de, D>(
/// &mut self,
/// registration: &TypeRegistration,
/// _registry: &TypeRegistry,
/// deserializer: D,
/// ) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
/// where
/// D: serde::Deserializer<'de>
/// {
/// if registration.type_id() == TypeId::of::<i32>() {
/// _ = deserializer.deserialize_ignored_any(IgnoredAny);
/// Ok(Ok(Box::new(42_i32)))
/// } else {
/// Ok(Err(deserializer))
/// }
/// }
/// }
/// ```
///
/// [`TypedReflectDeserializer`]: crate::serde::TypedReflectDeserializer
/// [`FromReflect`]: crate::FromReflect
fn try_deserialize<'de, D>(
&mut self,
registration: &TypeRegistration,
registry: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: serde::Deserializer<'de>;
}
impl ReflectDeserializerProcessor for () {
fn try_deserialize<'de, D>(
&mut self,
_registration: &TypeRegistration,
_registry: &TypeRegistry,
deserializer: D,
) -> Result<Result<Box<dyn PartialReflect>, D>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(Err(deserializer))
}
}

View file

@ -5,28 +5,25 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{SeqAccess, Visitor}; use serde::de::{SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Set`] values. /// A [`Visitor`] for deserializing [`Set`] values.
/// ///
/// [`Set`]: crate::Set /// [`Set`]: crate::Set
pub(super) struct SetVisitor<'a> { pub(super) struct SetVisitor<'a, P> {
set_info: &'static SetInfo, pub set_info: &'static SetInfo,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> SetVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for SetVisitor<'_, P> {
pub fn new(set_info: &'static SetInfo, registry: &'a TypeRegistry) -> Self {
Self { set_info, registry }
}
}
impl<'a, 'de> Visitor<'de> for SetVisitor<'a> {
type Value = DynamicSet; type Value = DynamicSet;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
formatter.write_str("reflected set value") formatter.write_str("reflected set value")
} }
fn visit_seq<V>(self, mut set: V) -> Result<Self::Value, V::Error> fn visit_seq<V>(mut self, mut set: V) -> Result<Self::Value, V::Error>
where where
V: SeqAccess<'de>, V: SeqAccess<'de>,
{ {
@ -35,6 +32,7 @@ impl<'a, 'de> Visitor<'de> for SetVisitor<'a> {
while let Some(value) = set.next_element_seed(TypedReflectDeserializer::new_internal( while let Some(value) = set.next_element_seed(TypedReflectDeserializer::new_internal(
value_registration, value_registration,
self.registry, self.registry,
self.processor.as_deref_mut(),
))? { ))? {
dynamic_set.insert_boxed(value); dynamic_set.insert_boxed(value);
} }

View file

@ -12,6 +12,8 @@ use crate::{
use core::slice::Iter; use core::slice::Iter;
use serde::de::{Error, MapAccess, SeqAccess}; use serde::de::{Error, MapAccess, SeqAccess};
use super::ReflectDeserializerProcessor;
/// A helper trait for accessing type information from struct-like types. /// A helper trait for accessing type information from struct-like types.
pub(super) trait StructLikeInfo { pub(super) trait StructLikeInfo {
fn field<E: Error>(&self, name: &str) -> Result<&NamedField, E>; fn field<E: Error>(&self, name: &str) -> Result<&NamedField, E>;
@ -83,15 +85,17 @@ impl StructLikeInfo for StructVariantInfo {
/// Deserializes a [struct-like] type from a mapping of fields, returning a [`DynamicStruct`]. /// Deserializes a [struct-like] type from a mapping of fields, returning a [`DynamicStruct`].
/// ///
/// [struct-like]: StructLikeInfo /// [struct-like]: StructLikeInfo
pub(super) fn visit_struct<'de, T, V>( pub(super) fn visit_struct<'de, T, V, P>(
map: &mut V, map: &mut V,
info: &'static T, info: &'static T,
registration: &TypeRegistration, registration: &TypeRegistration,
registry: &TypeRegistry, registry: &TypeRegistry,
mut processor: Option<&mut P>,
) -> Result<DynamicStruct, V::Error> ) -> Result<DynamicStruct, V::Error>
where where
T: StructLikeInfo, T: StructLikeInfo,
V: MapAccess<'de>, V: MapAccess<'de>,
P: ReflectDeserializerProcessor,
{ {
let mut dynamic_struct = DynamicStruct::default(); let mut dynamic_struct = DynamicStruct::default();
while let Some(Ident(key)) = map.next_key::<Ident>()? { while let Some(Ident(key)) = map.next_key::<Ident>()? {
@ -107,6 +111,7 @@ where
let value = map.next_value_seed(TypedReflectDeserializer::new_internal( let value = map.next_value_seed(TypedReflectDeserializer::new_internal(
registration, registration,
registry, registry,
processor.as_deref_mut(),
))?; ))?;
dynamic_struct.insert_boxed(&key, value); dynamic_struct.insert_boxed(&key, value);
} }
@ -129,15 +134,17 @@ where
/// Deserializes a [struct-like] type from a sequence of fields, returning a [`DynamicStruct`]. /// Deserializes a [struct-like] type from a sequence of fields, returning a [`DynamicStruct`].
/// ///
/// [struct-like]: StructLikeInfo /// [struct-like]: StructLikeInfo
pub(super) fn visit_struct_seq<'de, T, V>( pub(super) fn visit_struct_seq<'de, T, V, P>(
seq: &mut V, seq: &mut V,
info: &T, info: &T,
registration: &TypeRegistration, registration: &TypeRegistration,
registry: &TypeRegistry, registry: &TypeRegistry,
mut processor: Option<&mut P>,
) -> Result<DynamicStruct, V::Error> ) -> Result<DynamicStruct, V::Error>
where where
T: StructLikeInfo, T: StructLikeInfo,
V: SeqAccess<'de>, V: SeqAccess<'de>,
P: ReflectDeserializerProcessor,
{ {
let mut dynamic_struct = DynamicStruct::default(); let mut dynamic_struct = DynamicStruct::default();
@ -167,6 +174,7 @@ where
.next_element_seed(TypedReflectDeserializer::new_internal( .next_element_seed(TypedReflectDeserializer::new_internal(
try_get_registration(*info.field_at(index)?.ty(), registry)?, try_get_registration(*info.field_at(index)?.ty(), registry)?,
registry, registry,
processor.as_deref_mut(),
))? ))?
.ok_or_else(|| Error::invalid_length(index, &len.to_string().as_str()))?; .ok_or_else(|| Error::invalid_length(index, &len.to_string().as_str()))?;
dynamic_struct.insert_boxed(name, value); dynamic_struct.insert_boxed(name, value);

View file

@ -5,30 +5,19 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{MapAccess, SeqAccess, Visitor}; use serde::de::{MapAccess, SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Struct`] values. /// A [`Visitor`] for deserializing [`Struct`] values.
/// ///
/// [`Struct`]: crate::Struct /// [`Struct`]: crate::Struct
pub(super) struct StructVisitor<'a> { pub(super) struct StructVisitor<'a, P> {
struct_info: &'static StructInfo, pub struct_info: &'static StructInfo,
registration: &'a TypeRegistration, pub registration: &'a TypeRegistration,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> StructVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for StructVisitor<'_, P> {
pub fn new(
struct_info: &'static StructInfo,
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
) -> Self {
Self {
struct_info,
registration,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for StructVisitor<'a> {
type Value = DynamicStruct; type Value = DynamicStruct;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -39,13 +28,25 @@ impl<'a, 'de> Visitor<'de> for StructVisitor<'a> {
where where
A: SeqAccess<'de>, A: SeqAccess<'de>,
{ {
visit_struct_seq(&mut seq, self.struct_info, self.registration, self.registry) visit_struct_seq(
&mut seq,
self.struct_info,
self.registration,
self.registry,
self.processor,
)
} }
fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error> fn visit_map<V>(self, mut map: V) -> Result<Self::Value, V::Error>
where where
V: MapAccess<'de>, V: MapAccess<'de>,
{ {
visit_struct(&mut map, self.struct_info, self.registration, self.registry) visit_struct(
&mut map,
self.struct_info,
self.registration,
self.registry,
self.processor,
)
} }
} }

View file

@ -7,30 +7,19 @@ use serde::de::{DeserializeSeed, SeqAccess, Visitor};
use super::{registration_utils::try_get_registration, TypedReflectDeserializer}; use super::{registration_utils::try_get_registration, TypedReflectDeserializer};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`TupleStruct`] values. /// A [`Visitor`] for deserializing [`TupleStruct`] values.
/// ///
/// [`TupleStruct`]: crate::TupleStruct /// [`TupleStruct`]: crate::TupleStruct
pub(super) struct TupleStructVisitor<'a> { pub(super) struct TupleStructVisitor<'a, P> {
tuple_struct_info: &'static TupleStructInfo, pub tuple_struct_info: &'static TupleStructInfo,
registration: &'a TypeRegistration, pub registration: &'a TypeRegistration,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> TupleStructVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleStructVisitor<'_, P> {
pub fn new(
tuple_struct_info: &'static TupleStructInfo,
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
) -> Self {
Self {
tuple_struct_info,
registration,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for TupleStructVisitor<'a> {
type Value = DynamicTupleStruct; type Value = DynamicTupleStruct;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -46,6 +35,7 @@ impl<'a, 'de> Visitor<'de> for TupleStructVisitor<'a> {
self.tuple_struct_info, self.tuple_struct_info,
self.registration, self.registration,
self.registry, self.registry,
self.processor,
) )
.map(DynamicTupleStruct::from) .map(DynamicTupleStruct::from)
} }
@ -71,7 +61,7 @@ impl<'a, 'de> Visitor<'de> for TupleStructVisitor<'a> {
self.registry, self.registry,
)?; )?;
let reflect_deserializer = let reflect_deserializer =
TypedReflectDeserializer::new_internal(registration, self.registry); TypedReflectDeserializer::new_internal(registration, self.registry, self.processor);
let value = reflect_deserializer.deserialize(deserializer)?; let value = reflect_deserializer.deserialize(deserializer)?;
tuple.insert_boxed(value.into_partial_reflect()); tuple.insert_boxed(value.into_partial_reflect());

View file

@ -8,6 +8,8 @@ use crate::{
}; };
use serde::de::{Error, SeqAccess}; use serde::de::{Error, SeqAccess};
use super::ReflectDeserializerProcessor;
pub(super) trait TupleLikeInfo { pub(super) trait TupleLikeInfo {
fn field_at<E: Error>(&self, index: usize) -> Result<&UnnamedField, E>; fn field_at<E: Error>(&self, index: usize) -> Result<&UnnamedField, E>;
fn field_len(&self) -> usize; fn field_len(&self) -> usize;
@ -64,15 +66,17 @@ impl TupleLikeInfo for TupleVariantInfo {
/// Deserializes a [tuple-like] type from a sequence of elements, returning a [`DynamicTuple`]. /// Deserializes a [tuple-like] type from a sequence of elements, returning a [`DynamicTuple`].
/// ///
/// [tuple-like]: TupleLikeInfo /// [tuple-like]: TupleLikeInfo
pub(super) fn visit_tuple<'de, T, V>( pub(super) fn visit_tuple<'de, T, V, P>(
seq: &mut V, seq: &mut V,
info: &T, info: &T,
registration: &TypeRegistration, registration: &TypeRegistration,
registry: &TypeRegistry, registry: &TypeRegistry,
mut processor: Option<&mut P>,
) -> Result<DynamicTuple, V::Error> ) -> Result<DynamicTuple, V::Error>
where where
T: TupleLikeInfo, T: TupleLikeInfo,
V: SeqAccess<'de>, V: SeqAccess<'de>,
P: ReflectDeserializerProcessor,
{ {
let mut tuple = DynamicTuple::default(); let mut tuple = DynamicTuple::default();
@ -95,6 +99,7 @@ where
.next_element_seed(TypedReflectDeserializer::new_internal( .next_element_seed(TypedReflectDeserializer::new_internal(
try_get_registration(*info.field_at(index)?.ty(), registry)?, try_get_registration(*info.field_at(index)?.ty(), registry)?,
registry, registry,
processor.as_deref_mut(),
))? ))?
.ok_or_else(|| Error::invalid_length(index, &len.to_string().as_str()))?; .ok_or_else(|| Error::invalid_length(index, &len.to_string().as_str()))?;
tuple.insert_boxed(value); tuple.insert_boxed(value);

View file

@ -4,30 +4,19 @@ use crate::{
use core::{fmt, fmt::Formatter}; use core::{fmt, fmt::Formatter};
use serde::de::{SeqAccess, Visitor}; use serde::de::{SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Tuple`] values. /// A [`Visitor`] for deserializing [`Tuple`] values.
/// ///
/// [`Tuple`]: crate::Tuple /// [`Tuple`]: crate::Tuple
pub(super) struct TupleVisitor<'a> { pub(super) struct TupleVisitor<'a, P> {
tuple_info: &'static TupleInfo, pub tuple_info: &'static TupleInfo,
registration: &'a TypeRegistration, pub registration: &'a TypeRegistration,
registry: &'a TypeRegistry, pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
} }
impl<'a> TupleVisitor<'a> { impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleVisitor<'_, P> {
pub fn new(
tuple_info: &'static TupleInfo,
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
) -> Self {
Self {
tuple_info,
registration,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for TupleVisitor<'a> {
type Value = DynamicTuple; type Value = DynamicTuple;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result { fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -38,6 +27,12 @@ impl<'a, 'de> Visitor<'de> for TupleVisitor<'a> {
where where
V: SeqAccess<'de>, V: SeqAccess<'de>,
{ {
visit_tuple(&mut seq, self.tuple_info, self.registration, self.registry) visit_tuple(
&mut seq,
self.tuple_info,
self.registration,
self.registry,
self.processor,
)
} }
} }

View file

@ -305,7 +305,7 @@ use bevy_app::{prelude::*, MainScheduleOrder};
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, entity::Entity,
schedule::{IntoSystemConfigs, ScheduleLabel}, schedule::{IntoSystemConfigs, IntoSystemSetConfigs, ScheduleLabel, SystemSet},
system::{Commands, In, IntoSystem, ResMut, Resource, System, SystemId}, system::{Commands, In, IntoSystem, ResMut, Resource, System, SystemId},
world::World, world::World,
}; };
@ -442,21 +442,36 @@ impl Plugin for RemotePlugin {
app.insert_resource(remote_methods) app.insert_resource(remote_methods)
.init_resource::<RemoteWatchingRequests>() .init_resource::<RemoteWatchingRequests>()
.add_systems(PreStartup, setup_mailbox_channel) .add_systems(PreStartup, setup_mailbox_channel)
.configure_sets(
RemoteLast,
(RemoteSet::ProcessRequests, RemoteSet::Cleanup).chain(),
)
.add_systems( .add_systems(
RemoteLast, RemoteLast,
( (
process_remote_requests, (process_remote_requests, process_ongoing_watching_requests)
process_ongoing_watching_requests, .chain()
remove_closed_watching_requests, .in_set(RemoteSet::ProcessRequests),
) remove_closed_watching_requests.in_set(RemoteSet::Cleanup),
.chain(), ),
); );
} }
} }
/// Schedule that contains all systems to process Bevy Remote Protocol requests /// Schedule that contains all systems to process Bevy Remote Protocol requests
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)] #[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
struct RemoteLast; pub struct RemoteLast;
/// The systems sets of the [`RemoteLast`] schedule.
///
/// These can be useful for ordering.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum RemoteSet {
/// Processing of remote requests.
ProcessRequests,
/// Cleanup (remove closed watchers etc)
Cleanup,
}
/// A type to hold the allowed types of systems to be used as method handlers. /// A type to hold the allowed types of systems to be used as method handlers.
#[derive(Debug)] #[derive(Debug)]

View file

@ -1,6 +1,4 @@
use super::{ClearColorConfig, Projection}; use super::{ClearColorConfig, Projection};
use crate::sync_world::TemporaryRenderEntity;
use crate::view::RenderVisibleEntities;
use crate::{ use crate::{
batching::gpu_preprocessing::GpuPreprocessingSupport, batching::gpu_preprocessing::GpuPreprocessingSupport,
camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews}, camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews},
@ -8,11 +6,12 @@ use crate::{
render_asset::RenderAssets, render_asset::RenderAssets,
render_graph::{InternedRenderSubGraph, RenderSubGraph}, render_graph::{InternedRenderSubGraph, RenderSubGraph},
render_resource::TextureView, render_resource::TextureView,
sync_world::TemporaryRenderEntity,
sync_world::{RenderEntity, SyncToRenderWorld}, sync_world::{RenderEntity, SyncToRenderWorld},
texture::GpuImage, texture::GpuImage,
view::{ view::{
ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers, Visibility, ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers,
VisibleEntities, RenderVisibleEntities, ViewUniformOffset, Visibility, VisibleEntities,
}, },
Extract, Extract,
}; };
@ -441,7 +440,10 @@ impl Camera {
#[inline] #[inline]
pub fn target_scaling_factor(&self) -> Option<f32> { pub fn target_scaling_factor(&self) -> Option<f32> {
self.computed.target_info.as_ref().map(|t| t.scale_factor) self.computed
.target_info
.as_ref()
.map(|t: &RenderTargetInfo| t.scale_factor)
} }
/// The projection matrix computed using this camera's [`CameraProjection`]. /// The projection matrix computed using this camera's [`CameraProjection`].
@ -1062,6 +1064,7 @@ pub fn extract_cameras(
RenderLayers, RenderLayers,
Projection, Projection,
GpuCulling, GpuCulling,
ViewUniformOffset,
)>(); )>();
continue; continue;
} }

View file

@ -80,6 +80,7 @@ impl ColorAttachment {
} }
/// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`]. /// A wrapper for a [`TextureView`] that is used as a depth-only [`RenderPassDepthStencilAttachment`].
#[derive(Clone)]
pub struct DepthAttachment { pub struct DepthAttachment {
pub view: TextureView, pub view: TextureView,
clear_value: Option<f32>, clear_value: Option<f32>,

View file

@ -71,8 +71,13 @@ pub fn sprite_picking(
continue; continue;
}; };
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position) let viewport_pos = camera
else { .logical_viewport_rect()
.map(|v| v.min)
.unwrap_or_default();
let pos_in_viewport = location.position - viewport_pos;
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, pos_in_viewport) else {
continue; continue;
}; };
let cursor_ray_len = cam_ortho.far - cam_ortho.near; let cursor_ray_len = cam_ortho.far - cam_ortho.near;

View file

@ -46,6 +46,16 @@ impl TaskPoolBuilder {
self self
} }
/// No op on the single threaded task pool
pub fn on_thread_spawn(self, _f: impl Fn() + Send + Sync + 'static) -> Self {
self
}
/// No op on the single threaded task pool
pub fn on_thread_destroy(self, _f: impl Fn() + Send + Sync + 'static) -> Self {
self
}
/// Creates a new [`TaskPool`] /// Creates a new [`TaskPool`]
pub fn build(self) -> TaskPool { pub fn build(self) -> TaskPool {
TaskPool::new_internal() TaskPool::new_internal()

View file

@ -11,7 +11,6 @@ use bevy_ecs::component::Component;
use bevy_ecs::{ use bevy_ecs::{
change_detection::{DetectChanges, Ref}, change_detection::{DetectChanges, Ref},
entity::Entity, entity::Entity,
event::EventReader,
prelude::{ReflectComponent, With}, prelude::{ReflectComponent, With},
query::{Changed, Without}, query::{Changed, Without},
system::{Commands, Local, Query, Res, ResMut}, system::{Commands, Local, Query, Res, ResMut},
@ -30,7 +29,7 @@ use bevy_sprite::{Anchor, ExtractedSprite, ExtractedSprites, SpriteSource, Textu
use bevy_transform::components::Transform; use bevy_transform::components::Transform;
use bevy_transform::prelude::GlobalTransform; use bevy_transform::prelude::GlobalTransform;
use bevy_utils::HashSet; use bevy_utils::HashSet;
use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged}; use bevy_window::{PrimaryWindow, Window};
/// [`Text2dBundle`] was removed in favor of required components. /// [`Text2dBundle`] was removed in favor of required components.
/// The core component is now [`Text2d`] which can contain a single text segment. /// The core component is now [`Text2d`] which can contain a single text segment.
@ -235,12 +234,12 @@ pub fn extract_text2d_sprite(
/// It does not modify or observe existing ones. /// It does not modify or observe existing ones.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn update_text2d_layout( pub fn update_text2d_layout(
mut last_scale_factor: Local<f32>,
// Text items which should be reprocessed again, generally when the font hasn't loaded yet. // Text items which should be reprocessed again, generally when the font hasn't loaded yet.
mut queue: Local<HashSet<Entity>>, mut queue: Local<HashSet<Entity>>,
mut textures: ResMut<Assets<Image>>, mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>, fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>, windows: Query<&Window, With<PrimaryWindow>>,
mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>, mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_sets: ResMut<FontAtlasSets>, mut font_atlas_sets: ResMut<FontAtlasSets>,
mut text_pipeline: ResMut<TextPipeline>, mut text_pipeline: ResMut<TextPipeline>,
@ -255,9 +254,6 @@ pub fn update_text2d_layout(
mut font_system: ResMut<CosmicFontSystem>, mut font_system: ResMut<CosmicFontSystem>,
mut swash_cache: ResMut<SwashCache>, mut swash_cache: ResMut<SwashCache>,
) { ) {
// We need to consume the entire iterator, hence `last`
let factor_changed = scale_factor_changed.read().last().is_some();
// TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621 // TODO: Support window-independent scaling: https://github.com/bevyengine/bevy/issues/5621
let scale_factor = windows let scale_factor = windows
.get_single() .get_single()
@ -266,6 +262,9 @@ pub fn update_text2d_layout(
let inverse_scale_factor = scale_factor.recip(); let inverse_scale_factor = scale_factor.recip();
let factor_changed = *last_scale_factor != scale_factor;
*last_scale_factor = scale_factor;
for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query { for (entity, block, bounds, text_layout_info, mut computed) in &mut text_query {
if factor_changed if factor_changed
|| computed.needs_rerender() || computed.needs_rerender()
@ -359,7 +358,7 @@ mod tests {
use bevy_app::{App, Update}; use bevy_app::{App, Update};
use bevy_asset::{load_internal_binary_asset, Handle}; use bevy_asset::{load_internal_binary_asset, Handle};
use bevy_ecs::{event::Events, schedule::IntoSystemConfigs}; use bevy_ecs::schedule::IntoSystemConfigs;
use crate::{detect_text_needs_rerender, TextIterScratch}; use crate::{detect_text_needs_rerender, TextIterScratch};
@ -374,7 +373,6 @@ mod tests {
.init_resource::<Assets<Image>>() .init_resource::<Assets<Image>>()
.init_resource::<Assets<TextureAtlasLayout>>() .init_resource::<Assets<TextureAtlasLayout>>()
.init_resource::<FontAtlasSets>() .init_resource::<FontAtlasSets>()
.init_resource::<Events<WindowScaleFactorChanged>>()
.init_resource::<TextPipeline>() .init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>() .init_resource::<CosmicFontSystem>()
.init_resource::<SwashCache>() .init_resource::<SwashCache>()

View file

@ -27,7 +27,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", features = [
bevy_render = { path = "../bevy_render", version = "0.15.0-dev" } bevy_render = { path = "../bevy_render", version = "0.15.0-dev" }
bevy_animation = { path = "../bevy_animation", version = "0.15.0-dev" } bevy_animation = { path = "../bevy_animation", version = "0.15.0-dev" }
bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" } bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.15.0-dev", optional = true } bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true } bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true }
bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" } bevy_transform = { path = "../bevy_transform", version = "0.15.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.15.0-dev" } bevy_window = { path = "../bevy_window", version = "0.15.0-dev" }

View file

@ -278,7 +278,12 @@ with UI components as a child of an entity without UI components, your UI layout
let text_buffers = &mut buffer_query; let text_buffers = &mut buffer_query;
// clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used) // clean up removed nodes after syncing children to avoid potential panic (invalid SlotMap key used)
ui_surface.remove_entities(removed_components.removed_nodes.read()); ui_surface.remove_entities(
removed_components
.removed_nodes
.read()
.filter(|entity| !node_query.contains(*entity)),
);
// Re-sync changed children: avoid layout glitches caused by removed nodes that are still set as a child of another node // Re-sync changed children: avoid layout glitches caused by removed nodes that are still set as a child of another node
computed_node_query.iter().for_each(|(entity, _)| { computed_node_query.iter().for_each(|(entity, _)| {
@ -771,6 +776,38 @@ mod tests {
assert!(ui_surface.entity_to_taffy.is_empty()); assert!(ui_surface.entity_to_taffy.is_empty());
} }
/// bugfix test, see [#16288](https://github.com/bevyengine/bevy/pull/16288)
#[test]
fn node_removal_and_reinsert_should_work() {
let (mut world, mut ui_schedule) = setup_ui_test_world();
ui_schedule.run(&mut world);
// no UI entities in world, none in UiSurface
let ui_surface = world.resource::<UiSurface>();
assert!(ui_surface.entity_to_taffy.is_empty());
let ui_entity = world.spawn(Node::default()).id();
// `ui_layout_system` should map `ui_entity` to a ui node in `UiSurface::entity_to_taffy`
ui_schedule.run(&mut world);
let ui_surface = world.resource::<UiSurface>();
assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
assert_eq!(ui_surface.entity_to_taffy.len(), 1);
// remove and re-insert Node to trigger removal code in `ui_layout_system`
world.entity_mut(ui_entity).remove::<Node>();
world.entity_mut(ui_entity).insert(Node::default());
// `ui_layout_system` should still have `ui_entity`
ui_schedule.run(&mut world);
let ui_surface = world.resource::<UiSurface>();
assert!(ui_surface.entity_to_taffy.contains_key(&ui_entity));
assert_eq!(ui_surface.entity_to_taffy.len(), 1);
}
/// regression test for >=0.13.1 root node layouts /// regression test for >=0.13.1 root node layouts
/// ensure root nodes act like they are absolutely positioned /// ensure root nodes act like they are absolutely positioned
/// without explicitly declaring it. /// without explicitly declaring it.

View file

@ -371,6 +371,7 @@ pub fn queue_shadows(
), ),
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
}); });
} }
} }

View file

@ -528,6 +528,11 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
#[derive(Component)] #[derive(Component)]
pub struct DefaultCameraView(pub Entity); pub struct DefaultCameraView(pub Entity);
#[derive(Component)]
pub struct ExtractedAA {
pub scale_factor: f32,
}
/// Extracts all UI elements associated with a camera into the render world. /// Extracts all UI elements associated with a camera into the render world.
pub fn extract_default_ui_camera_view( pub fn extract_default_ui_camera_view(
mut commands: Commands, mut commands: Commands,
@ -555,7 +560,7 @@ pub fn extract_default_ui_camera_view(
commands commands
.get_entity(entity) .get_entity(entity)
.expect("Camera entity wasn't synced.") .expect("Camera entity wasn't synced.")
.remove::<(DefaultCameraView, UiAntiAlias, UiBoxShadowSamples)>(); .remove::<(DefaultCameraView, ExtractedAA, UiBoxShadowSamples)>();
continue; continue;
} }
@ -566,10 +571,12 @@ pub fn extract_default_ui_camera_view(
.. ..
}), }),
Some(physical_size), Some(physical_size),
Some(scale_factor),
) = ( ) = (
camera.logical_viewport_size(), camera.logical_viewport_size(),
camera.physical_viewport_rect(), camera.physical_viewport_rect(),
camera.physical_viewport_size(), camera.physical_viewport_size(),
camera.target_scaling_factor(),
) { ) {
// use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection // use a projection matrix with the origin in the top left instead of the bottom left that comes with OrthographicProjection
let projection_matrix = Mat4::orthographic_rh( let projection_matrix = Mat4::orthographic_rh(
@ -580,6 +587,7 @@ pub fn extract_default_ui_camera_view(
0.0, 0.0,
UI_CAMERA_FAR, UI_CAMERA_FAR,
); );
let default_camera_view = commands let default_camera_view = commands
.spawn(( .spawn((
ExtractedView { ExtractedView {
@ -606,8 +614,10 @@ pub fn extract_default_ui_camera_view(
.get_entity(entity) .get_entity(entity)
.expect("Camera entity wasn't synced."); .expect("Camera entity wasn't synced.");
entity_commands.insert(DefaultCameraView(default_camera_view)); entity_commands.insert(DefaultCameraView(default_camera_view));
if let Some(ui_anti_alias) = ui_anti_alias { if ui_anti_alias != Some(&UiAntiAlias::Off) {
entity_commands.insert(*ui_anti_alias); entity_commands.insert(ExtractedAA {
scale_factor: (scale_factor * ui_scale.0),
});
} }
if let Some(shadow_samples) = shadow_samples { if let Some(shadow_samples) = shadow_samples {
entity_commands.insert(*shadow_samples); entity_commands.insert(*shadow_samples);
@ -785,6 +795,7 @@ struct UiVertex {
pub size: [f32; 2], pub size: [f32; 2],
/// Position relative to the center of the UI node. /// Position relative to the center of the UI node.
pub point: [f32; 2], pub point: [f32; 2],
pub inverse_scale_factor: f32,
} }
#[derive(Resource)] #[derive(Resource)]
@ -835,13 +846,13 @@ pub fn queue_uinodes(
ui_pipeline: Res<UiPipeline>, ui_pipeline: Res<UiPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>, mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>,
mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>, mut transparent_render_phases: ResMut<ViewSortedRenderPhases<TransparentUi>>,
mut views: Query<(Entity, &ExtractedView, Option<&UiAntiAlias>)>, mut views: Query<(Entity, &ExtractedView, Option<&ExtractedAA>)>,
pipeline_cache: Res<PipelineCache>, pipeline_cache: Res<PipelineCache>,
draw_functions: Res<DrawFunctions<TransparentUi>>, draw_functions: Res<DrawFunctions<TransparentUi>>,
) { ) {
let draw_function = draw_functions.read().id::<DrawUi>(); let draw_function = draw_functions.read().id::<DrawUi>();
for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() {
let Ok((view_entity, view, ui_anti_alias)) = views.get_mut(extracted_uinode.camera_entity) let Ok((view_entity, view, extracted_aa)) = views.get_mut(extracted_uinode.camera_entity)
else { else {
continue; continue;
}; };
@ -855,7 +866,7 @@ pub fn queue_uinodes(
&ui_pipeline, &ui_pipeline,
UiPipelineKey { UiPipelineKey {
hdr: view.hdr, hdr: view.hdr,
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)), anti_alias: extracted_aa.is_some(),
}, },
); );
transparent_phase.add(TransparentUi { transparent_phase.add(TransparentUi {
@ -869,6 +880,7 @@ pub fn queue_uinodes(
// batch_range will be calculated in prepare_uinodes // batch_range will be calculated in prepare_uinodes
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: extracted_aa.map(|aa| aa.scale_factor).unwrap_or(1.),
}); });
} }
} }
@ -1139,6 +1151,7 @@ pub fn prepare_uinodes(
border: [border.left, border.top, border.right, border.bottom], border: [border.left, border.top, border.right, border.bottom],
size: rect_size.xy().into(), size: rect_size.xy().into(),
point: points[i].into(), point: points[i].into(),
inverse_scale_factor: item.inverse_scale_factor,
}); });
} }
@ -1242,6 +1255,7 @@ pub fn prepare_uinodes(
border: [0.0; 4], border: [0.0; 4],
size: size.into(), size: size.into(),
point: [0.0; 2], point: [0.0; 2],
inverse_scale_factor: item.inverse_scale_factor,
}); });
} }

View file

@ -74,6 +74,8 @@ impl SpecializedRenderPipeline for UiPipeline {
VertexFormat::Float32x2, VertexFormat::Float32x2,
// position relative to the center // position relative to the center
VertexFormat::Float32x2, VertexFormat::Float32x2,
// inverse scale factor
VertexFormat::Float32,
], ],
); );
let shader_defs = if key.anti_alias { let shader_defs = if key.anti_alias {

View file

@ -97,6 +97,7 @@ pub struct TransparentUi {
pub draw_function: DrawFunctionId, pub draw_function: DrawFunctionId,
pub batch_range: Range<u32>, pub batch_range: Range<u32>,
pub extra_index: PhaseItemExtraIndex, pub extra_index: PhaseItemExtraIndex,
pub inverse_scale_factor: f32,
} }
impl PhaseItem for TransparentUi { impl PhaseItem for TransparentUi {
@ -206,6 +207,7 @@ impl<P: PhaseItem, const I: usize> RenderCommand<P> for SetUiTextureBindGroup<I>
RenderCommandResult::Success RenderCommandResult::Success
} }
} }
pub struct DrawUiNode; pub struct DrawUiNode;
impl<P: PhaseItem> RenderCommand<P> for DrawUiNode { impl<P: PhaseItem> RenderCommand<P> for DrawUiNode {
type Param = SRes<UiMeta>; type Param = SRes<UiMeta>;

View file

@ -22,6 +22,7 @@ struct VertexOutput {
// Position relative to the center of the rectangle. // Position relative to the center of the rectangle.
@location(6) point: vec2<f32>, @location(6) point: vec2<f32>,
@location(7) @interpolate(flat) scale_factor: f32,
@builtin(position) position: vec4<f32>, @builtin(position) position: vec4<f32>,
}; };
@ -39,6 +40,7 @@ fn vertex(
@location(5) border: vec4<f32>, @location(5) border: vec4<f32>,
@location(6) size: vec2<f32>, @location(6) size: vec2<f32>,
@location(7) point: vec2<f32>, @location(7) point: vec2<f32>,
@location(8) scale_factor: f32,
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.uv = vertex_uv; out.uv = vertex_uv;
@ -49,6 +51,7 @@ fn vertex(
out.size = size; out.size = size;
out.border = border; out.border = border;
out.point = point; out.point = point;
out.scale_factor = scale_factor;
return out; return out;
} }
@ -115,10 +118,9 @@ fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, in
} }
// get alpha for antialiasing for sdf // get alpha for antialiasing for sdf
fn antialias(distance: f32) -> f32 { fn antialias(distance: f32, scale_factor: f32) -> f32 {
// Using the fwidth(distance) was causing artifacts, so just use the distance. // Using the fwidth(distance) was causing artifacts, so just use the distance.
// This antialiases between the distance values of 0.25 and -0.25 return clamp(0.0, 1.0, (0.5 - scale_factor * distance));
return clamp(0.0, 1.0, 0.5 - 2.0 * distance);
} }
fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> { fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
@ -149,7 +151,7 @@ fn draw(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
// This select statement ensures we only perform anti-aliasing where a non-zero width border // This select statement ensures we only perform anti-aliasing where a non-zero width border
// is present, otherwise an outline about the external boundary would be drawn even without // is present, otherwise an outline about the external boundary would be drawn even without
// a border. // a border.
let t = select(1.0 - step(0.0, border_distance), antialias(border_distance), external_distance < internal_distance); let t = select(1.0 - step(0.0, border_distance), antialias(border_distance, in.scale_factor), external_distance < internal_distance);
#else #else
let t = 1.0 - step(0.0, border_distance); let t = 1.0 - step(0.0, border_distance);
#endif #endif
@ -165,7 +167,7 @@ fn draw_background(in: VertexOutput, texture_color: vec4<f32>) -> vec4<f32> {
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
#ifdef ANTI_ALIAS #ifdef ANTI_ALIAS
let t = antialias(internal_distance); let t = antialias(internal_distance, in.scale_factor);
#else #else
let t = 1.0 - step(0.0, internal_distance); let t = 1.0 - step(0.0, internal_distance);
#endif #endif

View file

@ -655,6 +655,7 @@ pub fn queue_ui_material_nodes<M: UiMaterial>(
), ),
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
}); });
} }
} }

View file

@ -372,6 +372,7 @@ pub fn queue_ui_slices(
), ),
batch_range: 0..0, batch_range: 0..0,
extra_index: PhaseItemExtraIndex::NONE, extra_index: PhaseItemExtraIndex::NONE,
inverse_scale_factor: 1.,
}); });
} }
} }

View file

@ -269,7 +269,9 @@ pub fn update_image_content_size_system(
* ui_scale.0; * ui_scale.0;
for (mut content_size, image, mut image_size) in &mut query { for (mut content_size, image, mut image_size) in &mut query {
if !matches!(image.image_mode, NodeImageMode::Auto) { if !matches!(image.image_mode, NodeImageMode::Auto)
|| image.image.id() == TRANSPARENT_IMAGE_HANDLE.id()
{
if image.is_changed() { if image.is_changed() {
// Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present. // Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
content_size.measure = None; content_size.measure = None;

View file

@ -34,6 +34,7 @@ The default feature set enables most of the expected features of a game engine,
|bevy_text|Provides text functionality| |bevy_text|Provides text functionality|
|bevy_ui|A custom ECS-driven UI framework| |bevy_ui|A custom ECS-driven UI framework|
|bevy_ui_picking_backend|Provides an implementation for picking ui| |bevy_ui_picking_backend|Provides an implementation for picking ui|
|bevy_window|Windowing layer|
|bevy_winit|winit window and input backend| |bevy_winit|winit window and input backend|
|custom_cursor|Enable winit custom cursor support| |custom_cursor|Enable winit custom cursor support|
|default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase| |default_font|Include a default font, containing only ASCII characters, at the cost of a 20kB binary size increase|
@ -67,6 +68,7 @@ The default feature set enables most of the expected features of a game engine,
|detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in| |detailed_trace|Enable detailed trace event logging. These trace events are expensive even when off, thus they require compile time opt-in|
|dynamic_linking|Force dynamic linking, which improves iterative compile times| |dynamic_linking|Force dynamic linking, which improves iterative compile times|
|embedded_watcher|Enables watching in memory asset providers for Bevy Asset hot-reloading| |embedded_watcher|Enables watching in memory asset providers for Bevy Asset hot-reloading|
|experimental_pbr_pcss|Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs|
|exr|EXR image format support| |exr|EXR image format support|
|ff|Farbfeld image format support| |ff|Farbfeld image format support|
|file_watcher|Enables watching the filesystem for Bevy Asset hot-reloading| |file_watcher|Enables watching the filesystem for Bevy Asset hot-reloading|
@ -83,7 +85,6 @@ The default feature set enables most of the expected features of a game engine,
|mp3|MP3 audio format support| |mp3|MP3 audio format support|
|pbr_anisotropy_texture|Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_anisotropy_texture|Enable support for anisotropy texture in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs|
|pbr_multi_layer_material_textures|Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_multi_layer_material_textures|Enable support for multi-layer material textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs|
|pbr_pcss|Enable support for PCSS, at the risk of blowing past the global, per-shader sampler limit on older/lower-end GPUs|
|pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs| |pbr_transmission_textures|Enable support for transmission-related textures in the `StandardMaterial`, at the risk of blowing past the global, per-shader texture limit on older/lower-end GPUs|
|pnm|PNM image format support, includes pam, pbm, pgm and ppm| |pnm|PNM image format support, includes pam, pbm, pgm and ppm|
|qoi|QOI image format support| |qoi|QOI image format support|

View file

@ -460,6 +460,7 @@ Example | Description
[Bevymark](../examples/stress_tests/bevymark.rs) | A heavy sprite rendering workload to benchmark your system with Bevy [Bevymark](../examples/stress_tests/bevymark.rs) | A heavy sprite rendering workload to benchmark your system with Bevy
[Many Animated Sprites](../examples/stress_tests/many_animated_sprites.rs) | Displays many animated sprites in a grid arrangement with slight offsets to their animation timers. Used for performance testing. [Many Animated Sprites](../examples/stress_tests/many_animated_sprites.rs) | Displays many animated sprites in a grid arrangement with slight offsets to their animation timers. Used for performance testing.
[Many Buttons](../examples/stress_tests/many_buttons.rs) | Test rendering of many UI elements [Many Buttons](../examples/stress_tests/many_buttons.rs) | Test rendering of many UI elements
[Many Cameras & Lights](../examples/stress_tests/many_cameras_lights.rs) | Test rendering of many cameras and lights
[Many Cubes](../examples/stress_tests/many_cubes.rs) | Simple benchmark to test per-entity draw overhead. Run with the `sphere` argument to test frustum culling [Many Cubes](../examples/stress_tests/many_cubes.rs) | Simple benchmark to test per-entity draw overhead. Run with the `sphere` argument to test frustum culling
[Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000 [Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000
[Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos [Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos

View file

@ -106,7 +106,7 @@ impl AnimationInfo {
// The easing curve is parametrized over [0, 1], so we reparametrize it and // The easing curve is parametrized over [0, 1], so we reparametrize it and
// then ping-pong, which makes it spend another 3 seconds on the return journey. // then ping-pong, which makes it spend another 3 seconds on the return journey.
let translation_curve = easing_curve( let translation_curve = EasingCurve::new(
vec3(-6., 2., 0.), vec3(-6., 2., 0.),
vec3(6., 2., 0.), vec3(6., 2., 0.),
EaseFunction::CubicInOut, EaseFunction::CubicInOut,
@ -119,7 +119,7 @@ impl AnimationInfo {
// Something similar for rotation. The repetition here is an illusion caused // Something similar for rotation. The repetition here is an illusion caused
// by the symmetry of the cube; it rotates on the forward journey and never // by the symmetry of the cube; it rotates on the forward journey and never
// rotates back. // rotates back.
let rotation_curve = easing_curve( let rotation_curve = EasingCurve::new(
Quat::IDENTITY, Quat::IDENTITY,
Quat::from_rotation_y(FRAC_PI_2), Quat::from_rotation_y(FRAC_PI_2),
EaseFunction::ElasticInOut, EaseFunction::ElasticInOut,

View file

@ -138,7 +138,7 @@ fn display_curves(
); );
// Draw the curve // Draw the curve
let f = easing_curve(0.0, 1.0, *function); let f = EasingCurve::new(0.0, 1.0, *function);
let drawn_curve = f.by_ref().graph().map(|(x, y)| { let drawn_curve = f.by_ref().graph().map(|(x, y)| {
Vec2::new( Vec2::new(
x * SIZE_PER_FUNCTION + transform.translation.x, x * SIZE_PER_FUNCTION + transform.translation.x,

View file

@ -1,26 +1,37 @@
//! This example only enables a minimal set of plugins required for bevy to run. //! This example shows how to configure the `ScheduleRunnerPlugin` to run your
//! You can also completely remove rendering / windowing Plugin code from bevy //! application without windowing. You can completely remove rendering / windowing
//! by making your import look like this in your Cargo.toml. //! Plugin code from bevy by making your import look like this in your Cargo.toml.
//! //!
//! ```toml //! ```toml
//! [dependencies] //! [dependencies]
//! bevy = { version = "*", default-features = false } //! bevy = { version = "*", default-features = false }
//! # replace "*" with the most recent version of bevy //! # replace "*" with the most recent version of bevy
//! ``` //! ```
//!
//! And then enabling the features you need.
//! See the full list: <https://docs.rs/bevy/latest/bevy/#cargo-features>
use bevy::{app::ScheduleRunnerPlugin, log::LogPlugin, prelude::*, utils::Duration}; use bevy::{app::ScheduleRunnerPlugin, log::LogPlugin, prelude::*, utils::Duration};
fn main() { fn main() {
if cfg!(feature = "bevy_window") {
println!("This example is running with the bevy_window feature enabled and will not run headless.");
println!("Disable the default features and rerun the example to run headless.");
println!("To do so, run:");
println!();
println!(" cargo run --example headless --no-default-features");
return;
}
// This app runs once // This app runs once
App::new() App::new()
.add_plugins(HeadlessPlugins.set(ScheduleRunnerPlugin::run_once())) .add_plugins(DefaultPlugins.set(ScheduleRunnerPlugin::run_once()))
.add_systems(Update, hello_world_system) .add_systems(Update, hello_world_system)
.run(); .run();
// This app loops forever at 60 fps // This app loops forever at 60 fps
App::new() App::new()
.add_plugins( .add_plugins(
HeadlessPlugins DefaultPlugins
.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64( .set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(
1.0 / 60.0, 1.0 / 60.0,
))) )))

View file

@ -69,7 +69,7 @@ fn draw_example_collection(
gizmos.cross_2d(Vec2::new(-160., 120.), 12., FUCHSIA); gizmos.cross_2d(Vec2::new(-160., 120.), 12., FUCHSIA);
let domain = Interval::EVERYWHERE; let domain = Interval::EVERYWHERE;
let curve = function_curve(domain, |t| Vec2::new(t, ops::sin(t / 25.0) * 100.0)); let curve = FunctionCurve::new(domain, |t| Vec2::new(t, ops::sin(t / 25.0) * 100.0));
let resolution = ((ops::sin(time.elapsed_secs()) + 1.0) * 50.0) as usize; let resolution = ((ops::sin(time.elapsed_secs()) + 1.0) * 50.0) as usize;
let times_and_colors = (0..=resolution) let times_and_colors = (0..=resolution)
.map(|n| n as f32 / resolution as f32) .map(|n| n as f32 / resolution as f32)

View file

@ -122,7 +122,7 @@ fn draw_example_collection(
gizmos.cross(Vec3::new(-1., 1., 1.), 0.5, FUCHSIA); gizmos.cross(Vec3::new(-1., 1., 1.), 0.5, FUCHSIA);
let domain = Interval::EVERYWHERE; let domain = Interval::EVERYWHERE;
let curve = function_curve(domain, |t| { let curve = FunctionCurve::new(domain, |t| {
(Vec2::from(ops::sin_cos(t * 10.0))).extend(t - 6.0) (Vec2::from(ops::sin_cos(t * 10.0))).extend(t - 6.0)
}); });
let resolution = ((ops::sin(time.elapsed_secs()) + 1.0) * 100.0) as usize; let resolution = ((ops::sin(time.elapsed_secs()) + 1.0) * 100.0) as usize;

View file

@ -13,7 +13,6 @@ use bevy::{
query::ROQueryItem, query::ROQueryItem,
system::{lifetimeless::SRes, SystemParamItem}, system::{lifetimeless::SRes, SystemParamItem},
}, },
math::{vec3, Vec3A},
prelude::*, prelude::*,
render::{ render::{
extract_component::{ExtractComponent, ExtractComponentPlugin}, extract_component::{ExtractComponent, ExtractComponentPlugin},

View file

@ -42,6 +42,10 @@ struct Args {
/// use the grid layout model /// use the grid layout model
#[argh(switch)] #[argh(switch)]
grid: bool, grid: bool,
/// at the start of each frame despawn any existing UI nodes and spawn a new UI tree
#[argh(switch)]
respawn: bool,
} }
/// This example shows what happens when there is a lot of buttons on screen. /// This example shows what happens when there is a lot of buttons on screen.
@ -52,6 +56,8 @@ fn main() {
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
let args = Args::from_args(&[], &[]).unwrap(); let args = Args::from_args(&[], &[]).unwrap();
warn!(include_str!("warning_string.txt"));
let mut app = App::new(); let mut app = App::new();
app.add_plugins(( app.add_plugins((
@ -72,6 +78,10 @@ fn main() {
}) })
.add_systems(Update, (button_system, set_text_colors_changed)); .add_systems(Update, (button_system, set_text_colors_changed));
app.add_systems(Startup, |mut commands: Commands| {
commands.spawn(Camera2d);
});
if args.grid { if args.grid {
app.add_systems(Startup, setup_grid); app.add_systems(Startup, setup_grid);
} else { } else {
@ -92,6 +102,14 @@ fn main() {
}); });
} }
if args.respawn {
if args.grid {
app.add_systems(Update, (despawn_ui, setup_grid).chain());
} else {
app.add_systems(Update, (despawn_ui, setup_flex).chain());
}
}
app.insert_resource(args).run(); app.insert_resource(args).run();
} }
@ -119,7 +137,6 @@ fn button_system(
} }
fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) { fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
warn!(include_str!("warning_string.txt"));
let image = if 0 < args.image_freq { let image = if 0 < args.image_freq {
Some(asset_server.load("branding/icon.png")) Some(asset_server.load("branding/icon.png"))
} else { } else {
@ -134,7 +151,6 @@ fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<
}; };
let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8); let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
commands.spawn(Camera2d);
commands commands
.spawn(Node { .spawn(Node {
flex_direction: FlexDirection::Column, flex_direction: FlexDirection::Column,
@ -171,7 +187,6 @@ fn setup_flex(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<
} }
fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) { fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<Args>) {
warn!(include_str!("warning_string.txt"));
let image = if 0 < args.image_freq { let image = if 0 < args.image_freq {
Some(asset_server.load("branding/icon.png")) Some(asset_server.load("branding/icon.png"))
} else { } else {
@ -186,7 +201,6 @@ fn setup_grid(mut commands: Commands, asset_server: Res<AssetServer>, args: Res<
}; };
let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8); let as_rainbow = |i: usize| Color::hsl((i as f32 / buttons_f) * 360.0, 0.9, 0.8);
commands.spawn(Camera2d);
commands commands
.spawn(Node { .spawn(Node {
display: Display::Grid, display: Display::Grid,
@ -268,3 +282,7 @@ fn spawn_button(
}); });
} }
} }
fn despawn_ui(mut commands: Commands, root_node: Single<Entity, (With<Node>, Without<Parent>)>) {
commands.entity(*root_node).despawn_recursive();
}

View file

@ -0,0 +1,92 @@
//! Test rendering of many cameras and lights
use std::f32::consts::PI;
use bevy::{
math::ops::{cos, sin},
prelude::*,
render::camera::Viewport,
};
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_systems(Startup, setup)
.add_systems(Update, rotate_cameras)
.run();
}
const CAMERA_ROWS: usize = 4;
const CAMERA_COLS: usize = 4;
const NUM_LIGHTS: usize = 5;
/// set up a simple 3D scene
fn setup(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
window: Query<&Window>,
) {
// circular base
commands.spawn((
Mesh3d(meshes.add(Circle::new(4.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
));
// cube
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
MeshMaterial3d(materials.add(Color::WHITE)),
Transform::from_xyz(0.0, 0.5, 0.0),
));
// lights
for i in 0..NUM_LIGHTS {
let angle = (i as f32) / (NUM_LIGHTS as f32) * PI * 2.0;
commands.spawn((
PointLight {
color: Color::hsv(angle.to_degrees(), 1.0, 1.0),
intensity: 2_000_000.0 / NUM_LIGHTS as f32,
shadows_enabled: true,
..default()
},
Transform::from_xyz(sin(angle) * 4.0, 2.0, cos(angle) * 4.0),
));
}
// cameras
let window = window.single();
let width = window.resolution.width() / CAMERA_COLS as f32 * window.resolution.scale_factor();
let height = window.resolution.height() / CAMERA_ROWS as f32 * window.resolution.scale_factor();
let mut i = 0;
for y in 0..CAMERA_COLS {
for x in 0..CAMERA_ROWS {
let angle = i as f32 / (CAMERA_ROWS * CAMERA_COLS) as f32 * PI * 2.0;
commands.spawn((
Camera3d::default(),
Camera {
viewport: Some(Viewport {
physical_position: UVec2::new(
(x as f32 * width) as u32,
(y as f32 * height) as u32,
),
physical_size: UVec2::new(width as u32, height as u32),
..default()
}),
order: i,
..default()
},
Transform::from_xyz(sin(angle) * 4.0, 2.5, cos(angle) * 4.0)
.looking_at(Vec3::ZERO, Vec3::Y),
));
i += 1;
}
}
}
fn rotate_cameras(time: Res<Time>, mut query: Query<&mut Transform, With<Camera>>) {
for mut transform in query.iter_mut() {
transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(time.delta_secs()));
}
}

View file

@ -18,7 +18,7 @@ fn main() {
} }
fn setup(mut commands: Commands) { fn setup(mut commands: Commands) {
commands.spawn((Camera2d, UiAntiAlias::Off)); commands.spawn((Camera2d, UiAntiAlias::On));
commands commands
.spawn(( .spawn((

View file

@ -8,7 +8,6 @@
//! If you want to hot reload asset changes, enable the `file_watcher` cargo feature. //! If you want to hot reload asset changes, enable the `file_watcher` cargo feature.
use bevy::{ use bevy::{
math::Vec3A,
prelude::*, prelude::*,
render::primitives::{Aabb, Sphere}, render::primitives::{Aabb, Sphere},
}; };