diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 526ac4c401..28a3c16a5b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ env: # If nightly is breaking CI, modify this variable to target a specific nightly version. NIGHTLY_TOOLCHAIN: nightly RUSTFLAGS: "-D warnings" - BINSTALL_VERSION: "v1.12.3" + BINSTALL_VERSION: "v1.12.5" concurrency: group: ${{github.workflow}}-${{github.ref}} @@ -272,7 +272,7 @@ jobs: timeout-minutes: 30 steps: - uses: actions/checkout@v4 - - uses: cargo-bins/cargo-binstall@v1.12.3 + - uses: cargo-bins/cargo-binstall@v1.12.5 - name: Install taplo run: cargo binstall taplo-cli@0.9.3 --locked - name: Run Taplo diff --git a/Cargo.toml b/Cargo.toml index e6c75611ba..87904d32fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -330,6 +330,9 @@ trace = ["bevy_internal/trace", "dep:tracing"] # Basis Universal compressed texture support basis-universal = ["bevy_internal/basis-universal"] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = ["bevy_internal/compressed_image_saver"] + # BMP image format support bmp = ["bevy_internal/bmp"] @@ -845,6 +848,17 @@ description = "Generates a texture atlas (sprite sheet) from individual sprites" category = "2D Rendering" wasm = false +[[example]] +name = "tilemap_chunk" +path = "examples/2d/tilemap_chunk.rs" +doc-scrape-examples = true + +[package.metadata.example.tilemap_chunk] +name = "Tilemap Chunk" +description = "Renders a tilemap chunk" +category = "2D Rendering" +wasm = true + [[example]] name = "transparency_2d" path = "examples/2d/transparency_2d.rs" diff --git a/benches/benches/bevy_ecs/entity_cloning.rs b/benches/benches/bevy_ecs/entity_cloning.rs index 0eaae27ce4..44ffa1d52b 100644 --- a/benches/benches/bevy_ecs/entity_cloning.rs +++ b/benches/benches/bevy_ecs/entity_cloning.rs @@ -1,7 +1,7 @@ use core::hint::black_box; use benches::bench; -use bevy_ecs::bundle::Bundle; +use bevy_ecs::bundle::{Bundle, InsertMode}; use bevy_ecs::component::ComponentCloneBehavior; use bevy_ecs::entity::EntityCloner; use bevy_ecs::hierarchy::ChildOf; @@ -17,41 +17,15 @@ criterion_group!( hierarchy_tall, hierarchy_wide, hierarchy_many, + filter ); #[derive(Component, Reflect, Default, Clone)] -struct C1(Mat4); +struct C(Mat4); -#[derive(Component, Reflect, Default, Clone)] -struct C2(Mat4); +type ComplexBundle = (C<1>, C<2>, C<3>, C<4>, C<5>, C<6>, C<7>, C<8>, C<9>, C<10>); -#[derive(Component, Reflect, Default, Clone)] -struct C3(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C4(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C5(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C6(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C7(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C8(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C9(Mat4); - -#[derive(Component, Reflect, Default, Clone)] -struct C10(Mat4); - -type ComplexBundle = (C1, C2, C3, C4, C5, C6, C7, C8, C9, C10); - -/// Sets the [`ComponentCloneHandler`] for all explicit and required components in a bundle `B` to +/// Sets the [`ComponentCloneBehavior`] for all explicit and required components in a bundle `B` to /// use the [`Reflect`] trait instead of [`Clone`]. fn reflection_cloner( world: &mut World, @@ -71,7 +45,7 @@ fn reflection_cloner( // this bundle are saved. let component_ids: Vec<_> = world.register_bundle::().contributed_components().into(); - let mut builder = EntityCloner::build(world); + let mut builder = EntityCloner::build_opt_out(world); // Overwrite the clone handler for all components in the bundle to use `Reflect`, not `Clone`. for component in component_ids { @@ -82,16 +56,15 @@ fn reflection_cloner( builder.finish() } -/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a -/// bundle `B`. +/// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`. /// /// The bundle must implement [`Default`], which is used to create the first entity that gets cloned /// in the benchmark. /// -/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneHandler`] for all -/// components (which is usually [`ComponentCloneHandler::clone_handler()`]). If `clone_via_reflect` +/// If `clone_via_reflect` is false, this will use the default [`ComponentCloneBehavior`] for all +/// components (which is usually [`ComponentCloneBehavior::clone()`]). If `clone_via_reflect` /// is true, it will overwrite the handler for all components in the bundle to be -/// [`ComponentCloneHandler::reflect_handler()`]. +/// [`ComponentCloneBehavior::reflect()`]. fn bench_clone( b: &mut Bencher, clone_via_reflect: bool, @@ -114,8 +87,7 @@ fn bench_clone( }); } -/// A helper function that benchmarks running the [`EntityCommands::clone_and_spawn()`] command on a -/// bundle `B`. +/// A helper function that benchmarks running [`EntityCloner::spawn_clone`] with a bundle `B`. /// /// As compared to [`bench_clone()`], this benchmarks recursively cloning an entity with several /// children. It does so by setting up an entity tree with a given `height` where each entity has a @@ -135,7 +107,7 @@ fn bench_clone_hierarchy( let mut cloner = if clone_via_reflect { reflection_cloner::(&mut world, true) } else { - let mut builder = EntityCloner::build(&mut world); + let mut builder = EntityCloner::build_opt_out(&mut world); builder.linked_cloning(true); builder.finish() }; @@ -169,7 +141,7 @@ fn bench_clone_hierarchy( // Each benchmark runs twice: using either the `Clone` or `Reflect` traits to clone entities. This // constant represents this as an easy array that can be used in a `for` loop. -const SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)]; +const CLONE_SCENARIOS: [(&str, bool); 2] = [("clone", false), ("reflect", true)]; /// Benchmarks cloning a single entity with 10 components and no children. fn single(c: &mut Criterion) { @@ -178,7 +150,7 @@ fn single(c: &mut Criterion) { // We're cloning 1 entity. group.throughput(Throughput::Elements(1)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { bench_clone::(b, clone_via_reflect); }); @@ -194,9 +166,9 @@ fn hierarchy_tall(c: &mut Criterion) { // We're cloning both the root entity and its 50 descendents. group.throughput(Throughput::Elements(51)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { - bench_clone_hierarchy::(b, 50, 1, clone_via_reflect); + bench_clone_hierarchy::>(b, 50, 1, clone_via_reflect); }); } @@ -210,9 +182,9 @@ fn hierarchy_wide(c: &mut Criterion) { // We're cloning both the root entity and its 50 direct children. group.throughput(Throughput::Elements(51)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { - bench_clone_hierarchy::(b, 1, 50, clone_via_reflect); + bench_clone_hierarchy::>(b, 1, 50, clone_via_reflect); }); } @@ -228,7 +200,7 @@ fn hierarchy_many(c: &mut Criterion) { // of entities spawned in `bench_clone_hierarchy()` with a `println!()` statement. :) group.throughput(Throughput::Elements(364)); - for (id, clone_via_reflect) in SCENARIOS { + for (id, clone_via_reflect) in CLONE_SCENARIOS { group.bench_function(id, |b| { bench_clone_hierarchy::(b, 5, 3, clone_via_reflect); }); @@ -236,3 +208,157 @@ fn hierarchy_many(c: &mut Criterion) { group.finish(); } + +/// Filter scenario variant for bot opt-in and opt-out filters +#[derive(Clone, Copy)] +#[expect( + clippy::enum_variant_names, + reason = "'Opt' is not understood as an prefix but `OptOut'/'OptIn' are" +)] +enum FilterScenario { + OptOutNone, + OptOutNoneKeep(bool), + OptOutAll, + OptInNone, + OptInAll, + OptInAllWithoutRequired, + OptInAllKeep(bool), + OptInAllKeepWithoutRequired(bool), +} + +impl From for String { + fn from(value: FilterScenario) -> Self { + match value { + FilterScenario::OptOutNone => "opt_out_none", + FilterScenario::OptOutNoneKeep(true) => "opt_out_none_keep_none", + FilterScenario::OptOutNoneKeep(false) => "opt_out_none_keep_all", + FilterScenario::OptOutAll => "opt_out_all", + FilterScenario::OptInNone => "opt_in_none", + FilterScenario::OptInAll => "opt_in_all", + FilterScenario::OptInAllWithoutRequired => "opt_in_all_without_required", + FilterScenario::OptInAllKeep(true) => "opt_in_all_keep_none", + FilterScenario::OptInAllKeep(false) => "opt_in_all_keep_all", + FilterScenario::OptInAllKeepWithoutRequired(true) => { + "opt_in_all_keep_none_without_required" + } + FilterScenario::OptInAllKeepWithoutRequired(false) => { + "opt_in_all_keep_all_without_required" + } + } + .into() + } +} + +/// Common scenarios for different filter to be benchmarked. +const FILTER_SCENARIOS: [FilterScenario; 11] = [ + FilterScenario::OptOutNone, + FilterScenario::OptOutNoneKeep(true), + FilterScenario::OptOutNoneKeep(false), + FilterScenario::OptOutAll, + FilterScenario::OptInNone, + FilterScenario::OptInAll, + FilterScenario::OptInAllWithoutRequired, + FilterScenario::OptInAllKeep(true), + FilterScenario::OptInAllKeep(false), + FilterScenario::OptInAllKeepWithoutRequired(true), + FilterScenario::OptInAllKeepWithoutRequired(false), +]; + +/// A helper function that benchmarks running [`EntityCloner::clone_entity`] with a bundle `B`. +/// +/// The bundle must implement [`Default`], which is used to create the first entity that gets its components cloned +/// in the benchmark. It may also be used to populate the target entity depending on the scenario. +fn bench_filter(b: &mut Bencher, scenario: FilterScenario) { + let mut world = World::default(); + let mut spawn = |empty| match empty { + false => world.spawn(B::default()).id(), + true => world.spawn_empty().id(), + }; + let source = spawn(false); + let (target, mut cloner); + + match scenario { + FilterScenario::OptOutNone => { + target = spawn(true); + cloner = EntityCloner::default(); + } + FilterScenario::OptOutNoneKeep(is_new) => { + target = spawn(is_new); + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.insert_mode(InsertMode::Keep); + cloner = builder.finish(); + } + FilterScenario::OptOutAll => { + target = spawn(true); + let mut builder = EntityCloner::build_opt_out(&mut world); + builder.deny::(); + cloner = builder.finish(); + } + FilterScenario::OptInNone => { + target = spawn(true); + let builder = EntityCloner::build_opt_in(&mut world); + cloner = builder.finish(); + } + FilterScenario::OptInAll => { + target = spawn(true); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.allow::(); + cloner = builder.finish(); + } + FilterScenario::OptInAllWithoutRequired => { + target = spawn(true); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.without_required_components(|builder| { + builder.allow::(); + }); + cloner = builder.finish(); + } + FilterScenario::OptInAllKeep(is_new) => { + target = spawn(is_new); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.allow_if_new::(); + cloner = builder.finish(); + } + FilterScenario::OptInAllKeepWithoutRequired(is_new) => { + target = spawn(is_new); + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.without_required_components(|builder| { + builder.allow_if_new::(); + }); + cloner = builder.finish(); + } + } + + b.iter(|| { + // clones the given entity into the target + cloner.clone_entity(&mut world, black_box(source), black_box(target)); + world.flush(); + }); +} + +/// Benchmarks filtering of cloning a single entity with 5 unclonable components (each requiring 1 unclonable component) into a target. +fn filter(c: &mut Criterion) { + #[derive(Component, Default)] + #[component(clone_behavior = Ignore)] + struct C; + + #[derive(Component, Default)] + #[component(clone_behavior = Ignore)] + #[require(C::)] + struct R; + + type RequiringBundle = (R<1>, R<2>, R<3>, R<4>, R<5>); + + let mut group = c.benchmark_group(bench!("filter")); + + // We're cloning 1 entity into a target. + group.throughput(Throughput::Elements(1)); + + for scenario in FILTER_SCENARIOS { + group.bench_function(scenario, |b| { + bench_filter::(b, scenario); + }); + } + + group.finish(); +} diff --git a/benches/benches/bevy_picking/ray_mesh_intersection.rs b/benches/benches/bevy_picking/ray_mesh_intersection.rs index 871a6d1062..e9fd0caf9f 100644 --- a/benches/benches/bevy_picking/ray_mesh_intersection.rs +++ b/benches/benches/bevy_picking/ray_mesh_intersection.rs @@ -155,6 +155,7 @@ fn bench(c: &mut Criterion) { &mesh.positions, Some(&mesh.normals), Some(&mesh.indices), + None, backface_culling, ); diff --git a/crates/bevy_animation/Cargo.toml b/crates/bevy_animation/Cargo.toml index adf17a8580..637231fd4c 100644 --- a/crates/bevy_animation/Cargo.toml +++ b/crates/bevy_animation/Cargo.toml @@ -37,7 +37,7 @@ serde = "1" blake3 = { version = "1.0" } downcast-rs = { version = "2", default-features = false, features = ["std"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } either = "1.13" thread_local = "1" uuid = { version = "1.13.1", features = ["v4"] } diff --git a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs index 0b4a99fb59..d1de3f4cea 100644 --- a/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs +++ b/crates/bevy_anti_aliasing/src/contrast_adaptive_sharpening/mod.rs @@ -3,7 +3,7 @@ use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_image::BevyDefault as _; @@ -163,7 +163,8 @@ impl Plugin for CasPlugin { pub struct CasPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } impl FromWorld for CasPipeline { @@ -187,7 +188,11 @@ impl FromWorld for CasPipeline { CasPipeline { texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "robust_contrast_adaptive_sharpening.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!( + render_world, + "robust_contrast_adaptive_sharpening.wgsl" + ), } } } @@ -209,9 +214,9 @@ impl SpecializedRenderPipeline for CasPipeline { RenderPipelineDescriptor { label: Some("contrast_adaptive_sharpening".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_anti_aliasing/src/fxaa/mod.rs b/crates/bevy_anti_aliasing/src/fxaa/mod.rs index 6b914c4e86..adc2a3d5a2 100644 --- a/crates/bevy_anti_aliasing/src/fxaa/mod.rs +++ b/crates/bevy_anti_aliasing/src/fxaa/mod.rs @@ -3,7 +3,7 @@ use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; use bevy_ecs::prelude::*; use bevy_image::BevyDefault as _; @@ -130,7 +130,8 @@ impl Plugin for FxaaPlugin { pub struct FxaaPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } impl FromWorld for FxaaPipeline { @@ -157,7 +158,8 @@ impl FromWorld for FxaaPipeline { FxaaPipeline { texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "fxaa.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "fxaa.wgsl"), } } } @@ -181,9 +183,9 @@ impl SpecializedRenderPipeline for FxaaPipeline { RenderPipelineDescriptor { label: Some("fxaa".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![ format!("EDGE_THRESH_{}", key.edge_threshold.get_str()).into(), format!("EDGE_THRESH_MIN_{}", key.edge_threshold_min.get_str()).into(), diff --git a/crates/bevy_anti_aliasing/src/taa/mod.rs b/crates/bevy_anti_aliasing/src/taa/mod.rs index 0f706146b1..6a00e9c0cf 100644 --- a/crates/bevy_anti_aliasing/src/taa/mod.rs +++ b/crates/bevy_anti_aliasing/src/taa/mod.rs @@ -2,9 +2,9 @@ use bevy_app::{App, Plugin}; use bevy_asset::{embedded_asset, load_embedded_asset, Handle}; use bevy_core_pipeline::{ core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, prelude::Camera3d, prepass::{DepthPrepass, MotionVectorPrepass, ViewPrepassTextures}, + FullscreenShader, }; use bevy_diagnostic::FrameCount; use bevy_ecs::{ @@ -238,7 +238,8 @@ struct TaaPipeline { taa_bind_group_layout: BindGroupLayout, nearest_sampler: Sampler, linear_sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } impl FromWorld for TaaPipeline { @@ -283,7 +284,8 @@ impl FromWorld for TaaPipeline { taa_bind_group_layout, nearest_sampler, linear_sampler, - shader: load_embedded_asset!(world, "taa.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "taa.wgsl"), } } } @@ -314,9 +316,9 @@ impl SpecializedRenderPipeline for TaaPipeline { RenderPipelineDescriptor { label: Some("taa_pipeline".into()), layout: vec![self.taa_bind_group_layout.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "taa".into(), targets: vec![ diff --git a/crates/bevy_asset/Cargo.toml b/crates/bevy_asset/Cargo.toml index 0835b3b49a..edf8986130 100644 --- a/crates/bevy_asset/Cargo.toml +++ b/crates/bevy_asset/Cargo.toml @@ -57,7 +57,7 @@ parking_lot = { version = "0.12", default-features = false, features = [ ron = { version = "0.10", default-features = false } serde = { version = "1", default-features = false, features = ["derive"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } uuid = { version = "1.13.1", default-features = false, features = [ "v4", "serde", diff --git a/crates/bevy_asset/src/lib.rs b/crates/bevy_asset/src/lib.rs index 4b29beae79..16da4313f0 100644 --- a/crates/bevy_asset/src/lib.rs +++ b/crates/bevy_asset/src/lib.rs @@ -264,7 +264,7 @@ pub struct AssetPlugin { /// [`AssetSource`](io::AssetSource). Subfolders within these folders are also valid. /// /// It is strongly discouraged to use [`Allow`](UnapprovedPathMode::Allow) if your -/// app will include scripts or modding support, as it could allow allow arbitrary file +/// app will include scripts or modding support, as it could allow arbitrary file /// access for malicious code. /// /// See [`AssetPath::is_unapproved`](crate::AssetPath::is_unapproved) @@ -272,10 +272,10 @@ pub struct AssetPlugin { pub enum UnapprovedPathMode { /// Unapproved asset loading is allowed. This is strongly discouraged. Allow, - /// Fails to load any asset that is is unapproved, unless an override method is used, like + /// Fails to load any asset that is unapproved, unless an override method is used, like /// [`AssetServer::load_override`]. Deny, - /// Fails to load any asset that is is unapproved. + /// Fails to load any asset that is unapproved. #[default] Forbid, } diff --git a/crates/bevy_color/Cargo.toml b/crates/bevy_color/Cargo.toml index 298f80e54f..99ac8a70da 100644 --- a/crates/bevy_color/Cargo.toml +++ b/crates/bevy_color/Cargo.toml @@ -19,7 +19,7 @@ serde = { version = "1.0", features = [ "derive", ], default-features = false, optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } wgpu-types = { version = "24", default-features = false, optional = true } encase = { version = "0.10", default-features = false, optional = true } diff --git a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs index e2ffe1a6c4..8b6d2593c9 100644 --- a/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs +++ b/crates/bevy_core_pipeline/src/auto_exposure/compensation_curve.rs @@ -196,6 +196,7 @@ impl RenderAsset for GpuAutoExposureCompensationCurve { source: Self::SourceAsset, _: AssetId, (render_device, render_queue): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let texture = render_device.create_texture_with_data( render_queue, diff --git a/crates/bevy_core_pipeline/src/blit/mod.rs b/crates/bevy_core_pipeline/src/blit/mod.rs index 111b6e443b..8dc655e91f 100644 --- a/crates/bevy_core_pipeline/src/blit/mod.rs +++ b/crates/bevy_core_pipeline/src/blit/mod.rs @@ -10,7 +10,7 @@ use bevy_render::{ RenderApp, }; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use crate::FullscreenShader; /// Adds support for specialized "blit pipelines", which can be used to write one texture to another. pub struct BlitPlugin; @@ -38,7 +38,8 @@ impl Plugin for BlitPlugin { pub struct BlitPipeline { pub texture_bind_group: BindGroupLayout, pub sampler: Sampler, - pub shader: Handle, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, } impl FromWorld for BlitPipeline { @@ -61,7 +62,8 @@ impl FromWorld for BlitPipeline { BlitPipeline { texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "blit.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "blit.wgsl"), } } } @@ -80,9 +82,9 @@ impl SpecializedRenderPipeline for BlitPipeline { RenderPipelineDescriptor { label: Some("blit pipeline".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "fs_main".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs index 88da2db0cc..201d5a6cbd 100644 --- a/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/downsampling_pipeline.rs @@ -1,5 +1,6 @@ +use crate::FullscreenShader; + use super::{Bloom, BLOOM_TEXTURE_FORMAT}; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, @@ -27,8 +28,10 @@ pub struct BloomDownsamplingPipeline { /// Layout with a texture, a sampler, and uniforms pub bind_group_layout: BindGroupLayout, pub sampler: Sampler, - /// The shader asset handle. - pub shader: Handle, + /// The asset handle for the fullscreen vertex shader. + pub fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + pub fragment_shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -81,7 +84,8 @@ impl FromWorld for BloomDownsamplingPipeline { BloomDownsamplingPipeline { bind_group_layout, sampler, - shader: load_embedded_asset!(world, "bloom.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "bloom.wgsl"), } } } @@ -122,9 +126,9 @@ impl SpecializedRenderPipeline for BloomDownsamplingPipeline { .into(), ), layout, - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point, targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs index f381e664a9..c49a9d5b16 100644 --- a/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs +++ b/crates/bevy_core_pipeline/src/bloom/upsampling_pipeline.rs @@ -1,7 +1,8 @@ +use crate::FullscreenShader; + use super::{ downsampling_pipeline::BloomUniforms, Bloom, BloomCompositeMode, BLOOM_TEXTURE_FORMAT, }; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_asset::{load_embedded_asset, Handle}; use bevy_ecs::{ prelude::{Component, Entity}, @@ -27,8 +28,10 @@ pub struct UpsamplingPipelineIds { #[derive(Resource)] pub struct BloomUpsamplingPipeline { pub bind_group_layout: BindGroupLayout, - /// The shader asset handle. - pub shader: Handle, + /// The asset handle for the fullscreen vertex shader. + pub fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + pub fragment_shader: Handle, } #[derive(PartialEq, Eq, Hash, Clone)] @@ -58,7 +61,8 @@ impl FromWorld for BloomUpsamplingPipeline { BloomUpsamplingPipeline { bind_group_layout, - shader: load_embedded_asset!(world, "bloom.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "bloom.wgsl"), } } } @@ -108,9 +112,9 @@ impl SpecializedRenderPipeline for BloomUpsamplingPipeline { RenderPipelineDescriptor { label: Some("bloom_upsampling_pipeline".into()), layout: vec![self.bind_group_layout.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "upsample".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs index 0e9465aafa..2cf44a4015 100644 --- a/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs +++ b/crates/bevy_core_pipeline/src/deferred/copy_lighting_id.rs @@ -1,6 +1,6 @@ use crate::{ - fullscreen_vertex_shader::fullscreen_shader_vertex_state, prepass::{DeferredPrepass, ViewPrepassTextures}, + FullscreenShader, }; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, load_embedded_asset}; @@ -130,6 +130,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { ), ); + let vertex_state = world.resource::().to_vertex_state(); let shader = load_embedded_asset!(world, "copy_deferred_lighting_id.wgsl"); let pipeline_id = @@ -138,7 +139,7 @@ impl FromWorld for CopyDeferredLightingIdPipeline { .queue_render_pipeline(RenderPipelineDescriptor { label: Some("copy_deferred_lighting_id_pipeline".into()), layout: vec![layout.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state, fragment: Some(FragmentState { shader, shader_defs: vec![], diff --git a/crates/bevy_core_pipeline/src/dof/mod.rs b/crates/bevy_core_pipeline/src/dof/mod.rs index c27d81180d..7e2f52e3fc 100644 --- a/crates/bevy_core_pipeline/src/dof/mod.rs +++ b/crates/bevy_core_pipeline/src/dof/mod.rs @@ -66,7 +66,7 @@ use crate::{ graph::{Core3d, Node3d}, Camera3d, DEPTH_TEXTURE_SAMPLING_SUPPORTED, }, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; /// A plugin that adds support for the depth of field effect to Bevy. @@ -325,8 +325,10 @@ pub struct DepthOfFieldPipeline { /// The bind group layout shared among all invocations of the depth of field /// shader. global_bind_group_layout: BindGroupLayout, - /// The shader asset handle. - shader: Handle, + /// The asset handle for the fullscreen vertex shader. + fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + fragment_shader: Handle, } impl ViewNode for DepthOfFieldNode { @@ -678,13 +680,15 @@ pub fn prepare_depth_of_field_pipelines( &ViewDepthOfFieldBindGroupLayouts, &Msaa, )>, + fullscreen_shader: Res, asset_server: Res, ) { for (entity, view, depth_of_field, view_bind_group_layouts, msaa) in view_targets.iter() { let dof_pipeline = DepthOfFieldPipeline { view_bind_group_layouts: view_bind_group_layouts.clone(), global_bind_group_layout: global_bind_group_layout.layout.clone(), - shader: load_embedded_asset!(asset_server.as_ref(), "dof.wgsl"), + fullscreen_shader: fullscreen_shader.clone(), + fragment_shader: load_embedded_asset!(asset_server.as_ref(), "dof.wgsl"), }; // We'll need these two flags to create the `DepthOfFieldPipelineKey`s. @@ -797,12 +801,12 @@ impl SpecializedRenderPipeline for DepthOfFieldPipeline { label: Some("depth of field pipeline".into()), layout, push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), primitive: default(), depth_stencil: None, multisample: default(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: match key.pass { DofPass::GaussianHorizontal => "gaussian_horizontal".into(), diff --git a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs index fee17d1ec6..de8aa856c9 100644 --- a/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs +++ b/crates/bevy_core_pipeline/src/fullscreen_vertex_shader/mod.rs @@ -1,25 +1,40 @@ -use bevy_asset::{weak_handle, Handle}; +use bevy_asset::{load_embedded_asset, Handle}; +use bevy_ecs::{resource::Resource, world::FromWorld}; use bevy_render::{prelude::Shader, render_resource::VertexState}; -pub const FULLSCREEN_SHADER_HANDLE: Handle = - weak_handle!("481fb759-d0b1-4175-8319-c439acde30a2"); +/// A shader that renders to the whole screen. Useful for post-processing. +#[derive(Resource, Clone)] +pub struct FullscreenShader(Handle); -/// uses the [`FULLSCREEN_SHADER_HANDLE`] to output a -/// ```wgsl -/// struct FullscreenVertexOutput { -/// [[builtin(position)]] -/// position: vec4; -/// [[location(0)]] -/// uv: vec2; -/// }; -/// ``` -/// from the vertex shader. -/// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);` -pub fn fullscreen_shader_vertex_state() -> VertexState { - VertexState { - shader: FULLSCREEN_SHADER_HANDLE, - shader_defs: Vec::new(), - entry_point: "fullscreen_vertex_shader".into(), - buffers: Vec::new(), +impl FromWorld for FullscreenShader { + fn from_world(world: &mut bevy_ecs::world::World) -> Self { + Self(load_embedded_asset!(world, "fullscreen.wgsl")) + } +} + +impl FullscreenShader { + /// Gets the raw shader handle. + pub fn shader(&self) -> Handle { + self.0.clone() + } + + /// Creates a [`VertexState`] that uses the [`FullscreenShader`] to output a + /// ```wgsl + /// struct FullscreenVertexOutput { + /// @builtin(position) + /// position: vec4; + /// @location(0) + /// uv: vec2; + /// }; + /// ``` + /// from the vertex shader. + /// The draw call should render one triangle: `render_pass.draw(0..3, 0..1);` + pub fn to_vertex_state(&self) -> VertexState { + VertexState { + shader: self.0.clone(), + shader_defs: Vec::new(), + entry_point: "fullscreen_vertex_shader".into(), + buffers: Vec::new(), + } } } diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index 6c6bc7ccc7..3526b3e8fb 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -14,18 +14,20 @@ pub mod core_3d; pub mod deferred; pub mod dof; pub mod experimental; -pub mod fullscreen_vertex_shader; pub mod motion_blur; pub mod msaa_writeback; pub mod oit; pub mod post_process; pub mod prepass; -mod skybox; pub mod tonemapping; pub mod upscaling; +pub use fullscreen_vertex_shader::FullscreenShader; pub use skybox::Skybox; +mod fullscreen_vertex_shader; +mod skybox; + /// The core pipeline prelude. /// /// This includes the most common types in this crate, re-exported for your convenience. @@ -42,7 +44,6 @@ use crate::{ deferred::copy_lighting_id::CopyDeferredLightingIdPlugin, dof::DepthOfFieldPlugin, experimental::mip_generation::MipGenerationPlugin, - fullscreen_vertex_shader::FULLSCREEN_SHADER_HANDLE, motion_blur::MotionBlurPlugin, msaa_writeback::MsaaWritebackPlugin, post_process::PostProcessingPlugin, @@ -51,8 +52,8 @@ use crate::{ upscaling::UpscalingPlugin, }; use bevy_app::{App, Plugin}; -use bevy_asset::load_internal_asset; -use bevy_render::prelude::Shader; +use bevy_asset::embedded_asset; +use bevy_render::RenderApp; use oit::OrderIndependentTransparencyPlugin; #[derive(Default)] @@ -60,17 +61,13 @@ pub struct CorePipelinePlugin; impl Plugin for CorePipelinePlugin { fn build(&self, app: &mut App) { - load_internal_asset!( - app, - FULLSCREEN_SHADER_HANDLE, - "fullscreen_vertex_shader/fullscreen.wgsl", - Shader::from_wgsl - ); + embedded_asset!(app, "fullscreen_vertex_shader/fullscreen.wgsl"); app.register_type::() .register_type::() .register_type::() .register_type::() + .init_resource::() .add_plugins((Core2dPlugin, Core3dPlugin, CopyDeferredLightingIdPlugin)) .add_plugins(( BlitPlugin, @@ -85,4 +82,11 @@ impl Plugin for CorePipelinePlugin { MipGenerationPlugin, )); } + + fn finish(&self, app: &mut App) { + let Some(render_app) = app.get_sub_app_mut(RenderApp) else { + return; + }; + render_app.init_resource::(); + } } diff --git a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs index dfd4bca103..9e36e508dc 100644 --- a/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs +++ b/crates/bevy_core_pipeline/src/motion_blur/pipeline.rs @@ -25,7 +25,7 @@ use bevy_render::{ view::{ExtractedView, Msaa, ViewTarget}, }; -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; +use crate::FullscreenShader; use super::MotionBlurUniform; @@ -34,11 +34,16 @@ pub struct MotionBlurPipeline { pub(crate) sampler: Sampler, pub(crate) layout: BindGroupLayout, pub(crate) layout_msaa: BindGroupLayout, - pub(crate) shader: Handle, + pub(crate) fullscreen_shader: FullscreenShader, + pub(crate) fragment_shader: Handle, } impl MotionBlurPipeline { - pub(crate) fn new(render_device: &RenderDevice, shader: Handle) -> Self { + pub(crate) fn new( + render_device: &RenderDevice, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, + ) -> Self { let mb_layout = &BindGroupLayoutEntries::sequential( ShaderStages::FRAGMENT, ( @@ -84,7 +89,8 @@ impl MotionBlurPipeline { sampler, layout, layout_msaa, - shader, + fullscreen_shader, + fragment_shader, } } } @@ -93,8 +99,9 @@ impl FromWorld for MotionBlurPipeline { fn from_world(render_world: &mut bevy_ecs::world::World) -> Self { let render_device = render_world.resource::().clone(); - let shader = load_embedded_asset!(render_world, "motion_blur.wgsl"); - MotionBlurPipeline::new(&render_device, shader) + let fullscreen_shader = render_world.resource::().clone(); + let fragment_shader = load_embedded_asset!(render_world, "motion_blur.wgsl"); + MotionBlurPipeline::new(&render_device, fullscreen_shader, fragment_shader) } } @@ -128,9 +135,9 @@ impl SpecializedRenderPipeline for MotionBlurPipeline { RenderPipelineDescriptor { label: Some("motion_blur_pipeline".into()), layout, - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs index fe62d0c9b1..7067e5f83b 100644 --- a/crates/bevy_core_pipeline/src/oit/resolve/mod.rs +++ b/crates/bevy_core_pipeline/src/oit/resolve/mod.rs @@ -1,7 +1,4 @@ -use crate::{ - fullscreen_vertex_shader::fullscreen_shader_vertex_state, - oit::OrderIndependentTransparencySettings, -}; +use crate::{oit::OrderIndependentTransparencySettings, FullscreenShader}; use bevy_app::Plugin; use bevy_asset::{embedded_asset, load_embedded_asset, AssetServer}; use bevy_derive::Deref; @@ -156,6 +153,7 @@ pub fn queue_oit_resolve_pipeline( ), With, >, + fullscreen_shader: Res, asset_server: Res, // Store the key with the id to make the clean up logic easier. // This also means it will always replace the entry if the key changes so nothing to clean up. @@ -176,7 +174,12 @@ pub fn queue_oit_resolve_pipeline( } } - let desc = specialize_oit_resolve_pipeline(key, &resolve_pipeline, &asset_server); + let desc = specialize_oit_resolve_pipeline( + key, + &resolve_pipeline, + &fullscreen_shader, + &asset_server, + ); let pipeline_id = pipeline_cache.queue_render_pipeline(desc); commands.entity(e).insert(OitResolvePipelineId(pipeline_id)); @@ -194,6 +197,7 @@ pub fn queue_oit_resolve_pipeline( fn specialize_oit_resolve_pipeline( key: OitResolvePipelineKey, resolve_pipeline: &OitResolvePipeline, + fullscreen_shader: &FullscreenShader, asset_server: &AssetServer, ) -> RenderPipelineDescriptor { let format = if key.hdr { @@ -224,7 +228,7 @@ fn specialize_oit_resolve_pipeline( write_mask: ColorWrites::ALL, })], }), - vertex: fullscreen_shader_vertex_state(), + vertex: fullscreen_shader.to_vertex_state(), primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState::default(), diff --git a/crates/bevy_core_pipeline/src/post_process/mod.rs b/crates/bevy_core_pipeline/src/post_process/mod.rs index 0f188d1d73..f7d2501b41 100644 --- a/crates/bevy_core_pipeline/src/post_process/mod.rs +++ b/crates/bevy_core_pipeline/src/post_process/mod.rs @@ -44,7 +44,7 @@ use bevy_utils::prelude::default; use crate::{ core_2d::graph::{Core2d, Node2d}, core_3d::graph::{Core3d, Node3d}, - fullscreen_vertex_shader, + FullscreenShader, }; /// The handle to the default chromatic aberration lookup texture. @@ -130,8 +130,10 @@ pub struct PostProcessingPipeline { source_sampler: Sampler, /// Specifies how to sample the chromatic aberration gradient. chromatic_aberration_lut_sampler: Sampler, - /// The shader asset handle. - shader: Handle, + /// The asset handle for the fullscreen vertex shader. + fullscreen_shader: FullscreenShader, + /// The fragment shader asset handle. + fragment_shader: Handle, } /// A key that uniquely identifies a built-in postprocessing pipeline. @@ -308,7 +310,8 @@ impl FromWorld for PostProcessingPipeline { bind_group_layout, source_sampler, chromatic_aberration_lut_sampler, - shader: load_embedded_asset!(world, "post_process.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "post_process.wgsl"), } } } @@ -320,9 +323,9 @@ impl SpecializedRenderPipeline for PostProcessingPipeline { RenderPipelineDescriptor { label: Some("postprocessing".into()), layout: vec![self.bind_group_layout.clone()], - vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "fragment_main".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_core_pipeline/src/skybox/prepass.rs b/crates/bevy_core_pipeline/src/skybox/prepass.rs index a027f69f93..ad63339d05 100644 --- a/crates/bevy_core_pipeline/src/skybox/prepass.rs +++ b/crates/bevy_core_pipeline/src/skybox/prepass.rs @@ -27,7 +27,7 @@ use crate::{ prepass_target_descriptors, MotionVectorPrepass, NormalPrepass, PreviousViewData, PreviousViewUniforms, }, - Skybox, + FullscreenShader, Skybox, }; /// This pipeline writes motion vectors to the prepass for all [`Skybox`]es. @@ -38,7 +38,8 @@ use crate::{ #[derive(Resource)] pub struct SkyboxPrepassPipeline { bind_group_layout: BindGroupLayout, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// Used to specialize the [`SkyboxPrepassPipeline`]. @@ -73,7 +74,8 @@ impl FromWorld for SkyboxPrepassPipeline { ), ), ), - shader: load_embedded_asset!(world, "skybox_prepass.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "skybox_prepass.wgsl"), } } } @@ -86,7 +88,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { label: Some("skybox_prepass_pipeline".into()), layout: vec![self.bind_group_layout.clone()], push_constant_ranges: vec![], - vertex: crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), primitive: default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -101,7 +103,7 @@ impl SpecializedRenderPipeline for SkyboxPrepassPipeline { alpha_to_coverage_enabled: false, }, fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs: vec![], entry_point: "fragment".into(), targets: prepass_target_descriptors(key.normal_prepass, true, false), diff --git a/crates/bevy_core_pipeline/src/tonemapping/mod.rs b/crates/bevy_core_pipeline/src/tonemapping/mod.rs index 19ac3ef7e2..7453b2bf19 100644 --- a/crates/bevy_core_pipeline/src/tonemapping/mod.rs +++ b/crates/bevy_core_pipeline/src/tonemapping/mod.rs @@ -1,4 +1,3 @@ -use crate::fullscreen_vertex_shader::fullscreen_shader_vertex_state; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, load_embedded_asset, Assets, Handle}; use bevy_ecs::prelude::*; @@ -28,6 +27,8 @@ mod node; use bevy_utils::default; pub use node::TonemappingNode; +use crate::FullscreenShader; + /// 3D LUT (look up table) textures used for tonemapping #[derive(Resource, Clone, ExtractResource)] pub struct TonemappingLuts { @@ -112,7 +113,8 @@ impl Plugin for TonemappingPlugin { pub struct TonemappingPipeline { texture_bind_group: BindGroupLayout, sampler: Sampler, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// Optionally enables a tonemapping shader that attempts to map linear input stimulus into a perceptually uniform image for a given [`Camera`] entity. @@ -273,9 +275,9 @@ impl SpecializedRenderPipeline for TonemappingPipeline { RenderPipelineDescriptor { label: Some("tonemapping pipeline".into()), layout: vec![self.texture_bind_group.clone()], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { @@ -319,7 +321,8 @@ impl FromWorld for TonemappingPipeline { TonemappingPipeline { texture_bind_group: tonemap_texture_bind_group, sampler, - shader: load_embedded_asset!(render_world, "tonemapping.wgsl"), + fullscreen_shader: render_world.resource::().clone(), + fragment_shader: load_embedded_asset!(render_world, "tonemapping.wgsl"), } } } @@ -461,5 +464,6 @@ pub fn lut_placeholder() -> Image { sampler: ImageSampler::Default, texture_view_descriptor: None, asset_usage: RenderAssetUsages::RENDER_WORLD, + copy_on_resize: false, } } diff --git a/crates/bevy_core_widgets/src/core_radio.rs b/crates/bevy_core_widgets/src/core_radio.rs new file mode 100644 index 0000000000..d5dd18fb1a --- /dev/null +++ b/crates/bevy_core_widgets/src/core_radio.rs @@ -0,0 +1,213 @@ +use accesskit::Role; +use bevy_a11y::AccessibilityNode; +use bevy_app::{App, Plugin}; +use bevy_ecs::hierarchy::{ChildOf, Children}; +use bevy_ecs::query::Has; +use bevy_ecs::system::In; +use bevy_ecs::{ + component::Component, + entity::Entity, + observer::On, + query::With, + system::{Commands, Query, SystemId}, +}; +use bevy_input::keyboard::{KeyCode, KeyboardInput}; +use bevy_input::ButtonState; +use bevy_input_focus::FocusedInput; +use bevy_picking::events::{Click, Pointer}; +use bevy_ui::{Checked, InteractionDisabled}; + +/// Headless widget implementation for a "radio button group". This component is used to group +/// multiple [`CoreRadio`] components together, allowing them to behave as a single unit. It +/// implements the tab navigation logic and keyboard shortcuts for radio buttons. +/// +/// The [`CoreRadioGroup`] component does not have any state itself, and makes no assumptions about +/// what, if any, value is associated with each radio button, or what Rust type that value might be. +/// Instead, the output of the group is the entity id of the selected button. The app can then +/// derive the selected value from this using app-specific means, such as accessing a component on +/// the individual buttons. +/// +/// The [`CoreRadioGroup`] doesn't actually set the [`Checked`] states directly, that is presumed to +/// happen by the app or via some external data-binding scheme. Typically, each button would be +/// associated with a particular constant value, and would be checked whenever that value is equal +/// to the group's value. This also means that as long as each button's associated value is unique +/// within the group, it should never be the case that more than one button is selected at a time. +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::RadioGroup)))] +pub struct CoreRadioGroup { + /// Callback which is called when the selected radio button changes. + pub on_change: Option>>, +} + +/// Headless widget implementation for radio buttons. These should be enclosed within a +/// [`CoreRadioGroup`] widget, which is responsible for the mutual exclusion logic. +/// +/// According to the WAI-ARIA best practices document, radio buttons should not be focusable, +/// but rather the enclosing group should be focusable. +/// See / +#[derive(Component, Debug)] +#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checked)] +pub struct CoreRadio; + +fn radio_group_on_key_input( + mut ev: On>, + q_group: Query<&CoreRadioGroup>, + q_radio: Query<(Has, Has), With>, + q_children: Query<&Children>, + mut commands: Commands, +) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) { + let event = &ev.event().input; + if event.state == ButtonState::Pressed + && !event.repeat + && matches!( + event.key_code, + KeyCode::ArrowUp + | KeyCode::ArrowDown + | KeyCode::ArrowLeft + | KeyCode::ArrowRight + | KeyCode::Home + | KeyCode::End + ) + { + let key_code = event.key_code; + ev.propagate(false); + + // Find all radio descendants that are not disabled + let radio_buttons = q_children + .iter_descendants(ev.target()) + .filter_map(|child_id| match q_radio.get(child_id) { + Ok((checked, false)) => Some((child_id, checked)), + Ok((_, true)) | Err(_) => None, + }) + .collect::>(); + if radio_buttons.is_empty() { + return; // No enabled radio buttons in the group + } + let current_index = radio_buttons + .iter() + .position(|(_, checked)| *checked) + .unwrap_or(usize::MAX); // Default to invalid index if none are checked + + let next_index = match key_code { + KeyCode::ArrowUp | KeyCode::ArrowLeft => { + // Navigate to the previous radio button in the group + if current_index == 0 || current_index >= radio_buttons.len() { + // If we're at the first one, wrap around to the last + radio_buttons.len() - 1 + } else { + // Move to the previous one + current_index - 1 + } + } + KeyCode::ArrowDown | KeyCode::ArrowRight => { + // Navigate to the next radio button in the group + if current_index >= radio_buttons.len() - 1 { + // If we're at the last one, wrap around to the first + 0 + } else { + // Move to the next one + current_index + 1 + } + } + KeyCode::Home => { + // Navigate to the first radio button in the group + 0 + } + KeyCode::End => { + // Navigate to the last radio button in the group + radio_buttons.len() - 1 + } + _ => { + return; + } + }; + + if current_index == next_index { + // If the next index is the same as the current, do nothing + return; + } + + let (next_id, _) = radio_buttons[next_index]; + + // Trigger the on_change event for the newly checked radio button + if let Some(on_change) = on_change { + commands.run_system_with(*on_change, next_id); + } + } + } +} + +fn radio_group_on_button_click( + mut ev: On>, + q_group: Query<&CoreRadioGroup>, + q_radio: Query<(Has, Has), With>, + q_parents: Query<&ChildOf>, + q_children: Query<&Children>, + mut commands: Commands, +) { + if let Ok(CoreRadioGroup { on_change }) = q_group.get(ev.target()) { + // Starting with the original target, search upward for a radio button. + let radio_id = if q_radio.contains(ev.original_target()) { + ev.original_target() + } else { + // Search ancestors for the first radio button + let mut found_radio = None; + for ancestor in q_parents.iter_ancestors(ev.original_target()) { + if q_group.contains(ancestor) { + // We reached a radio group before finding a radio button, bail out + return; + } + if q_radio.contains(ancestor) { + found_radio = Some(ancestor); + break; + } + } + + match found_radio { + Some(radio) => radio, + None => return, // No radio button found in the ancestor chain + } + }; + + // Gather all the enabled radio group descendants for exclusion. + let radio_buttons = q_children + .iter_descendants(ev.target()) + .filter_map(|child_id| match q_radio.get(child_id) { + Ok((checked, false)) => Some((child_id, checked)), + Ok((_, true)) | Err(_) => None, + }) + .collect::>(); + + if radio_buttons.is_empty() { + return; // No enabled radio buttons in the group + } + + // Pick out the radio button that is currently checked. + ev.propagate(false); + let current_radio = radio_buttons + .iter() + .find(|(_, checked)| *checked) + .map(|(id, _)| *id); + + if current_radio == Some(radio_id) { + // If they clicked the currently checked radio button, do nothing + return; + } + + // Trigger the on_change event for the newly checked radio button + if let Some(on_change) = on_change { + commands.run_system_with(*on_change, radio_id); + } + } +} + +/// Plugin that adds the observers for the [`CoreRadioGroup`] widget. +pub struct CoreRadioGroupPlugin; + +impl Plugin for CoreRadioGroupPlugin { + fn build(&self, app: &mut App) { + app.add_observer(radio_group_on_key_input) + .add_observer(radio_group_on_button_click); + } +} diff --git a/crates/bevy_core_widgets/src/core_slider.rs b/crates/bevy_core_widgets/src/core_slider.rs index 5a6b90636a..d85f12dd22 100644 --- a/crates/bevy_core_widgets/src/core_slider.rs +++ b/crates/bevy_core_widgets/src/core_slider.rs @@ -23,7 +23,7 @@ use bevy_picking::events::{Drag, DragEnd, DragStart, Pointer, Press}; use bevy_ui::{ComputedNode, ComputedNodeTarget, InteractionDisabled, UiGlobalTransform, UiScale}; /// Defines how the slider should behave when you click on the track (not the thumb). -#[derive(Debug, Default)] +#[derive(Debug, Default, PartialEq, Clone, Copy)] pub enum TrackClick { /// Clicking on the track lets you drag to edit the value, just like clicking on the thumb. #[default] diff --git a/crates/bevy_core_widgets/src/lib.rs b/crates/bevy_core_widgets/src/lib.rs index cdb9142b52..ef9f3db51c 100644 --- a/crates/bevy_core_widgets/src/lib.rs +++ b/crates/bevy_core_widgets/src/lib.rs @@ -16,12 +16,14 @@ mod core_button; mod core_checkbox; +mod core_radio; mod core_slider; use bevy_app::{App, Plugin}; pub use core_button::{CoreButton, CoreButtonPlugin}; pub use core_checkbox::{CoreCheckbox, CoreCheckboxPlugin, SetChecked, ToggleChecked}; +pub use core_radio::{CoreRadio, CoreRadioGroup, CoreRadioGroupPlugin}; pub use core_slider::{ CoreSlider, CoreSliderDragState, CoreSliderPlugin, CoreSliderThumb, SetSliderValue, SliderRange, SliderStep, SliderValue, TrackClick, @@ -33,6 +35,11 @@ pub struct CoreWidgetsPlugin; impl Plugin for CoreWidgetsPlugin { fn build(&self, app: &mut App) { - app.add_plugins((CoreButtonPlugin, CoreCheckboxPlugin, CoreSliderPlugin)); + app.add_plugins(( + CoreButtonPlugin, + CoreCheckboxPlugin, + CoreRadioGroupPlugin, + CoreSliderPlugin, + )); } } diff --git a/crates/bevy_ecs/Cargo.toml b/crates/bevy_ecs/Cargo.toml index 5aa3dfe5ef..1e94aa5eed 100644 --- a/crates/bevy_ecs/Cargo.toml +++ b/crates/bevy_ecs/Cargo.toml @@ -104,7 +104,7 @@ serde = { version = "1", default-features = false, features = [ "serde_derive", ], optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = [ +derive_more = { version = "2", default-features = false, features = [ "from", "display", "into", diff --git a/crates/bevy_ecs/macros/src/component.rs b/crates/bevy_ecs/macros/src/component.rs index ef7fad99f4..040c1b26b6 100644 --- a/crates/bevy_ecs/macros/src/component.rs +++ b/crates/bevy_ecs/macros/src/component.rs @@ -128,9 +128,11 @@ pub fn derive_component(input: TokenStream) -> TokenStream { let map_entities = map_entities( &ast.data, + &bevy_ecs_path, Ident::new("this", Span::call_site()), relationship.is_some(), relationship_target.is_some(), + attrs.map_entities ).map(|map_entities_impl| quote! { fn map_entities(this: &mut Self, mapper: &mut M) { use #bevy_ecs_path::entity::MapEntities; @@ -339,10 +341,19 @@ const ENTITIES: &str = "entities"; pub(crate) fn map_entities( data: &Data, + bevy_ecs_path: &Path, self_ident: Ident, is_relationship: bool, is_relationship_target: bool, + map_entities_attr: Option, ) -> Option { + if let Some(map_entities_override) = map_entities_attr { + let map_entities_tokens = map_entities_override.to_token_stream(bevy_ecs_path); + return Some(quote!( + #map_entities_tokens(#self_ident, mapper) + )); + } + match data { Data::Struct(DataStruct { fields, .. }) => { let mut map = Vec::with_capacity(fields.len()); @@ -430,6 +441,7 @@ pub const ON_INSERT: &str = "on_insert"; pub const ON_REPLACE: &str = "on_replace"; pub const ON_REMOVE: &str = "on_remove"; pub const ON_DESPAWN: &str = "on_despawn"; +pub const MAP_ENTITIES: &str = "map_entities"; pub const IMMUTABLE: &str = "immutable"; pub const CLONE_BEHAVIOR: &str = "clone_behavior"; @@ -484,6 +496,56 @@ impl Parse for HookAttributeKind { } } +#[derive(Debug)] +pub(super) enum MapEntitiesAttributeKind { + /// expressions like function or struct names + /// + /// structs will throw compile errors on the code generation so this is safe + Path(ExprPath), + /// When no value is specified + Default, +} + +impl MapEntitiesAttributeKind { + fn from_expr(value: Expr) -> Result { + match value { + Expr::Path(path) => Ok(Self::Path(path)), + // throw meaningful error on all other expressions + _ => Err(syn::Error::new( + value.span(), + [ + "Not supported in this position, please use one of the following:", + "- path to function", + "- nothing to default to MapEntities implementation", + ] + .join("\n"), + )), + } + } + + fn to_token_stream(&self, bevy_ecs_path: &Path) -> TokenStream2 { + match self { + MapEntitiesAttributeKind::Path(path) => path.to_token_stream(), + MapEntitiesAttributeKind::Default => { + quote!( + ::map_entities + ) + } + } + } +} + +impl Parse for MapEntitiesAttributeKind { + fn parse(input: syn::parse::ParseStream) -> Result { + if input.peek(Token![=]) { + input.parse::()?; + input.parse::().and_then(Self::from_expr) + } else { + Ok(Self::Default) + } + } +} + struct Attrs { storage: StorageTy, requires: Option>, @@ -496,6 +558,7 @@ struct Attrs { relationship_target: Option, immutable: bool, clone_behavior: Option, + map_entities: Option, } #[derive(Clone, Copy)] @@ -535,6 +598,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { relationship_target: None, immutable: false, clone_behavior: None, + map_entities: None, }; let mut require_paths = HashSet::new(); @@ -573,6 +637,9 @@ fn parse_component_attr(ast: &DeriveInput) -> Result { } else if nested.path.is_ident(CLONE_BEHAVIOR) { attrs.clone_behavior = Some(nested.value()?.parse()?); Ok(()) + } else if nested.path.is_ident(MAP_ENTITIES) { + attrs.map_entities = Some(nested.input.parse::()?); + Ok(()) } else { Err(nested.error("Unsupported attribute")) } diff --git a/crates/bevy_ecs/macros/src/lib.rs b/crates/bevy_ecs/macros/src/lib.rs index 7750f97259..9bc3e5913e 100644 --- a/crates/bevy_ecs/macros/src/lib.rs +++ b/crates/bevy_ecs/macros/src/lib.rs @@ -220,9 +220,11 @@ pub fn derive_map_entities(input: TokenStream) -> TokenStream { let map_entities_impl = map_entities( &ast.data, + &ecs_path, Ident::new("self", Span::call_site()), false, false, + None, ); let struct_name = &ast.ident; diff --git a/crates/bevy_ecs/src/component.rs b/crates/bevy_ecs/src/component.rs index 831402240d..615c5903f8 100644 --- a/crates/bevy_ecs/src/component.rs +++ b/crates/bevy_ecs/src/component.rs @@ -578,6 +578,65 @@ pub trait Component: Send + Sync + 'static { /// items: Vec> /// } /// ``` + /// + /// You might need more specialized logic. A likely cause of this is your component contains collections of entities that + /// don't implement [`MapEntities`](crate::entity::MapEntities). In that case, you can annotate your component with + /// `#[component(map_entities)]`. Using this attribute, you must implement `MapEntities` for the + /// component itself, and this method will simply call that implementation. + /// + /// ``` + /// # use bevy_ecs::{component::Component, entity::{Entity, MapEntities, EntityMapper}}; + /// # use std::collections::HashMap; + /// #[derive(Component)] + /// #[component(map_entities)] + /// struct Inventory { + /// items: HashMap + /// } + /// + /// impl MapEntities for Inventory { + /// fn map_entities(&mut self, entity_mapper: &mut M) { + /// self.items = self.items + /// .drain() + /// .map(|(id, count)|(entity_mapper.get_mapped(id), count)) + /// .collect(); + /// } + /// } + /// # let a = Entity::from_bits(0x1_0000_0001); + /// # let b = Entity::from_bits(0x1_0000_0002); + /// # let mut inv = Inventory { items: Default::default() }; + /// # inv.items.insert(a, 10); + /// # ::map_entities(&mut inv, &mut (a,b)); + /// # assert_eq!(inv.items.get(&b), Some(&10)); + /// ```` + /// + /// Alternatively, you can specify the path to a function with `#[component(map_entities = function_path)]`, similar to component hooks. + /// In this case, the inputs of the function should mirror the inputs to this method, with the second parameter being generic. + /// + /// ``` + /// # use bevy_ecs::{component::Component, entity::{Entity, MapEntities, EntityMapper}}; + /// # use std::collections::HashMap; + /// #[derive(Component)] + /// #[component(map_entities = map_the_map)] + /// // Also works: map_the_map:: or map_the_map::<_> + /// struct Inventory { + /// items: HashMap + /// } + /// + /// fn map_the_map(inv: &mut Inventory, entity_mapper: &mut M) { + /// inv.items = inv.items + /// .drain() + /// .map(|(id, count)|(entity_mapper.get_mapped(id), count)) + /// .collect(); + /// } + /// # let a = Entity::from_bits(0x1_0000_0001); + /// # let b = Entity::from_bits(0x1_0000_0002); + /// # let mut inv = Inventory { items: Default::default() }; + /// # inv.items.insert(a, 10); + /// # ::map_entities(&mut inv, &mut (a,b)); + /// # assert_eq!(inv.items.get(&b), Some(&10)); + /// ```` + /// + /// You can use the turbofish (`::`) to specify parameters when a function is generic, using either M or _ for the type of the mapper parameter. #[inline] fn map_entities(_this: &mut Self, _mapper: &mut E) {} } diff --git a/crates/bevy_ecs/src/entity/clone_entities.rs b/crates/bevy_ecs/src/entity/clone_entities.rs index 02d2491b7a..08da93c261 100644 --- a/crates/bevy_ecs/src/entity/clone_entities.rs +++ b/crates/bevy_ecs/src/entity/clone_entities.rs @@ -1,13 +1,14 @@ -use alloc::{borrow::ToOwned, boxed::Box, collections::VecDeque, vec::Vec}; -use bevy_platform::collections::{HashMap, HashSet}; +use alloc::{boxed::Box, collections::VecDeque, vec::Vec}; +use bevy_platform::collections::{hash_map::Entry, HashMap, HashSet}; use bevy_ptr::{Ptr, PtrMut}; use bevy_utils::prelude::DebugName; use bumpalo::Bump; -use core::any::TypeId; +use core::{any::TypeId, cell::LazyCell, ops::Range}; +use derive_more::derive::From; use crate::{ archetype::Archetype, - bundle::Bundle, + bundle::{Bundle, BundleId, InsertMode}, component::{Component, ComponentCloneBehavior, ComponentCloneFn, ComponentId, ComponentInfo}, entity::{hash_map::EntityHashMap, Entities, Entity, EntityMapper}, query::DebugCheckedUnwrap, @@ -81,7 +82,7 @@ pub struct ComponentCloneCtx<'a, 'b> { source: Entity, target: Entity, component_info: &'a ComponentInfo, - entity_cloner: &'a mut EntityCloner, + state: &'a mut EntityClonerState, mapper: &'a mut dyn EntityMapper, #[cfg(feature = "bevy_reflect")] type_registry: Option<&'a crate::reflect::AppTypeRegistry>, @@ -105,7 +106,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { bundle_scratch: &'a mut BundleScratch<'b>, entities: &'a Entities, component_info: &'a ComponentInfo, - entity_cloner: &'a mut EntityCloner, + entity_cloner: &'a mut EntityClonerState, mapper: &'a mut dyn EntityMapper, #[cfg(feature = "bevy_reflect")] type_registry: Option<&'a crate::reflect::AppTypeRegistry>, #[cfg(not(feature = "bevy_reflect"))] type_registry: Option<&'a ()>, @@ -120,7 +121,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { entities, mapper, component_info, - entity_cloner, + state: entity_cloner, type_registry, } } @@ -155,7 +156,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// [`RelationshipTarget::LINKED_SPAWN`](crate::relationship::RelationshipTarget::LINKED_SPAWN) will also be cloned. #[inline] pub fn linked_cloning(&self) -> bool { - self.entity_cloner.linked_cloning + self.state.linked_cloning } /// Returns this context's [`EntityMapper`]. @@ -272,7 +273,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { pub fn queue_entity_clone(&mut self, entity: Entity) { let target = self.entities.reserve_entity(); self.mapper.set_mapped(entity, target); - self.entity_cloner.clone_queue.push_back(entity); + self.state.clone_queue.push_back(entity); } /// Queues a deferred clone operation, which will run with exclusive [`World`] access immediately after calling the clone handler for each component on an entity. @@ -281,13 +282,12 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { &mut self, deferred: impl FnOnce(&mut World, &mut dyn EntityMapper) + 'static, ) { - self.entity_cloner - .deferred_commands - .push_back(Box::new(deferred)); + self.state.deferred_commands.push_back(Box::new(deferred)); } } -/// A configuration determining how to clone entities. This can be built using [`EntityCloner::build`], which +/// A configuration determining how to clone entities. This can be built using [`EntityCloner::build_opt_out`]/ +/// [`opt_in`](EntityCloner::build_opt_in), which /// returns an [`EntityClonerBuilder`]. /// /// After configuration is complete an entity can be cloned using [`Self::clone_entity`]. @@ -308,7 +308,7 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// let entity = world.spawn(component.clone()).id(); /// let entity_clone = world.spawn_empty().id(); /// -/// EntityCloner::build(&mut world).clone_entity(entity, entity_clone); +/// EntityCloner::build_opt_out(&mut world).clone_entity(entity, entity_clone); /// /// assert!(world.get::(entity_clone).is_some_and(|c| *c == component)); ///``` @@ -340,32 +340,10 @@ impl<'a, 'b> ComponentCloneCtx<'a, 'b> { /// 2. component-defined handler using [`Component::clone_behavior`] /// 3. default handler override using [`EntityClonerBuilder::with_default_clone_fn`]. /// 4. reflect-based or noop default clone handler depending on if `bevy_reflect` feature is enabled or not. +#[derive(Default)] pub struct EntityCloner { - filter_allows_components: bool, - filter: HashSet, - filter_required: HashSet, - clone_behavior_overrides: HashMap, - move_components: bool, - linked_cloning: bool, - default_clone_fn: ComponentCloneFn, - clone_queue: VecDeque, - deferred_commands: VecDeque>, -} - -impl Default for EntityCloner { - fn default() -> Self { - Self { - filter_allows_components: false, - move_components: false, - linked_cloning: false, - default_clone_fn: ComponentCloneBehavior::global_default_fn(), - filter: Default::default(), - filter_required: Default::default(), - clone_behavior_overrides: Default::default(), - clone_queue: Default::default(), - deferred_commands: Default::default(), - } - } + filter: EntityClonerFilter, + state: EntityClonerState, } /// An expandable scratch space for defining a dynamic bundle. @@ -433,12 +411,33 @@ impl<'a> BundleScratch<'a> { } impl EntityCloner { - /// Returns a new [`EntityClonerBuilder`] using the given `world`. - pub fn build(world: &mut World) -> EntityClonerBuilder { + /// Returns a new [`EntityClonerBuilder`] using the given `world` with the [`OptOut`] configuration. + /// + /// This builder tries to clone every component from the source entity except for components that were + /// explicitly denied, for example by using the [`deny`](EntityClonerBuilder::deny) method. + /// + /// Required components are not considered by denied components and must be explicitly denied as well if desired. + pub fn build_opt_out(world: &mut World) -> EntityClonerBuilder { EntityClonerBuilder { world, - attach_required_components: true, - entity_cloner: EntityCloner::default(), + filter: Default::default(), + state: Default::default(), + } + } + + /// Returns a new [`EntityClonerBuilder`] using the given `world` with the [`OptIn`] configuration. + /// + /// This builder tries to clone every component that was explicitly allowed from the source entity, + /// for example by using the [`allow`](EntityClonerBuilder::allow) method. + /// + /// Components allowed to be cloned through this builder would also allow their required components, + /// which will be cloned from the source entity only if the target entity does not contain them already. + /// To skip adding required components see [`without_required_components`](EntityClonerBuilder::without_required_components). + pub fn build_opt_in(world: &mut World) -> EntityClonerBuilder { + EntityClonerBuilder { + world, + filter: Default::default(), + state: Default::default(), } } @@ -446,116 +445,7 @@ impl EntityCloner { /// This will produce "deep" / recursive clones of relationship trees that have "linked spawn". #[inline] pub fn linked_cloning(&self) -> bool { - self.linked_cloning - } - - /// Clones and inserts components from the `source` entity into the entity mapped by `mapper` from `source` using the stored configuration. - fn clone_entity_internal( - &mut self, - world: &mut World, - source: Entity, - mapper: &mut dyn EntityMapper, - relationship_hook_insert_mode: RelationshipHookMode, - ) -> Entity { - let target = mapper.get_mapped(source); - // PERF: reusing allocated space across clones would be more efficient. Consider an allocation model similar to `Commands`. - let bundle_scratch_allocator = Bump::new(); - let mut bundle_scratch: BundleScratch; - { - let world = world.as_unsafe_world_cell(); - let source_entity = world.get_entity(source).expect("Source entity must exist"); - let target_archetype = (!self.filter_required.is_empty()).then(|| { - world - .get_entity(target) - .expect("Target entity must exist") - .archetype() - }); - - #[cfg(feature = "bevy_reflect")] - // SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone - // the registry, which prevents future conflicts. - let app_registry = unsafe { - world - .get_resource::() - .cloned() - }; - #[cfg(not(feature = "bevy_reflect"))] - let app_registry = Option::<()>::None; - - let archetype = source_entity.archetype(); - bundle_scratch = BundleScratch::with_capacity(archetype.component_count()); - - for component in archetype.components() { - if !self.is_cloning_allowed(&component, target_archetype) { - continue; - } - - let handler = match self.clone_behavior_overrides.get(&component) { - Some(clone_behavior) => clone_behavior.resolve(self.default_clone_fn), - None => world - .components() - .get_info(component) - .map(|info| info.clone_behavior().resolve(self.default_clone_fn)) - .unwrap_or(self.default_clone_fn), - }; - - // SAFETY: This component exists because it is present on the archetype. - let info = unsafe { world.components().get_info_unchecked(component) }; - - // SAFETY: - // - There are no other mutable references to source entity. - // - `component` is from `source_entity`'s archetype - let source_component_ptr = - unsafe { source_entity.get_by_id(component).debug_checked_unwrap() }; - - let source_component = SourceComponent { - info, - ptr: source_component_ptr, - }; - - // SAFETY: - // - `components` and `component` are from the same world - // - `source_component_ptr` is valid and points to the same type as represented by `component` - let mut ctx = unsafe { - ComponentCloneCtx::new( - component, - source, - target, - &bundle_scratch_allocator, - &mut bundle_scratch, - world.entities(), - info, - self, - mapper, - app_registry.as_ref(), - ) - }; - - (handler)(&source_component, &mut ctx); - } - } - - world.flush(); - - for deferred in self.deferred_commands.drain(..) { - (deferred)(world, mapper); - } - - if !world.entities.contains(target) { - panic!("Target entity does not exist"); - } - - if self.move_components { - world - .entity_mut(source) - .remove_by_ids(&bundle_scratch.component_ids); - } - - // SAFETY: - // - All `component_ids` are from the same world as `target` entity - // - All `component_data_ptrs` are valid types represented by `component_ids` - unsafe { bundle_scratch.write(world, target, relationship_hook_insert_mode) }; - target + self.state.linked_cloning } /// Clones and inserts components from the `source` entity into `target` entity using the stored configuration. @@ -587,10 +477,29 @@ impl EntityCloner { world: &mut World, source: Entity, mapper: &mut dyn EntityMapper, + ) -> Entity { + Self::clone_entity_mapped_internal(&mut self.state, &mut self.filter, world, source, mapper) + } + + #[track_caller] + #[inline] + fn clone_entity_mapped_internal( + state: &mut EntityClonerState, + filter: &mut impl CloneByFilter, + world: &mut World, + source: Entity, + mapper: &mut dyn EntityMapper, ) -> Entity { // All relationships on the root should have their hooks run - let target = self.clone_entity_internal(world, source, mapper, RelationshipHookMode::Run); - let child_hook_insert_mode = if self.linked_cloning { + let target = Self::clone_entity_internal( + state, + filter, + world, + source, + mapper, + RelationshipHookMode::Run, + ); + let child_hook_insert_mode = if state.linked_cloning { // When spawning "linked relationships", we want to ignore hooks for relationships we are spawning, while // still registering with original relationship targets that are "not linked" to the current recursive spawn. RelationshipHookMode::RunIfNotLinked @@ -600,9 +509,16 @@ impl EntityCloner { RelationshipHookMode::Run }; loop { - let queued = self.clone_queue.pop_front(); + let queued = state.clone_queue.pop_front(); if let Some(queued) = queued { - self.clone_entity_internal(world, queued, mapper, child_hook_insert_mode); + Self::clone_entity_internal( + state, + filter, + world, + queued, + mapper, + child_hook_insert_mode, + ); } else { break; } @@ -610,58 +526,170 @@ impl EntityCloner { target } - fn is_cloning_allowed( - &self, - component: &ComponentId, - target_archetype: Option<&Archetype>, - ) -> bool { - if self.filter_allows_components { - self.filter.contains(component) - || target_archetype.is_some_and(|archetype| { - !archetype.contains(*component) && self.filter_required.contains(component) - }) - } else { - !self.filter.contains(component) && !self.filter_required.contains(component) + /// Clones and inserts components from the `source` entity into the entity mapped by `mapper` from `source` using the stored configuration. + fn clone_entity_internal( + state: &mut EntityClonerState, + filter: &mut impl CloneByFilter, + world: &mut World, + source: Entity, + mapper: &mut dyn EntityMapper, + relationship_hook_insert_mode: RelationshipHookMode, + ) -> Entity { + let target = mapper.get_mapped(source); + // PERF: reusing allocated space across clones would be more efficient. Consider an allocation model similar to `Commands`. + let bundle_scratch_allocator = Bump::new(); + let mut bundle_scratch: BundleScratch; + { + let world = world.as_unsafe_world_cell(); + let source_entity = world.get_entity(source).expect("Source entity must exist"); + + #[cfg(feature = "bevy_reflect")] + // SAFETY: we have unique access to `world`, nothing else accesses the registry at this moment, and we clone + // the registry, which prevents future conflicts. + let app_registry = unsafe { + world + .get_resource::() + .cloned() + }; + #[cfg(not(feature = "bevy_reflect"))] + let app_registry = Option::<()>::None; + + let source_archetype = source_entity.archetype(); + bundle_scratch = BundleScratch::with_capacity(source_archetype.component_count()); + + let target_archetype = LazyCell::new(|| { + world + .get_entity(target) + .expect("Target entity must exist") + .archetype() + }); + + filter.clone_components(source_archetype, target_archetype, |component| { + let handler = match state.clone_behavior_overrides.get(&component) { + Some(clone_behavior) => clone_behavior.resolve(state.default_clone_fn), + None => world + .components() + .get_info(component) + .map(|info| info.clone_behavior().resolve(state.default_clone_fn)) + .unwrap_or(state.default_clone_fn), + }; + + // SAFETY: This component exists because it is present on the archetype. + let info = unsafe { world.components().get_info_unchecked(component) }; + + // SAFETY: + // - There are no other mutable references to source entity. + // - `component` is from `source_entity`'s archetype + let source_component_ptr = + unsafe { source_entity.get_by_id(component).debug_checked_unwrap() }; + + let source_component = SourceComponent { + info, + ptr: source_component_ptr, + }; + + // SAFETY: + // - `components` and `component` are from the same world + // - `source_component_ptr` is valid and points to the same type as represented by `component` + let mut ctx = unsafe { + ComponentCloneCtx::new( + component, + source, + target, + &bundle_scratch_allocator, + &mut bundle_scratch, + world.entities(), + info, + state, + mapper, + app_registry.as_ref(), + ) + }; + + (handler)(&source_component, &mut ctx); + }); + } + + world.flush(); + + for deferred in state.deferred_commands.drain(..) { + (deferred)(world, mapper); + } + + if !world.entities.contains(target) { + panic!("Target entity does not exist"); + } + + if state.move_components { + world + .entity_mut(source) + .remove_by_ids(&bundle_scratch.component_ids); + } + + // SAFETY: + // - All `component_ids` are from the same world as `target` entity + // - All `component_data_ptrs` are valid types represented by `component_ids` + unsafe { bundle_scratch.write(world, target, relationship_hook_insert_mode) }; + target + } +} + +/// Part of the [`EntityCloner`], see there for more information. +struct EntityClonerState { + clone_behavior_overrides: HashMap, + move_components: bool, + linked_cloning: bool, + default_clone_fn: ComponentCloneFn, + clone_queue: VecDeque, + deferred_commands: VecDeque>, +} + +impl Default for EntityClonerState { + fn default() -> Self { + Self { + move_components: false, + linked_cloning: false, + default_clone_fn: ComponentCloneBehavior::global_default_fn(), + clone_behavior_overrides: Default::default(), + clone_queue: Default::default(), + deferred_commands: Default::default(), } } } /// A builder for configuring [`EntityCloner`]. See [`EntityCloner`] for more information. -pub struct EntityClonerBuilder<'w> { +pub struct EntityClonerBuilder<'w, Filter> { world: &'w mut World, - entity_cloner: EntityCloner, - attach_required_components: bool, + filter: Filter, + state: EntityClonerState, } -impl<'w> EntityClonerBuilder<'w> { +impl<'w, Filter: CloneByFilter> EntityClonerBuilder<'w, Filter> { /// Internally calls [`EntityCloner::clone_entity`] on the builder's [`World`]. pub fn clone_entity(&mut self, source: Entity, target: Entity) -> &mut Self { - self.entity_cloner.clone_entity(self.world, source, target); + let mut mapper = EntityHashMap::::new(); + mapper.set_mapped(source, target); + EntityCloner::clone_entity_mapped_internal( + &mut self.state, + &mut self.filter, + self.world, + source, + &mut mapper, + ); self } - /// Finishes configuring [`EntityCloner`] returns it. - pub fn finish(self) -> EntityCloner { - self.entity_cloner - } - /// By default, any components allowed/denied through the filter will automatically - /// allow/deny all of their required components. - /// - /// This method allows for a scoped mode where any changes to the filter - /// will not involve required components. - pub fn without_required_components( - &mut self, - builder: impl FnOnce(&mut EntityClonerBuilder), - ) -> &mut Self { - self.attach_required_components = false; - builder(self); - self.attach_required_components = true; - self + /// Finishes configuring [`EntityCloner`] returns it. + pub fn finish(self) -> EntityCloner { + EntityCloner { + filter: self.filter.into(), + state: self.state, + } } /// Sets the default clone function to use. pub fn with_default_clone_fn(&mut self, clone_fn: ComponentCloneFn) -> &mut Self { - self.entity_cloner.default_clone_fn = clone_fn; + self.state.default_clone_fn = clone_fn; self } @@ -673,86 +701,7 @@ impl<'w> EntityClonerBuilder<'w> { /// The setting only applies to components that are allowed through the filter /// at the time [`EntityClonerBuilder::clone_entity`] is called. pub fn move_components(&mut self, enable: bool) -> &mut Self { - self.entity_cloner.move_components = enable; - self - } - - /// Adds all components of the bundle to the list of components to clone. - /// - /// Note that all components are allowed by default, to clone only explicitly allowed components make sure to call - /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. - pub fn allow(&mut self) -> &mut Self { - let bundle = self.world.register_bundle::(); - let ids = bundle.explicit_components().to_owned(); - for id in ids { - self.filter_allow(id); - } - self - } - - /// Extends the list of components to clone. - /// - /// Note that all components are allowed by default, to clone only explicitly allowed components make sure to call - /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. - pub fn allow_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for id in ids { - self.filter_allow(id); - } - self - } - - /// Extends the list of components to clone using [`TypeId`]s. - /// - /// Note that all components are allowed by default, to clone only explicitly allowed components make sure to call - /// [`deny_all`](`Self::deny_all`) before calling any of the `allow` methods. - pub fn allow_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for type_id in ids { - if let Some(id) = self.world.components().get_valid_id(type_id) { - self.filter_allow(id); - } - } - self - } - - /// Resets the filter to allow all components to be cloned. - pub fn allow_all(&mut self) -> &mut Self { - self.entity_cloner.filter_allows_components = false; - self.entity_cloner.filter.clear(); - self - } - - /// Disallows all components of the bundle from being cloned. - pub fn deny(&mut self) -> &mut Self { - let bundle = self.world.register_bundle::(); - let ids = bundle.explicit_components().to_owned(); - for id in ids { - self.filter_deny(id); - } - self - } - - /// Extends the list of components that shouldn't be cloned. - pub fn deny_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for id in ids { - self.filter_deny(id); - } - self - } - - /// Extends the list of components that shouldn't be cloned by type ids. - pub fn deny_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { - for type_id in ids { - if let Some(id) = self.world.components().get_valid_id(type_id) { - self.filter_deny(id); - } - } - self - } - - /// Sets the filter to deny all components. - pub fn deny_all(&mut self) -> &mut Self { - self.entity_cloner.filter_allows_components = true; - self.entity_cloner.filter.clear(); + self.state.move_components = enable; self } @@ -765,7 +714,7 @@ impl<'w> EntityClonerBuilder<'w> { clone_behavior: ComponentCloneBehavior, ) -> &mut Self { if let Some(id) = self.world.components().valid_component_id::() { - self.entity_cloner + self.state .clone_behavior_overrides .insert(id, clone_behavior); } @@ -781,7 +730,7 @@ impl<'w> EntityClonerBuilder<'w> { component_id: ComponentId, clone_behavior: ComponentCloneBehavior, ) -> &mut Self { - self.entity_cloner + self.state .clone_behavior_overrides .insert(component_id, clone_behavior); self @@ -790,7 +739,7 @@ impl<'w> EntityClonerBuilder<'w> { /// Removes a previously set override of [`ComponentCloneBehavior`] for a component in this builder. pub fn remove_clone_behavior_override(&mut self) -> &mut Self { if let Some(id) = self.world.components().valid_component_id::() { - self.entity_cloner.clone_behavior_overrides.remove(&id); + self.state.clone_behavior_overrides.remove(&id); } self } @@ -800,53 +749,317 @@ impl<'w> EntityClonerBuilder<'w> { &mut self, component_id: ComponentId, ) -> &mut Self { - self.entity_cloner - .clone_behavior_overrides - .remove(&component_id); + self.state.clone_behavior_overrides.remove(&component_id); self } /// When true this cloner will be configured to clone entities referenced in cloned components via [`RelationshipTarget::LINKED_SPAWN`](crate::relationship::RelationshipTarget::LINKED_SPAWN). /// This will produce "deep" / recursive clones of relationship trees that have "linked spawn". pub fn linked_cloning(&mut self, linked_cloning: bool) -> &mut Self { - self.entity_cloner.linked_cloning = linked_cloning; + self.state.linked_cloning = linked_cloning; + self + } +} + +impl<'w> EntityClonerBuilder<'w, OptOut> { + /// By default, any components denied through the filter will automatically + /// deny all of components they are required by too. + /// + /// This method allows for a scoped mode where any changes to the filter + /// will not involve these requiring components. + /// + /// If component `A` is denied in the `builder` closure here and component `B` + /// requires `A`, then `A` will be inserted with the value defined in `B`'s + /// [`Component` derive](https://docs.rs/bevy/latest/bevy/ecs/component/trait.Component.html#required-components). + /// This assumes `A` is missing yet at the target entity. + pub fn without_required_by_components(&mut self, builder: impl FnOnce(&mut Self)) -> &mut Self { + self.filter.attach_required_by_components = false; + builder(self); + self.filter.attach_required_by_components = true; self } - /// Helper function that allows a component through the filter. - fn filter_allow(&mut self, id: ComponentId) { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.insert(id); - } else { - self.entity_cloner.filter.remove(&id); + /// Sets whether components are always cloned ([`InsertMode::Replace`], the default) or only if it is missing + /// ([`InsertMode::Keep`]) at the target entity. + /// + /// This makes no difference if the target is spawned by the cloner. + pub fn insert_mode(&mut self, insert_mode: InsertMode) -> &mut Self { + self.filter.insert_mode = insert_mode; + self + } + + /// Disallows all components of the bundle from being cloned. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny(&mut self) -> &mut Self { + let bundle_id = self.world.register_bundle::().id(); + self.deny_by_bundle_id(bundle_id) + } + + /// Disallows all components of the bundle ID from being cloned. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny_by_bundle_id(&mut self, bundle_id: BundleId) -> &mut Self { + if let Some(bundle) = self.world.bundles().get(bundle_id) { + let ids = bundle.explicit_components().iter(); + for &id in ids { + self.filter.filter_deny(id, self.world); + } } - if self.attach_required_components { - if let Some(info) = self.world.components().get_info(id) { - for required_id in info.required_components().iter_ids() { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter_required.insert(required_id); - } else { - self.entity_cloner.filter_required.remove(&required_id); - } - } + self + } + + /// Extends the list of components that shouldn't be cloned. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for id in ids { + self.filter.filter_deny(id, self.world); + } + self + } + + /// Extends the list of components that shouldn't be cloned by type ids. + /// + /// If component `A` is denied here and component `B` requires `A`, then `A` + /// is denied as well. See [`Self::without_required_by_components`] to alter + /// this behavior. + pub fn deny_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for type_id in ids { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter.filter_deny(id, self.world); + } + } + self + } +} + +impl<'w> EntityClonerBuilder<'w, OptIn> { + /// By default, any components allowed through the filter will automatically + /// allow all of their required components. + /// + /// This method allows for a scoped mode where any changes to the filter + /// will not involve required components. + /// + /// If component `A` is allowed in the `builder` closure here and requires + /// component `B`, then `B` will be inserted with the value defined in `A`'s + /// [`Component` derive](https://docs.rs/bevy/latest/bevy/ecs/component/trait.Component.html#required-components). + /// This assumes `B` is missing yet at the target entity. + pub fn without_required_components(&mut self, builder: impl FnOnce(&mut Self)) -> &mut Self { + self.filter.attach_required_components = false; + builder(self); + self.filter.attach_required_components = true; + self + } + + /// Adds all components of the bundle to the list of components to clone. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow(&mut self) -> &mut Self { + let bundle_id = self.world.register_bundle::().id(); + self.allow_by_bundle_id(bundle_id) + } + + /// Adds all components of the bundle to the list of components to clone if + /// the target does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_if_new(&mut self) -> &mut Self { + let bundle_id = self.world.register_bundle::().id(); + self.allow_by_bundle_id_if_new(bundle_id) + } + + /// Adds all components of the bundle ID to the list of components to clone. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_bundle_id(&mut self, bundle_id: BundleId) -> &mut Self { + if let Some(bundle) = self.world.bundles().get(bundle_id) { + let ids = bundle.explicit_components().iter(); + for &id in ids { + self.filter + .filter_allow(id, self.world, InsertMode::Replace); + } + } + self + } + + /// Adds all components of the bundle ID to the list of components to clone + /// if the target does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_bundle_id_if_new(&mut self, bundle_id: BundleId) -> &mut Self { + if let Some(bundle) = self.world.bundles().get(bundle_id) { + let ids = bundle.explicit_components().iter(); + for &id in ids { + self.filter.filter_allow(id, self.world, InsertMode::Keep); + } + } + self + } + + /// Extends the list of components to clone. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for id in ids { + self.filter + .filter_allow(id, self.world, InsertMode::Replace); + } + self + } + + /// Extends the list of components to clone if the target does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_ids_if_new(&mut self, ids: impl IntoIterator) -> &mut Self { + for id in ids { + self.filter.filter_allow(id, self.world, InsertMode::Keep); + } + self + } + + /// Extends the list of components to clone using [`TypeId`]s. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_type_ids(&mut self, ids: impl IntoIterator) -> &mut Self { + for type_id in ids { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter + .filter_allow(id, self.world, InsertMode::Replace); + } + } + self + } + + /// Extends the list of components to clone using [`TypeId`]s if the target + /// does not contain them. + /// + /// If component `A` is allowed here and requires component `B`, then `B` + /// is allowed as well. See [`Self::without_required_components`] + /// to alter this behavior. + pub fn allow_by_type_ids_if_new(&mut self, ids: impl IntoIterator) -> &mut Self { + for type_id in ids { + if let Some(id) = self.world.components().get_valid_id(type_id) { + self.filter.filter_allow(id, self.world, InsertMode::Keep); + } + } + self + } +} + +/// Filters that can selectively clone components depending on its inner configuration are unified with this trait. +#[doc(hidden)] +pub trait CloneByFilter: Into { + /// The filter will call `clone_component` for every [`ComponentId`] that passes it. + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + clone_component: impl FnMut(ComponentId), + ); +} + +/// Part of the [`EntityCloner`], see there for more information. +#[doc(hidden)] +#[derive(From)] +pub enum EntityClonerFilter { + OptOut(OptOut), + OptIn(OptIn), +} + +impl Default for EntityClonerFilter { + fn default() -> Self { + Self::OptOut(Default::default()) + } +} + +impl CloneByFilter for EntityClonerFilter { + #[inline] + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + clone_component: impl FnMut(ComponentId), + ) { + match self { + Self::OptOut(filter) => { + filter.clone_components(source_archetype, target_archetype, clone_component); + } + Self::OptIn(filter) => { + filter.clone_components(source_archetype, target_archetype, clone_component); } } } +} - /// Helper function that disallows a component through the filter. - fn filter_deny(&mut self, id: ComponentId) { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter.remove(&id); - } else { - self.entity_cloner.filter.insert(id); +/// Generic for [`EntityClonerBuilder`] that makes the cloner try to clone every component from the source entity +/// except for components that were explicitly denied, for example by using the +/// [`deny`](EntityClonerBuilder::deny) method. +/// +/// Required components are not considered by denied components and must be explicitly denied as well if desired. +pub struct OptOut { + /// Contains the components that should not be cloned. + deny: HashSet, + + /// Determines if a component is inserted when it is existing already. + insert_mode: InsertMode, + + /// Is `true` unless during [`EntityClonerBuilder::without_required_by_components`] which will suppress + /// components that require denied components to be denied as well, causing them to be created independent + /// from the value at the source entity if needed. + attach_required_by_components: bool, +} + +impl Default for OptOut { + fn default() -> Self { + Self { + deny: Default::default(), + insert_mode: InsertMode::Replace, + attach_required_by_components: true, } - if self.attach_required_components { - if let Some(info) = self.world.components().get_info(id) { - for required_id in info.required_components().iter_ids() { - if self.entity_cloner.filter_allows_components { - self.entity_cloner.filter_required.remove(&required_id); - } else { - self.entity_cloner.filter_required.insert(required_id); + } +} + +impl CloneByFilter for OptOut { + #[inline] + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + mut clone_component: impl FnMut(ComponentId), + ) { + match self.insert_mode { + InsertMode::Replace => { + for component in source_archetype.components() { + if !self.deny.contains(&component) { + clone_component(component); + } + } + } + InsertMode::Keep => { + for component in source_archetype.components() { + if !target_archetype.contains(component) && !self.deny.contains(&component) { + clone_component(component); } } } @@ -854,30 +1067,264 @@ impl<'w> EntityClonerBuilder<'w> { } } +impl OptOut { + /// Denies a component through the filter, also deny components that require `id` if + /// [`Self::attach_required_by_components`] is true. + #[inline] + fn filter_deny(&mut self, id: ComponentId, world: &World) { + self.deny.insert(id); + if self.attach_required_by_components { + if let Some(required_by) = world.components().get_required_by(id) { + self.deny.extend(required_by.iter()); + }; + } + } +} + +/// Generic for [`EntityClonerBuilder`] that makes the cloner try to clone every component that was explicitly +/// allowed from the source entity, for example by using the [`allow`](EntityClonerBuilder::allow) method. +/// +/// Required components are also cloned when the target entity does not contain them. +pub struct OptIn { + /// Contains the components explicitly allowed to be cloned. + allow: HashMap, + + /// Lists of required components, [`Explicit`] refers to a range in it. + required_of_allow: Vec, + + /// Contains the components required by those in [`Self::allow`]. + /// Also contains the number of components in [`Self::allow`] each is required by to track + /// when to skip cloning a required component after skipping explicit components that require it. + required: HashMap, + + /// Is `true` unless during [`EntityClonerBuilder::without_required_components`] which will suppress + /// evaluating required components to clone, causing them to be created independent from the value at + /// the source entity if needed. + attach_required_components: bool, +} + +impl Default for OptIn { + fn default() -> Self { + Self { + allow: Default::default(), + required_of_allow: Default::default(), + required: Default::default(), + attach_required_components: true, + } + } +} + +impl CloneByFilter for OptIn { + #[inline] + fn clone_components<'a>( + &mut self, + source_archetype: &Archetype, + target_archetype: LazyCell<&'a Archetype, impl FnOnce() -> &'a Archetype>, + mut clone_component: impl FnMut(ComponentId), + ) { + // track the amount of components left not being cloned yet to exit this method early + let mut uncloned_components = source_archetype.component_count(); + + // track if any `Required::required_by_reduced` has been reduced so they are reset + let mut reduced_any = false; + + // clone explicit components + for (&component, explicit) in self.allow.iter() { + if uncloned_components == 0 { + // exhausted all source components, reset changed `Required::required_by_reduced` + if reduced_any { + self.required + .iter_mut() + .for_each(|(_, required)| required.reset()); + } + return; + } + + let do_clone = source_archetype.contains(component) + && (explicit.insert_mode == InsertMode::Replace + || !target_archetype.contains(component)); + if do_clone { + clone_component(component); + uncloned_components -= 1; + } else if let Some(range) = explicit.required_range.clone() { + for component in self.required_of_allow[range].iter() { + // may be None if required component was also added as explicit later + if let Some(required) = self.required.get_mut(component) { + required.required_by_reduced -= 1; + reduced_any = true; + } + } + } + } + + let mut required_iter = self.required.iter_mut(); + + // clone required components + let required_components = required_iter + .by_ref() + .filter_map(|(&component, required)| { + let do_clone = required.required_by_reduced > 0 // required by a cloned component + && source_archetype.contains(component) // must exist to clone, may miss if removed + && !target_archetype.contains(component); // do not overwrite existing values + + // reset changed `Required::required_by_reduced` as this is done being checked here + required.reset(); + + do_clone.then_some(component) + }) + .take(uncloned_components); + + for required_component in required_components { + clone_component(required_component); + } + + // if the `required_components` iterator has not been exhausted yet because the source has no more + // components to clone, iterate the rest to reset changed `Required::required_by_reduced` for the + // next clone + if reduced_any { + required_iter.for_each(|(_, required)| required.reset()); + } + } +} + +impl OptIn { + /// Allows a component through the filter, also allow required components if + /// [`Self::attach_required_components`] is true. + #[inline] + fn filter_allow(&mut self, id: ComponentId, world: &World, mut insert_mode: InsertMode) { + match self.allow.entry(id) { + Entry::Vacant(explicit) => { + // explicit components should not appear in the required map + self.required.remove(&id); + + if !self.attach_required_components { + explicit.insert(Explicit { + insert_mode, + required_range: None, + }); + } else { + self.filter_allow_with_required(id, world, insert_mode); + } + } + Entry::Occupied(mut explicit) => { + let explicit = explicit.get_mut(); + + // set required component range if it was inserted with `None` earlier + if self.attach_required_components && explicit.required_range.is_none() { + if explicit.insert_mode == InsertMode::Replace { + // do not overwrite with Keep if component was allowed as Replace earlier + insert_mode = InsertMode::Replace; + } + + self.filter_allow_with_required(id, world, insert_mode); + } else if explicit.insert_mode == InsertMode::Keep { + // potentially overwrite Keep with Replace + explicit.insert_mode = insert_mode; + } + } + }; + } + + // Allow a component through the filter and include required components. + #[inline] + fn filter_allow_with_required( + &mut self, + id: ComponentId, + world: &World, + insert_mode: InsertMode, + ) { + let Some(info) = world.components().get_info(id) else { + return; + }; + + let iter = info + .required_components() + .iter_ids() + .filter(|id| !self.allow.contains_key(id)) + .inspect(|id| { + // set or increase the number of components this `id` is required by + self.required + .entry(*id) + .and_modify(|required| { + required.required_by += 1; + required.required_by_reduced += 1; + }) + .or_insert(Required { + required_by: 1, + required_by_reduced: 1, + }); + }); + + let start = self.required_of_allow.len(); + self.required_of_allow.extend(iter); + let end = self.required_of_allow.len(); + + self.allow.insert( + id, + Explicit { + insert_mode, + required_range: Some(start..end), + }, + ); + } +} + +/// Contains the components explicitly allowed to be cloned. +struct Explicit { + /// If component was added via [`allow`](EntityClonerBuilder::allow) etc, this is `Overwrite`. + /// + /// If component was added via [`allow_if_new`](EntityClonerBuilder::allow_if_new) etc, this is `Keep`. + insert_mode: InsertMode, + + /// Contains the range in [`OptIn::required_of_allow`] for this component containing its + /// required components. + /// + /// Is `None` if [`OptIn::attach_required_components`] was `false` when added. + /// It may be set to `Some` later if the component is later added explicitly again with + /// [`OptIn::attach_required_components`] being `true`. + /// + /// Range is empty if this component has no required components that are not also explicitly allowed. + required_range: Option>, +} + +struct Required { + /// Amount of explicit components this component is required by. + required_by: u32, + + /// As [`Self::required_by`] but is reduced during cloning when an explicit component is not cloned, + /// either because [`Explicit::insert_mode`] is `Keep` or the source entity does not contain it. + /// + /// If this is zero, the required component is not cloned. + /// + /// The counter is reset to `required_by` when the cloning is over in case another entity needs to be + /// cloned by the same [`EntityCloner`]. + required_by_reduced: u32, +} + +impl Required { + // Revert reductions for the next entity to clone with this EntityCloner + #[inline] + fn reset(&mut self) { + self.required_by_reduced = self.required_by; + } +} + #[cfg(test)] mod tests { - use super::ComponentCloneCtx; + use super::*; use crate::{ - component::{Component, ComponentCloneBehavior, ComponentDescriptor, StorageType}, - entity::{Entity, EntityCloner, EntityHashMap, SourceComponent}, + component::{ComponentDescriptor, StorageType}, prelude::{ChildOf, Children, Resource}, - reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld}, world::{FromWorld, World}, }; - use alloc::vec::Vec; use bevy_ptr::OwningPtr; - use bevy_reflect::Reflect; use core::marker::PhantomData; use core::{alloc::Layout, ops::Deref}; #[cfg(feature = "bevy_reflect")] mod reflect { use super::*; - use crate::{ - component::{Component, ComponentCloneBehavior}, - entity::{EntityCloner, SourceComponent}, - reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld}, - }; + use crate::reflect::{AppTypeRegistry, ReflectComponent, ReflectFromWorld}; use alloc::vec; use bevy_reflect::{std_traits::ReflectDefault, FromType, Reflect, ReflectFromPtr}; @@ -900,7 +1347,7 @@ mod tests { let e = world.spawn(component.clone()).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior::(ComponentCloneBehavior::reflect()) .clone_entity(e, e_clone); @@ -985,7 +1432,7 @@ mod tests { .id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior_with_id(a_id, ComponentCloneBehavior::reflect()) .override_clone_behavior_with_id(b_id, ComponentCloneBehavior::reflect()) .override_clone_behavior_with_id(c_id, ComponentCloneBehavior::reflect()) @@ -1018,7 +1465,7 @@ mod tests { let mut registry = registry.write(); registry.register::(); registry - .get_mut(core::any::TypeId::of::()) + .get_mut(TypeId::of::()) .unwrap() .insert(>::from_type()); } @@ -1026,7 +1473,7 @@ mod tests { let e = world.spawn(A).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior::(ComponentCloneBehavior::Custom(test_handler)) .clone_entity(e, e_clone); } @@ -1055,7 +1502,7 @@ mod tests { let e = world.spawn(component.clone()).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(e, e_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(e, e_clone); assert!(world .get::(e_clone) @@ -1079,7 +1526,7 @@ mod tests { // No AppTypeRegistry let e = world.spawn((A, B(Default::default()))).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .override_clone_behavior::(ComponentCloneBehavior::reflect()) .override_clone_behavior::(ComponentCloneBehavior::reflect()) .clone_entity(e, e_clone); @@ -1093,10 +1540,50 @@ mod tests { let e = world.spawn((A, B(Default::default()))).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(e, e_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(e, e_clone); assert_eq!(world.get::(e_clone), None); assert_eq!(world.get::(e_clone), None); } + + #[test] + fn clone_with_reflect_from_world() { + #[derive(Component, Reflect, PartialEq, Eq, Debug)] + #[reflect(Component, FromWorld, from_reflect = false)] + struct SomeRef( + #[entities] Entity, + // We add an ignored field here to ensure `reflect_clone` fails and `FromWorld` is used + #[reflect(ignore)] PhantomData<()>, + ); + + #[derive(Resource)] + struct FromWorldCalled(bool); + + impl FromWorld for SomeRef { + fn from_world(world: &mut World) -> Self { + world.insert_resource(FromWorldCalled(true)); + SomeRef(Entity::PLACEHOLDER, Default::default()) + } + } + let mut world = World::new(); + let registry = AppTypeRegistry::default(); + registry.write().register::(); + world.insert_resource(registry); + + let a = world.spawn_empty().id(); + let b = world.spawn_empty().id(); + let c = world.spawn(SomeRef(a, Default::default())).id(); + let d = world.spawn_empty().id(); + let mut map = EntityHashMap::::new(); + map.insert(a, b); + map.insert(c, d); + + let cloned = EntityCloner::default().clone_entity_mapped(&mut world, c, &mut map); + assert_eq!( + *world.entity(cloned).get::().unwrap(), + SomeRef(b, Default::default()) + ); + assert!(world.resource::().0); + } } #[test] @@ -1113,7 +1600,7 @@ mod tests { let e = world.spawn(component.clone()).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(e, e_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(e, e_clone); assert!(world.get::(e_clone).is_some_and(|c| *c == component)); } @@ -1135,8 +1622,7 @@ mod tests { let e = world.spawn((component.clone(), B)).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .allow::() .clone_entity(e, e_clone); @@ -1152,9 +1638,10 @@ mod tests { } #[derive(Component, Clone)] + #[require(C)] struct B; - #[derive(Component, Clone)] + #[derive(Component, Clone, Default)] struct C; let mut world = World::default(); @@ -1164,72 +1651,8 @@ mod tests { let e = world.spawn((component.clone(), B, C)).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny::() - .clone_entity(e, e_clone); - - assert!(world.get::(e_clone).is_some_and(|c| *c == component)); - assert!(world.get::(e_clone).is_none()); - assert!(world.get::(e_clone).is_some()); - } - - #[test] - fn clone_entity_with_override_allow_filter() { - #[derive(Component, Clone, PartialEq, Eq)] - struct A { - field: usize, - } - - #[derive(Component, Clone)] - struct B; - - #[derive(Component, Clone)] - struct C; - - let mut world = World::default(); - - let component = A { field: 5 }; - - let e = world.spawn((component.clone(), B, C)).id(); - let e_clone = world.spawn_empty().id(); - - EntityCloner::build(&mut world) - .deny_all() - .allow::() - .allow::() - .allow::() - .deny::() - .clone_entity(e, e_clone); - - assert!(world.get::(e_clone).is_some_and(|c| *c == component)); - assert!(world.get::(e_clone).is_none()); - assert!(world.get::(e_clone).is_some()); - } - - #[test] - fn clone_entity_with_override_bundle() { - #[derive(Component, Clone, PartialEq, Eq)] - struct A { - field: usize, - } - - #[derive(Component, Clone)] - struct B; - - #[derive(Component, Clone)] - struct C; - - let mut world = World::default(); - - let component = A { field: 5 }; - - let e = world.spawn((component.clone(), B, C)).id(); - let e_clone = world.spawn_empty().id(); - - EntityCloner::build(&mut world) - .deny_all() - .allow::<(A, B, C)>() - .deny::<(B, C)>() + EntityCloner::build_opt_out(&mut world) + .deny::() .clone_entity(e, e_clone); assert!(world.get::(e_clone).is_some_and(|c| *c == component)); @@ -1237,6 +1660,171 @@ mod tests { assert!(world.get::(e_clone).is_none()); } + #[test] + fn clone_entity_with_deny_filter_without_required_by() { + #[derive(Component, Clone)] + #[require(B { field: 5 })] + struct A; + + #[derive(Component, Clone, PartialEq, Eq)] + struct B { + field: usize, + } + + let mut world = World::default(); + + let e = world.spawn((A, B { field: 10 })).id(); + let e_clone = world.spawn_empty().id(); + + EntityCloner::build_opt_out(&mut world) + .without_required_by_components(|builder| { + builder.deny::(); + }) + .clone_entity(e, e_clone); + + assert!(world.get::(e_clone).is_some()); + assert!(world + .get::(e_clone) + .is_some_and(|c| *c == B { field: 5 })); + } + + #[test] + fn clone_entity_with_deny_filter_if_new() { + #[derive(Component, Clone, PartialEq, Eq)] + struct A { + field: usize, + } + + #[derive(Component, Clone)] + struct B; + + #[derive(Component, Clone)] + struct C; + + let mut world = World::default(); + + let e = world.spawn((A { field: 5 }, B, C)).id(); + let e_clone = world.spawn(A { field: 8 }).id(); + + EntityCloner::build_opt_out(&mut world) + .deny::() + .insert_mode(InsertMode::Keep) + .clone_entity(e, e_clone); + + assert!(world + .get::(e_clone) + .is_some_and(|c| *c == A { field: 8 })); + assert!(world.get::(e_clone).is_none()); + assert!(world.get::(e_clone).is_some()); + } + + #[test] + fn allow_and_allow_if_new_always_allows() { + #[derive(Component, Clone, PartialEq, Debug)] + struct A(u8); + + let mut world = World::default(); + let e = world.spawn(A(1)).id(); + let e_clone1 = world.spawn(A(2)).id(); + + EntityCloner::build_opt_in(&mut world) + .allow_if_new::() + .allow::() + .clone_entity(e, e_clone1); + + assert_eq!(world.get::(e_clone1), Some(&A(1))); + + let e_clone2 = world.spawn(A(2)).id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .allow_if_new::() + .clone_entity(e, e_clone2); + + assert_eq!(world.get::(e_clone2), Some(&A(1))); + } + + #[test] + fn with_and_without_required_components_include_required() { + #[derive(Component, Clone, PartialEq, Debug)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u8); + + let mut world = World::default(); + let e = world.spawn((A, B(10))).id(); + let e_clone1 = world.spawn_empty().id(); + EntityCloner::build_opt_in(&mut world) + .without_required_components(|builder| { + builder.allow::(); + }) + .allow::() + .clone_entity(e, e_clone1); + + assert_eq!(world.get::(e_clone1), Some(&B(10))); + + let e_clone2 = world.spawn_empty().id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .without_required_components(|builder| { + builder.allow::(); + }) + .clone_entity(e, e_clone2); + + assert_eq!(world.get::(e_clone2), Some(&B(10))); + } + + #[test] + fn clone_required_becoming_explicit() { + #[derive(Component, Clone, PartialEq, Debug)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u8); + + let mut world = World::default(); + let e = world.spawn((A, B(10))).id(); + let e_clone1 = world.spawn(B(20)).id(); + EntityCloner::build_opt_in(&mut world) + .allow::() + .allow::() + .clone_entity(e, e_clone1); + + assert_eq!(world.get::(e_clone1), Some(&B(10))); + + let e_clone2 = world.spawn(B(20)).id(); + EntityCloner::build_opt_in(&mut world) + .allow::() + .allow::() + .clone_entity(e, e_clone2); + + assert_eq!(world.get::(e_clone2), Some(&B(10))); + } + + #[test] + fn required_not_cloned_because_requiring_missing() { + #[derive(Component, Clone)] + #[require(B)] + struct A; + + #[derive(Component, Clone, Default)] + struct B; + + let mut world = World::default(); + let e = world.spawn(B).id(); + let e_clone1 = world.spawn_empty().id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .clone_entity(e, e_clone1); + + assert!(world.get::(e_clone1).is_none()); + } + #[test] fn clone_entity_with_required_components() { #[derive(Component, Clone, PartialEq, Debug)] @@ -1255,8 +1843,7 @@ mod tests { let e = world.spawn(A).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .allow::() .clone_entity(e, e_clone); @@ -1283,8 +1870,7 @@ mod tests { let e = world.spawn((A, C(0))).id(); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .without_required_components(|builder| { builder.allow::(); }) @@ -1295,6 +1881,60 @@ mod tests { assert_eq!(world.entity(e_clone).get::(), Some(&C(5))); } + #[test] + fn clone_entity_with_missing_required_components() { + #[derive(Component, Clone, PartialEq, Debug)] + #[require(B)] + struct A; + + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(C(5))] + struct B; + + #[derive(Component, Clone, PartialEq, Debug)] + struct C(u32); + + let mut world = World::default(); + + let e = world.spawn(A).remove::().id(); + let e_clone = world.spawn_empty().id(); + + EntityCloner::build_opt_in(&mut world) + .allow::() + .clone_entity(e, e_clone); + + assert_eq!(world.entity(e_clone).get::(), Some(&A)); + assert_eq!(world.entity(e_clone).get::(), Some(&B)); + assert_eq!(world.entity(e_clone).get::(), Some(&C(5))); + } + + #[test] + fn skipped_required_components_counter_is_reset_on_early_return() { + #[derive(Component, Clone, PartialEq, Debug, Default)] + #[require(B(5))] + struct A; + + #[derive(Component, Clone, PartialEq, Debug)] + struct B(u32); + + #[derive(Component, Clone, PartialEq, Debug, Default)] + struct C; + + let mut world = World::default(); + + let e1 = world.spawn(C).id(); + let e2 = world.spawn((A, B(0))).id(); + let e_clone = world.spawn_empty().id(); + + let mut builder = EntityCloner::build_opt_in(&mut world); + builder.allow::<(A, C)>(); + let mut cloner = builder.finish(); + cloner.clone_entity(&mut world, e1, e_clone); + cloner.clone_entity(&mut world, e2, e_clone); + + assert_eq!(world.entity(e_clone).get::(), Some(&B(0))); + } + #[test] fn clone_entity_with_dynamic_components() { const COMPONENT_SIZE: usize = 10; @@ -1335,7 +1975,7 @@ mod tests { let entity = entity.id(); let entity_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world).clone_entity(entity, entity_clone); + EntityCloner::build_opt_out(&mut world).clone_entity(entity, entity_clone); let ptr = world.get_by_id(entity, component_id).unwrap(); let clone_ptr = world.get_by_id(entity_clone, component_id).unwrap(); @@ -1357,7 +1997,7 @@ mod tests { let child2 = world.spawn(ChildOf(root)).id(); let clone_root = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .linked_cloning(true) .clone_entity(root, clone_root); @@ -1382,46 +2022,6 @@ mod tests { ); } - #[test] - fn clone_with_reflect_from_world() { - #[derive(Component, Reflect, PartialEq, Eq, Debug)] - #[reflect(Component, FromWorld, from_reflect = false)] - struct SomeRef( - #[entities] Entity, - // We add an ignored field here to ensure `reflect_clone` fails and `FromWorld` is used - #[reflect(ignore)] PhantomData<()>, - ); - - #[derive(Resource)] - struct FromWorldCalled(bool); - - impl FromWorld for SomeRef { - fn from_world(world: &mut World) -> Self { - world.insert_resource(FromWorldCalled(true)); - SomeRef(Entity::PLACEHOLDER, Default::default()) - } - } - let mut world = World::new(); - let registry = AppTypeRegistry::default(); - registry.write().register::(); - world.insert_resource(registry); - - let a = world.spawn_empty().id(); - let b = world.spawn_empty().id(); - let c = world.spawn(SomeRef(a, Default::default())).id(); - let d = world.spawn_empty().id(); - let mut map = EntityHashMap::::new(); - map.insert(a, b); - map.insert(c, d); - - let cloned = EntityCloner::default().clone_entity_mapped(&mut world, c, &mut map); - assert_eq!( - *world.entity(cloned).get::().unwrap(), - SomeRef(b, Default::default()) - ); - assert!(world.resource::().0); - } - #[test] fn cloning_with_required_components_preserves_existing() { #[derive(Component, Clone, PartialEq, Debug, Default)] @@ -1436,21 +2036,11 @@ mod tests { let e = world.spawn((A, B(0))).id(); let e_clone = world.spawn(B(1)).id(); - EntityCloner::build(&mut world) - .deny_all() + EntityCloner::build_opt_in(&mut world) .allow::() .clone_entity(e, e_clone); assert_eq!(world.entity(e_clone).get::(), Some(&A)); assert_eq!(world.entity(e_clone).get::(), Some(&B(1))); - - let e_clone2 = world.spawn(B(2)).id(); - - EntityCloner::build(&mut world) - .allow_all() - .deny::() - .clone_entity(e, e_clone2); - - assert_eq!(world.entity(e_clone2).get::(), Some(&B(2))); } } diff --git a/crates/bevy_ecs/src/hierarchy.rs b/crates/bevy_ecs/src/hierarchy.rs index d99e89b355..fe9bf571c9 100644 --- a/crates/bevy_ecs/src/hierarchy.rs +++ b/crates/bevy_ecs/src/hierarchy.rs @@ -330,7 +330,7 @@ impl<'w> EntityWorldMut<'w> { /// /// # Panics /// - /// Panics when debug assertions are enabled if an invariant is is broken and the command is executed. + /// Panics when debug assertions are enabled if an invariant is broken and the command is executed. pub fn replace_children_with_difference( &mut self, entities_to_unrelate: &[Entity], @@ -420,7 +420,7 @@ impl<'a> EntityCommands<'a> { /// /// # Panics /// - /// Panics when debug assertions are enabled if an invariant is is broken and the command is executed. + /// Panics when debug assertions are enabled if an invariant is broken and the command is executed. pub fn replace_children_with_difference( &mut self, entities_to_unrelate: &[Entity], diff --git a/crates/bevy_ecs/src/observer/entity_cloning.rs b/crates/bevy_ecs/src/observer/entity_cloning.rs index ee37300e64..7c7a4f69e9 100644 --- a/crates/bevy_ecs/src/observer/entity_cloning.rs +++ b/crates/bevy_ecs/src/observer/entity_cloning.rs @@ -2,14 +2,16 @@ use crate::{ component::ComponentCloneBehavior, - entity::{ComponentCloneCtx, EntityClonerBuilder, EntityMapper, SourceComponent}, + entity::{ + CloneByFilter, ComponentCloneCtx, EntityClonerBuilder, EntityMapper, SourceComponent, + }, observer::ObservedBy, world::World, }; use super::Observer; -impl EntityClonerBuilder<'_> { +impl EntityClonerBuilder<'_, Filter> { /// Sets the option to automatically add cloned entities to the observers targeting source entity. pub fn add_observers(&mut self, add_observers: bool) -> &mut Self { if add_observers { @@ -98,7 +100,7 @@ mod tests { world.trigger_targets(E, e); let e_clone = world.spawn_empty().id(); - EntityCloner::build(&mut world) + EntityCloner::build_opt_out(&mut world) .add_observers(true) .clone_entity(e, e_clone); diff --git a/crates/bevy_ecs/src/system/commands/entity_command.rs b/crates/bevy_ecs/src/system/commands/entity_command.rs index 87bd2d858b..098493a148 100644 --- a/crates/bevy_ecs/src/system/commands/entity_command.rs +++ b/crates/bevy_ecs/src/system/commands/entity_command.rs @@ -11,7 +11,7 @@ use crate::{ bundle::{Bundle, InsertMode}, change_detection::MaybeLocation, component::{Component, ComponentId, ComponentInfo}, - entity::{Entity, EntityClonerBuilder}, + entity::{Entity, EntityClonerBuilder, OptIn, OptOut}, event::EntityEvent, relationship::RelationshipHookMode, system::IntoObserverSystem, @@ -243,12 +243,36 @@ pub fn trigger(event: impl EntityEvent) -> impl EntityCommand { /// An [`EntityCommand`] that clones parts of an entity onto another entity, /// configured through [`EntityClonerBuilder`]. -pub fn clone_with( +/// +/// This builder tries to clone every component from the source entity except +/// for components that were explicitly denied, for example by using the +/// [`deny`](EntityClonerBuilder::deny) method. +/// +/// Required components are not considered by denied components and must be +/// explicitly denied as well if desired. +pub fn clone_with_opt_out( target: Entity, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> impl EntityCommand { move |mut entity: EntityWorldMut| { - entity.clone_with(target, config); + entity.clone_with_opt_out(target, config); + } +} + +/// An [`EntityCommand`] that clones parts of an entity onto another entity, +/// configured through [`EntityClonerBuilder`]. +/// +/// This builder tries to clone every component that was explicitly allowed +/// from the source entity, for example by using the +/// [`allow`](EntityClonerBuilder::allow) method. +/// +/// Required components are also cloned when the target entity does not contain them. +pub fn clone_with_opt_in( + target: Entity, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, +) -> impl EntityCommand { + move |mut entity: EntityWorldMut| { + entity.clone_with_opt_in(target, config); } } diff --git a/crates/bevy_ecs/src/system/commands/mod.rs b/crates/bevy_ecs/src/system/commands/mod.rs index ee6e01de13..0751e26770 100644 --- a/crates/bevy_ecs/src/system/commands/mod.rs +++ b/crates/bevy_ecs/src/system/commands/mod.rs @@ -18,7 +18,7 @@ use crate::{ bundle::{Bundle, InsertMode, NoBundleEffect}, change_detection::{MaybeLocation, Mut}, component::{Component, ComponentId, Mutable}, - entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError}, + entity::{Entities, Entity, EntityClonerBuilder, EntityDoesNotExistError, OptIn, OptOut}, error::{ignore, warn, BevyError, CommandWithEntity, ErrorContext, HandleError}, event::{BufferedEvent, EntityEvent, Event}, observer::{Observer, TriggerTargets}, @@ -1978,8 +1978,9 @@ impl<'a> EntityCommands<'a> { /// Clones parts of an entity (components, observers, etc.) onto another entity, /// configured through [`EntityClonerBuilder`]. /// - /// By default, the other entity will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The other entity will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. /// /// # Panics /// @@ -1987,7 +1988,7 @@ impl<'a> EntityCommands<'a> { /// /// # Example /// - /// Configure through [`EntityClonerBuilder`] as follows: + /// Configure through [`EntityClonerBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; /// #[derive(Component, Clone)] @@ -2002,8 +2003,8 @@ impl<'a> EntityCommands<'a> { /// // Create a new entity and keep its EntityCommands. /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); /// - /// // Clone only ComponentA onto the target. - /// entity.clone_with(target, |builder| { + /// // Clone ComponentA but not ComponentB onto the target. + /// entity.clone_with_opt_out(target, |builder| { /// builder.deny::(); /// }); /// } @@ -2011,12 +2012,57 @@ impl<'a> EntityCommands<'a> { /// ``` /// /// See [`EntityClonerBuilder`] for more options. - pub fn clone_with( + pub fn clone_with_opt_out( &mut self, target: Entity, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> &mut Self { - self.queue(entity_command::clone_with(target, config)) + self.queue(entity_command::clone_with_opt_out(target, config)) + } + + /// Clones parts of an entity (components, observers, etc.) onto another entity, + /// configured through [`EntityClonerBuilder`]. + /// + /// The other entity will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// # Panics + /// + /// The command will panic when applied if the target entity does not exist. + /// + /// # Example + /// + /// Configure through [`EntityClonerBuilder`] as follows: + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component, Clone)] + /// struct ComponentA(u32); + /// #[derive(Component, Clone)] + /// struct ComponentB(u32); + /// + /// fn example_system(mut commands: Commands) { + /// // Create an empty entity. + /// let target = commands.spawn_empty().id(); + /// + /// // Create a new entity and keep its EntityCommands. + /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); + /// + /// // Clone ComponentA but not ComponentB onto the target. + /// entity.clone_with_opt_in(target, |builder| { + /// builder.allow::(); + /// }); + /// } + /// # bevy_ecs::system::assert_is_system(example_system); + /// ``` + /// + /// See [`EntityClonerBuilder`] for more options. + pub fn clone_with_opt_in( + &mut self, + target: Entity, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> &mut Self { + self.queue(entity_command::clone_with_opt_in(target, config)) } /// Spawns a clone of this entity and returns the [`EntityCommands`] of the clone. @@ -2025,7 +2071,8 @@ impl<'a> EntityCommands<'a> { /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). /// /// To configure cloning behavior (such as only cloning certain components), - /// use [`EntityCommands::clone_and_spawn_with`]. + /// use [`EntityCommands::clone_and_spawn_with_opt_out`]/ + /// [`opt_out`](EntityCommands::clone_and_spawn_with_opt_out). /// /// # Note /// @@ -2045,25 +2092,22 @@ impl<'a> EntityCommands<'a> { /// // Create a new entity and store its EntityCommands. /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); /// - /// // Create a clone of the first entity. + /// // Create a clone of the entity. /// let mut entity_clone = entity.clone_and_spawn(); /// } /// # bevy_ecs::system::assert_is_system(example_system); pub fn clone_and_spawn(&mut self) -> EntityCommands<'_> { - self.clone_and_spawn_with(|_| {}) + self.clone_and_spawn_with_opt_out(|_| {}) } /// Spawns a clone of this entity and allows configuring cloning behavior /// using [`EntityClonerBuilder`], returning the [`EntityCommands`] of the clone. /// - /// By default, the clone will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The clone will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. /// - /// To exclude specific components, use [`EntityClonerBuilder::deny`]. - /// To only include specific components, use [`EntityClonerBuilder::deny_all`] - /// followed by [`EntityClonerBuilder::allow`]. - /// - /// See the methods on [`EntityClonerBuilder`] for more options. + /// See the methods on [`EntityClonerBuilder`] for more options. /// /// # Note /// @@ -2083,18 +2127,63 @@ impl<'a> EntityCommands<'a> { /// // Create a new entity and store its EntityCommands. /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); /// - /// // Create a clone of the first entity, but without ComponentB. - /// let mut entity_clone = entity.clone_and_spawn_with(|builder| { + /// // Create a clone of the entity with ComponentA but without ComponentB. + /// let mut entity_clone = entity.clone_and_spawn_with_opt_out(|builder| { /// builder.deny::(); /// }); /// } /// # bevy_ecs::system::assert_is_system(example_system); - pub fn clone_and_spawn_with( + pub fn clone_and_spawn_with_opt_out( &mut self, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> EntityCommands<'_> { let entity_clone = self.commands().spawn_empty().id(); - self.clone_with(entity_clone, config); + self.clone_with_opt_out(entity_clone, config); + EntityCommands { + commands: self.commands_mut().reborrow(), + entity: entity_clone, + } + } + + /// Spawns a clone of this entity and allows configuring cloning behavior + /// using [`EntityClonerBuilder`], returning the [`EntityCommands`] of the clone. + /// + /// The clone will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// See the methods on [`EntityClonerBuilder`] for more options. + /// + /// # Note + /// + /// If the original entity does not exist when this command is applied, + /// the returned entity will have no components. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// #[derive(Component, Clone)] + /// struct ComponentA(u32); + /// #[derive(Component, Clone)] + /// struct ComponentB(u32); + /// + /// fn example_system(mut commands: Commands) { + /// // Create a new entity and store its EntityCommands. + /// let mut entity = commands.spawn((ComponentA(10), ComponentB(20))); + /// + /// // Create a clone of the entity with ComponentA but without ComponentB. + /// let mut entity_clone = entity.clone_and_spawn_with_opt_in(|builder| { + /// builder.allow::(); + /// }); + /// } + /// # bevy_ecs::system::assert_is_system(example_system); + pub fn clone_and_spawn_with_opt_in( + &mut self, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> EntityCommands<'_> { + let entity_clone = self.commands().spawn_empty().id(); + self.clone_with_opt_in(entity_clone, config); EntityCommands { commands: self.commands_mut().reborrow(), entity: entity_clone, diff --git a/crates/bevy_ecs/src/world/entity_ref.rs b/crates/bevy_ecs/src/world/entity_ref.rs index d29c3db428..9b7f8eb551 100644 --- a/crates/bevy_ecs/src/world/entity_ref.rs +++ b/crates/bevy_ecs/src/world/entity_ref.rs @@ -11,7 +11,7 @@ use crate::{ }, entity::{ ContainsEntity, Entity, EntityCloner, EntityClonerBuilder, EntityEquivalent, - EntityIdLocation, EntityLocation, + EntityIdLocation, EntityLocation, OptIn, OptOut, }, event::EntityEvent, lifecycle::{DESPAWN, REMOVE, REPLACE}, @@ -2672,10 +2672,12 @@ impl<'w> EntityWorldMut<'w> { /// Clones parts of an entity (components, observers, etc.) onto another entity, /// configured through [`EntityClonerBuilder`]. /// - /// By default, the other entity will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The other entity will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. + /// + /// # Example /// - /// Configure through [`EntityClonerBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; /// # #[derive(Component, Clone, PartialEq, Debug)] @@ -2685,27 +2687,76 @@ impl<'w> EntityWorldMut<'w> { /// # let mut world = World::new(); /// # let entity = world.spawn((ComponentA, ComponentB)).id(); /// # let target = world.spawn_empty().id(); - /// world.entity_mut(entity).clone_with(target, |builder| { - /// builder.deny::(); + /// // Clone all components except ComponentA onto the target. + /// world.entity_mut(entity).clone_with_opt_out(target, |builder| { + /// builder.deny::(); /// }); - /// # assert_eq!(world.get::(target), Some(&ComponentA)); - /// # assert_eq!(world.get::(target), None); + /// # assert_eq!(world.get::(target), None); + /// # assert_eq!(world.get::(target), Some(&ComponentB)); /// ``` /// - /// See [`EntityClonerBuilder`] for more options. + /// See [`EntityClonerBuilder`] for more options. /// /// # Panics /// /// - If this entity has been despawned while this `EntityWorldMut` is still alive. /// - If the target entity does not exist. - pub fn clone_with( + pub fn clone_with_opt_out( &mut self, target: Entity, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> &mut Self { self.assert_not_despawned(); - let mut builder = EntityCloner::build(self.world); + let mut builder = EntityCloner::build_opt_out(self.world); + config(&mut builder); + builder.clone_entity(self.entity, target); + + self.world.flush(); + self.update_location(); + self + } + + /// Clones parts of an entity (components, observers, etc.) onto another entity, + /// configured through [`EntityClonerBuilder`]. + /// + /// The other entity will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentA; + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentB; + /// # let mut world = World::new(); + /// # let entity = world.spawn((ComponentA, ComponentB)).id(); + /// # let target = world.spawn_empty().id(); + /// // Clone only ComponentA onto the target. + /// world.entity_mut(entity).clone_with_opt_in(target, |builder| { + /// builder.allow::(); + /// }); + /// # assert_eq!(world.get::(target), Some(&ComponentA)); + /// # assert_eq!(world.get::(target), None); + /// ``` + /// + /// See [`EntityClonerBuilder`] for more options. + /// + /// # Panics + /// + /// - If this entity has been despawned while this `EntityWorldMut` is still alive. + /// - If the target entity does not exist. + pub fn clone_with_opt_in( + &mut self, + target: Entity, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> &mut Self { + self.assert_not_despawned(); + + let mut builder = EntityCloner::build_opt_in(self.world); config(&mut builder); builder.clone_entity(self.entity, target); @@ -2720,52 +2771,104 @@ impl<'w> EntityWorldMut<'w> { /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). /// /// To configure cloning behavior (such as only cloning certain components), - /// use [`EntityWorldMut::clone_and_spawn_with`]. + /// use [`EntityWorldMut::clone_and_spawn_with_opt_out`]/ + /// [`opt_in`](`EntityWorldMut::clone_and_spawn_with_opt_in`). /// /// # Panics /// /// If this entity has been despawned while this `EntityWorldMut` is still alive. pub fn clone_and_spawn(&mut self) -> Entity { - self.clone_and_spawn_with(|_| {}) + self.clone_and_spawn_with_opt_out(|_| {}) } /// Spawns a clone of this entity and allows configuring cloning behavior /// using [`EntityClonerBuilder`], returning the [`Entity`] of the clone. /// - /// By default, the clone will receive all the components of the original that implement - /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect). + /// The clone will receive all the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) except those that are + /// [denied](EntityClonerBuilder::deny) in the `config`. + /// + /// # Example /// - /// Configure through [`EntityClonerBuilder`] as follows: /// ``` /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let entity = world.spawn((ComponentA, ComponentB)).id(); /// # #[derive(Component, Clone, PartialEq, Debug)] /// # struct ComponentA; /// # #[derive(Component, Clone, PartialEq, Debug)] /// # struct ComponentB; - /// # let mut world = World::new(); - /// # let entity = world.spawn((ComponentA, ComponentB)).id(); - /// let entity_clone = world.entity_mut(entity).clone_and_spawn_with(|builder| { - /// builder.deny::(); + /// // Create a clone of an entity but without ComponentA. + /// let entity_clone = world.entity_mut(entity).clone_and_spawn_with_opt_out(|builder| { + /// builder.deny::(); /// }); - /// # assert_eq!(world.get::(entity_clone), Some(&ComponentA)); - /// # assert_eq!(world.get::(entity_clone), None); + /// # assert_eq!(world.get::(entity_clone), None); + /// # assert_eq!(world.get::(entity_clone), Some(&ComponentB)); /// ``` /// - /// See [`EntityClonerBuilder`] for more options. + /// See [`EntityClonerBuilder`] for more options. /// /// # Panics /// /// If this entity has been despawned while this `EntityWorldMut` is still alive. - pub fn clone_and_spawn_with( + pub fn clone_and_spawn_with_opt_out( &mut self, - config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, ) -> Entity { self.assert_not_despawned(); let entity_clone = self.world.entities.reserve_entity(); self.world.flush(); - let mut builder = EntityCloner::build(self.world); + let mut builder = EntityCloner::build_opt_out(self.world); + config(&mut builder); + builder.clone_entity(self.entity, entity_clone); + + self.world.flush(); + self.update_location(); + entity_clone + } + + /// Spawns a clone of this entity and allows configuring cloning behavior + /// using [`EntityClonerBuilder`], returning the [`Entity`] of the clone. + /// + /// The clone will receive only the components of the original that implement + /// [`Clone`] or [`Reflect`](bevy_reflect::Reflect) and are + /// [allowed](EntityClonerBuilder::allow) in the `config`. + /// + /// # Example + /// + /// ``` + /// # use bevy_ecs::prelude::*; + /// # let mut world = World::new(); + /// # let entity = world.spawn((ComponentA, ComponentB)).id(); + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentA; + /// # #[derive(Component, Clone, PartialEq, Debug)] + /// # struct ComponentB; + /// // Create a clone of an entity but only with ComponentA. + /// let entity_clone = world.entity_mut(entity).clone_and_spawn_with_opt_in(|builder| { + /// builder.allow::(); + /// }); + /// # assert_eq!(world.get::(entity_clone), Some(&ComponentA)); + /// # assert_eq!(world.get::(entity_clone), None); + /// ``` + /// + /// See [`EntityClonerBuilder`] for more options. + /// + /// # Panics + /// + /// If this entity has been despawned while this `EntityWorldMut` is still alive. + pub fn clone_and_spawn_with_opt_in( + &mut self, + config: impl FnOnce(&mut EntityClonerBuilder) + Send + Sync + 'static, + ) -> Entity { + self.assert_not_despawned(); + + let entity_clone = self.world.entities.reserve_entity(); + self.world.flush(); + + let mut builder = EntityCloner::build_opt_in(self.world); config(&mut builder); builder.clone_entity(self.entity, entity_clone); @@ -2786,8 +2889,7 @@ impl<'w> EntityWorldMut<'w> { pub fn clone_components(&mut self, target: Entity) -> &mut Self { self.assert_not_despawned(); - EntityCloner::build(self.world) - .deny_all() + EntityCloner::build_opt_in(self.world) .allow::() .clone_entity(self.entity, target); @@ -2809,8 +2911,7 @@ impl<'w> EntityWorldMut<'w> { pub fn move_components(&mut self, target: Entity) -> &mut Self { self.assert_not_despawned(); - EntityCloner::build(self.world) - .deny_all() + EntityCloner::build_opt_in(self.world) .allow::() .move_components(true) .clone_entity(self.entity, target); @@ -5998,12 +6099,12 @@ mod tests { #[test] fn entity_world_mut_clone_with_move_and_require() { #[derive(Component, Clone, PartialEq, Debug)] - #[require(B)] + #[require(B(3))] struct A; #[derive(Component, Clone, PartialEq, Debug, Default)] #[require(C(3))] - struct B; + struct B(u32); #[derive(Component, Clone, PartialEq, Debug, Default)] #[require(D)] @@ -6013,22 +6114,25 @@ mod tests { struct D; let mut world = World::new(); - let entity_a = world.spawn(A).id(); + let entity_a = world.spawn((A, B(5))).id(); let entity_b = world.spawn_empty().id(); - world.entity_mut(entity_a).clone_with(entity_b, |builder| { - builder - .move_components(true) - .without_required_components(|builder| { - builder.deny::(); - }); - }); + world + .entity_mut(entity_a) + .clone_with_opt_in(entity_b, |builder| { + builder + .move_components(true) + .allow::() + .without_required_components(|builder| { + builder.allow::(); + }); + }); - assert_eq!(world.entity(entity_a).get::(), Some(&A)); - assert_eq!(world.entity(entity_b).get::(), None); + assert_eq!(world.entity(entity_a).get::(), None); + assert_eq!(world.entity(entity_b).get::(), Some(&A)); - assert_eq!(world.entity(entity_a).get::(), None); - assert_eq!(world.entity(entity_b).get::(), Some(&B)); + assert_eq!(world.entity(entity_a).get::(), Some(&B(5))); + assert_eq!(world.entity(entity_b).get::(), Some(&B(3))); assert_eq!(world.entity(entity_a).get::(), None); assert_eq!(world.entity(entity_b).get::(), Some(&C(3))); diff --git a/crates/bevy_gizmos/src/lib.rs b/crates/bevy_gizmos/src/lib.rs index 5804729e06..f9512bc7ab 100755 --- a/crates/bevy_gizmos/src/lib.rs +++ b/crates/bevy_gizmos/src/lib.rs @@ -554,6 +554,7 @@ impl RenderAsset for GpuLineGizmo { gizmo: Self::SourceAsset, _: AssetId, render_device: &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let list_position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor { usage: BufferUsages::VERTEX, diff --git a/crates/bevy_gltf/src/lib.rs b/crates/bevy_gltf/src/lib.rs index 4262d43eb7..bbcb13a908 100644 --- a/crates/bevy_gltf/src/lib.rs +++ b/crates/bevy_gltf/src/lib.rs @@ -156,8 +156,24 @@ impl DefaultGltfImageSampler { pub struct GltfPlugin { /// The default image sampler to lay glTF sampler data on top of. /// - /// Can be modified with [`DefaultGltfImageSampler`] resource. + /// Can be modified with the [`DefaultGltfImageSampler`] resource. pub default_sampler: ImageSamplerDescriptor, + + /// Whether to convert glTF coordinates to Bevy's coordinate system by default. + /// If set to `true`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system + /// such that objects looking forward in glTF will also look forward in Bevy. + /// + /// The exact coordinate system conversion is as follows: + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + pub convert_coordinates: bool, + /// Registry for custom vertex attributes. /// /// To specify, use [`GltfPlugin::add_custom_vertex_attribute`]. @@ -169,6 +185,7 @@ impl Default for GltfPlugin { GltfPlugin { default_sampler: ImageSamplerDescriptor::linear(), custom_vertex_attributes: HashMap::default(), + convert_coordinates: false, } } } @@ -219,10 +236,12 @@ impl Plugin for GltfPlugin { let default_sampler_resource = DefaultGltfImageSampler::new(&self.default_sampler); let default_sampler = default_sampler_resource.get_internal(); app.insert_resource(default_sampler_resource); + app.register_asset_loader(GltfLoader { supported_compressed_formats, custom_vertex_attributes: self.custom_vertex_attributes.clone(), default_sampler, + default_convert_coordinates: self.convert_coordinates, }); } } diff --git a/crates/bevy_gltf/src/loader/mod.rs b/crates/bevy_gltf/src/loader/mod.rs index 5e0f752e50..a326af0526 100644 --- a/crates/bevy_gltf/src/loader/mod.rs +++ b/crates/bevy_gltf/src/loader/mod.rs @@ -151,6 +151,20 @@ pub struct GltfLoader { pub custom_vertex_attributes: HashMap, MeshVertexAttribute>, /// Arc to default [`ImageSamplerDescriptor`]. pub default_sampler: Arc>, + /// Whether to convert glTF coordinates to Bevy's coordinate system by default. + /// If set to `true`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system + /// such that objects looking forward in glTF will also look forward in Bevy. + /// + /// The exact coordinate system conversion is as follows: + /// - glTF: + /// - forward: Z + /// - up: Y + /// - right: -X + /// - Bevy: + /// - forward: -Z + /// - up: Y + /// - right: X + pub default_convert_coordinates: bool, } /// Specifies optional settings for processing gltfs at load time. By default, all recognized contents of @@ -188,11 +202,16 @@ pub struct GltfLoaderSettings { pub include_source: bool, /// Overrides the default sampler. Data from sampler node is added on top of that. /// - /// If None, uses global default which is stored in `DefaultGltfImageSampler` resource. + /// If None, uses the global default which is stored in the [`DefaultGltfImageSampler`](crate::DefaultGltfImageSampler) resource. pub default_sampler: Option, /// If true, the loader will ignore sampler data from gltf and use the default sampler. pub override_sampler: bool, - /// If true, the loader will convert glTF coordinates to Bevy's coordinate system. + /// Overrides the default glTF coordinate conversion setting. + /// + /// If set to `Some(true)`, the loader will convert the coordinate system of loaded glTF assets to Bevy's coordinate system + /// such that objects looking forward in glTF will also look forward in Bevy. + /// + /// The exact coordinate system conversion is as follows: /// - glTF: /// - forward: Z /// - up: Y @@ -201,7 +220,9 @@ pub struct GltfLoaderSettings { /// - forward: -Z /// - up: Y /// - right: X - pub convert_coordinates: bool, + /// + /// If `None`, uses the global default set by [`GltfPlugin::convert_coordinates`](crate::GltfPlugin::convert_coordinates). + pub convert_coordinates: Option, } impl Default for GltfLoaderSettings { @@ -214,7 +235,7 @@ impl Default for GltfLoaderSettings { include_source: false, default_sampler: None, override_sampler: false, - convert_coordinates: false, + convert_coordinates: None, } } } @@ -274,6 +295,11 @@ async fn load_gltf<'a, 'b, 'c>( paths }; + let convert_coordinates = match settings.convert_coordinates { + Some(convert_coordinates) => convert_coordinates, + None => loader.default_convert_coordinates, + }; + #[cfg(feature = "bevy_animation")] let (animations, named_animations, animation_roots) = { use bevy_animation::{animated_field, animation_curves::*, gltf_curves::*, VariableCurve}; @@ -318,7 +344,7 @@ async fn load_gltf<'a, 'b, 'c>( let translations: Vec = tr .map(Vec3::from) .map(|verts| { - if settings.convert_coordinates { + if convert_coordinates { Vec3::convert_coordinates(verts) } else { verts @@ -375,7 +401,7 @@ async fn load_gltf<'a, 'b, 'c>( .into_f32() .map(Quat::from_array) .map(|quat| { - if settings.convert_coordinates { + if convert_coordinates { Quat::convert_coordinates(quat) } else { quat @@ -663,7 +689,7 @@ async fn load_gltf<'a, 'b, 'c>( accessor, &buffer_data, &loader.custom_vertex_attributes, - settings.convert_coordinates, + convert_coordinates, ) { Ok((attribute, values)) => mesh.insert_attribute(attribute, values), Err(err) => warn!("{}", err), @@ -786,7 +812,7 @@ async fn load_gltf<'a, 'b, 'c>( .map(|mats| { mats.map(|mat| Mat4::from_cols_array_2d(&mat)) .map(|mat| { - if settings.convert_coordinates { + if convert_coordinates { mat.convert_coordinates() } else { mat @@ -875,7 +901,7 @@ async fn load_gltf<'a, 'b, 'c>( &node, children, mesh, - node_transform(&node, settings.convert_coordinates), + node_transform(&node, convert_coordinates), skin, node.extras().as_deref().map(GltfExtras::from), ); @@ -926,6 +952,7 @@ async fn load_gltf<'a, 'b, 'c>( #[cfg(feature = "bevy_animation")] None, &gltf.document, + convert_coordinates, ); if result.is_err() { err = Some(result); @@ -1345,9 +1372,10 @@ fn load_node( #[cfg(feature = "bevy_animation")] animation_roots: &HashSet, #[cfg(feature = "bevy_animation")] mut animation_context: Option, document: &Document, + convert_coordinates: bool, ) -> Result<(), GltfError> { let mut gltf_error = None; - let transform = node_transform(gltf_node, settings.convert_coordinates); + let transform = node_transform(gltf_node, convert_coordinates); let world_transform = *parent_transform * transform; // according to https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#instantiation, // if the determinant of the transform is negative we must invert the winding order of @@ -1616,6 +1644,7 @@ fn load_node( #[cfg(feature = "bevy_animation")] animation_context.clone(), document, + convert_coordinates, ) { gltf_error = Some(err); return; diff --git a/crates/bevy_gltf/src/vertex_attributes.rs b/crates/bevy_gltf/src/vertex_attributes.rs index 2620d608a0..5d6ce9eb3e 100644 --- a/crates/bevy_gltf/src/vertex_attributes.rs +++ b/crates/bevy_gltf/src/vertex_attributes.rs @@ -141,12 +141,21 @@ impl<'a> VertexAttributeIter<'a> { VertexAttributeIter::F32x2(it) => Ok(Values::Float32x2(it.collect())), VertexAttributeIter::U32x2(it) => Ok(Values::Uint32x2(it.collect())), VertexAttributeIter::F32x3(it) => Ok(if convert_coordinates { + // The following f32x3 values need to be converted to the correct coordinate system + // - Positions + // - Normals + // + // See Values::Float32x3(it.map(ConvertCoordinates::convert_coordinates).collect()) } else { Values::Float32x3(it.collect()) }), VertexAttributeIter::U32x3(it) => Ok(Values::Uint32x3(it.collect())), VertexAttributeIter::F32x4(it) => Ok(if convert_coordinates { + // The following f32x4 values need to be converted to the correct coordinate system + // - Tangents + // + // See Values::Float32x4(it.map(ConvertCoordinates::convert_coordinates).collect()) } else { Values::Float32x4(it.collect()) diff --git a/crates/bevy_image/Cargo.toml b/crates/bevy_image/Cargo.toml index c26a9da1bb..54559d60d3 100644 --- a/crates/bevy_image/Cargo.toml +++ b/crates/bevy_image/Cargo.toml @@ -38,6 +38,9 @@ serialize = ["bevy_reflect", "bevy_platform/serialize"] zlib = ["flate2"] zstd = ["ruzstd"] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = ["basis-universal"] + [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.17.0-dev" } diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index e7548bb2bd..a2157357ed 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -356,6 +356,8 @@ pub struct Image { pub sampler: ImageSampler, pub texture_view_descriptor: Option>>, pub asset_usage: RenderAssetUsages, + /// Whether this image should be copied on the GPU when resized. + pub copy_on_resize: bool, } /// Used in [`Image`], this determines what image sampler to use when rendering. The default setting, @@ -747,12 +749,15 @@ impl Image { label: None, mip_level_count: 1, sample_count: 1, - usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + usage: TextureUsages::TEXTURE_BINDING + | TextureUsages::COPY_DST + | TextureUsages::COPY_SRC, view_formats: &[], }, sampler: ImageSampler::Default, texture_view_descriptor: None, asset_usage, + copy_on_resize: false, } } @@ -887,13 +892,15 @@ impl Image { /// When growing, the new space is filled with 0. When shrinking, the image is clipped. /// /// For faster resizing when keeping pixel data intact is not important, use [`Image::resize`]. - pub fn resize_in_place(&mut self, new_size: Extent3d) -> Result<(), ResizeError> { + pub fn resize_in_place(&mut self, new_size: Extent3d) { let old_size = self.texture_descriptor.size; let pixel_size = self.texture_descriptor.format.pixel_size(); let byte_len = self.texture_descriptor.format.pixel_size() * new_size.volume(); + self.texture_descriptor.size = new_size; let Some(ref mut data) = self.data else { - return Err(ResizeError::ImageWithoutData); + self.copy_on_resize = true; + return; }; let mut new: Vec = vec![0; byte_len]; @@ -923,10 +930,6 @@ impl Image { } self.data = Some(new); - - self.texture_descriptor.size = new_size; - - Ok(()) } /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets @@ -1591,14 +1594,6 @@ pub enum TextureError { IncompleteCubemap, } -/// An error that occurs when an image cannot be resized. -#[derive(Error, Debug)] -pub enum ResizeError { - /// Failed to resize an Image because it has no data. - #[error("resize method requires cpu-side image data but none was present")] - ImageWithoutData, -} - /// The type of a raw image buffer. #[derive(Debug)] pub enum ImageType<'a> { @@ -1822,13 +1817,11 @@ mod test { } // Grow image - image - .resize_in_place(Extent3d { - width: 4, - height: 4, - depth_or_array_layers: 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 4, + height: 4, + depth_or_array_layers: 1, + }); // After growing, the test pattern should be the same. assert!(matches!( @@ -1849,13 +1842,11 @@ mod test { )); // Shrink - image - .resize_in_place(Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }); // Images outside of the new dimensions should be clipped assert!(image.get_color_at(1, 1).is_err()); @@ -1898,13 +1889,11 @@ mod test { } // Grow image - image - .resize_in_place(Extent3d { - width: 4, - height: 4, - depth_or_array_layers: LAYERS + 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 4, + height: 4, + depth_or_array_layers: LAYERS + 1, + }); // After growing, the test pattern should be the same. assert!(matches!( @@ -1929,13 +1918,11 @@ mod test { } // Shrink - image - .resize_in_place(Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 1, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 1, + }); // Images outside of the new dimensions should be clipped assert!(image.get_color_at_3d(1, 1, 0).is_err()); @@ -1944,13 +1931,11 @@ mod test { assert!(image.get_color_at_3d(0, 0, 1).is_err()); // Grow layers - image - .resize_in_place(Extent3d { - width: 1, - height: 1, - depth_or_array_layers: 2, - }) - .unwrap(); + image.resize_in_place(Extent3d { + width: 1, + height: 1, + depth_or_array_layers: 2, + }); // Pixels in the newly added layer should be zeroes. assert!(matches!( diff --git a/crates/bevy_image/src/lib.rs b/crates/bevy_image/src/lib.rs index 55f74a5f14..02c3785ced 100644 --- a/crates/bevy_image/src/lib.rs +++ b/crates/bevy_image/src/lib.rs @@ -14,7 +14,7 @@ mod image; pub use self::image::*; #[cfg(feature = "basis-universal")] mod basis; -#[cfg(feature = "basis-universal")] +#[cfg(feature = "compressed_image_saver")] mod compressed_image_saver; #[cfg(feature = "dds")] mod dds; @@ -29,7 +29,7 @@ mod ktx2; mod texture_atlas; mod texture_atlas_builder; -#[cfg(feature = "basis-universal")] +#[cfg(feature = "compressed_image_saver")] pub use compressed_image_saver::*; #[cfg(feature = "dds")] pub use dds::*; diff --git a/crates/bevy_input/Cargo.toml b/crates/bevy_input/Cargo.toml index 2961c0d115..7c69aad54a 100644 --- a/crates/bevy_input/Cargo.toml +++ b/crates/bevy_input/Cargo.toml @@ -75,7 +75,7 @@ serde = { version = "1", features = [ "derive", ], default-features = false, optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } smol_str = { version = "0.2", default-features = false, optional = true } log = { version = "0.4", default-features = false } diff --git a/crates/bevy_internal/Cargo.toml b/crates/bevy_internal/Cargo.toml index 4692fe9d15..1edc0e317e 100644 --- a/crates/bevy_internal/Cargo.toml +++ b/crates/bevy_internal/Cargo.toml @@ -28,6 +28,12 @@ detailed_trace = ["bevy_ecs/detailed_trace", "bevy_render?/detailed_trace"] sysinfo_plugin = ["bevy_diagnostic/sysinfo_plugin"] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = [ + "bevy_image/compressed_image_saver", + "bevy_render/compressed_image_saver", +] + # Texture formats that have specific rendering support (HDR enabled by default) basis-universal = ["bevy_image/basis-universal", "bevy_render/basis-universal"] exr = ["bevy_image/exr", "bevy_render/exr"] @@ -100,6 +106,7 @@ serialize = [ "bevy_window?/serialize", "bevy_winit?/serialize", "bevy_platform/serialize", + "bevy_render/serialize", ] multi_threaded = [ "std", diff --git a/crates/bevy_math/Cargo.toml b/crates/bevy_math/Cargo.toml index f28e9466ec..3fad8e6209 100644 --- a/crates/bevy_math/Cargo.toml +++ b/crates/bevy_math/Cargo.toml @@ -12,7 +12,7 @@ rust-version = "1.85.0" [dependencies] glam = { version = "0.29.3", default-features = false, features = ["bytemuck"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = [ +derive_more = { version = "2", default-features = false, features = [ "from", "into", ] } diff --git a/crates/bevy_mesh/Cargo.toml b/crates/bevy_mesh/Cargo.toml index c65c648cfd..0b77bdb619 100644 --- a/crates/bevy_mesh/Cargo.toml +++ b/crates/bevy_mesh/Cargo.toml @@ -28,11 +28,21 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea bitflags = { version = "2.3", features = ["serde"] } bytemuck = { version = "1.5" } wgpu-types = { version = "24", default-features = false } -serde = { version = "1", features = ["derive"] } +serde = { version = "1", default-features = false, features = [ + "derive", +], optional = true } hexasphere = "15.0" thiserror = { version = "2", default-features = false } tracing = { version = "0.1", default-features = false, features = ["std"] } +[dev-dependencies] +serde_json = "1.0.140" + +[features] +default = [] +## Adds serialization support through `serde`. +serialize = ["dep:serde", "wgpu-types/serde"] + [lints] workspace = true diff --git a/crates/bevy_mesh/src/index.rs b/crates/bevy_mesh/src/index.rs index ca84d63bfb..87d81cc3f2 100644 --- a/crates/bevy_mesh/src/index.rs +++ b/crates/bevy_mesh/src/index.rs @@ -1,6 +1,8 @@ use bevy_reflect::Reflect; use core::iter; use core::iter::FusedIterator; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::IndexFormat; @@ -69,8 +71,9 @@ pub enum MeshTrianglesError { /// An array of indices into the [`VertexAttributeValues`](super::VertexAttributeValues) for a mesh. /// /// It describes the order in which the vertex attributes should be joined into faces. -#[derive(Debug, Clone, Reflect)] +#[derive(Debug, Clone, Reflect, PartialEq)] #[reflect(Clone)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub enum Indices { U16(Vec), U32(Vec), diff --git a/crates/bevy_mesh/src/mesh.rs b/crates/bevy_mesh/src/mesh.rs index 893c84ecc5..3492788c4b 100644 --- a/crates/bevy_mesh/src/mesh.rs +++ b/crates/bevy_mesh/src/mesh.rs @@ -7,12 +7,18 @@ use super::{ MeshVertexAttributeId, MeshVertexBufferLayout, MeshVertexBufferLayoutRef, MeshVertexBufferLayouts, MeshWindingInvertError, VertexAttributeValues, VertexBufferLayout, }; +#[cfg(feature = "serialize")] +use crate::SerializedMeshAttributeData; use alloc::collections::BTreeMap; use bevy_asset::{Asset, Handle, RenderAssetUsages}; use bevy_image::Image; use bevy_math::{primitives::Triangle3d, *}; +#[cfg(feature = "serialize")] +use bevy_platform::collections::HashMap; use bevy_reflect::Reflect; use bytemuck::cast_slice; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use tracing::warn; use wgpu_types::{VertexAttribute, VertexFormat, VertexStepMode}; @@ -104,7 +110,7 @@ pub const VERTEX_ATTRIBUTE_BUFFER_ID: u64 = 10; /// - Vertex winding order: by default, `StandardMaterial.cull_mode` is `Some(Face::Back)`, /// which means that Bevy would *only* render the "front" of each triangle, which /// is the side of the triangle from where the vertices appear in a *counter-clockwise* order. -#[derive(Asset, Debug, Clone, Reflect)] +#[derive(Asset, Debug, Clone, Reflect, PartialEq)] #[reflect(Clone)] pub struct Mesh { #[reflect(ignore, clone)] @@ -207,6 +213,10 @@ impl Mesh { pub const ATTRIBUTE_JOINT_INDEX: MeshVertexAttribute = MeshVertexAttribute::new("Vertex_JointIndex", 7, VertexFormat::Uint16x4); + /// The first index that can be used for custom vertex attributes. + /// Only the attributes with an index below this are used by Bevy. + pub const FIRST_AVAILABLE_CUSTOM_ATTRIBUTE: u64 = 8; + /// Construct a new mesh. You need to provide a [`PrimitiveTopology`] so that the /// renderer knows how to treat the vertex data. Most of the time this will be /// [`PrimitiveTopology::TriangleList`]. @@ -1252,6 +1262,133 @@ impl core::ops::Mul for Transform { } } +/// A version of [`Mesh`] suitable for serializing for short-term transfer. +/// +/// [`Mesh`] does not implement [`Serialize`] / [`Deserialize`] because it is made with the renderer in mind. +/// It is not a general-purpose mesh implementation, and its internals are subject to frequent change. +/// As such, storing a [`Mesh`] on disk is highly discouraged. +/// +/// But there are still some valid use cases for serializing a [`Mesh`], namely transferring meshes between processes. +/// To support this, you can create a [`SerializedMesh`] from a [`Mesh`] with [`SerializedMesh::from_mesh`], +/// and then deserialize it with [`SerializedMesh::deserialize`]. The caveats are: +/// - The mesh representation is not valid across different versions of Bevy. +/// - This conversion is lossy. Only the following information is preserved: +/// - Primitive topology +/// - Vertex attributes +/// - Indices +/// - Custom attributes that were not specified with [`MeshDeserializer::add_custom_vertex_attribute`] will be ignored while deserializing. +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SerializedMesh { + primitive_topology: PrimitiveTopology, + attributes: Vec<(MeshVertexAttributeId, SerializedMeshAttributeData)>, + indices: Option, +} + +#[cfg(feature = "serialize")] +impl SerializedMesh { + /// Create a [`SerializedMesh`] from a [`Mesh`]. See the documentation for [`SerializedMesh`] for caveats. + pub fn from_mesh(mesh: Mesh) -> Self { + Self { + primitive_topology: mesh.primitive_topology, + attributes: mesh + .attributes + .into_iter() + .map(|(id, data)| { + ( + id, + SerializedMeshAttributeData::from_mesh_attribute_data(data), + ) + }) + .collect(), + indices: mesh.indices, + } + } + + /// Create a [`Mesh`] from a [`SerializedMesh`]. See the documentation for [`SerializedMesh`] for caveats. + /// + /// Use [`MeshDeserializer`] if you need to pass extra options to the deserialization process, such as specifying custom vertex attributes. + pub fn into_mesh(self) -> Mesh { + MeshDeserializer::default().deserialize(self) + } +} + +/// Use to specify extra options when deserializing a [`SerializedMesh`] into a [`Mesh`]. +#[cfg(feature = "serialize")] +pub struct MeshDeserializer { + custom_vertex_attributes: HashMap, MeshVertexAttribute>, +} + +#[cfg(feature = "serialize")] +impl Default for MeshDeserializer { + fn default() -> Self { + // Written like this so that the compiler can validate that we use all the built-in attributes. + // If you just added a new attribute and got a compile error, please add it to this list :) + const BUILTINS: [MeshVertexAttribute; Mesh::FIRST_AVAILABLE_CUSTOM_ATTRIBUTE as usize] = [ + Mesh::ATTRIBUTE_POSITION, + Mesh::ATTRIBUTE_NORMAL, + Mesh::ATTRIBUTE_UV_0, + Mesh::ATTRIBUTE_UV_1, + Mesh::ATTRIBUTE_TANGENT, + Mesh::ATTRIBUTE_COLOR, + Mesh::ATTRIBUTE_JOINT_WEIGHT, + Mesh::ATTRIBUTE_JOINT_INDEX, + ]; + Self { + custom_vertex_attributes: BUILTINS + .into_iter() + .map(|attribute| (attribute.name.into(), attribute)) + .collect(), + } + } +} + +#[cfg(feature = "serialize")] +impl MeshDeserializer { + /// Create a new [`MeshDeserializer`]. + pub fn new() -> Self { + Self::default() + } + + /// Register a custom vertex attribute to the deserializer. Custom vertex attributes that were not added with this method will be ignored while deserializing. + pub fn add_custom_vertex_attribute( + &mut self, + name: &str, + attribute: MeshVertexAttribute, + ) -> &mut Self { + self.custom_vertex_attributes.insert(name.into(), attribute); + self + } + + /// Deserialize a [`SerializedMesh`] into a [`Mesh`]. + /// + /// See the documentation for [`SerializedMesh`] for caveats. + pub fn deserialize(&self, serialized_mesh: SerializedMesh) -> Mesh { + Mesh { + attributes: + serialized_mesh + .attributes + .into_iter() + .filter_map(|(id, data)| { + let attribute = data.attribute.clone(); + let Some(data) = + data.try_into_mesh_attribute_data(&self.custom_vertex_attributes) + else { + warn!( + "Deserialized mesh contains custom vertex attribute {attribute:?} that \ + was not specified with `MeshDeserializer::add_custom_vertex_attribute`. Ignoring." + ); + return None; + }; + Some((id, data)) + }) + .collect(), + indices: serialized_mesh.indices, + ..Mesh::new(serialized_mesh.primitive_topology, RenderAssetUsages::default()) + } + } +} + /// Error that can occur when calling [`Mesh::merge`]. #[derive(Error, Debug, Clone)] #[error("Incompatible vertex attribute types {} and {}", self_attribute.name, other_attribute.map(|a| a.name).unwrap_or("None"))] @@ -1263,6 +1400,8 @@ pub struct MergeMeshError { #[cfg(test)] mod tests { use super::Mesh; + #[cfg(feature = "serialize")] + use super::SerializedMesh; use crate::mesh::{Indices, MeshWindingInvertError, VertexAttributeValues}; use crate::PrimitiveTopology; use bevy_asset::RenderAssetUsages; @@ -1567,4 +1706,26 @@ mod tests { mesh.triangles().unwrap().collect::>() ); } + + #[cfg(feature = "serialize")] + #[test] + fn serialize_deserialize_mesh() { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ); + + mesh.insert_attribute( + Mesh::ATTRIBUTE_POSITION, + vec![[0., 0., 0.], [2., 0., 0.], [0., 1., 0.], [0., 0., 1.]], + ); + mesh.insert_indices(Indices::U16(vec![0, 1, 2, 0, 2, 3])); + + let serialized_mesh = SerializedMesh::from_mesh(mesh.clone()); + let serialized_string = serde_json::to_string(&serialized_mesh).unwrap(); + let serialized_mesh_from_string: SerializedMesh = + serde_json::from_str(&serialized_string).unwrap(); + let deserialized_mesh = serialized_mesh_from_string.into_mesh(); + assert_eq!(mesh, deserialized_mesh); + } } diff --git a/crates/bevy_mesh/src/vertex.rs b/crates/bevy_mesh/src/vertex.rs index 949e355b4c..fd683ef60d 100644 --- a/crates/bevy_mesh/src/vertex.rs +++ b/crates/bevy_mesh/src/vertex.rs @@ -2,13 +2,17 @@ use alloc::sync::Arc; use bevy_derive::EnumVariantMeta; use bevy_ecs::resource::Resource; use bevy_math::Vec3; +#[cfg(feature = "serialize")] +use bevy_platform::collections::HashMap; use bevy_platform::collections::HashSet; use bytemuck::cast_slice; use core::hash::{Hash, Hasher}; +#[cfg(feature = "serialize")] +use serde::{Deserialize, Serialize}; use thiserror::Error; use wgpu_types::{BufferAddress, VertexAttribute, VertexFormat, VertexStepMode}; -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, PartialEq)] pub struct MeshVertexAttribute { /// The friendly name of the vertex attribute pub name: &'static str, @@ -22,6 +26,37 @@ pub struct MeshVertexAttribute { pub format: VertexFormat, } +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct SerializedMeshVertexAttribute { + pub(crate) name: String, + pub(crate) id: MeshVertexAttributeId, + pub(crate) format: VertexFormat, +} + +#[cfg(feature = "serialize")] +impl SerializedMeshVertexAttribute { + pub(crate) fn from_mesh_vertex_attribute(attribute: MeshVertexAttribute) -> Self { + Self { + name: attribute.name.to_string(), + id: attribute.id, + format: attribute.format, + } + } + + pub(crate) fn try_into_mesh_vertex_attribute( + self, + possible_attributes: &HashMap, MeshVertexAttribute>, + ) -> Option { + let attr = possible_attributes.get(self.name.as_str())?; + if attr.id == self.id { + Some(*attr) + } else { + None + } + } +} + impl MeshVertexAttribute { pub const fn new(name: &'static str, id: u64, format: VertexFormat) -> Self { Self { @@ -37,6 +72,7 @@ impl MeshVertexAttribute { } #[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub struct MeshVertexAttributeId(u64); impl From for MeshVertexAttributeId { @@ -132,12 +168,42 @@ impl VertexAttributeDescriptor { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub(crate) struct MeshAttributeData { pub(crate) attribute: MeshVertexAttribute, pub(crate) values: VertexAttributeValues, } +#[cfg(feature = "serialize")] +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct SerializedMeshAttributeData { + pub(crate) attribute: SerializedMeshVertexAttribute, + pub(crate) values: VertexAttributeValues, +} + +#[cfg(feature = "serialize")] +impl SerializedMeshAttributeData { + pub(crate) fn from_mesh_attribute_data(data: MeshAttributeData) -> Self { + Self { + attribute: SerializedMeshVertexAttribute::from_mesh_vertex_attribute(data.attribute), + values: data.values, + } + } + + pub(crate) fn try_into_mesh_attribute_data( + self, + possible_attributes: &HashMap, MeshVertexAttribute>, + ) -> Option { + let attribute = self + .attribute + .try_into_mesh_vertex_attribute(possible_attributes)?; + Some(MeshAttributeData { + attribute, + values: self.values, + }) + } +} + /// Compute a vector whose direction is the normal of the triangle formed by /// points a, b, c, and whose magnitude is double the area of the triangle. This /// is useful for computing smooth normals where the contributing normals are @@ -167,7 +233,8 @@ pub fn face_normal(a: [f32; 3], b: [f32; 3], c: [f32; 3]) -> [f32; 3] { /// Contains an array where each entry describes a property of a single vertex. /// Matches the [`VertexFormats`](VertexFormat). -#[derive(Clone, Debug, EnumVariantMeta)] +#[derive(Clone, Debug, EnumVariantMeta, PartialEq)] +#[cfg_attr(feature = "serialize", derive(Serialize, Deserialize))] pub enum VertexAttributeValues { Float32(Vec), Sint32(Vec), diff --git a/crates/bevy_pbr/Cargo.toml b/crates/bevy_pbr/Cargo.toml index 4ecf53a773..f2e973eae5 100644 --- a/crates/bevy_pbr/Cargo.toml +++ b/crates/bevy_pbr/Cargo.toml @@ -54,7 +54,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea bitflags = "2.3" fixedbitset = "0.5" thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } # meshlet lz4_flex = { version = "0.11", default-features = false, features = [ "frame", diff --git a/crates/bevy_pbr/src/atmosphere/resources.rs b/crates/bevy_pbr/src/atmosphere/resources.rs index d7c93c4418..aa682fbb03 100644 --- a/crates/bevy_pbr/src/atmosphere/resources.rs +++ b/crates/bevy_pbr/src/atmosphere/resources.rs @@ -1,7 +1,5 @@ use bevy_asset::{load_embedded_asset, Handle}; -use bevy_core_pipeline::{ - core_3d::Camera3d, fullscreen_vertex_shader::fullscreen_shader_vertex_state, -}; +use bevy_core_pipeline::{core_3d::Camera3d, FullscreenShader}; use bevy_ecs::{ component::Component, entity::Entity, @@ -36,7 +34,8 @@ pub(crate) struct AtmosphereBindGroupLayouts { pub(crate) struct RenderSkyBindGroupLayouts { pub render_sky: BindGroupLayout, pub render_sky_msaa: BindGroupLayout, - pub shader: Handle, + pub fullscreen_shader: FullscreenShader, + pub fragment_shader: Handle, } impl FromWorld for AtmosphereBindGroupLayouts { @@ -205,7 +204,8 @@ impl FromWorld for RenderSkyBindGroupLayouts { Self { render_sky, render_sky_msaa, - shader: load_embedded_asset!(world, "render_sky.wgsl"), + fullscreen_shader: world.resource::().clone(), + fragment_shader: load_embedded_asset!(world, "render_sky.wgsl"), } } } @@ -358,7 +358,7 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { self.render_sky_msaa.clone() }], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), primitive: PrimitiveState::default(), depth_stencil: None, multisample: MultisampleState { @@ -368,7 +368,7 @@ impl SpecializedRenderPipeline for RenderSkyBindGroupLayouts { }, zero_initialize_workgroup_memory: false, fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "main".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_pbr/src/light/mod.rs b/crates/bevy_pbr/src/light/mod.rs index 8273ae4b6d..e50ffcc0fc 100644 --- a/crates/bevy_pbr/src/light/mod.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -7,7 +7,7 @@ use bevy_ecs::{ use bevy_math::{ops, Mat4, Vec3A, Vec4}; use bevy_reflect::prelude::*; use bevy_render::{ - camera::{Camera, CameraProjection, Projection}, + camera::{Camera, Projection}, extract_component::ExtractComponent, extract_resource::ExtractResource, mesh::Mesh3d, diff --git a/crates/bevy_pbr/src/material.rs b/crates/bevy_pbr/src/material.rs index af11db1ba6..d17599c106 100644 --- a/crates/bevy_pbr/src/material.rs +++ b/crates/bevy_pbr/src/material.rs @@ -1410,6 +1410,7 @@ impl RenderAsset for PreparedMaterial { alpha_mask_deferred_draw_functions, material_param, ): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let draw_opaque_pbr = opaque_draw_functions.read().id::>(); let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::>(); diff --git a/crates/bevy_pbr/src/material_bind_groups.rs b/crates/bevy_pbr/src/material_bind_groups.rs index 735bc77c99..39028fed2d 100644 --- a/crates/bevy_pbr/src/material_bind_groups.rs +++ b/crates/bevy_pbr/src/material_bind_groups.rs @@ -2051,7 +2051,7 @@ impl MaterialDataBuffer { /// The size of the piece of data supplied to this method must equal the /// [`Self::aligned_element_size`] provided to [`MaterialDataBuffer::new`]. fn insert(&mut self, data: &[u8]) -> u32 { - // Make the the data is of the right length. + // Make sure the data is of the right length. debug_assert_eq!(data.len(), self.aligned_element_size as usize); // Grab a slot. diff --git a/crates/bevy_pbr/src/meshlet/pipelines.rs b/crates/bevy_pbr/src/meshlet/pipelines.rs index c25d896b8a..243bbddf22 100644 --- a/crates/bevy_pbr/src/meshlet/pipelines.rs +++ b/crates/bevy_pbr/src/meshlet/pipelines.rs @@ -2,7 +2,7 @@ use super::resource_manager::ResourceManager; use bevy_asset::{weak_handle, Handle}; use bevy_core_pipeline::{ core_3d::CORE_3D_DEPTH_FORMAT, experimental::mip_generation::DOWNSAMPLE_DEPTH_SHADER_HANDLE, - fullscreen_vertex_shader::fullscreen_shader_vertex_state, + FullscreenShader, }; use bevy_ecs::{ resource::Resource, @@ -80,6 +80,8 @@ impl FromWorld for MeshletPipelines { let remap_1d_to_2d_dispatch_layout = resource_manager .remap_1d_to_2d_dispatch_bind_group_layout .clone(); + + let vertex_state = world.resource::().to_vertex_state(); let pipeline_cache = world.resource_mut::(); Self { @@ -400,7 +402,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_layout], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state.clone(), primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -424,7 +426,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_resolve_depth_pipeline".into()), layout: vec![resolve_depth_shadow_view_layout], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state.clone(), primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: CORE_3D_DEPTH_FORMAT, @@ -449,7 +451,7 @@ impl FromWorld for MeshletPipelines { label: Some("meshlet_resolve_material_depth_pipeline".into()), layout: vec![resolve_material_depth_layout], push_constant_ranges: vec![], - vertex: fullscreen_shader_vertex_state(), + vertex: vertex_state, primitive: PrimitiveState::default(), depth_stencil: Some(DepthStencilState { format: TextureFormat::Depth16Unorm, diff --git a/crates/bevy_pbr/src/ssr/mod.rs b/crates/bevy_pbr/src/ssr/mod.rs index 22d5acd1e9..67ca025ce7 100644 --- a/crates/bevy_pbr/src/ssr/mod.rs +++ b/crates/bevy_pbr/src/ssr/mod.rs @@ -7,8 +7,8 @@ use bevy_core_pipeline::{ graph::{Core3d, Node3d}, DEPTH_TEXTURE_SAMPLING_SUPPORTED, }, - fullscreen_vertex_shader, prepass::{DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass}, + FullscreenShader, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -155,7 +155,8 @@ pub struct ScreenSpaceReflectionsPipeline { depth_nearest_sampler: Sampler, bind_group_layout: BindGroupLayout, binding_arrays_are_usable: bool, - shader: Handle, + fullscreen_shader: FullscreenShader, + fragment_shader: Handle, } /// A GPU buffer that stores the screen space reflection settings for each view. @@ -397,9 +398,10 @@ impl FromWorld for ScreenSpaceReflectionsPipeline { depth_nearest_sampler, bind_group_layout, binding_arrays_are_usable: binding_arrays_are_usable(render_device, render_adapter), + fullscreen_shader: world.resource::().clone(), // Even though ssr was loaded using load_shader_library, we can still access it like a // normal embedded asset (so we can use it as both a library or a kernel). - shader: load_embedded_asset!(world, "ssr.wgsl"), + fragment_shader: load_embedded_asset!(world, "ssr.wgsl"), } } } @@ -536,9 +538,9 @@ impl SpecializedRenderPipeline for ScreenSpaceReflectionsPipeline { RenderPipelineDescriptor { label: Some("SSR pipeline".into()), layout: vec![mesh_view_layout.clone(), self.bind_group_layout.clone()], - vertex: fullscreen_vertex_shader::fullscreen_shader_vertex_state(), + vertex: self.fullscreen_shader.to_vertex_state(), fragment: Some(FragmentState { - shader: self.shader.clone(), + shader: self.fragment_shader.clone(), shader_defs, entry_point: "fragment".into(), targets: vec![Some(ColorTargetState { diff --git a/crates/bevy_pbr/src/wireframe.rs b/crates/bevy_pbr/src/wireframe.rs index e42e1309ec..b710141ea3 100644 --- a/crates/bevy_pbr/src/wireframe.rs +++ b/crates/bevy_pbr/src/wireframe.rs @@ -474,6 +474,7 @@ impl RenderAsset for RenderWireframeMaterial { source_asset: Self::SourceAsset, _asset_id: AssetId, _param: &mut SystemParamItem, + _previous_asset: Option<&Self>, ) -> Result> { Ok(RenderWireframeMaterial { color: source_asset.color.to_linear().to_f32_array(), diff --git a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs index 5ac2d89887..d521fe1213 100644 --- a/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs +++ b/crates/bevy_picking/src/mesh_picking/ray_cast/intersections.rs @@ -1,5 +1,5 @@ -use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec3, Vec3A}; -use bevy_mesh::{Indices, Mesh, PrimitiveTopology}; +use bevy_math::{bounding::Aabb3d, Dir3, Mat4, Ray3d, Vec2, Vec3, Vec3A}; +use bevy_mesh::{Indices, Mesh, PrimitiveTopology, VertexAttributeValues}; use bevy_reflect::Reflect; use super::Backfaces; @@ -18,6 +18,8 @@ pub struct RayMeshHit { pub distance: f32, /// The vertices of the triangle that was hit. pub triangle: Option<[Vec3; 3]>, + /// UV coordinate of the hit, if the mesh has UV attributes. + pub uv: Option, /// The index of the triangle that was hit. pub triangle_index: Option, } @@ -26,6 +28,10 @@ pub struct RayMeshHit { #[derive(Default, Debug)] pub struct RayTriangleHit { pub distance: f32, + /// Note this uses the convention from the Moller-Trumbore algorithm: + /// P = (1 - u - v)A + uB + vC + /// This is different from the more common convention of + /// P = uA + vB + (1 - u - v)C pub barycentric_coords: (f32, f32), } @@ -34,7 +40,7 @@ pub(super) fn ray_intersection_over_mesh( mesh: &Mesh, transform: &Mat4, ray: Ray3d, - culling: Backfaces, + cull: Backfaces, ) -> Option { if mesh.primitive_topology() != PrimitiveTopology::TriangleList { return None; // ray_mesh_intersection assumes vertices are laid out in a triangle list @@ -47,26 +53,37 @@ pub(super) fn ray_intersection_over_mesh( .attribute(Mesh::ATTRIBUTE_NORMAL) .and_then(|normal_values| normal_values.as_float3()); + let uvs = mesh + .attribute(Mesh::ATTRIBUTE_UV_0) + .and_then(|uvs| match uvs { + VertexAttributeValues::Float32x2(uvs) => Some(uvs.as_slice()), + _ => None, + }); + match mesh.indices() { Some(Indices::U16(indices)) => { - ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling) + ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull) } Some(Indices::U32(indices)) => { - ray_mesh_intersection(ray, transform, positions, normals, Some(indices), culling) + ray_mesh_intersection(ray, transform, positions, normals, Some(indices), uvs, cull) } - None => ray_mesh_intersection::(ray, transform, positions, normals, None, culling), + None => ray_mesh_intersection::(ray, transform, positions, normals, None, uvs, cull), } } /// Checks if a ray intersects a mesh, and returns the nearest intersection if one exists. -pub fn ray_mesh_intersection + Clone + Copy>( +pub fn ray_mesh_intersection( ray: Ray3d, mesh_transform: &Mat4, positions: &[[f32; 3]], vertex_normals: Option<&[[f32; 3]]>, indices: Option<&[I]>, + uvs: Option<&[[f32; 2]]>, backface_culling: Backfaces, -) -> Option { +) -> Option +where + I: TryInto + Clone + Copy, +{ let world_to_mesh = mesh_transform.inverse(); let ray = Ray3d::new( @@ -139,17 +156,12 @@ pub fn ray_mesh_intersection + Clone + Copy>( closest_hit.and_then(|(tri_idx, hit)| { let [a, b, c] = match indices { Some(indices) => { - let triangle = indices.get((tri_idx * 3)..(tri_idx * 3 + 3))?; - - let [Ok(a), Ok(b), Ok(c)] = [ - triangle[0].try_into(), - triangle[1].try_into(), - triangle[2].try_into(), - ] else { - return None; - }; - - [a, b, c] + let [i, j, k] = [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2]; + [ + indices.get(i).copied()?.try_into().ok()?, + indices.get(j).copied()?.try_into().ok()?, + indices.get(k).copied()?.try_into().ok()?, + ] } None => [tri_idx * 3, tri_idx * 3 + 1, tri_idx * 3 + 2], }; @@ -168,10 +180,12 @@ pub fn ray_mesh_intersection + Clone + Copy>( }); let point = ray.get_point(hit.distance); + // Note that we need to convert from the Möller-Trumbore convention to the more common + // P = uA + vB + (1 - u - v)C convention. let u = hit.barycentric_coords.0; let v = hit.barycentric_coords.1; let w = 1.0 - u - v; - let barycentric = Vec3::new(u, v, w); + let barycentric = Vec3::new(w, u, v); let normal = if let Some(normals) = tri_normals { normals[1] * u + normals[2] * v + normals[0] * w @@ -181,9 +195,29 @@ pub fn ray_mesh_intersection + Clone + Copy>( .normalize() }; + let uv = uvs.and_then(|uvs| { + let tri_uvs = if let Some(indices) = indices { + let i = tri_idx * 3; + [ + uvs[indices[i].try_into().ok()?], + uvs[indices[i + 1].try_into().ok()?], + uvs[indices[i + 2].try_into().ok()?], + ] + } else { + let i = tri_idx * 3; + [uvs[i], uvs[i + 1], uvs[i + 2]] + }; + Some( + barycentric.x * Vec2::from(tri_uvs[0]) + + barycentric.y * Vec2::from(tri_uvs[1]) + + barycentric.z * Vec2::from(tri_uvs[2]), + ) + }); + Some(RayMeshHit { point: mesh_transform.transform_point3(point), normal: mesh_transform.transform_vector3(normal), + uv, barycentric_coords: barycentric, distance: mesh_transform .transform_vector3(ray.direction * hit.distance) @@ -329,6 +363,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -350,6 +385,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -372,6 +408,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -394,6 +431,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -415,6 +453,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -436,6 +475,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -457,6 +497,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); @@ -478,6 +519,7 @@ mod tests { positions, vertex_normals, indices, + None, backface_culling, ); diff --git a/crates/bevy_reflect/Cargo.toml b/crates/bevy_reflect/Cargo.toml index 087cdb44db..60f8478bac 100644 --- a/crates/bevy_reflect/Cargo.toml +++ b/crates/bevy_reflect/Cargo.toml @@ -93,7 +93,7 @@ erased-serde = { version = "0.4", default-features = false, features = [ disqualified = { version = "1.0", default-features = false } downcast-rs = { version = "2", default-features = false } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } serde = { version = "1", default-features = false, features = ["alloc"] } assert_type_match = "0.1.1" smallvec = { version = "1.11", default-features = false, optional = true } diff --git a/crates/bevy_reflect/src/lib.rs b/crates/bevy_reflect/src/lib.rs index eabfdc0eac..8b50c4b5b2 100644 --- a/crates/bevy_reflect/src/lib.rs +++ b/crates/bevy_reflect/src/lib.rs @@ -1001,7 +1001,7 @@ mod tests { /// If we don't append the strings in the `TypePath` derive correctly (i.e. explicitly specifying the type), /// we'll get a compilation error saying that "`&String` cannot be added to `String`". /// - /// So this test just ensures that we do do that correctly. + /// So this test just ensures that we do that correctly. /// /// This problem is a known issue and is unexpectedly expected behavior: /// - diff --git a/crates/bevy_render/Cargo.toml b/crates/bevy_render/Cargo.toml index 143a1f0270..9ecbbfc744 100644 --- a/crates/bevy_render/Cargo.toml +++ b/crates/bevy_render/Cargo.toml @@ -21,11 +21,14 @@ keywords = ["bevy"] # wgpu-types = { git = "https://github.com/gfx-rs/wgpu", rev = "..." } decoupled_naga = [] +# Enables compressed KTX2 UASTC texture output on the asset processor +compressed_image_saver = ["bevy_image/compressed_image_saver"] + # Texture formats (require more than just image support) basis-universal = ["bevy_image/basis-universal"] exr = ["bevy_image/exr"] hdr = ["bevy_image/hdr"] -ktx2 = ["dep:ktx2", "bevy_image/ktx2"] +ktx2 = ["bevy_image/ktx2"] multi_threaded = ["bevy_tasks/multi_threaded"] @@ -46,6 +49,8 @@ ci_limits = [] webgl = ["wgpu/webgl"] webgpu = ["wgpu/webgpu"] detailed_trace = [] +## Adds serialization support through `serde`. +serialize = ["bevy_mesh/serialize"] [dependencies] # bevy @@ -97,9 +102,8 @@ serde = { version = "1", features = ["derive"] } bytemuck = { version = "1.5", features = ["derive", "must_cast"] } downcast-rs = { version = "2", default-features = false, features = ["std"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } futures-lite = "2.0.1" -ktx2 = { version = "0.4.0", optional = true } encase = { version = "0.10", features = ["glam"] } # For wgpu profiling using tracing. Use `RUST_LOG=info` to also capture the wgpu spans. profiling = { version = "1", features = [ diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 2bddcd0d05..2732a44316 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -5,7 +5,7 @@ use super::{ClearColorConfig, Projection}; use crate::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, - camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews}, + camera::{ManualTextureViewHandle, ManualTextureViews}, primitives::Frustum, render_asset::RenderAssets, render_graph::{InternedRenderSubGraph, RenderSubGraph}, @@ -311,8 +311,8 @@ pub enum ViewportConversionError { #[error("computed coordinate beyond `Camera`'s far plane")] PastFarPlane, /// The Normalized Device Coordinates could not be computed because the `camera_transform`, the - /// `world_position`, or the projection matrix defined by [`CameraProjection`] contained `NAN` - /// (see [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]). + /// `world_position`, or the projection matrix defined by [`Projection`] contained `NAN` (see + /// [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]). #[error("found NaN while computing NDC")] InvalidData, } @@ -490,7 +490,7 @@ impl Camera { .map(|t: &RenderTargetInfo| t.scale_factor) } - /// The projection matrix computed using this camera's [`CameraProjection`]. + /// The projection matrix computed using this camera's [`Projection`]. #[inline] pub fn clip_from_view(&self) -> Mat4 { self.computed.clip_from_view @@ -655,7 +655,7 @@ impl Camera { /// To get the coordinates in the render target's viewport dimensions, you should use /// [`world_to_viewport`](Self::world_to_viewport). /// - /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. + /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`Projection`] contain `NAN`. /// /// # Panics /// @@ -681,7 +681,7 @@ impl Camera { /// To get the world space coordinates with the viewport position, you should use /// [`world_to_viewport`](Self::world_to_viewport). /// - /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. + /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`Projection`] contain `NAN`. /// /// # Panics /// diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index ee2a5080d2..9fa8831432 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -1,9 +1,9 @@ use core::fmt::Debug; +use core::ops::{Deref, DerefMut}; use crate::{primitives::Frustum, view::VisibilitySystems}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_asset::AssetEventSystems; -use bevy_derive::{Deref, DerefMut}; use bevy_ecs::prelude::*; use bevy_math::{ops, AspectRatio, Mat4, Rect, Vec2, Vec3A, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect, ReflectDeserialize, ReflectSerialize}; @@ -131,11 +131,10 @@ mod sealed { /// custom projection. /// /// The contained dynamic object can be downcast into a static type using [`CustomProjection::get`]. -#[derive(Component, Debug, Reflect, Deref, DerefMut)] +#[derive(Debug, Reflect)] #[reflect(Default, Clone)] pub struct CustomProjection { #[reflect(ignore)] - #[deref] dyn_projection: Box, } @@ -204,6 +203,20 @@ impl CustomProjection { } } +impl Deref for CustomProjection { + type Target = dyn CameraProjection; + + fn deref(&self) -> &Self::Target { + self.dyn_projection.as_ref() + } +} + +impl DerefMut for CustomProjection { + fn deref_mut(&mut self) -> &mut Self::Target { + self.dyn_projection.as_mut() + } +} + /// Component that defines how to compute a [`Camera`]'s projection matrix. /// /// Common projections, like perspective and orthographic, are provided out of the box to handle the @@ -240,7 +253,7 @@ impl Projection { // that, say, the `Debug` implementation is missing. Wrapping these traits behind a super // trait or some other indirection will make the errors harder to understand. // - // For example, we don't use the `DynCameraProjection`` trait bound, because it is not the + // For example, we don't use the `DynCameraProjection` trait bound, because it is not the // trait the user should be implementing - they only need to worry about implementing // `CameraProjection`. P: CameraProjection + Debug + Send + Sync + Clone + 'static, @@ -251,44 +264,24 @@ impl Projection { } } -impl CameraProjection for Projection { - fn get_clip_from_view(&self) -> Mat4 { +impl Deref for Projection { + type Target = dyn CameraProjection; + + fn deref(&self) -> &Self::Target { match self { - Projection::Perspective(projection) => projection.get_clip_from_view(), - Projection::Orthographic(projection) => projection.get_clip_from_view(), - Projection::Custom(projection) => projection.get_clip_from_view(), + Projection::Perspective(projection) => projection, + Projection::Orthographic(projection) => projection, + Projection::Custom(projection) => projection.deref(), } } +} - fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { +impl DerefMut for Projection { + fn deref_mut(&mut self) -> &mut Self::Target { match self { - Projection::Perspective(projection) => projection.get_clip_from_view_for_sub(sub_view), - Projection::Orthographic(projection) => projection.get_clip_from_view_for_sub(sub_view), - Projection::Custom(projection) => projection.get_clip_from_view_for_sub(sub_view), - } - } - - fn update(&mut self, width: f32, height: f32) { - match self { - Projection::Perspective(projection) => projection.update(width, height), - Projection::Orthographic(projection) => projection.update(width, height), - Projection::Custom(projection) => projection.update(width, height), - } - } - - fn far(&self) -> f32 { - match self { - Projection::Perspective(projection) => projection.far(), - Projection::Orthographic(projection) => projection.far(), - Projection::Custom(projection) => projection.far(), - } - } - - fn get_frustum_corners(&self, z_near: f32, z_far: f32) -> [Vec3A; 8] { - match self { - Projection::Perspective(projection) => projection.get_frustum_corners(z_near, z_far), - Projection::Orthographic(projection) => projection.get_frustum_corners(z_near, z_far), - Projection::Custom(projection) => projection.get_frustum_corners(z_near, z_far), + Projection::Perspective(projection) => projection, + Projection::Orthographic(projection) => projection, + Projection::Custom(projection) => projection.deref_mut(), } } } diff --git a/crates/bevy_render/src/mesh/mod.rs b/crates/bevy_render/src/mesh/mod.rs index d15468376f..c981e75cee 100644 --- a/crates/bevy_render/src/mesh/mod.rs +++ b/crates/bevy_render/src/mesh/mod.rs @@ -209,6 +209,7 @@ impl RenderAsset for RenderMesh { mesh: Self::SourceAsset, _: AssetId, (images, mesh_vertex_buffer_layouts): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { let morph_targets = match mesh.morph_targets() { Some(mt) => { diff --git a/crates/bevy_render/src/render_asset.rs b/crates/bevy_render/src/render_asset.rs index 1fa5758ca3..0a5ad3e4ec 100644 --- a/crates/bevy_render/src/render_asset.rs +++ b/crates/bevy_render/src/render_asset.rs @@ -73,6 +73,7 @@ pub trait RenderAsset: Send + Sync + 'static + Sized { source_asset: Self::SourceAsset, asset_id: AssetId, param: &mut SystemParamItem, + previous_asset: Option<&Self>, ) -> Result>; /// Called whenever the [`RenderAsset::SourceAsset`] has been removed. @@ -355,7 +356,8 @@ pub fn prepare_assets( 0 }; - match A::prepare_asset(extracted_asset, id, &mut param) { + let previous_asset = render_assets.get(id); + match A::prepare_asset(extracted_asset, id, &mut param, previous_asset) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); @@ -382,7 +384,7 @@ pub fn prepare_assets( // we remove previous here to ensure that if we are updating the asset then // any users will not see the old asset after a new asset is extracted, // even if the new asset is not yet ready or we are out of bytes to write. - render_assets.remove(id); + let previous_asset = render_assets.remove(id); let write_bytes = if let Some(size) = A::byte_len(&extracted_asset) { if bpf.exhausted() { @@ -394,7 +396,7 @@ pub fn prepare_assets( 0 }; - match A::prepare_asset(extracted_asset, id, &mut param) { + match A::prepare_asset(extracted_asset, id, &mut param, previous_asset.as_ref()) { Ok(prepared_asset) => { render_assets.insert(id, prepared_asset); bpf.write_bytes(write_bytes); diff --git a/crates/bevy_render/src/render_resource/buffer_vec.rs b/crates/bevy_render/src/render_resource/buffer_vec.rs index 4e6c787fba..1fdb26655d 100644 --- a/crates/bevy_render/src/render_resource/buffer_vec.rs +++ b/crates/bevy_render/src/render_resource/buffer_vec.rs @@ -183,6 +183,31 @@ impl RawBufferVec { } } + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// Before queuing the write, a [`reserve`](RawBufferVec::reserve) operation + /// is executed. + /// + /// This will only write the data contained in the given range. It is useful if you only want + /// to update a part of the buffer. + pub fn write_buffer_range( + &mut self, + device: &RenderDevice, + render_queue: &RenderQueue, + range: core::ops::Range, + ) { + if self.values.is_empty() { + return; + } + self.reserve(self.values.len(), device); + if let Some(buffer) = &self.buffer { + // Cast only the bytes we need to write + let bytes: &[u8] = must_cast_slice(&self.values[range.start..range.end]); + render_queue.write_buffer(buffer, (range.start * self.item_size) as u64, bytes); + } + } + /// Reduces the length of the buffer. pub fn truncate(&mut self, len: usize) { self.values.truncate(len); @@ -389,6 +414,31 @@ where queue.write_buffer(buffer, 0, &self.data); } + /// Queues writing of data from system RAM to VRAM using the [`RenderDevice`] + /// and the provided [`RenderQueue`]. + /// + /// Before queuing the write, a [`reserve`](BufferVec::reserve) operation + /// is executed. + /// + /// This will only write the data contained in the given range. It is useful if you only want + /// to update a part of the buffer. + pub fn write_buffer_range( + &mut self, + device: &RenderDevice, + render_queue: &RenderQueue, + range: core::ops::Range, + ) { + if self.data.is_empty() { + return; + } + let item_size = u64::from(T::min_size()) as usize; + self.reserve(self.data.len() / item_size, device); + if let Some(buffer) = &self.buffer { + let bytes = &self.data[range.start..range.end]; + render_queue.write_buffer(buffer, (range.start * item_size) as u64, bytes); + } + } + /// Reduces the length of the buffer. pub fn truncate(&mut self, len: usize) { self.data.truncate(u64::from(T::min_size()) as usize * len); diff --git a/crates/bevy_render/src/storage.rs b/crates/bevy_render/src/storage.rs index 0046b4e6ac..6084271fee 100644 --- a/crates/bevy_render/src/storage.rs +++ b/crates/bevy_render/src/storage.rs @@ -116,6 +116,7 @@ impl RenderAsset for GpuShaderStorageBuffer { source_asset: Self::SourceAsset, _: AssetId, render_device: &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { match source_asset.data { Some(data) => { diff --git a/crates/bevy_render/src/texture/gpu_image.rs b/crates/bevy_render/src/texture/gpu_image.rs index 551bd3ee02..1337df5e00 100644 --- a/crates/bevy_render/src/texture/gpu_image.rs +++ b/crates/bevy_render/src/texture/gpu_image.rs @@ -7,6 +7,7 @@ use bevy_asset::AssetId; use bevy_ecs::system::{lifetimeless::SRes, SystemParamItem}; use bevy_image::{Image, ImageSampler}; use bevy_math::{AspectRatio, UVec2}; +use tracing::warn; use wgpu::{Extent3d, TextureFormat, TextureViewDescriptor}; /// The GPU-representation of an [`Image`]. @@ -44,6 +45,7 @@ impl RenderAsset for GpuImage { image: Self::SourceAsset, _: AssetId, (render_device, render_queue, default_sampler): &mut SystemParamItem, + previous_asset: Option<&Self>, ) -> Result> { let texture = if let Some(ref data) = image.data { render_device.create_texture_with_data( @@ -54,7 +56,38 @@ impl RenderAsset for GpuImage { data, ) } else { - render_device.create_texture(&image.texture_descriptor) + let new_texture = render_device.create_texture(&image.texture_descriptor); + if image.copy_on_resize { + if let Some(previous) = previous_asset { + let mut command_encoder = + render_device.create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("copy_image_on_resize"), + }); + let copy_size = Extent3d { + width: image.texture_descriptor.size.width.min(previous.size.width), + height: image + .texture_descriptor + .size + .height + .min(previous.size.height), + depth_or_array_layers: image + .texture_descriptor + .size + .depth_or_array_layers + .min(previous.size.depth_or_array_layers), + }; + + command_encoder.copy_texture_to_texture( + previous.texture.as_image_copy(), + new_texture.as_image_copy(), + copy_size, + ); + render_queue.submit([command_encoder.finish()]); + } else { + warn!("No previous asset to copy from for image: {:?}", image); + } + } + new_texture }; let texture_view = texture.create_view( diff --git a/crates/bevy_render/src/texture/mod.rs b/crates/bevy_render/src/texture/mod.rs index 6c2dc67aae..fe37dd4310 100644 --- a/crates/bevy_render/src/texture/mod.rs +++ b/crates/bevy_render/src/texture/mod.rs @@ -4,7 +4,7 @@ mod texture_attachment; mod texture_cache; pub use crate::render_resource::DefaultImageSampler; -#[cfg(feature = "basis-universal")] +#[cfg(feature = "compressed_image_saver")] use bevy_image::CompressedImageSaver; #[cfg(feature = "hdr")] use bevy_image::HdrTextureLoader; @@ -84,7 +84,7 @@ impl Plugin for ImagePlugin { image_assets.insert(&Handle::default(), Image::default()); image_assets.insert(&TRANSPARENT_IMAGE_HANDLE, Image::transparent()); - #[cfg(feature = "basis-universal")] + #[cfg(feature = "compressed_image_saver")] if let Some(processor) = app .world() .get_resource::() diff --git a/crates/bevy_render/src/view/visibility/mod.rs b/crates/bevy_render/src/view/visibility/mod.rs index 13b8ac74d4..11abdd8803 100644 --- a/crates/bevy_render/src/view/visibility/mod.rs +++ b/crates/bevy_render/src/view/visibility/mod.rs @@ -20,7 +20,7 @@ use smallvec::SmallVec; use super::NoCpuCulling; use crate::{ - camera::{Camera, CameraProjection, Projection}, + camera::{Camera, Projection}, mesh::{Mesh, Mesh3d, MeshAabb}, primitives::{Aabb, Frustum, Sphere}, sync_world::MainEntity, diff --git a/crates/bevy_scene/Cargo.toml b/crates/bevy_scene/Cargo.toml index 78de17e26f..48d718b410 100644 --- a/crates/bevy_scene/Cargo.toml +++ b/crates/bevy_scene/Cargo.toml @@ -15,6 +15,7 @@ serialize = [ "uuid/serde", "bevy_ecs/serialize", "bevy_platform/serialize", + "bevy_render?/serialize", ] [dependencies] @@ -35,7 +36,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea serde = { version = "1.0", features = ["derive"], optional = true } uuid = { version = "1.13.1", features = ["v4"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } [target.'cfg(target_arch = "wasm32")'.dependencies] # TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption. diff --git a/crates/bevy_solari/Cargo.toml b/crates/bevy_solari/Cargo.toml index 03976dea3f..ffaca58ba3 100644 --- a/crates/bevy_solari/Cargo.toml +++ b/crates/bevy_solari/Cargo.toml @@ -29,7 +29,7 @@ bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" } # other bytemuck = { version = "1" } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } tracing = { version = "0.1", default-features = false, features = ["std"] } [lints] diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 1d356fdc40..1538ac1ebc 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -36,7 +36,7 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea # other bytemuck = { version = "1", features = ["derive", "must_cast"] } fixedbitset = "0.5" -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } bitflags = "2.3" radsort = "0.1" nonmax = "0.5" diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index 882ec5857c..3e15499dd0 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -16,6 +16,7 @@ mod picking_backend; mod render; mod sprite; mod texture_slice; +mod tilemap_chunk; /// The sprite prelude. /// @@ -40,6 +41,7 @@ pub use picking_backend::*; pub use render::*; pub use sprite::*; pub use texture_slice::*; +pub use tilemap_chunk::*; use bevy_app::prelude::*; use bevy_asset::{embedded_asset, AssetEventSystems, Assets}; @@ -87,7 +89,12 @@ impl Plugin for SpritePlugin { .register_type::() .register_type::() .register_type::() - .add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin)) + .add_plugins(( + Mesh2dRenderPlugin, + ColorMaterialPlugin, + TilemapChunkPlugin, + TilemapChunkMaterialPlugin, + )) .add_systems( PostUpdate, ( diff --git a/crates/bevy_sprite/src/mesh2d/material.rs b/crates/bevy_sprite/src/mesh2d/material.rs index 3f76b516cd..fa784bd9af 100644 --- a/crates/bevy_sprite/src/mesh2d/material.rs +++ b/crates/bevy_sprite/src/mesh2d/material.rs @@ -967,6 +967,7 @@ impl RenderAsset for PreparedMaterial2d { transparent_draw_functions, material_param, ): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) { Ok(prepared) => { diff --git a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs index 03de94be1c..e30c5b1f6c 100644 --- a/crates/bevy_sprite/src/mesh2d/wireframe2d.rs +++ b/crates/bevy_sprite/src/mesh2d/wireframe2d.rs @@ -473,6 +473,7 @@ impl RenderAsset for RenderWireframeMaterial { source_asset: Self::SourceAsset, _asset_id: AssetId, _param: &mut SystemParamItem, + _previous_asset: Option<&Self>, ) -> Result> { Ok(RenderWireframeMaterial { color: source_asset.color.to_linear().to_f32_array(), diff --git a/crates/bevy_sprite/src/picking_backend.rs b/crates/bevy_sprite/src/picking_backend.rs index 57c1acc6bd..bde1a34b63 100644 --- a/crates/bevy_sprite/src/picking_backend.rs +++ b/crates/bevy_sprite/src/picking_backend.rs @@ -7,7 +7,7 @@ //! //! ## Implementation Notes //! -//! - The `position` reported in `HitData` in in world space, and the `normal` is a normalized +//! - The `position` reported in `HitData` in world space, and the `normal` is a normalized //! vector provided by the target's `GlobalTransform::back()`. use crate::{Anchor, Sprite}; diff --git a/crates/bevy_sprite/src/tilemap_chunk/mod.rs b/crates/bevy_sprite/src/tilemap_chunk/mod.rs new file mode 100644 index 0000000000..8b4ce755f6 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap_chunk/mod.rs @@ -0,0 +1,264 @@ +use crate::{AlphaMode2d, Anchor, MeshMaterial2d}; +use bevy_app::{App, Plugin, Update}; +use bevy_asset::{Assets, Handle, RenderAssetUsages}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + component::Component, + entity::Entity, + lifecycle::Add, + observer::On, + query::Changed, + resource::Resource, + system::{Commands, Query, ResMut}, +}; +use bevy_image::{Image, ImageSampler}; +use bevy_math::{FloatOrd, UVec2, Vec2, Vec3}; +use bevy_platform::collections::HashMap; +use bevy_render::{ + mesh::{Indices, Mesh, Mesh2d, PrimitiveTopology}, + render_resource::{ + Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages, + }, +}; +use tracing::warn; + +mod tilemap_chunk_material; + +pub use tilemap_chunk_material::*; + +/// Plugin that handles the initialization and updating of tilemap chunks. +/// Adds systems for processing newly added tilemap chunks and updating their indices. +pub struct TilemapChunkPlugin; + +impl Plugin for TilemapChunkPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_observer(on_add_tilemap_chunk) + .add_systems(Update, update_tilemap_chunk_indices); + } +} + +type TilemapChunkMeshCacheKey = (UVec2, FloatOrd, FloatOrd, FloatOrd, FloatOrd); + +/// A resource storing the meshes for each tilemap chunk size. +#[derive(Resource, Default, Deref, DerefMut)] +pub struct TilemapChunkMeshCache(HashMap>); + +/// A component representing a chunk of a tilemap. +/// Each chunk is a rectangular section of tiles that is rendered as a single mesh. +#[derive(Component, Clone, Debug, Default)] +#[require(Mesh2d, MeshMaterial2d, TilemapChunkIndices, Anchor)] +pub struct TilemapChunk { + /// The size of the chunk in tiles + pub chunk_size: UVec2, + /// The size to use for each tile, not to be confused with the size of a tile in the tileset image. + /// The size of the tile in the tileset image is determined by the tileset image's dimensions. + pub tile_display_size: UVec2, + /// Handle to the tileset image containing all tile textures + pub tileset: Handle, + /// The alpha mode to use for the tilemap chunk + pub alpha_mode: AlphaMode2d, +} + +/// Component storing the indices of tiles within a chunk. +/// Each index corresponds to a specific tile in the tileset. +#[derive(Component, Clone, Debug, Default, Deref, DerefMut)] +pub struct TilemapChunkIndices(pub Vec>); + +fn on_add_tilemap_chunk( + trigger: On, + tilemap_chunk_query: Query<(&TilemapChunk, &TilemapChunkIndices, &Anchor)>, + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut images: ResMut>, + mut tilemap_chunk_mesh_cache: ResMut, +) { + let chunk_entity = trigger.target(); + let Ok(( + TilemapChunk { + chunk_size, + tile_display_size, + tileset, + alpha_mode, + }, + indices, + anchor, + )) = tilemap_chunk_query.get(chunk_entity) + else { + warn!("Tilemap chunk {} not found", chunk_entity); + return; + }; + + let expected_indices_length = chunk_size.element_product() as usize; + if indices.len() != expected_indices_length { + warn!( + "Invalid indices length for tilemap chunk {} of size {}. Expected {}, got {}", + chunk_entity, + chunk_size, + indices.len(), + expected_indices_length + ); + return; + } + + let indices_image = make_chunk_image(chunk_size, &indices.0); + + let display_size = (chunk_size * tile_display_size).as_vec2(); + + let mesh_key: TilemapChunkMeshCacheKey = ( + *chunk_size, + FloatOrd(display_size.x), + FloatOrd(display_size.y), + FloatOrd(anchor.as_vec().x), + FloatOrd(anchor.as_vec().y), + ); + + let mesh = tilemap_chunk_mesh_cache + .entry(mesh_key) + .or_insert_with(|| meshes.add(make_chunk_mesh(chunk_size, &display_size, anchor))); + + commands.entity(chunk_entity).insert(( + Mesh2d(mesh.clone()), + MeshMaterial2d(materials.add(TilemapChunkMaterial { + tileset: tileset.clone(), + indices: images.add(indices_image), + alpha_mode: *alpha_mode, + })), + )); +} + +fn update_tilemap_chunk_indices( + query: Query< + ( + Entity, + &TilemapChunk, + &TilemapChunkIndices, + &MeshMaterial2d, + ), + Changed, + >, + mut materials: ResMut>, + mut images: ResMut>, +) { + for (chunk_entity, TilemapChunk { chunk_size, .. }, indices, material) in query { + let expected_indices_length = chunk_size.element_product() as usize; + if indices.len() != expected_indices_length { + warn!( + "Invalid TilemapChunkIndices length for tilemap chunk {} of size {}. Expected {}, got {}", + chunk_entity, + chunk_size, + indices.len(), + expected_indices_length + ); + continue; + } + + let Some(material) = materials.get_mut(material.id()) else { + warn!( + "TilemapChunkMaterial not found for tilemap chunk {}", + chunk_entity + ); + continue; + }; + let Some(indices_image) = images.get_mut(&material.indices) else { + warn!( + "TilemapChunkMaterial indices image not found for tilemap chunk {}", + chunk_entity + ); + continue; + }; + let Some(data) = indices_image.data.as_mut() else { + warn!( + "TilemapChunkMaterial indices image data not found for tilemap chunk {}", + chunk_entity + ); + continue; + }; + data.clear(); + data.extend( + indices + .iter() + .copied() + .flat_map(|i| u16::to_ne_bytes(i.unwrap_or(u16::MAX))), + ); + } +} + +fn make_chunk_image(size: &UVec2, indices: &[Option]) -> Image { + Image { + data: Some( + indices + .iter() + .copied() + .flat_map(|i| u16::to_ne_bytes(i.unwrap_or(u16::MAX))) + .collect(), + ), + texture_descriptor: TextureDescriptor { + size: Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + dimension: TextureDimension::D2, + format: TextureFormat::R16Uint, + label: None, + mip_level_count: 1, + sample_count: 1, + usage: TextureUsages::TEXTURE_BINDING | TextureUsages::COPY_DST, + view_formats: &[], + }, + sampler: ImageSampler::nearest(), + texture_view_descriptor: None, + asset_usage: RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD, + copy_on_resize: false, + } +} + +fn make_chunk_mesh(size: &UVec2, display_size: &Vec2, anchor: &Anchor) -> Mesh { + let mut mesh = Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::RENDER_WORLD | RenderAssetUsages::MAIN_WORLD, + ); + + let offset = display_size * (Vec2::splat(-0.5) - anchor.as_vec()); + + let num_quads = size.element_product() as usize; + let quad_size = display_size / size.as_vec2(); + + let mut positions = Vec::with_capacity(4 * num_quads); + let mut uvs = Vec::with_capacity(4 * num_quads); + let mut indices = Vec::with_capacity(6 * num_quads); + + for y in 0..size.y { + for x in 0..size.x { + let i = positions.len() as u32; + + let p0 = offset + quad_size * UVec2::new(x, y).as_vec2(); + let p1 = p0 + quad_size; + + positions.extend([ + Vec3::new(p0.x, p0.y, 0.0), + Vec3::new(p1.x, p0.y, 0.0), + Vec3::new(p0.x, p1.y, 0.0), + Vec3::new(p1.x, p1.y, 0.0), + ]); + + uvs.extend([ + Vec2::new(0.0, 1.0), + Vec2::new(1.0, 1.0), + Vec2::new(0.0, 0.0), + Vec2::new(1.0, 0.0), + ]); + + indices.extend([i, i + 2, i + 1]); + indices.extend([i + 3, i + 1, i + 2]); + } + } + + mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, positions); + mesh.insert_attribute(Mesh::ATTRIBUTE_UV_0, uvs); + mesh.insert_indices(Indices::U32(indices)); + + mesh +} diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs new file mode 100644 index 0000000000..c8879a58f1 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.rs @@ -0,0 +1,70 @@ +use crate::{AlphaMode2d, Material2d, Material2dKey, Material2dPlugin}; +use bevy_app::{App, Plugin}; +use bevy_asset::{embedded_asset, embedded_path, Asset, AssetPath, Handle}; +use bevy_image::Image; +use bevy_reflect::prelude::*; +use bevy_render::{ + mesh::{Mesh, MeshVertexBufferLayoutRef}, + render_resource::*, +}; + +/// Plugin that adds support for tilemap chunk materials. +#[derive(Default)] +pub struct TilemapChunkMaterialPlugin; + +impl Plugin for TilemapChunkMaterialPlugin { + fn build(&self, app: &mut App) { + embedded_asset!(app, "tilemap_chunk_material.wgsl"); + + app.add_plugins(Material2dPlugin::::default()); + } +} + +/// Material used for rendering tilemap chunks. +/// +/// This material is used internally by the tilemap system to render chunks of tiles +/// efficiently using a single draw call per chunk. +#[derive(Asset, TypePath, AsBindGroup, Debug, Clone)] +pub struct TilemapChunkMaterial { + pub alpha_mode: AlphaMode2d, + + #[texture(0, dimension = "2d_array")] + #[sampler(1)] + pub tileset: Handle, + + #[texture(2, sample_type = "u_int")] + pub indices: Handle, +} + +impl Material2d for TilemapChunkMaterial { + fn fragment_shader() -> ShaderRef { + ShaderRef::Path( + AssetPath::from_path_buf(embedded_path!("tilemap_chunk_material.wgsl")) + .with_source("embedded"), + ) + } + + fn vertex_shader() -> ShaderRef { + ShaderRef::Path( + AssetPath::from_path_buf(embedded_path!("tilemap_chunk_material.wgsl")) + .with_source("embedded"), + ) + } + + fn alpha_mode(&self) -> AlphaMode2d { + self.alpha_mode + } + + fn specialize( + descriptor: &mut RenderPipelineDescriptor, + layout: &MeshVertexBufferLayoutRef, + _key: Material2dKey, + ) -> Result<(), SpecializedMeshPipelineError> { + let vertex_layout = layout.0.get_layout(&[ + Mesh::ATTRIBUTE_POSITION.at_shader_location(0), + Mesh::ATTRIBUTE_UV_0.at_shader_location(1), + ])?; + descriptor.vertex.buffers = vec![vertex_layout]; + Ok(()) + } +} diff --git a/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl new file mode 100644 index 0000000000..7424995e22 --- /dev/null +++ b/crates/bevy_sprite/src/tilemap_chunk/tilemap_chunk_material.wgsl @@ -0,0 +1,58 @@ +#import bevy_sprite::{ + mesh2d_functions as mesh_functions, + mesh2d_view_bindings::view, +} + +struct Vertex { + @builtin(instance_index) instance_index: u32, + @builtin(vertex_index) vertex_index: u32, + @location(0) position: vec3, + @location(1) uv: vec2, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, + @location(1) tile_index: u32, +} + +@group(2) @binding(0) var tileset: texture_2d_array; +@group(2) @binding(1) var tileset_sampler: sampler; +@group(2) @binding(2) var tile_indices: texture_2d; + +@vertex +fn vertex(vertex: Vertex) -> VertexOutput { + var out: VertexOutput; + + let world_from_local = mesh_functions::get_world_from_local(vertex.instance_index); + let world_position = mesh_functions::mesh2d_position_local_to_world( + world_from_local, + vec4(vertex.position, 1.0) + ); + + out.position = mesh_functions::mesh2d_position_world_to_clip(world_position); + out.uv = vertex.uv; + out.tile_index = vertex.vertex_index / 4u; + + return out; +} + +@fragment +fn fragment(in: VertexOutput) -> @location(0) vec4 { + let chunk_size = textureDimensions(tile_indices, 0); + let tile_xy = vec2( + in.tile_index % chunk_size.x, + in.tile_index / chunk_size.x + ); + let tile_id = textureLoad(tile_indices, tile_xy, 0).r; + + if tile_id == 0xffffu { + discard; + } + + let color = textureSample(tileset, tileset_sampler, in.uv, tile_id); + if color.a < 0.001 { + discard; + } + return color; +} \ No newline at end of file diff --git a/crates/bevy_tasks/Cargo.toml b/crates/bevy_tasks/Cargo.toml index 61b07448ed..e28d7fc88d 100644 --- a/crates/bevy_tasks/Cargo.toml +++ b/crates/bevy_tasks/Cargo.toml @@ -55,7 +55,7 @@ futures-lite = { version = "2.0.1", default-features = false, features = [ "alloc", ] } async-task = { version = "4.4.0", default-features = false } -derive_more = { version = "1", default-features = false, features = [ +derive_more = { version = "2", default-features = false, features = [ "deref", "deref_mut", ] } diff --git a/crates/bevy_transform/Cargo.toml b/crates/bevy_transform/Cargo.toml index 6c86be0da6..bec0f4fb50 100644 --- a/crates/bevy_transform/Cargo.toml +++ b/crates/bevy_transform/Cargo.toml @@ -21,7 +21,7 @@ serde = { version = "1", default-features = false, features = [ "derive", ], optional = true } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } [dev-dependencies] bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" } diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 1f05d7a53b..3ffe730fbd 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -38,7 +38,7 @@ serde = { version = "1", features = ["derive"], optional = true } uuid = { version = "1.1", features = ["v4"], optional = true } bytemuck = { version = "1.5", features = ["derive"] } thiserror = { version = "2", default-features = false } -derive_more = { version = "1", default-features = false, features = ["from"] } +derive_more = { version = "2", default-features = false, features = ["from"] } nonmax = "0.5" smallvec = "1.11" accesskit = "0.19" @@ -51,6 +51,7 @@ serialize = [ "smallvec/serde", "bevy_math/serialize", "bevy_platform/serialize", + "bevy_render/serialize", ] bevy_ui_picking_backend = ["bevy_picking", "dep:uuid"] bevy_ui_debug = [] diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs index 3ad4f4ea6a..5d2201e609 100644 --- a/crates/bevy_ui/src/render/ui_material_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -581,6 +581,7 @@ impl RenderAsset for PreparedUiMaterial { material: Self::SourceAsset, _: AssetId, (render_device, pipeline, material_param): &mut SystemParamItem, + _: Option<&Self>, ) -> Result> { match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) { Ok(prepared) => Ok(PreparedUiMaterial { diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 403801e9d0..77728543e7 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -751,11 +751,10 @@ pub struct CursorOptions { /// /// ## Platform-specific /// - /// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] /// - **`iOS/Android`** don't have cursors. /// - /// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. + /// Since `macOS` doesn't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. pub grab_mode: CursorGrabMode, /// Set whether or not mouse events within *this* window are captured or fall through to the Window below. @@ -1064,11 +1063,10 @@ impl From for WindowResolution { /// /// ## Platform-specific /// -/// - **`Windows`** doesn't support [`CursorGrabMode::Locked`] /// - **`macOS`** doesn't support [`CursorGrabMode::Confined`] /// - **`iOS/Android`** don't have cursors. /// -/// Since `Windows` and `macOS` have different [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. +/// Since `macOS` doesn't have full [`CursorGrabMode`] support, we first try to set the grab mode that was asked for. If it doesn't work then use the alternate grab mode. #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr( feature = "bevy_reflect", diff --git a/docs/cargo_features.md b/docs/cargo_features.md index 5998d8cb90..20e6e53698 100644 --- a/docs/cargo_features.md +++ b/docs/cargo_features.md @@ -73,6 +73,7 @@ The default feature set enables most of the expected features of a game engine, |bevy_solari|Provides raytraced lighting (experimental)| |bevy_ui_debug|Provides a debug overlay for bevy UI| |bmp|BMP image format support| +|compressed_image_saver|Enables compressed KTX2 UASTC texture output on the asset processor| |critical-section|`critical-section` provides the building blocks for synchronization primitives on all platforms, including `no_std`.| |dds|DDS compressed texture support| |debug_glam_assert|Enable assertions in debug builds to check the validity of parameters passed to glam| diff --git a/examples/2d/tilemap_chunk.rs b/examples/2d/tilemap_chunk.rs new file mode 100644 index 0000000000..35c2694ff5 --- /dev/null +++ b/examples/2d/tilemap_chunk.rs @@ -0,0 +1,70 @@ +//! Shows a tilemap chunk rendered with a single draw call. + +use bevy::{ + prelude::*, + sprite::{TilemapChunk, TilemapChunkIndices}, +}; +use rand::{Rng, SeedableRng}; +use rand_chacha::ChaCha8Rng; + +fn main() { + App::new() + .add_plugins((DefaultPlugins.set(ImagePlugin::default_nearest()),)) + .add_systems(Startup, setup) + .add_systems(Update, (update_tileset_image, update_tilemap)) + .run(); +} + +#[derive(Component, Deref, DerefMut)] +struct UpdateTimer(Timer); + +fn setup(mut commands: Commands, assets: Res) { + let mut rng = ChaCha8Rng::seed_from_u64(42); + let chunk_size = UVec2::splat(64); + let tile_display_size = UVec2::splat(8); + let indices: Vec> = (0..chunk_size.element_product()) + .map(|_| rng.gen_range(0..5)) + .map(|i| if i == 0 { None } else { Some(i - 1) }) + .collect(); + + commands.spawn(( + TilemapChunk { + chunk_size, + tile_display_size, + tileset: assets.load("textures/array_texture.png"), + ..default() + }, + TilemapChunkIndices(indices), + UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + )); + + commands.spawn(Camera2d); +} + +fn update_tileset_image( + chunk_query: Single<&TilemapChunk>, + mut events: EventReader>, + mut images: ResMut>, +) { + let chunk = *chunk_query; + for event in events.read() { + if event.is_loaded_with_dependencies(chunk.tileset.id()) { + let image = images.get_mut(&chunk.tileset).unwrap(); + image.reinterpret_stacked_2d_as_array(4); + } + } +} + +fn update_tilemap(time: Res