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
if: failure()
run: |
echo 'To fix toml fmt, please run `taplo fmt`'
echo 'To check for a diff, run `taplo fmt --check --diff'
echo 'To fix toml fmt, please run `taplo fmt`.'
echo 'To check for a diff, run `taplo fmt --check --diff`.'
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'
typos:
@ -242,7 +242,7 @@ jobs:
steps:
- uses: actions/checkout@v4
- name: Check for typos
uses: crate-ci/typos@v1.27.0
uses: crate-ci/typos@v1.27.3
- name: Typos info
if: failure()
run: |

View file

@ -31,7 +31,7 @@ jobs:
with:
path: |
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
- name: Add iOS targets

View file

@ -59,28 +59,10 @@ jobs:
# See tools/ci/src/main.rs for the commands this runs
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:
name: Warn that weekly CI fails
runs-on: ubuntu-latest
needs: [test, lint, check-compiles, check-doc]
needs: [test, lint, check-compiles]
permissions:
issues: write
# 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
/target
crates/*/target
target
crates/**/target
benches/**/target
tools/**/target
**/*.rs.bk
/benches/target
/tools/compile_fail_utils/target
# Cargo
Cargo.lock
@ -11,8 +11,8 @@ Cargo.lock
.cargo/config.toml
# IDE files
/.idea
/.vscode
.idea
.vscode
.zed
dxcompiler.dll
dxil.dll

View file

@ -125,6 +125,7 @@ default = [
"bevy_text",
"bevy_ui",
"bevy_ui_picking_backend",
"bevy_window",
"bevy_winit",
"custom_cursor",
"default_font",
@ -218,6 +219,9 @@ bevy_ui = [
"bevy_ui_picking_backend",
]
# Windowing layer
bevy_window = ["bevy_internal/bevy_window"]
# winit window and input backend
bevy_winit = ["bevy_internal/bevy_winit"]
@ -405,7 +409,7 @@ pbr_multi_layer_material_textures = [
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
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.
webgl2 = ["bevy_internal/webgl"]
@ -2706,6 +2710,17 @@ description = "Test rendering of many UI elements"
category = "Stress Tests"
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]]
name = "many_cubes"
path = "examples/stress_tests/many_cubes.rs"
@ -3791,7 +3806,7 @@ wasm = true
name = "pcss"
path = "examples/3d/pcss.rs"
doc-scrape-examples = true
required-features = ["pbr_pcss"]
required-features = ["experimental_pbr_pcss"]
[package.metadata.example.pcss]
name = "Percentage-closer soft shadows"

View file

@ -1,6 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use bevy_math::{prelude::*, *};
use bevy_math::prelude::*;
fn easing(c: &mut Criterion) {
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
//! 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;
//! let wobble_curve = function_curve(
//! let wobble_curve = FunctionCurve::new(
//! Interval::UNIT,
//! |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
//! [`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_animation::animation_curves::*;
//! # let wobble_curve = function_curve(
//! # let wobble_curve = FunctionCurve::new(
//! # Interval::UNIT,
//! # |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
//! 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_core::Name;
//! # use bevy_math::vec3;
//! # let wobble_curve = function_curve(
//! # let wobble_curve = FunctionCurve::new(
//! # Interval::UNIT,
//! # |t| { vec3(t.cos(), 0.0, 0.0) },
//! # );
@ -71,7 +71,7 @@
//! Animation of arbitrary components can be accomplished using [`AnimatableProperty`] in
//! conjunction with [`AnimatableCurve`]. See the documentation [there] for details.
//!
//! [using a function]: bevy_math::curve::function_curve
//! [using a function]: bevy_math::curve::FunctionCurve
//! [translation component of a `Transform`]: bevy_transform::prelude::Transform::translation
//! [`AnimationClip`]: crate::AnimationClip
//! [there]: AnimatableProperty

View file

@ -1,9 +1,12 @@
use bevy_tasks::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool, TaskPoolBuilder};
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
/// and number of total cores
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct TaskPoolThreadAssignmentPolicy {
/// Force using at least this many threads
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
/// permitted to use 1.0 to try to use all remaining threads
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 {
@ -61,6 +80,8 @@ impl Default for TaskPoolOptions {
min_threads: 1,
max_threads: 4,
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
@ -68,6 +89,8 @@ impl Default for TaskPoolOptions {
min_threads: 1,
max_threads: 4,
percent: 0.25,
on_thread_spawn: None,
on_thread_destroy: None,
},
// Use all remaining cores for compute (at least 1)
@ -75,6 +98,8 @@ impl Default for TaskPoolOptions {
min_threads: 1,
max_threads: usize::MAX,
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);
IoTaskPool::get_or_init(|| {
TaskPoolBuilder::default()
let mut builder = TaskPoolBuilder::default()
.num_threads(io_threads)
.thread_name("IO Task Pool".to_string())
.build()
.thread_name("IO Task Pool".to_string());
#[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);
AsyncComputeTaskPool::get_or_init(|| {
TaskPoolBuilder::default()
let mut builder = TaskPoolBuilder::default()
.num_threads(async_compute_threads)
.thread_name("Async Compute Task Pool".to_string())
.build()
.thread_name("Async Compute Task Pool".to_string());
#[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);
ComputeTaskPool::get_or_init(|| {
TaskPoolBuilder::default()
let mut builder = TaskPoolBuilder::default()
.num_threads(compute_threads)
.thread_name("Compute Task Pool".to_string())
.build()
.thread_name("Compute Task Pool".to_string());
#[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.
//!
//! [SMAA]: https://www.iryoku.com/smaa/
#[cfg(not(feature = "smaa_luts"))]
use crate::tonemapping::lut_placeholder;
use crate::{
core_2d::graph::{Core2d, Node2d},
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_transform = { path = "../bevy_transform", 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_text",
] }
bevy_ui = { path = "../bevy_ui", 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_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
/// 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.
/// It is recommended to only used this plugin during testing (manual or
/// 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 {
let ident = &require.path;
register_recursive_requires.push(quote! {
<#ident as Component>::register_required_components(
<#ident as #bevy_ecs_path::component::Component>::register_required_components(
requiree,
components,
storages,

View file

@ -772,7 +772,7 @@ impl<T: SparseSetIndex> Access<T> {
/// `Access`, it's not recommended. Prefer to manage your own lists of
/// accessible components if your application needs to do that.
#[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) {
(
self.component_read_and_writes

View file

@ -4,6 +4,7 @@ use core::{marker::PhantomData, panic::Location};
use super::{
Deferred, IntoObserverSystem, IntoSystem, RegisterSystem, Resource, RunSystemCachedWith,
UnregisterSystem,
};
use crate::{
self as bevy_ecs,
@ -890,6 +891,17 @@ impl<'w, 's> Commands<'w, 's> {
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
/// [`CachedSystemId`](crate::system::CachedSystemId) resource.
///

View file

@ -30,7 +30,7 @@ pub struct SystemIdMarker;
/// A system that has been removed from the registry.
/// 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 = ()> {
initialized: bool,
system: BoxedSystem<I, O>,
@ -172,7 +172,7 @@ impl World {
///
/// 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.
pub fn remove_system<I, O>(
pub fn unregister_system<I, O>(
&mut self,
id: SystemId<I, O>,
) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>>
@ -412,7 +412,7 @@ impl World {
/// Removes a cached system and its [`CachedSystemId`] resource.
///
/// 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,
_system: S,
) -> Result<RemovedSystem<I, O>, RegisteredSystemError<I, O>>
@ -424,7 +424,7 @@ impl World {
let id = self
.remove_resource::<CachedSystemId<S::System>>()
.ok_or(RegisteredSystemError::SystemNotCached)?;
self.remove_system(id.0)
self.unregister_system(id.0)
}
/// 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
/// [`Commands`](crate::system::Commands).
///
@ -834,7 +860,7 @@ mod tests {
let new = world.register_system_cached(four);
assert_eq!(old, new);
let result = world.remove_system_cached(four);
let result = world.unregister_system_cached(four);
assert!(result.is_ok());
let new = world.register_system_cached(four);
assert_ne!(old, new);

View file

@ -730,6 +730,34 @@ impl<'w> EntityMut<'w> {
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)
/// to component(s) with lifetime `'w` for the current entity, based on the
/// given [`ComponentId`]s.
@ -4425,4 +4453,27 @@ mod tests {
.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};
/// fn system(mut gizmos: Gizmos) {
/// 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);
/// }
/// # bevy_ecs::system::assert_is_system(system);
@ -67,7 +67,7 @@ where
/// # use bevy_color::palettes::basic::{RED};
/// fn system(mut gizmos: Gizmos) {
/// let domain = Interval::UNIT;
/// let curve = function_curve(domain, |t| {
/// let curve = FunctionCurve::new(domain, |t| {
/// let (x,y) = t.sin_cos();
/// Vec3::new(x, y, t)
/// });
@ -104,7 +104,7 @@ where
/// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}};
/// fn system(mut gizmos: Gizmos) {
/// 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(
/// curve,
/// (0..=100).map(|n| n as f32 / 100.0)
@ -147,7 +147,7 @@ where
/// # use bevy_color::{Mix, palettes::basic::{GREEN, RED}};
/// fn system(mut gizmos: Gizmos) {
/// let domain = Interval::UNIT;
/// let curve = function_curve(domain, |t| {
/// let curve = FunctionCurve::new(domain, |t| {
/// let (x,y) = t.sin_cos();
/// Vec3::new(x, y, t)
/// });

View file

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

View file

@ -97,7 +97,7 @@ serialize = [
"bevy_time/serialize",
"bevy_transform/serialize",
"bevy_ui?/serialize",
"bevy_window/serialize",
"bevy_window?/serialize",
"bevy_winit?/serialize",
]
multi_threaded = [
@ -135,7 +135,7 @@ pbr_anisotropy_texture = [
]
# Percentage-closer soft shadows
pbr_pcss = ["bevy_pbr?/pbr_pcss"]
experimental_pbr_pcss = ["bevy_pbr?/experimental_pbr_pcss"]
# Optimise for WebGL2
webgl = [
@ -162,6 +162,7 @@ animation = ["bevy_animation", "bevy_gltf?/bevy_animation"]
bevy_sprite = ["dep:bevy_sprite", "bevy_gizmos?/bevy_sprite"]
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
dynamic_linking = ["bevy_diagnostic/dynamic_linking"]
@ -173,7 +174,7 @@ android_shared_stdcxx = ["bevy_audio/android_shared_stdcxx"]
# screen readers and forks.)
accesskit_unix = ["bevy_winit/accesskit_unix"]
bevy_text = ["dep:bevy_text", "bevy_ui?/bevy_text"]
bevy_text = ["dep:bevy_text"]
bevy_render = [
"dep:bevy_render",
@ -248,7 +249,7 @@ ghost_nodes = ["bevy_ui/ghost_nodes"]
[dependencies]
# 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_core = { path = "../bevy_core", 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_transform = { path = "../bevy_transform", 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 (optional)
bevy_animation = { path = "../bevy_animation", optional = true, version = "0.15.0-dev" }

View file

@ -13,7 +13,11 @@ plugin_group! {
bevy_hierarchy:::HierarchyPlugin,
bevy_diagnostic:::DiagnosticsPlugin,
bevy_input:::InputPlugin,
#[custom(cfg(not(feature = "bevy_window")))]
bevy_app:::ScheduleRunnerPlugin,
#[cfg(feature = "bevy_window")]
bevy_window:::WindowPlugin,
#[cfg(feature = "bevy_window")]
bevy_a11y:::AccessibilityPlugin,
#[custom(cfg(not(target_arch = "wasm32")))]
bevy_app:::TerminalCtrlCHandlerPlugin,
@ -72,55 +76,6 @@ plugin_group! {
///
/// [`DefaultPlugins`] contains all the plugins typically required to build
/// 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`].
}
@ -162,8 +117,6 @@ plugin_group! {
}
/// This plugin group represents the absolute minimum, bare-bones, bevy application.
/// 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)
/// 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;
pub use default_plugins::*;
#[cfg(feature = "bevy_window")]
pub use bevy_a11y as a11y;
#[cfg(feature = "bevy_animation")]
pub use bevy_animation as animation;
@ -66,6 +67,7 @@ pub use bevy_transform as transform;
#[cfg(feature = "bevy_ui")]
pub use bevy_ui as ui;
pub use bevy_utils as utils;
#[cfg(feature = "bevy_window")]
pub use bevy_window as window;
#[cfg(feature = "bevy_winit")]
pub use bevy_winit as winit;

View file

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

View file

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

View file

@ -3,9 +3,10 @@
//!
//! [easing functions]: EaseFunction
use crate::{Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace};
use super::{function_curve, Curve, Interval};
use crate::{
curve::{FunctionCurve, Interval},
Curve, Dir2, Dir3, Dir3A, Quat, Rot2, VectorSpace,
};
// TODO: Think about merging `Ease` with `StableInterpolate`
@ -28,13 +29,13 @@ pub trait Ease: Sized {
impl<V: VectorSpace> Ease for V {
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 {
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 difference = end_adjusted * start.inverse();
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
})
}
@ -52,7 +53,7 @@ impl Ease for Quat {
impl Ease for Dir2 {
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
///
/// - an initial `start` sample value at `t = 0`
@ -104,6 +91,22 @@ pub struct EasingCurve<T> {
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>
where
T: Ease + Clone,

View file

@ -56,13 +56,13 @@
//! # use bevy_math::vec3;
//! # use bevy_math::curve::*;
//! // A sinusoid:
//! let sine_curve = function_curve(Interval::EVERYWHERE, f32::sin);
//! let sine_curve = FunctionCurve::new(Interval::EVERYWHERE, f32::sin);
//!
//! // 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:
//! 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
@ -127,7 +127,7 @@
//! # use bevy_math::{vec2, prelude::*};
//! # use std::f32::consts::TAU;
//! // Our original curve, which may look something like this:
//! let ellipse_curve = function_curve(
//! let ellipse_curve = FunctionCurve::new(
//! interval(0.0, TAU).unwrap(),
//! |t| vec2(t.cos(), t.sin() * 2.0)
//! );
@ -141,7 +141,7 @@
//! ```rust
//! # use bevy_math::{vec2, prelude::*};
//! # 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));
//! // Change the domain to `[0, 1]` instead of `[0, TAU]`:
//! 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:
//! let start = 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.
//! //
@ -177,13 +177,13 @@
//! # use bevy_math::{vec2, prelude::*};
//! # use std::f32::consts::PI;
//! // A line segment connecting `(-1, 0)` to `(0, 0)`:
//! let line_curve = function_curve(
//! let line_curve = FunctionCurve::new(
//! Interval::UNIT,
//! |t| vec2(-1.0, 0.0).lerp(vec2(0.0, 0.0), t)
//! );
//!
//! // 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(),
//! |t| vec2(t.cos() * -1.0 + 1.0, t.sin())
//! );
@ -198,10 +198,10 @@
//! ```rust
//! # use bevy_math::{vec2, prelude::*};
//! // 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.)
//! 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:
//! let position_and_orientation = position_curve.zip(orientation_curve).unwrap();
@ -220,7 +220,7 @@
//! ```rust
//! # use bevy_math::{vec2, prelude::*};
//! // 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
//! // 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:
//! ```rust
//! # 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)>`.
//! let my_curve = some_magic_constructor();
//!
@ -272,7 +272,7 @@
//! [changing parametrizations]: Curve::reparametrize
//! [mapping output]: Curve::map
//! [rasterization]: Curve::resample
//! [functions]: function_curve
//! [functions]: FunctionCurve
//! [sample interpolation]: SampleCurve
//! [splines]: crate::cubic_splines
//! [easings]: easing
@ -282,7 +282,7 @@
//! [`zip`]: Curve::zip
//! [`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.
pub mod adaptors;
@ -414,7 +414,7 @@ pub trait Curve<T> {
/// factor rather than multiplying:
/// ```
/// # 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);
/// ```
/// This kind of linear remapping is provided by the convenience method
@ -425,12 +425,12 @@ pub trait Curve<T> {
/// // Reverse a curve:
/// # use bevy_math::curve::*;
/// # 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 reversed_curve = my_curve.reparametrize(domain, |t| domain.end() - (t - domain.start()));
///
/// // 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);
/// ```
#[must_use]
@ -712,7 +712,7 @@ pub trait Curve<T> {
/// ```
/// # use bevy_math::*;
/// # 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:
/// let resampled_rotation = quarter_rotation.resample(3, |x, y, t| x.nlerp(*y, t));
/// ```
@ -863,7 +863,7 @@ pub trait Curve<T> {
/// # Example
/// ```
/// # 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
/// // ownership of its input.
@ -976,27 +976,8 @@ pub enum ResamplingError {
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)]
mod tests {
use super::easing::*;
use super::*;
use crate::{ops, Quat};
use approx::{assert_abs_diff_eq, AbsDiffEq};
@ -1005,7 +986,7 @@ mod tests {
#[test]
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;
assert_eq!(curve.sample(1.0), Some(42.0));
@ -1014,21 +995,21 @@ mod tests {
#[test]
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);
let curve = constant_curve(Interval::UNIT, true);
let curve = ConstantCurve::new(Interval::UNIT, true);
assert!(curve.sample_unchecked(2.0));
assert!(curve.sample(2.0).is_none());
}
#[test]
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(-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!(curve.sample_unchecked(-1.0).is_nan());
assert!(curve.sample(-1.0).is_none());
@ -1038,7 +1019,7 @@ mod tests {
fn linear_curve() {
let start = Vec2::ZERO;
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;
@ -1054,7 +1035,7 @@ mod tests {
let start = Vec2::ZERO;
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.124, start),
@ -1078,7 +1059,7 @@ mod tests {
let start = Vec2::ZERO;
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.25, Vec2::new(0.0625, 0.125)),
@ -1093,7 +1074,7 @@ mod tests {
#[test]
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);
assert_eq!(mapped_curve.sample_unchecked(3.5), (3.5 * 3.0 + 1.0) / 7.0);
assert_eq!(
@ -1102,7 +1083,7 @@ mod tests {
);
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);
assert_eq!(mapped_curve.sample_unchecked(0.0), Quat::IDENTITY);
assert!(mapped_curve.sample_unchecked(1.0).is_near_identity());
@ -1111,7 +1092,7 @@ mod tests {
#[test]
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();
assert_eq!(rev_curve.sample(-0.1), None);
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.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();
assert_eq!(rev_curve.sample(-2.1), None);
assert_eq!(rev_curve.sample(-2.0), Some(1.0 * 3.0 + 1.0));
@ -1130,7 +1111,7 @@ mod tests {
#[test]
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();
assert_eq!(repeat_curve.sample(-0.1), None);
assert_eq!(repeat_curve.sample(0.0), Some(0.0 * 3.0 + 1.0));
@ -1159,7 +1140,7 @@ mod tests {
#[test]
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();
assert_eq!(ping_pong_curve.sample(-0.1), None);
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.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();
assert_eq!(ping_pong_curve.sample(-2.1), None);
assert_eq!(ping_pong_curve.sample(-2.0), Some(-2.0 * 3.0 + 1.0));
@ -1182,8 +1163,8 @@ mod tests {
#[test]
fn continue_chain() {
let first = function_curve(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 first = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * 3.0 + 1.0);
let second = FunctionCurve::new(Interval::new(0.0, 1.0).unwrap(), |t| t * t);
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.0), Some(0.0 * 3.0 + 1.0));
@ -1196,7 +1177,7 @@ mod tests {
#[test]
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
.by_ref()
.reparametrize(interval(0.0, f32::INFINITY).unwrap(), ops::exp2);
@ -1216,7 +1197,7 @@ mod tests {
#[test]
fn multiple_maps() {
// 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 second_mapped = first_mapped.map(|x| x * -2.0);
assert_abs_diff_eq!(second_mapped.sample_unchecked(0.0), 0.0);
@ -1227,7 +1208,7 @@ mod tests {
#[test]
fn multiple_reparams() {
// 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 second_reparam = first_reparam.reparametrize(Interval::UNIT, |t| t + 1.0);
assert_abs_diff_eq!(second_reparam.sample_unchecked(0.0), 1.0);
@ -1237,7 +1218,7 @@ mod tests {
#[test]
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.
let nice_try = curve.by_ref().resample_auto(0);
@ -1257,7 +1238,7 @@ mod tests {
}
// 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();
for test_pt in curve.domain().spaced_points(1001).unwrap() {
let expected = curve.sample_unchecked(test_pt);
@ -1271,7 +1252,7 @@ mod tests {
#[test]
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.
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);
// 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 resampled_curve = curve.by_ref().resample_uneven_auto(sample_points).unwrap();
for idx in 0..10 {
@ -1306,7 +1287,7 @@ mod tests {
fn sample_iterators() {
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 [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!(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 [y0, y1, y2, y3, y4] = samples.try_into().unwrap();

View file

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

View file

@ -14,7 +14,7 @@ webgpu = []
pbr_transmission_textures = []
pbr_multi_layer_material_textures = []
pbr_anisotropy_texture = []
pbr_pcss = []
experimental_pbr_pcss = []
shader_format_glsl = ["bevy_render/shader_format_glsl"]
trace = ["bevy_render/trace"]
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
/// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadow_size: Option<f32>,
/// A value that adjusts the tradeoff between self-shadowing artifacts and
@ -120,9 +121,10 @@ impl Default for DirectionalLight {
color: Color::WHITE,
illuminance: light_consts::lux::AMBIENT_DAYLIGHT,
shadows_enabled: false,
soft_shadow_size: None,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_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 bevy_ecs::{entity::EntityHashMap, prelude::*};
use bevy_ecs::{
entity::{EntityHashMap, EntityHashSet},
prelude::*,
};
use bevy_math::{ops, Mat4, Vec3A, Vec4};
use bevy_reflect::prelude::*;
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(
visible_point_lights: Query<&VisibleClusterableObjects>,
mut point_lights: Query<(
@ -872,10 +876,17 @@ pub fn check_point_light_mesh_visibility(
visible_entity_ranges: Option<Res<VisibleEntityRanges>>,
mut cubemap_visible_entities_queue: Local<Parallel<[Vec<Entity>; 6]>>,
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();
for visible_lights in &visible_point_lights {
for light_entity in visible_lights.entities.iter().copied() {
if !checked_lights.insert(light_entity) {
continue;
}
// Point lights
if let Ok((
point_light,

View file

@ -61,6 +61,7 @@ pub struct PointLight {
///
/// Note that soft shadows are significantly more expensive to render than
/// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadows_enabled: bool,
/// 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,
radius: 0.0,
shadows_enabled: false,
soft_shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
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
/// hard shadows.
#[cfg(feature = "experimental_pbr_pcss")]
pub soft_shadows_enabled: bool,
/// A value that adjusts the tradeoff between self-shadowing artifacts and
@ -115,12 +116,13 @@ impl Default for SpotLight {
range: 20.0,
radius: 0.0,
shadows_enabled: false,
soft_shadows_enabled: false,
shadow_depth_bias: Self::DEFAULT_SHADOW_DEPTH_BIAS,
shadow_normal_bias: Self::DEFAULT_SHADOW_NORMAL_BIAS,
shadow_map_near_z: Self::DEFAULT_SHADOW_MAP_NEAR_Z,
inner_angle: 0.0,
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")]
pub use self::from_mesh::{
MeshToMeshletMeshConversionError, MESHLET_DEFAULT_VERTEX_POSITION_QUANTIZATION_FACTOR,

View file

@ -9,6 +9,7 @@ use bevy_ecs::{
system::lifetimeless::Read,
};
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::{
diagnostic::RecordDiagnostics,
@ -29,6 +30,7 @@ use bevy_utils::tracing::info_span;
use bevy_utils::{
default,
tracing::{error, warn},
HashMap,
};
use core::{hash::Hash, ops::Range};
@ -41,12 +43,12 @@ pub struct ExtractedPointLight {
pub radius: f32,
pub transform: GlobalTransform,
pub shadows_enabled: bool,
pub soft_shadows_enabled: bool,
pub shadow_depth_bias: f32,
pub shadow_normal_bias: f32,
pub shadow_map_near_z: f32,
pub spot_light_angles: Option<(f32, f32)>,
pub volumetric: bool,
pub soft_shadows_enabled: bool,
}
#[derive(Component, Debug)]
@ -56,13 +58,13 @@ pub struct ExtractedDirectionalLight {
pub transform: GlobalTransform,
pub shadows_enabled: bool,
pub volumetric: bool,
pub soft_shadow_size: Option<f32>,
pub shadow_depth_bias: f32,
pub shadow_normal_bias: f32,
pub cascade_shadow_config: CascadeShadowConfig,
pub cascades: EntityHashMap<Vec<Cascade>>,
pub frusta: EntityHashMap<Vec<Frustum>>,
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!
@ -147,10 +149,10 @@ pub const MAX_CASCADES_PER_LIGHT: usize = 1;
#[derive(Resource, Clone)]
pub struct ShadowSamplers {
pub point_light_comparison_sampler: Sampler,
#[cfg(feature = "pbr_pcss")]
#[cfg(feature = "experimental_pbr_pcss")]
pub point_light_linear_sampler: Sampler,
pub directional_light_comparison_sampler: Sampler,
#[cfg(feature = "pbr_pcss")]
#[cfg(feature = "experimental_pbr_pcss")]
pub directional_light_linear_sampler: Sampler,
}
@ -174,7 +176,7 @@ impl FromWorld for ShadowSamplers {
compare: Some(CompareFunction::GreaterEqual),
..base_sampler_descriptor
}),
#[cfg(feature = "pbr_pcss")]
#[cfg(feature = "experimental_pbr_pcss")]
point_light_linear_sampler: render_device.create_sampler(&base_sampler_descriptor),
directional_light_comparison_sampler: render_device.create_sampler(
&SamplerDescriptor {
@ -182,7 +184,7 @@ impl FromWorld for ShadowSamplers {
..base_sampler_descriptor
},
),
#[cfg(feature = "pbr_pcss")]
#[cfg(feature = "experimental_pbr_pcss")]
directional_light_linear_sampler: render_device
.create_sampler(&base_sampler_descriptor),
}
@ -291,7 +293,6 @@ pub fn extract_lights(
radius: point_light.radius,
transform: *transform,
shadows_enabled: point_light.shadows_enabled,
soft_shadows_enabled: point_light.soft_shadows_enabled,
shadow_depth_bias: point_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
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,
spot_light_angles: None,
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((
render_entity,
@ -350,7 +355,6 @@ pub fn extract_lights(
radius: spot_light.radius,
transform: *transform,
shadows_enabled: spot_light.shadows_enabled,
soft_shadows_enabled: spot_light.soft_shadows_enabled,
shadow_depth_bias: spot_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
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,
spot_light_angles: Some((spot_light.inner_angle, spot_light.outer_angle)),
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,
*frustum,
@ -430,7 +438,10 @@ pub fn extract_lights(
illuminance: directional_light.illuminance,
transform: *transform,
volumetric: volumetric_light.is_some(),
#[cfg(feature = "experimental_pbr_pcss")]
soft_shadow_size: directional_light.soft_shadow_size,
#[cfg(not(feature = "experimental_pbr_pcss"))]
soft_shadow_size: None,
shadows_enabled: directional_light.shadows_enabled,
shadow_depth_bias: directional_light.shadow_depth_bias,
// The factor of SQRT_2 is for the worst-case diagonal offset
@ -677,10 +688,11 @@ pub fn prepare_lights(
point_light_shadow_map: Res<PointLightShadowMap>,
directional_light_shadow_map: Res<DirectionalLightShadowMap>,
mut shadow_render_phases: ResMut<ViewBinnedRenderPhases<Shadow>>,
(mut max_directional_lights_warning_emitted, mut max_cascades_per_light_warning_emitted): (
Local<bool>,
Local<bool>,
),
(
mut max_directional_lights_warning_emitted,
mut max_cascades_per_light_warning_emitted,
mut live_shadow_mapping_lights,
): (Local<bool>, Local<bool>, Local<EntityHashSet>),
point_lights: Query<(
Entity,
&ExtractedPointLight,
@ -688,7 +700,7 @@ pub fn prepare_lights(
)>,
directional_lights: Query<(Entity, &ExtractedDirectionalLight)>,
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_count = views_iter.len();
@ -911,17 +923,17 @@ pub fn prepare_lights(
.extend(1.0 / (light.range * light.range)),
position_radius: light.transform.translation().extend(light.radius),
flags: flags.bits(),
soft_shadow_size: if light.soft_shadows_enabled {
light.radius
} else {
0.0
},
shadow_depth_bias: light.shadow_depth_bias,
shadow_normal_bias: light.shadow_normal_bias,
shadow_map_near_z: light.shadow_map_near_z,
spot_light_tan_angle,
pad_a: 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);
}
@ -984,50 +996,108 @@ pub fn prepare_lights(
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);
// 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);
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 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())
.enumerate()
{
let depth_texture_view =
point_light_depth_texture
.texture
.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: (light_index * 6 + face_index) as u32,
array_layer_count: Some(1u32),
});
let mut first = false;
let base_array_layer = (light_index * 6 + face_index) as u32;
let depth_attachment = point_light_depth_attachments
.entry(base_array_layer)
.or_insert_with(|| {
first = true;
let depth_texture_view =
point_light_depth_texture
.texture
.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((
ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)),
depth_attachment,
pass_name: format!(
"shadow pass point light {} {}",
light_index,
@ -1147,8 +1229,11 @@ pub fn prepare_lights(
view_lights.push(view_light_entity);
shadow_render_phases.insert_or_clear(view_light_entity);
live_shadow_mapping_lights.insert(view_light_entity);
if first {
// 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;
let spot_projection = spot_light_clip_from_view(angle, light.shadow_map_near_z);
let depth_texture_view =
directional_light_depth_texture
.texture
.create_view(&TextureViewDescriptor {
label: Some("spot_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: (num_directional_cascades_enabled + light_index) as u32,
array_layer_count: Some(1u32),
});
let mut first = false;
let base_array_layer = (num_directional_cascades_enabled + light_index) as u32;
let depth_attachment = directional_light_depth_attachments
.entry(base_array_layer)
.or_insert_with(|| {
first = true;
let depth_texture_view = directional_light_depth_texture.texture.create_view(
&TextureViewDescriptor {
label: Some("spot_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();
let light_view_entities = light_view_entities
.entry(entity)
@ -1199,7 +1295,7 @@ pub fn prepare_lights(
commands.entity(view_light_entity).insert((
ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)),
depth_attachment,
pass_name: format!("shadow pass spot light {light_index}"),
},
ExtractedView {
@ -1221,8 +1317,11 @@ pub fn prepare_lights(
view_lights.push(view_light_entity);
shadow_render_phases.insert_or_clear(view_light_entity);
live_shadow_mapping_lights.insert(view_light_entity);
if first {
// 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
@ -1307,6 +1406,12 @@ pub fn prepare_lights(
base_array_layer: directional_depth_texture_array_index,
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;
let mut frustum = *frustum;
@ -1316,7 +1421,7 @@ pub fn prepare_lights(
commands.entity(view_light_entity).insert((
ShadowView {
depth_attachment: DepthAttachment::new(depth_texture_view, Some(0.0)),
depth_attachment,
pass_name: format!(
"shadow pass directional light {light_index} cascade {cascade_index}"
),
@ -1342,65 +1447,19 @@ pub fn prepare_lights(
));
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);
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((
ViewShadowBindings {
point_light_depth_texture: point_light_depth_texture.texture,
point_light_depth_texture_view,
directional_light_depth_texture: directional_light_depth_texture.texture,
directional_light_depth_texture_view,
point_light_depth_texture: point_light_depth_texture.texture.clone(),
point_light_depth_texture_view: point_light_depth_texture_view.clone(),
directional_light_depth_texture: directional_light_depth_texture.texture.clone(),
directional_light_depth_texture_view: directional_light_depth_texture_view.clone(),
},
ViewLightEntities {
lights: view_lights,

View file

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

View file

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

View file

@ -6,10 +6,10 @@
//! detail.
//!
//! 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
//! for your UI. The [`PointerHits`]s produced by these various backends will be combined, sorted,
//! and used as a homogeneous input for the picking systems that consume these events.
//! for your UI. The [`PointerHits`] instances produced by these various backends will be combined,
//! sorted, and used as a homogeneous input for the picking systems that consume these events.
//!
//! ## Implementation
//!
@ -22,7 +22,7 @@
//!
//! - Backends do not need to consider the [`PickingBehavior`](crate::PickingBehavior) component, though they may
//! use it for optimization purposes. For example, a backend that traverses a spatial hierarchy
//! may want to early exit if it intersects an entity that blocks lower entities from being
//! may want to exit early if it intersects an entity that blocks lower entities from being
//! picked.
//!
//! ### 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
/// under a pointer.
///
/// Some backends may only support providing the topmost entity; this is a valid limitation of some
/// backends. For example, a picking shader might only have data on the topmost rendered output from
/// its buffer.
/// Some backends may only support providing the topmost entity; this is a valid limitation. For
/// example, a picking shader might only have data on the topmost rendered output from its buffer.
///
/// 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

View file

@ -321,28 +321,51 @@ pub struct PickingEventWriters<'w> {
/// Dispatches interaction events to the target entities.
///
/// 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:
/// + For each movement: The sequence [`DragStart`], [`Drag`], [`DragOver`], [`Move`].
/// + For each button press: Either [`Down`], or the sequence [`Click`], [`Up`], [`DragDrop`], [`DragEnd`], [`DragLeave`].
/// + For each pointer cancellation: Simply [`Cancel`].
/// + Finally the sequence [`Out`], [`DragLeave`].
/// + For each movement: [`DragStart`] → [`Drag`] → [`DragOver`] → [`Move`].
/// + For each button press: [`Down`] or [`Click`] → [`Up`] → [`DragDrop`] → [`DragEnd`] → [`DragLeave`].
/// + For each pointer cancellation: [`Cancel`].
///
/// 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:
/// + When a pointer moves over the target: [`Over`], [`Move`], [`Out`].
/// + When a pointer presses buttons on the target: [`Down`], [`Up`], [`Click`].
/// + 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.
/// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`].
/// The rest rely on additional data from the [`PointerInput`] event stream. To
/// receive these events for a custom pointer, you must add [`PointerInput`]
/// events.
///
/// Two events -- [`Over`] and [`Out`] -- are driven only by the [`HoverMap`]. The rest rely on additional data from the
/// [`PointerInput`] event stream. To receive these events for a custom pointer, you must add [`PointerInput`] events.
/// When the pointer goes from hovering entity A to entity B, entity A will
/// 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,
/// 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 which occurred before it was actually hovered.
/// When we account for event bubbling, this is no longer true. When focus shifts
/// between children, parent entities may receive redundant [`Out`] → [`Over`] pairs.
/// 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)]
pub fn pointer_events(
// Input
@ -366,6 +389,57 @@ pub fn pointer_events(
.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...
for (pointer_id, hovered_entity, hit) in hover_map
.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.
use bevy_ecs::prelude::*;
use bevy_math::{Rect, Vec2};
use bevy_math::Vec2;
use bevy_reflect::prelude::*;
use bevy_render::camera::{Camera, NormalizedRenderTarget};
use bevy_utils::HashMap;
@ -233,13 +233,9 @@ impl Location {
return false;
}
let position = Vec2::new(self.position.x, self.position.y);
camera
.logical_viewport_rect()
.map(|Rect { min, max }| {
(position - min).min_element() >= 0.0 && (position - max).max_element() <= 0.0
})
.map(|rect| rect.contains(self.position))
.unwrap_or(false)
}
}

View file

@ -5,31 +5,25 @@ use crate::{
use core::{fmt, fmt::Formatter};
use serde::de::{Error, SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Array`] values.
///
/// [`Array`]: crate::Array
pub(super) struct ArrayVisitor<'a> {
array_info: &'static ArrayInfo,
registry: &'a TypeRegistry,
pub(super) struct ArrayVisitor<'a, P> {
pub array_info: &'static ArrayInfo,
pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
}
impl<'a> ArrayVisitor<'a> {
pub fn new(array_info: &'static ArrayInfo, registry: &'a TypeRegistry) -> Self {
Self {
array_info,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for ArrayVisitor<'a> {
impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for ArrayVisitor<'_, P> {
type Value = DynamicArray;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
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
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(
registration,
self.registry,
self.processor.as_deref_mut(),
))? {
vec.push(value);
}

View file

@ -15,6 +15,8 @@ use crate::{
use core::{fmt, fmt::Formatter};
use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A general purpose deserializer for reflected types.
///
/// 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
/// 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
///
/// ```
@ -94,28 +100,57 @@ use serde::de::{DeserializeSeed, Error, IgnoredAny, MapAccess, Visitor};
/// [`Box<DynamicList>`]: crate::DynamicList
/// [`FromReflect`]: crate::FromReflect
/// [`ReflectFromReflect`]: crate::ReflectFromReflect
pub struct ReflectDeserializer<'a> {
/// [`with_processor`]: Self::with_processor
pub struct ReflectDeserializer<'a, P: ReflectDeserializerProcessor = ()> {
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 {
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>;
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: serde::Deserializer<'de>,
{
struct UntypedReflectDeserializerVisitor<'a> {
struct UntypedReflectDeserializerVisitor<'a, P> {
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>;
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))?
.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,
registry: self.registry,
})?;
self.registry,
self.processor,
))?;
if map.next_key::<IgnoredAny>()?.is_some() {
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 {
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
/// 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
///
/// ```
@ -226,13 +267,20 @@ impl<'a, 'de> DeserializeSeed<'de> for ReflectDeserializer<'a> {
/// [`Box<DynamicList>`]: crate::DynamicList
/// [`FromReflect`]: crate::FromReflect
/// [`ReflectFromReflect`]: crate::ReflectFromReflect
pub struct TypedReflectDeserializer<'a> {
/// [`with_processor`]: Self::with_processor
pub struct TypedReflectDeserializer<'a, P: ReflectDeserializerProcessor = ()> {
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
}
impl<'a> TypedReflectDeserializer<'a> {
/// Creates a new [`TypedReflectDeserializer`] for the given type registration.
impl<'a> TypedReflectDeserializer<'a, ()> {
/// 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 {
#[cfg(feature = "debug_stack")]
TYPE_INFO_STACK.set(crate::type_info_stack::TypeInfoStack::new());
@ -240,10 +288,12 @@ impl<'a> TypedReflectDeserializer<'a> {
Self {
registration,
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
///
@ -256,6 +306,30 @@ impl<'a> TypedReflectDeserializer<'a> {
Self {
registration,
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(
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
processor: Option<&'a mut P>,
) -> Self {
Self {
registration,
registry,
processor,
}
}
}
impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> {
impl<'de, P: ReflectDeserializerProcessor> DeserializeSeed<'de>
for TypedReflectDeserializer<'_, P>
{
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
D: serde::Deserializer<'de>,
{
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();
// 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(
struct_info.type_path_table().ident().unwrap(),
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()));
Ok(Box::new(dynamic_struct))
@ -310,57 +409,76 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> {
{
deserializer.deserialize_newtype_struct(
tuple_struct_info.type_path_table().ident().unwrap(),
TupleStructVisitor::new(
TupleStructVisitor {
tuple_struct_info,
self.registration,
self.registry,
),
registration: self.registration,
registry: self.registry,
processor: self.processor,
},
)?
} else {
deserializer.deserialize_tuple_struct(
tuple_struct_info.type_path_table().ident().unwrap(),
tuple_struct_info.field_len(),
TupleStructVisitor::new(
TupleStructVisitor {
tuple_struct_info,
self.registration,
self.registry,
),
registration: self.registration,
registry: self.registry,
processor: self.processor,
},
)?
};
dynamic_tuple_struct.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_tuple_struct))
}
TypeInfo::List(list_info) => {
let mut dynamic_list =
deserializer.deserialize_seq(ListVisitor::new(list_info, self.registry))?;
let mut dynamic_list = deserializer.deserialize_seq(ListVisitor {
list_info,
registry: self.registry,
processor: self.processor,
})?;
dynamic_list.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_list))
}
TypeInfo::Array(array_info) => {
let mut dynamic_array = deserializer.deserialize_tuple(
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()));
Ok(Box::new(dynamic_array))
}
TypeInfo::Map(map_info) => {
let mut dynamic_map =
deserializer.deserialize_map(MapVisitor::new(map_info, self.registry))?;
let mut dynamic_map = deserializer.deserialize_map(MapVisitor {
map_info,
registry: self.registry,
processor: self.processor,
})?;
dynamic_map.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_map))
}
TypeInfo::Set(set_info) => {
let mut dynamic_set =
deserializer.deserialize_seq(SetVisitor::new(set_info, self.registry))?;
let mut dynamic_set = deserializer.deserialize_seq(SetVisitor {
set_info,
registry: self.registry,
processor: self.processor,
})?;
dynamic_set.set_represented_type(Some(self.registration.type_info()));
Ok(Box::new(dynamic_set))
}
TypeInfo::Tuple(tuple_info) => {
let mut dynamic_tuple = deserializer.deserialize_tuple(
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()));
Ok(Box::new(dynamic_tuple))
@ -370,13 +488,21 @@ impl<'a, 'de> DeserializeSeed<'de> for TypedReflectDeserializer<'a> {
== Some("core::option")
&& enum_info.type_path_table().ident() == Some("Option")
{
deserializer
.deserialize_option(OptionVisitor::new(enum_info, self.registry))?
deserializer.deserialize_option(OptionVisitor {
enum_info,
registry: self.registry,
processor: self.processor,
})?
} else {
deserializer.deserialize_enum(
enum_info.type_path_table().ident().unwrap(),
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()));

View file

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

View file

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

View file

@ -1,5 +1,6 @@
pub use deserialize_with_registry::*;
pub use deserializer::*;
pub use processor::*;
pub use registrations::*;
mod arrays;
@ -11,6 +12,7 @@ mod helpers;
mod lists;
mod maps;
mod options;
mod processor;
mod registration_utils;
mod registrations;
mod sets;
@ -24,12 +26,15 @@ mod tuples;
mod tests {
use bincode::Options;
use core::{any::TypeId, f32::consts::PI, ops::RangeInclusive};
use serde::de::IgnoredAny;
use serde::Deserializer;
use serde::{de::DeserializeSeed, Deserialize};
use bevy_utils::{HashMap, HashSet};
use crate as bevy_reflect;
use crate::serde::ReflectDeserializerProcessor;
use crate::{self as bevy_reflect, TypeRegistration};
use crate::{
serde::{ReflectDeserializer, ReflectSerializer, TypedReflectDeserializer},
DynamicEnum, FromReflect, PartialReflect, Reflect, ReflectDeserialize, TypeRegistry,
@ -275,15 +280,14 @@ mod tests {
let mut registry = get_registry();
registry.register::<Foo>();
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 dynamic_output = reflect_deserializer
.deserialize(&mut ron_deserializer)
.unwrap();
let output =
<Foo as FromReflect>::from_reflect(dynamic_output.as_ref().as_partial_reflect())
.unwrap();
<Foo as FromReflect>::from_reflect(dynamic_output.as_partial_reflect()).unwrap();
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()));
}
#[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")]
mod functions {
use super::*;

View file

@ -8,22 +8,16 @@ use crate::{
use core::{fmt, fmt::Formatter};
use serde::de::{DeserializeSeed, Error, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Option`] values.
pub(super) struct OptionVisitor<'a> {
enum_info: &'static EnumInfo,
registry: &'a TypeRegistry,
pub(super) struct OptionVisitor<'a, P> {
pub enum_info: &'static EnumInfo,
pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
}
impl<'a> OptionVisitor<'a> {
pub fn new(enum_info: &'static EnumInfo, registry: &'a TypeRegistry) -> Self {
Self {
enum_info,
registry,
}
}
}
impl<'a, 'de> Visitor<'de> for OptionVisitor<'a> {
impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for OptionVisitor<'_, P> {
type Value = DynamicEnum;
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 => {
let field = tuple_info.field_at(0).unwrap();
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();
value.insert_boxed(de.deserialize(deserializer)?);
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 serde::de::{SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Set`] values.
///
/// [`Set`]: crate::Set
pub(super) struct SetVisitor<'a> {
set_info: &'static SetInfo,
registry: &'a TypeRegistry,
pub(super) struct SetVisitor<'a, P> {
pub set_info: &'static SetInfo,
pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
}
impl<'a> SetVisitor<'a> {
pub fn new(set_info: &'static SetInfo, registry: &'a TypeRegistry) -> Self {
Self { set_info, registry }
}
}
impl<'a, 'de> Visitor<'de> for SetVisitor<'a> {
impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for SetVisitor<'_, P> {
type Value = DynamicSet;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
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
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(
value_registration,
self.registry,
self.processor.as_deref_mut(),
))? {
dynamic_set.insert_boxed(value);
}

View file

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

View file

@ -5,30 +5,19 @@ use crate::{
use core::{fmt, fmt::Formatter};
use serde::de::{MapAccess, SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Struct`] values.
///
/// [`Struct`]: crate::Struct
pub(super) struct StructVisitor<'a> {
struct_info: &'static StructInfo,
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
pub(super) struct StructVisitor<'a, P> {
pub struct_info: &'static StructInfo,
pub registration: &'a TypeRegistration,
pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
}
impl<'a> StructVisitor<'a> {
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> {
impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for StructVisitor<'_, P> {
type Value = DynamicStruct;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -39,13 +28,25 @@ impl<'a, 'de> Visitor<'de> for StructVisitor<'a> {
where
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>
where
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::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`TupleStruct`] values.
///
/// [`TupleStruct`]: crate::TupleStruct
pub(super) struct TupleStructVisitor<'a> {
tuple_struct_info: &'static TupleStructInfo,
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
pub(super) struct TupleStructVisitor<'a, P> {
pub tuple_struct_info: &'static TupleStructInfo,
pub registration: &'a TypeRegistration,
pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
}
impl<'a> TupleStructVisitor<'a> {
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> {
impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleStructVisitor<'_, P> {
type Value = DynamicTupleStruct;
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.registration,
self.registry,
self.processor,
)
.map(DynamicTupleStruct::from)
}
@ -71,7 +61,7 @@ impl<'a, 'de> Visitor<'de> for TupleStructVisitor<'a> {
self.registry,
)?;
let reflect_deserializer =
TypedReflectDeserializer::new_internal(registration, self.registry);
TypedReflectDeserializer::new_internal(registration, self.registry, self.processor);
let value = reflect_deserializer.deserialize(deserializer)?;
tuple.insert_boxed(value.into_partial_reflect());

View file

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

View file

@ -4,30 +4,19 @@ use crate::{
use core::{fmt, fmt::Formatter};
use serde::de::{SeqAccess, Visitor};
use super::ReflectDeserializerProcessor;
/// A [`Visitor`] for deserializing [`Tuple`] values.
///
/// [`Tuple`]: crate::Tuple
pub(super) struct TupleVisitor<'a> {
tuple_info: &'static TupleInfo,
registration: &'a TypeRegistration,
registry: &'a TypeRegistry,
pub(super) struct TupleVisitor<'a, P> {
pub tuple_info: &'static TupleInfo,
pub registration: &'a TypeRegistration,
pub registry: &'a TypeRegistry,
pub processor: Option<&'a mut P>,
}
impl<'a> TupleVisitor<'a> {
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> {
impl<'de, P: ReflectDeserializerProcessor> Visitor<'de> for TupleVisitor<'_, P> {
type Value = DynamicTuple;
fn expecting(&self, formatter: &mut Formatter) -> fmt::Result {
@ -38,6 +27,12 @@ impl<'a, 'de> Visitor<'de> for TupleVisitor<'a> {
where
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_ecs::{
entity::Entity,
schedule::{IntoSystemConfigs, ScheduleLabel},
schedule::{IntoSystemConfigs, IntoSystemSetConfigs, ScheduleLabel, SystemSet},
system::{Commands, In, IntoSystem, ResMut, Resource, System, SystemId},
world::World,
};
@ -442,21 +442,36 @@ impl Plugin for RemotePlugin {
app.insert_resource(remote_methods)
.init_resource::<RemoteWatchingRequests>()
.add_systems(PreStartup, setup_mailbox_channel)
.configure_sets(
RemoteLast,
(RemoteSet::ProcessRequests, RemoteSet::Cleanup).chain(),
)
.add_systems(
RemoteLast,
(
process_remote_requests,
process_ongoing_watching_requests,
remove_closed_watching_requests,
)
.chain(),
(process_remote_requests, process_ongoing_watching_requests)
.chain()
.in_set(RemoteSet::ProcessRequests),
remove_closed_watching_requests.in_set(RemoteSet::Cleanup),
),
);
}
}
/// Schedule that contains all systems to process Bevy Remote Protocol requests
#[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.
#[derive(Debug)]

View file

@ -1,6 +1,4 @@
use super::{ClearColorConfig, Projection};
use crate::sync_world::TemporaryRenderEntity;
use crate::view::RenderVisibleEntities;
use crate::{
batching::gpu_preprocessing::GpuPreprocessingSupport,
camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews},
@ -8,11 +6,12 @@ use crate::{
render_asset::RenderAssets,
render_graph::{InternedRenderSubGraph, RenderSubGraph},
render_resource::TextureView,
sync_world::TemporaryRenderEntity,
sync_world::{RenderEntity, SyncToRenderWorld},
texture::GpuImage,
view::{
ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers, Visibility,
VisibleEntities,
ColorGrading, ExtractedView, ExtractedWindows, GpuCulling, Msaa, RenderLayers,
RenderVisibleEntities, ViewUniformOffset, Visibility, VisibleEntities,
},
Extract,
};
@ -441,7 +440,10 @@ impl Camera {
#[inline]
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`].
@ -1062,6 +1064,7 @@ pub fn extract_cameras(
RenderLayers,
Projection,
GpuCulling,
ViewUniformOffset,
)>();
continue;
}

View file

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

View file

@ -71,8 +71,13 @@ pub fn sprite_picking(
continue;
};
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position)
else {
let viewport_pos = camera
.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;
};
let cursor_ray_len = cam_ortho.far - cam_ortho.near;

View file

@ -46,6 +46,16 @@ impl TaskPoolBuilder {
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`]
pub fn build(self) -> TaskPool {
TaskPool::new_internal()

View file

@ -11,7 +11,6 @@ use bevy_ecs::component::Component;
use bevy_ecs::{
change_detection::{DetectChanges, Ref},
entity::Entity,
event::EventReader,
prelude::{ReflectComponent, With},
query::{Changed, Without},
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::prelude::GlobalTransform;
use bevy_utils::HashSet;
use bevy_window::{PrimaryWindow, Window, WindowScaleFactorChanged};
use bevy_window::{PrimaryWindow, Window};
/// [`Text2dBundle`] was removed in favor of required components.
/// 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.
#[allow(clippy::too_many_arguments)]
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.
mut queue: Local<HashSet<Entity>>,
mut textures: ResMut<Assets<Image>>,
fonts: Res<Assets<Font>>,
windows: Query<&Window, With<PrimaryWindow>>,
mut scale_factor_changed: EventReader<WindowScaleFactorChanged>,
mut texture_atlases: ResMut<Assets<TextureAtlasLayout>>,
mut font_atlas_sets: ResMut<FontAtlasSets>,
mut text_pipeline: ResMut<TextPipeline>,
@ -255,9 +254,6 @@ pub fn update_text2d_layout(
mut font_system: ResMut<CosmicFontSystem>,
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
let scale_factor = windows
.get_single()
@ -266,6 +262,9 @@ pub fn update_text2d_layout(
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 {
if factor_changed
|| computed.needs_rerender()
@ -359,7 +358,7 @@ mod tests {
use bevy_app::{App, Update};
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};
@ -374,7 +373,6 @@ mod tests {
.init_resource::<Assets<Image>>()
.init_resource::<Assets<TextureAtlasLayout>>()
.init_resource::<FontAtlasSets>()
.init_resource::<Events<WindowScaleFactorChanged>>()
.init_resource::<TextPipeline>()
.init_resource::<CosmicFontSystem>()
.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_animation = { path = "../bevy_animation", version = "0.15.0-dev" }
bevy_sprite = { path = "../bevy_sprite", version = "0.15.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.15.0-dev", optional = true }
bevy_text = { path = "../bevy_text", version = "0.15.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.15.0-dev", optional = true }
bevy_transform = { path = "../bevy_transform", 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;
// 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
computed_node_query.iter().for_each(|(entity, _)| {
@ -771,6 +776,38 @@ mod tests {
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
/// ensure root nodes act like they are absolutely positioned
/// without explicitly declaring it.

View file

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

View file

@ -528,6 +528,11 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
#[derive(Component)]
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.
pub fn extract_default_ui_camera_view(
mut commands: Commands,
@ -555,7 +560,7 @@ pub fn extract_default_ui_camera_view(
commands
.get_entity(entity)
.expect("Camera entity wasn't synced.")
.remove::<(DefaultCameraView, UiAntiAlias, UiBoxShadowSamples)>();
.remove::<(DefaultCameraView, ExtractedAA, UiBoxShadowSamples)>();
continue;
}
@ -566,10 +571,12 @@ pub fn extract_default_ui_camera_view(
..
}),
Some(physical_size),
Some(scale_factor),
) = (
camera.logical_viewport_size(),
camera.physical_viewport_rect(),
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
let projection_matrix = Mat4::orthographic_rh(
@ -580,6 +587,7 @@ pub fn extract_default_ui_camera_view(
0.0,
UI_CAMERA_FAR,
);
let default_camera_view = commands
.spawn((
ExtractedView {
@ -606,8 +614,10 @@ pub fn extract_default_ui_camera_view(
.get_entity(entity)
.expect("Camera entity wasn't synced.");
entity_commands.insert(DefaultCameraView(default_camera_view));
if let Some(ui_anti_alias) = ui_anti_alias {
entity_commands.insert(*ui_anti_alias);
if ui_anti_alias != Some(&UiAntiAlias::Off) {
entity_commands.insert(ExtractedAA {
scale_factor: (scale_factor * ui_scale.0),
});
}
if let Some(shadow_samples) = shadow_samples {
entity_commands.insert(*shadow_samples);
@ -785,6 +795,7 @@ struct UiVertex {
pub size: [f32; 2],
/// Position relative to the center of the UI node.
pub point: [f32; 2],
pub inverse_scale_factor: f32,
}
#[derive(Resource)]
@ -835,13 +846,13 @@ pub fn queue_uinodes(
ui_pipeline: Res<UiPipeline>,
mut pipelines: ResMut<SpecializedRenderPipelines<UiPipeline>>,
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>,
draw_functions: Res<DrawFunctions<TransparentUi>>,
) {
let draw_function = draw_functions.read().id::<DrawUi>();
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 {
continue;
};
@ -855,7 +866,7 @@ pub fn queue_uinodes(
&ui_pipeline,
UiPipelineKey {
hdr: view.hdr,
anti_alias: matches!(ui_anti_alias, None | Some(UiAntiAlias::On)),
anti_alias: extracted_aa.is_some(),
},
);
transparent_phase.add(TransparentUi {
@ -869,6 +880,7 @@ pub fn queue_uinodes(
// batch_range will be calculated in prepare_uinodes
batch_range: 0..0,
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],
size: rect_size.xy().into(),
point: points[i].into(),
inverse_scale_factor: item.inverse_scale_factor,
});
}
@ -1242,6 +1255,7 @@ pub fn prepare_uinodes(
border: [0.0; 4],
size: size.into(),
point: [0.0; 2],
inverse_scale_factor: item.inverse_scale_factor,
});
}

View file

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

View file

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

View file

@ -22,6 +22,7 @@ struct VertexOutput {
// Position relative to the center of the rectangle.
@location(6) point: vec2<f32>,
@location(7) @interpolate(flat) scale_factor: f32,
@builtin(position) position: vec4<f32>,
};
@ -39,6 +40,7 @@ fn vertex(
@location(5) border: vec4<f32>,
@location(6) size: vec2<f32>,
@location(7) point: vec2<f32>,
@location(8) scale_factor: f32,
) -> VertexOutput {
var out: VertexOutput;
out.uv = vertex_uv;
@ -49,6 +51,7 @@ fn vertex(
out.size = size;
out.border = border;
out.point = point;
out.scale_factor = scale_factor;
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
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.
// This antialiases between the distance values of 0.25 and -0.25
return clamp(0.0, 1.0, 0.5 - 2.0 * distance);
return clamp(0.0, 1.0, (0.5 - scale_factor * distance));
}
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
// is present, otherwise an outline about the external boundary would be drawn even without
// 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
let t = 1.0 - step(0.0, border_distance);
#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);
#ifdef ANTI_ALIAS
let t = antialias(internal_distance);
let t = antialias(internal_distance, in.scale_factor);
#else
let t = 1.0 - step(0.0, internal_distance);
#endif

View file

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

View file

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

View file

@ -269,7 +269,9 @@ pub fn update_image_content_size_system(
* ui_scale.0;
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() {
// Mutably derefs, marking the `ContentSize` as changed ensuring `ui_layout_system` will remove the node's measure func if present.
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_ui|A custom ECS-driven UI framework|
|bevy_ui_picking_backend|Provides an implementation for picking ui|
|bevy_window|Windowing layer|
|bevy_winit|winit window and input backend|
|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|
@ -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|
|dynamic_linking|Force dynamic linking, which improves iterative compile times|
|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|
|ff|Farbfeld image format support|
|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|
|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_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|
|pnm|PNM image format support, includes pam, pbm, pgm and ppm|
|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
[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 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 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

View file

@ -106,7 +106,7 @@ impl AnimationInfo {
// 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.
let translation_curve = easing_curve(
let translation_curve = EasingCurve::new(
vec3(-6., 2., 0.),
vec3(6., 2., 0.),
EaseFunction::CubicInOut,
@ -119,7 +119,7 @@ impl AnimationInfo {
// 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
// rotates back.
let rotation_curve = easing_curve(
let rotation_curve = EasingCurve::new(
Quat::IDENTITY,
Quat::from_rotation_y(FRAC_PI_2),
EaseFunction::ElasticInOut,

View file

@ -138,7 +138,7 @@ fn display_curves(
);
// 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)| {
Vec2::new(
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.
//! You can also completely remove rendering / windowing Plugin code from bevy
//! by making your import look like this in your Cargo.toml.
//! This example shows how to configure the `ScheduleRunnerPlugin` to run your
//! application without windowing. You can completely remove rendering / windowing
//! Plugin code from bevy by making your import look like this in your Cargo.toml.
//!
//! ```toml
//! [dependencies]
//! bevy = { version = "*", default-features = false }
//! # 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};
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
App::new()
.add_plugins(HeadlessPlugins.set(ScheduleRunnerPlugin::run_once()))
.add_plugins(DefaultPlugins.set(ScheduleRunnerPlugin::run_once()))
.add_systems(Update, hello_world_system)
.run();
// This app loops forever at 60 fps
App::new()
.add_plugins(
HeadlessPlugins
DefaultPlugins
.set(ScheduleRunnerPlugin::run_loop(Duration::from_secs_f64(
1.0 / 60.0,
)))

View file

@ -69,7 +69,7 @@ fn draw_example_collection(
gizmos.cross_2d(Vec2::new(-160., 120.), 12., FUCHSIA);
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 times_and_colors = (0..=resolution)
.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);
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)
});
let resolution = ((ops::sin(time.elapsed_secs()) + 1.0) * 100.0) as usize;

View file

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

View file

@ -42,6 +42,10 @@ struct Args {
/// use the grid layout model
#[argh(switch)]
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.
@ -52,6 +56,8 @@ fn main() {
#[cfg(target_arch = "wasm32")]
let args = Args::from_args(&[], &[]).unwrap();
warn!(include_str!("warning_string.txt"));
let mut app = App::new();
app.add_plugins((
@ -72,6 +78,10 @@ fn main() {
})
.add_systems(Update, (button_system, set_text_colors_changed));
app.add_systems(Startup, |mut commands: Commands| {
commands.spawn(Camera2d);
});
if args.grid {
app.add_systems(Startup, setup_grid);
} 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();
}
@ -119,7 +137,6 @@ fn button_system(
}
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 {
Some(asset_server.load("branding/icon.png"))
} 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);
commands.spawn(Camera2d);
commands
.spawn(Node {
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>) {
warn!(include_str!("warning_string.txt"));
let image = if 0 < args.image_freq {
Some(asset_server.load("branding/icon.png"))
} 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);
commands.spawn(Camera2d);
commands
.spawn(Node {
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) {
commands.spawn((Camera2d, UiAntiAlias::Off));
commands.spawn((Camera2d, UiAntiAlias::On));
commands
.spawn((

View file

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