Merge branch 'main' into proper-json-schema
This commit is contained in:
commit
2f03beb2d0
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
||||
14
Cargo.toml
14
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"
|
||||
|
||||
@ -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<const N: usize>(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<B: Bundle + GetTypeRegistration>(
|
||||
world: &mut World,
|
||||
@ -71,7 +45,7 @@ fn reflection_cloner<B: Bundle + GetTypeRegistration>(
|
||||
// this bundle are saved.
|
||||
let component_ids: Vec<_> = world.register_bundle::<B>().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<B: Bundle + GetTypeRegistration>(
|
||||
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: Bundle + Default + GetTypeRegistration>(
|
||||
b: &mut Bencher,
|
||||
clone_via_reflect: bool,
|
||||
@ -114,8 +87,7 @@ fn bench_clone<B: Bundle + Default + GetTypeRegistration>(
|
||||
});
|
||||
}
|
||||
|
||||
/// 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<B: Bundle + Default + GetTypeRegistration>(
|
||||
let mut cloner = if clone_via_reflect {
|
||||
reflection_cloner::<B>(&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<B: Bundle + Default + GetTypeRegistration>(
|
||||
|
||||
// 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::<ComplexBundle>(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::<C1>(b, 50, 1, clone_via_reflect);
|
||||
bench_clone_hierarchy::<C<1>>(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::<C1>(b, 1, 50, clone_via_reflect);
|
||||
bench_clone_hierarchy::<C<1>>(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::<ComplexBundle>(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<FilterScenario> 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: Bundle + Default>(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::<B>();
|
||||
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::<B>();
|
||||
cloner = builder.finish();
|
||||
}
|
||||
FilterScenario::OptInAllWithoutRequired => {
|
||||
target = spawn(true);
|
||||
let mut builder = EntityCloner::build_opt_in(&mut world);
|
||||
builder.without_required_components(|builder| {
|
||||
builder.allow::<B>();
|
||||
});
|
||||
cloner = builder.finish();
|
||||
}
|
||||
FilterScenario::OptInAllKeep(is_new) => {
|
||||
target = spawn(is_new);
|
||||
let mut builder = EntityCloner::build_opt_in(&mut world);
|
||||
builder.allow_if_new::<B>();
|
||||
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::<B>();
|
||||
});
|
||||
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<const N: usize>;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
#[component(clone_behavior = Ignore)]
|
||||
#[require(C::<N>)]
|
||||
struct R<const N: usize>;
|
||||
|
||||
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::<RequiringBundle>(b, scenario);
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
@ -155,6 +155,7 @@ fn bench(c: &mut Criterion) {
|
||||
&mesh.positions,
|
||||
Some(&mesh.normals),
|
||||
Some(&mesh.indices),
|
||||
None,
|
||||
backface_culling,
|
||||
);
|
||||
|
||||
|
||||
@ -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"] }
|
||||
|
||||
@ -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<Shader>,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
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::<FullscreenShader>().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 {
|
||||
|
||||
@ -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<Shader>,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
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::<FullscreenShader>().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(),
|
||||
|
||||
@ -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<Shader>,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
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::<FullscreenShader>().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![
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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,
|
||||
}
|
||||
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -196,6 +196,7 @@ impl RenderAsset for GpuAutoExposureCompensationCurve {
|
||||
source: Self::SourceAsset,
|
||||
_: AssetId<Self::SourceAsset>,
|
||||
(render_device, render_queue): &mut SystemParamItem<Self::Param>,
|
||||
_: Option<&Self>,
|
||||
) -> Result<Self, bevy_render::render_asset::PrepareAssetError<Self::SourceAsset>> {
|
||||
let texture = render_device.create_texture_with_data(
|
||||
render_queue,
|
||||
|
||||
@ -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<Shader>,
|
||||
pub fullscreen_shader: FullscreenShader,
|
||||
pub fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
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::<FullscreenShader>().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 {
|
||||
|
||||
@ -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<Shader>,
|
||||
/// The asset handle for the fullscreen vertex shader.
|
||||
pub fullscreen_shader: FullscreenShader,
|
||||
/// The fragment shader asset handle.
|
||||
pub fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
#[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::<FullscreenShader>().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 {
|
||||
|
||||
@ -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<Shader>,
|
||||
/// The asset handle for the fullscreen vertex shader.
|
||||
pub fullscreen_shader: FullscreenShader,
|
||||
/// The fragment shader asset handle.
|
||||
pub fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
#[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::<FullscreenShader>().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 {
|
||||
|
||||
@ -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::<FullscreenShader>().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![],
|
||||
|
||||
@ -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<Shader>,
|
||||
/// The asset handle for the fullscreen vertex shader.
|
||||
fullscreen_shader: FullscreenShader,
|
||||
/// The fragment shader asset handle.
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
impl ViewNode for DepthOfFieldNode {
|
||||
@ -678,13 +680,15 @@ pub fn prepare_depth_of_field_pipelines(
|
||||
&ViewDepthOfFieldBindGroupLayouts,
|
||||
&Msaa,
|
||||
)>,
|
||||
fullscreen_shader: Res<FullscreenShader>,
|
||||
asset_server: Res<AssetServer>,
|
||||
) {
|
||||
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(),
|
||||
|
||||
@ -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<Shader> =
|
||||
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<Shader>);
|
||||
|
||||
/// uses the [`FULLSCREEN_SHADER_HANDLE`] to output a
|
||||
/// ```wgsl
|
||||
/// struct FullscreenVertexOutput {
|
||||
/// [[builtin(position)]]
|
||||
/// position: vec4<f32>;
|
||||
/// [[location(0)]]
|
||||
/// uv: vec2<f32>;
|
||||
/// };
|
||||
/// ```
|
||||
/// 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<Shader> {
|
||||
self.0.clone()
|
||||
}
|
||||
|
||||
/// Creates a [`VertexState`] that uses the [`FullscreenShader`] to output a
|
||||
/// ```wgsl
|
||||
/// struct FullscreenVertexOutput {
|
||||
/// @builtin(position)
|
||||
/// position: vec4<f32>;
|
||||
/// @location(0)
|
||||
/// uv: vec2<f32>;
|
||||
/// };
|
||||
/// ```
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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::<DepthPrepass>()
|
||||
.register_type::<NormalPrepass>()
|
||||
.register_type::<MotionVectorPrepass>()
|
||||
.register_type::<DeferredPrepass>()
|
||||
.init_resource::<FullscreenShader>()
|
||||
.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::<FullscreenShader>();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Shader>,
|
||||
pub(crate) fullscreen_shader: FullscreenShader,
|
||||
pub(crate) fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
impl MotionBlurPipeline {
|
||||
pub(crate) fn new(render_device: &RenderDevice, shader: Handle<Shader>) -> Self {
|
||||
pub(crate) fn new(
|
||||
render_device: &RenderDevice,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
fragment_shader: Handle<Shader>,
|
||||
) -> 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::<RenderDevice>().clone();
|
||||
|
||||
let shader = load_embedded_asset!(render_world, "motion_blur.wgsl");
|
||||
MotionBlurPipeline::new(&render_device, shader)
|
||||
let fullscreen_shader = render_world.resource::<FullscreenShader>().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 {
|
||||
|
||||
@ -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<OrderIndependentTransparencySettings>,
|
||||
>,
|
||||
fullscreen_shader: Res<FullscreenShader>,
|
||||
asset_server: Res<AssetServer>,
|
||||
// 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(),
|
||||
|
||||
@ -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<Shader>,
|
||||
/// The asset handle for the fullscreen vertex shader.
|
||||
fullscreen_shader: FullscreenShader,
|
||||
/// The fragment shader asset handle.
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
/// 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::<FullscreenShader>().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 {
|
||||
|
||||
@ -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<Shader>,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
/// 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::<FullscreenShader>().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),
|
||||
|
||||
@ -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<Shader>,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
/// 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::<FullscreenShader>().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,
|
||||
}
|
||||
}
|
||||
|
||||
213
crates/bevy_core_widgets/src/core_radio.rs
Normal file
213
crates/bevy_core_widgets/src/core_radio.rs
Normal file
@ -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<SystemId<In<Entity>>>,
|
||||
}
|
||||
|
||||
/// 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 <https://www.w3.org/WAI/ARIA/apg/patterns/radio>/
|
||||
#[derive(Component, Debug)]
|
||||
#[require(AccessibilityNode(accesskit::Node::new(Role::RadioButton)), Checked)]
|
||||
pub struct CoreRadio;
|
||||
|
||||
fn radio_group_on_key_input(
|
||||
mut ev: On<FocusedInput<KeyboardInput>>,
|
||||
q_group: Query<&CoreRadioGroup>,
|
||||
q_radio: Query<(Has<Checked>, Has<InteractionDisabled>), With<CoreRadio>>,
|
||||
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::<Vec<_>>();
|
||||
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<Pointer<Click>>,
|
||||
q_group: Query<&CoreRadioGroup>,
|
||||
q_radio: Query<(Has<Checked>, Has<InteractionDisabled>), With<CoreRadio>>,
|
||||
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::<Vec<_>>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@ -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]
|
||||
|
||||
@ -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,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<M: #bevy_ecs_path::entity::EntityMapper>(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<MapEntitiesAttributeKind>,
|
||||
) -> Option<TokenStream2> {
|
||||
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<Self> {
|
||||
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!(
|
||||
<Self as #bevy_ecs_path::entity::MapEntities>::map_entities
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Parse for MapEntitiesAttributeKind {
|
||||
fn parse(input: syn::parse::ParseStream) -> Result<Self> {
|
||||
if input.peek(Token![=]) {
|
||||
input.parse::<Token![=]>()?;
|
||||
input.parse::<Expr>().and_then(Self::from_expr)
|
||||
} else {
|
||||
Ok(Self::Default)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct Attrs {
|
||||
storage: StorageTy,
|
||||
requires: Option<Punctuated<Require, Comma>>,
|
||||
@ -496,6 +558,7 @@ struct Attrs {
|
||||
relationship_target: Option<RelationshipTarget>,
|
||||
immutable: bool,
|
||||
clone_behavior: Option<Expr>,
|
||||
map_entities: Option<MapEntitiesAttributeKind>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
@ -535,6 +598,7 @@ fn parse_component_attr(ast: &DeriveInput) -> Result<Attrs> {
|
||||
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<Attrs> {
|
||||
} 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::<MapEntitiesAttributeKind>()?);
|
||||
Ok(())
|
||||
} else {
|
||||
Err(nested.error("Unsupported attribute"))
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -578,6 +578,65 @@ pub trait Component: Send + Sync + 'static {
|
||||
/// items: Vec<Option<Entity>>
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// 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<Entity, usize>
|
||||
/// }
|
||||
///
|
||||
/// impl MapEntities for Inventory {
|
||||
/// fn map_entities<M: EntityMapper>(&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);
|
||||
/// # <Inventory as Component>::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::<M> or map_the_map::<_>
|
||||
/// struct Inventory {
|
||||
/// items: HashMap<Entity, usize>
|
||||
/// }
|
||||
///
|
||||
/// fn map_the_map<M: EntityMapper>(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);
|
||||
/// # <Inventory as Component>::map_entities(&mut inv, &mut (a,b));
|
||||
/// # assert_eq!(inv.items.get(&b), Some(&10));
|
||||
/// ````
|
||||
///
|
||||
/// You can use the turbofish (`::<A,B,C>`) to specify parameters when a function is generic, using either M or _ for the type of the mapper parameter.
|
||||
#[inline]
|
||||
fn map_entities<E: EntityMapper>(_this: &mut Self, _mapper: &mut E) {}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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],
|
||||
|
||||
@ -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<Filter: CloneByFilter> 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);
|
||||
|
||||
|
||||
@ -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<OptOut>::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<OptOut>) + 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<OptIn>::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<OptIn>) + Send + Sync + 'static,
|
||||
) -> impl EntityCommand {
|
||||
move |mut entity: EntityWorldMut| {
|
||||
entity.clone_with_opt_in(target, config);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<OptOut>`] 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::<ComponentB>();
|
||||
/// });
|
||||
/// }
|
||||
@ -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<OptOut>) + 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<OptIn>`] 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::<ComponentA>();
|
||||
/// });
|
||||
/// }
|
||||
/// # 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<OptIn>) + 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<OptOut>`] 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::<ComponentB>();
|
||||
/// });
|
||||
/// }
|
||||
/// # 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<OptOut>) + 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<OptIn>`] 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::<ComponentA>();
|
||||
/// });
|
||||
/// }
|
||||
/// # bevy_ecs::system::assert_is_system(example_system);
|
||||
pub fn clone_and_spawn_with_opt_in(
|
||||
&mut self,
|
||||
config: impl FnOnce(&mut EntityClonerBuilder<OptIn>) + 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,
|
||||
|
||||
@ -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::<ComponentB>();
|
||||
/// // Clone all components except ComponentA onto the target.
|
||||
/// world.entity_mut(entity).clone_with_opt_out(target, |builder| {
|
||||
/// builder.deny::<ComponentA>();
|
||||
/// });
|
||||
/// # assert_eq!(world.get::<ComponentA>(target), Some(&ComponentA));
|
||||
/// # assert_eq!(world.get::<ComponentB>(target), None);
|
||||
/// # assert_eq!(world.get::<ComponentA>(target), None);
|
||||
/// # assert_eq!(world.get::<ComponentB>(target), Some(&ComponentB));
|
||||
/// ```
|
||||
///
|
||||
/// See [`EntityClonerBuilder`] for more options.
|
||||
/// See [`EntityClonerBuilder<OptOut>`] 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<OptOut>) + 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::<ComponentA>();
|
||||
/// });
|
||||
/// # assert_eq!(world.get::<ComponentA>(target), Some(&ComponentA));
|
||||
/// # assert_eq!(world.get::<ComponentB>(target), None);
|
||||
/// ```
|
||||
///
|
||||
/// See [`EntityClonerBuilder<OptIn>`] 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<OptIn>) + 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::<ComponentB>();
|
||||
/// // 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::<ComponentA>();
|
||||
/// });
|
||||
/// # assert_eq!(world.get::<ComponentA>(entity_clone), Some(&ComponentA));
|
||||
/// # assert_eq!(world.get::<ComponentB>(entity_clone), None);
|
||||
/// # assert_eq!(world.get::<ComponentA>(entity_clone), None);
|
||||
/// # assert_eq!(world.get::<ComponentB>(entity_clone), Some(&ComponentB));
|
||||
/// ```
|
||||
///
|
||||
/// See [`EntityClonerBuilder`] for more options.
|
||||
/// See [`EntityClonerBuilder<OptOut>`] 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<OptOut>) + 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::<ComponentA>();
|
||||
/// });
|
||||
/// # assert_eq!(world.get::<ComponentA>(entity_clone), Some(&ComponentA));
|
||||
/// # assert_eq!(world.get::<ComponentB>(entity_clone), None);
|
||||
/// ```
|
||||
///
|
||||
/// See [`EntityClonerBuilder<OptIn>`] 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<OptIn>) + 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<B: Bundle>(&mut self, target: Entity) -> &mut Self {
|
||||
self.assert_not_despawned();
|
||||
|
||||
EntityCloner::build(self.world)
|
||||
.deny_all()
|
||||
EntityCloner::build_opt_in(self.world)
|
||||
.allow::<B>()
|
||||
.clone_entity(self.entity, target);
|
||||
|
||||
@ -2809,8 +2911,7 @@ impl<'w> EntityWorldMut<'w> {
|
||||
pub fn move_components<B: Bundle>(&mut self, target: Entity) -> &mut Self {
|
||||
self.assert_not_despawned();
|
||||
|
||||
EntityCloner::build(self.world)
|
||||
.deny_all()
|
||||
EntityCloner::build_opt_in(self.world)
|
||||
.allow::<B>()
|
||||
.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::<A>();
|
||||
});
|
||||
});
|
||||
world
|
||||
.entity_mut(entity_a)
|
||||
.clone_with_opt_in(entity_b, |builder| {
|
||||
builder
|
||||
.move_components(true)
|
||||
.allow::<C>()
|
||||
.without_required_components(|builder| {
|
||||
builder.allow::<A>();
|
||||
});
|
||||
});
|
||||
|
||||
assert_eq!(world.entity(entity_a).get::<A>(), Some(&A));
|
||||
assert_eq!(world.entity(entity_b).get::<A>(), None);
|
||||
assert_eq!(world.entity(entity_a).get::<A>(), None);
|
||||
assert_eq!(world.entity(entity_b).get::<A>(), Some(&A));
|
||||
|
||||
assert_eq!(world.entity(entity_a).get::<B>(), None);
|
||||
assert_eq!(world.entity(entity_b).get::<B>(), Some(&B));
|
||||
assert_eq!(world.entity(entity_a).get::<B>(), Some(&B(5)));
|
||||
assert_eq!(world.entity(entity_b).get::<B>(), Some(&B(3)));
|
||||
|
||||
assert_eq!(world.entity(entity_a).get::<C>(), None);
|
||||
assert_eq!(world.entity(entity_b).get::<C>(), Some(&C(3)));
|
||||
|
||||
@ -554,6 +554,7 @@ impl RenderAsset for GpuLineGizmo {
|
||||
gizmo: Self::SourceAsset,
|
||||
_: AssetId<Self::SourceAsset>,
|
||||
render_device: &mut SystemParamItem<Self::Param>,
|
||||
_: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
let list_position_buffer = render_device.create_buffer_with_data(&BufferInitDescriptor {
|
||||
usage: BufferUsages::VERTEX,
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,6 +151,20 @@ pub struct GltfLoader {
|
||||
pub custom_vertex_attributes: HashMap<Box<str>, MeshVertexAttribute>,
|
||||
/// Arc to default [`ImageSamplerDescriptor`].
|
||||
pub default_sampler: Arc<Mutex<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 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<ImageSamplerDescriptor>,
|
||||
/// 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<bool>,
|
||||
}
|
||||
|
||||
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<Vec3> = 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<usize>,
|
||||
#[cfg(feature = "bevy_animation")] mut animation_context: Option<AnimationContext>,
|
||||
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;
|
||||
|
||||
@ -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 <https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview>
|
||||
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 <https://registry.khronos.org/glTF/specs/2.0/glTF-2.0.html#meshes-overview>
|
||||
Values::Float32x4(it.map(ConvertCoordinates::convert_coordinates).collect())
|
||||
} else {
|
||||
Values::Float32x4(it.collect())
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -356,6 +356,8 @@ pub struct Image {
|
||||
pub sampler: ImageSampler,
|
||||
pub texture_view_descriptor: Option<TextureViewDescriptor<Option<&'static str>>>,
|
||||
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<u8> = 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!(
|
||||
|
||||
@ -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::*;
|
||||
|
||||
@ -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 }
|
||||
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
] }
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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<u16>),
|
||||
U32(Vec<u32>),
|
||||
|
||||
@ -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<Mesh> 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<Indices>,
|
||||
}
|
||||
|
||||
#[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<Box<str>, 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::<Vec<Triangle3d>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<Box<str>, MeshVertexAttribute>,
|
||||
) -> Option<MeshVertexAttribute> {
|
||||
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<MeshVertexAttribute> 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<Box<str>, MeshVertexAttribute>,
|
||||
) -> Option<MeshAttributeData> {
|
||||
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<f32>),
|
||||
Sint32(Vec<i32>),
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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<Shader>,
|
||||
pub fullscreen_shader: FullscreenShader,
|
||||
pub fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
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::<FullscreenShader>().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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1410,6 +1410,7 @@ impl<M: Material> RenderAsset for PreparedMaterial<M> {
|
||||
alpha_mask_deferred_draw_functions,
|
||||
material_param,
|
||||
): &mut SystemParamItem<Self::Param>,
|
||||
_: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
let draw_opaque_pbr = opaque_draw_functions.read().id::<DrawMaterial<M>>();
|
||||
let draw_alpha_mask_pbr = alpha_mask_draw_functions.read().id::<DrawMaterial<M>>();
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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::<FullscreenShader>().to_vertex_state();
|
||||
let pipeline_cache = world.resource_mut::<PipelineCache>();
|
||||
|
||||
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,
|
||||
|
||||
@ -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<Shader>,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
fragment_shader: Handle<Shader>,
|
||||
}
|
||||
|
||||
/// 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::<FullscreenShader>().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 {
|
||||
|
||||
@ -474,6 +474,7 @@ impl RenderAsset for RenderWireframeMaterial {
|
||||
source_asset: Self::SourceAsset,
|
||||
_asset_id: AssetId<Self::SourceAsset>,
|
||||
_param: &mut SystemParamItem<Self::Param>,
|
||||
_previous_asset: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
Ok(RenderWireframeMaterial {
|
||||
color: source_asset.color.to_linear().to_f32_array(),
|
||||
|
||||
@ -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<Vec2>,
|
||||
/// The index of the triangle that was hit.
|
||||
pub triangle_index: Option<usize>,
|
||||
}
|
||||
@ -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<RayMeshHit> {
|
||||
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::<usize>(ray, transform, positions, normals, None, culling),
|
||||
None => ray_mesh_intersection::<usize>(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<I: TryInto<usize> + Clone + Copy>(
|
||||
pub fn ray_mesh_intersection<I>(
|
||||
ray: Ray3d,
|
||||
mesh_transform: &Mat4,
|
||||
positions: &[[f32; 3]],
|
||||
vertex_normals: Option<&[[f32; 3]]>,
|
||||
indices: Option<&[I]>,
|
||||
uvs: Option<&[[f32; 2]]>,
|
||||
backface_culling: Backfaces,
|
||||
) -> Option<RayMeshHit> {
|
||||
) -> Option<RayMeshHit>
|
||||
where
|
||||
I: TryInto<usize> + Clone + Copy,
|
||||
{
|
||||
let world_to_mesh = mesh_transform.inverse();
|
||||
|
||||
let ray = Ray3d::new(
|
||||
@ -139,17 +156,12 @@ pub fn ray_mesh_intersection<I: TryInto<usize> + 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<I: TryInto<usize> + 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<I: TryInto<usize> + 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,
|
||||
);
|
||||
|
||||
|
||||
@ -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 }
|
||||
|
||||
@ -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:
|
||||
/// - <https://github.com/rust-lang/rust/issues/77143>
|
||||
|
||||
@ -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 = [
|
||||
|
||||
@ -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
|
||||
///
|
||||
|
||||
@ -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<dyn sealed::DynCameraProjection>,
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,6 +209,7 @@ impl RenderAsset for RenderMesh {
|
||||
mesh: Self::SourceAsset,
|
||||
_: AssetId<Self::SourceAsset>,
|
||||
(images, mesh_vertex_buffer_layouts): &mut SystemParamItem<Self::Param>,
|
||||
_: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
let morph_targets = match mesh.morph_targets() {
|
||||
Some(mt) => {
|
||||
|
||||
@ -73,6 +73,7 @@ pub trait RenderAsset: Send + Sync + 'static + Sized {
|
||||
source_asset: Self::SourceAsset,
|
||||
asset_id: AssetId<Self::SourceAsset>,
|
||||
param: &mut SystemParamItem<Self::Param>,
|
||||
previous_asset: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>>;
|
||||
|
||||
/// Called whenever the [`RenderAsset::SourceAsset`] has been removed.
|
||||
@ -355,7 +356,8 @@ pub fn prepare_assets<A: RenderAsset>(
|
||||
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<A: RenderAsset>(
|
||||
// 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<A: RenderAsset>(
|
||||
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);
|
||||
|
||||
@ -183,6 +183,31 @@ impl<T: NoUninit> RawBufferVec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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<usize>,
|
||||
) {
|
||||
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<usize>,
|
||||
) {
|
||||
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);
|
||||
|
||||
@ -116,6 +116,7 @@ impl RenderAsset for GpuShaderStorageBuffer {
|
||||
source_asset: Self::SourceAsset,
|
||||
_: AssetId<Self::SourceAsset>,
|
||||
render_device: &mut SystemParamItem<Self::Param>,
|
||||
_: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
match source_asset.data {
|
||||
Some(data) => {
|
||||
|
||||
@ -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<Self::SourceAsset>,
|
||||
(render_device, render_queue, default_sampler): &mut SystemParamItem<Self::Param>,
|
||||
previous_asset: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
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(
|
||||
|
||||
@ -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::<bevy_asset::processor::AssetProcessor>()
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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::<TextureSlicer>()
|
||||
.register_type::<Anchor>()
|
||||
.register_type::<Mesh2d>()
|
||||
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
|
||||
.add_plugins((
|
||||
Mesh2dRenderPlugin,
|
||||
ColorMaterialPlugin,
|
||||
TilemapChunkPlugin,
|
||||
TilemapChunkMaterialPlugin,
|
||||
))
|
||||
.add_systems(
|
||||
PostUpdate,
|
||||
(
|
||||
|
||||
@ -967,6 +967,7 @@ impl<M: Material2d> RenderAsset for PreparedMaterial2d<M> {
|
||||
transparent_draw_functions,
|
||||
material_param,
|
||||
): &mut SystemParamItem<Self::Param>,
|
||||
_: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
match material.as_bind_group(&pipeline.material2d_layout, render_device, material_param) {
|
||||
Ok(prepared) => {
|
||||
|
||||
@ -473,6 +473,7 @@ impl RenderAsset for RenderWireframeMaterial {
|
||||
source_asset: Self::SourceAsset,
|
||||
_asset_id: AssetId<Self::SourceAsset>,
|
||||
_param: &mut SystemParamItem<Self::Param>,
|
||||
_previous_asset: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
Ok(RenderWireframeMaterial {
|
||||
color: source_asset.color.to_linear().to_f32_array(),
|
||||
|
||||
@ -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};
|
||||
|
||||
264
crates/bevy_sprite/src/tilemap_chunk/mod.rs
Normal file
264
crates/bevy_sprite/src/tilemap_chunk/mod.rs
Normal file
@ -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::<TilemapChunkMeshCache>()
|
||||
.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<TilemapChunkMeshCacheKey, Handle<Mesh>>);
|
||||
|
||||
/// 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<TilemapChunkMaterial>, 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<Image>,
|
||||
/// 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<Option<u16>>);
|
||||
|
||||
fn on_add_tilemap_chunk(
|
||||
trigger: On<Add, TilemapChunk>,
|
||||
tilemap_chunk_query: Query<(&TilemapChunk, &TilemapChunkIndices, &Anchor)>,
|
||||
mut commands: Commands,
|
||||
mut meshes: ResMut<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<TilemapChunkMaterial>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
mut tilemap_chunk_mesh_cache: ResMut<TilemapChunkMeshCache>,
|
||||
) {
|
||||
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<TilemapChunkMaterial>,
|
||||
),
|
||||
Changed<TilemapChunkIndices>,
|
||||
>,
|
||||
mut materials: ResMut<Assets<TilemapChunkMaterial>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
) {
|
||||
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<u16>]) -> 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
|
||||
}
|
||||
@ -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::<TilemapChunkMaterial>::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<Image>,
|
||||
|
||||
#[texture(2, sample_type = "u_int")]
|
||||
pub indices: Handle<Image>,
|
||||
}
|
||||
|
||||
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<Self>,
|
||||
) -> 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(())
|
||||
}
|
||||
}
|
||||
@ -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<f32>,
|
||||
@location(1) uv: vec2<f32>,
|
||||
};
|
||||
|
||||
struct VertexOutput {
|
||||
@builtin(position) position: vec4<f32>,
|
||||
@location(0) uv: vec2<f32>,
|
||||
@location(1) tile_index: u32,
|
||||
}
|
||||
|
||||
@group(2) @binding(0) var tileset: texture_2d_array<f32>;
|
||||
@group(2) @binding(1) var tileset_sampler: sampler;
|
||||
@group(2) @binding(2) var tile_indices: texture_2d<u32>;
|
||||
|
||||
@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<f32>(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<f32> {
|
||||
let chunk_size = textureDimensions(tile_indices, 0);
|
||||
let tile_xy = vec2<u32>(
|
||||
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;
|
||||
}
|
||||
@ -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",
|
||||
] }
|
||||
|
||||
@ -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" }
|
||||
|
||||
@ -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 = []
|
||||
|
||||
@ -581,6 +581,7 @@ impl<M: UiMaterial> RenderAsset for PreparedUiMaterial<M> {
|
||||
material: Self::SourceAsset,
|
||||
_: AssetId<Self::SourceAsset>,
|
||||
(render_device, pipeline, material_param): &mut SystemParamItem<Self::Param>,
|
||||
_: Option<&Self>,
|
||||
) -> Result<Self, PrepareAssetError<Self::SourceAsset>> {
|
||||
match material.as_bind_group(&pipeline.ui_layout, render_device, material_param) {
|
||||
Ok(prepared) => Ok(PreparedUiMaterial {
|
||||
|
||||
@ -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<DVec2> 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",
|
||||
|
||||
@ -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|
|
||||
|
||||
70
examples/2d/tilemap_chunk.rs
Normal file
70
examples/2d/tilemap_chunk.rs
Normal file
@ -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<AssetServer>) {
|
||||
let mut rng = ChaCha8Rng::seed_from_u64(42);
|
||||
let chunk_size = UVec2::splat(64);
|
||||
let tile_display_size = UVec2::splat(8);
|
||||
let indices: Vec<Option<u16>> = (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<AssetEvent<Image>>,
|
||||
mut images: ResMut<Assets<Image>>,
|
||||
) {
|
||||
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<Time>, mut query: Query<(&mut TilemapChunkIndices, &mut UpdateTimer)>) {
|
||||
for (mut indices, mut timer) in query.iter_mut() {
|
||||
timer.tick(time.delta());
|
||||
|
||||
if timer.just_finished() {
|
||||
let mut rng = ChaCha8Rng::from_entropy();
|
||||
for _ in 0..50 {
|
||||
let index = rng.gen_range(0..indices.len());
|
||||
indices[index] = Some(rng.gen_range(0..5));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,5 +1,12 @@
|
||||
//! This interactive example shows how to use distance fog,
|
||||
//! and allows playing around with different fog settings.
|
||||
//! Distance-based fog visual effects are used in many games to give a soft falloff of visibility to the player for performance and/or visual design reasons. The further away something in a 3D world is from the camera, the more it's mixed or completely overwritten by a given color.
|
||||
//!
|
||||
//! In Bevy we can add the [`DistanceFog`] component to the same entity as our [`Camera3d`] to apply a distance fog effect. It has fields for color, directional light parameters, and how the fog falls off over distance. And that's it! The distance fog is now applied to the camera.
|
||||
//!
|
||||
//! The [`FogFalloff`] field controls most of the behavior of the fog through different descriptions of fog "curves". I.e. [`FogFalloff::Linear`] lets us define a start and end distance where up until the start distance none of the fog color is mixed in and by the end distance the fog color is as mixed in as it can be. [`FogFalloff::Exponential`] on the other hand uses an exponential curve to drive how "visible" things are with a density value.
|
||||
//!
|
||||
//! [Atmospheric fog](https://bevyengine.org/examples/3d-rendering/atmospheric-fog/) is another fog type that uses this same method of setup, but isn't covered here as it is a kind of fog that is most often used to imply distance and size in clear weather, while the ones shown off here are much more "dense".
|
||||
//!
|
||||
//! The bulk of this example is spent building a scene that suites showing off that the fog is working as intended by creating a pyramid (a 3D structure with clear delineations), a light source, input handling to modify fog settings, and UI to show what the current fog settings are.
|
||||
//!
|
||||
//! ## Controls
|
||||
//!
|
||||
|
||||
@ -129,6 +129,7 @@ Example | Description
|
||||
[Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid
|
||||
[Text 2D](../examples/2d/text2d.rs) | Generates text in 2D
|
||||
[Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites
|
||||
[Tilemap Chunk](../examples/2d/tilemap_chunk.rs) | Renders a tilemap chunk
|
||||
[Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d
|
||||
|
||||
## 3D Rendering
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
use bevy::{
|
||||
core_pipeline::{
|
||||
core_3d::graph::{Core3d, Node3d},
|
||||
fullscreen_vertex_shader::fullscreen_shader_vertex_state,
|
||||
FullscreenShader,
|
||||
},
|
||||
ecs::query::QueryItem,
|
||||
prelude::*,
|
||||
@ -259,6 +259,8 @@ impl FromWorld for PostProcessPipeline {
|
||||
|
||||
// Get the shader handle
|
||||
let shader = world.load_asset(SHADER_ASSET_PATH);
|
||||
// This will setup a fullscreen triangle for the vertex state.
|
||||
let vertex_state = world.resource::<FullscreenShader>().to_vertex_state();
|
||||
|
||||
let pipeline_id = world
|
||||
.resource_mut::<PipelineCache>()
|
||||
@ -266,8 +268,7 @@ impl FromWorld for PostProcessPipeline {
|
||||
.queue_render_pipeline(RenderPipelineDescriptor {
|
||||
label: Some("post_process_pipeline".into()),
|
||||
layout: vec![layout.clone()],
|
||||
// This will setup a fullscreen triangle for the vertex state
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
vertex: vertex_state,
|
||||
fragment: Some(FragmentState {
|
||||
shader,
|
||||
shader_defs: vec![],
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
use bevy::{
|
||||
color::palettes::basic::*,
|
||||
core_widgets::{
|
||||
CoreButton, CoreCheckbox, CoreSlider, CoreSliderThumb, CoreWidgetsPlugin, SliderRange,
|
||||
SliderValue, TrackClick,
|
||||
CoreButton, CoreCheckbox, CoreRadio, CoreRadioGroup, CoreSlider, CoreSliderThumb,
|
||||
CoreWidgetsPlugin, SliderRange, SliderValue, TrackClick,
|
||||
},
|
||||
ecs::system::SystemId,
|
||||
input_focus::{
|
||||
@ -27,7 +27,10 @@ fn main() {
|
||||
))
|
||||
// Only run the app when there is user input. This will significantly reduce CPU/GPU use.
|
||||
.insert_resource(WinitSettings::desktop_app())
|
||||
.insert_resource(DemoWidgetStates { slider_value: 50.0 })
|
||||
.insert_resource(DemoWidgetStates {
|
||||
slider_value: 50.0,
|
||||
slider_click: TrackClick::Snap,
|
||||
})
|
||||
.add_systems(Startup, setup)
|
||||
.add_systems(
|
||||
Update,
|
||||
@ -37,8 +40,8 @@ fn main() {
|
||||
update_button_style2,
|
||||
update_slider_style.after(update_widget_values),
|
||||
update_slider_style2.after(update_widget_values),
|
||||
update_checkbox_style.after(update_widget_values),
|
||||
update_checkbox_style2.after(update_widget_values),
|
||||
update_checkbox_or_radio_style.after(update_widget_values),
|
||||
update_checkbox_or_radio_style2.after(update_widget_values),
|
||||
toggle_disabled,
|
||||
),
|
||||
)
|
||||
@ -69,6 +72,11 @@ struct DemoSliderThumb;
|
||||
#[derive(Component, Default)]
|
||||
struct DemoCheckbox;
|
||||
|
||||
/// Marker which identifies a styled radio button. We'll use this to change the track click
|
||||
/// behavior.
|
||||
#[derive(Component, Default)]
|
||||
struct DemoRadio(TrackClick);
|
||||
|
||||
/// A struct to hold the state of various widgets shown in the demo.
|
||||
///
|
||||
/// While it is possible to use the widget's own state components as the source of truth,
|
||||
@ -78,19 +86,33 @@ struct DemoCheckbox;
|
||||
#[derive(Resource)]
|
||||
struct DemoWidgetStates {
|
||||
slider_value: f32,
|
||||
slider_click: TrackClick,
|
||||
}
|
||||
|
||||
/// Update the widget states based on the changing resource.
|
||||
fn update_widget_values(
|
||||
res: Res<DemoWidgetStates>,
|
||||
mut sliders: Query<Entity, With<DemoSlider>>,
|
||||
mut sliders: Query<(Entity, &mut CoreSlider), With<DemoSlider>>,
|
||||
radios: Query<(Entity, &DemoRadio, Has<Checked>)>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
if res.is_changed() {
|
||||
for slider_ent in sliders.iter_mut() {
|
||||
for (slider_ent, mut slider) in sliders.iter_mut() {
|
||||
commands
|
||||
.entity(slider_ent)
|
||||
.insert(SliderValue(res.slider_value));
|
||||
slider.track_click = res.slider_click;
|
||||
}
|
||||
|
||||
for (radio_id, radio_value, checked) in radios.iter() {
|
||||
let will_be_checked = radio_value.0 == res.slider_click;
|
||||
if will_be_checked != checked {
|
||||
if will_be_checked {
|
||||
commands.entity(radio_id).insert(Checked);
|
||||
} else {
|
||||
commands.entity(radio_id).remove::<Checked>();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -109,15 +131,32 @@ fn setup(mut commands: Commands, assets: Res<AssetServer>) {
|
||||
},
|
||||
);
|
||||
|
||||
// System to update a resource when the radio group changes.
|
||||
let on_change_radio = commands.register_system(
|
||||
|value: In<Entity>,
|
||||
mut widget_states: ResMut<DemoWidgetStates>,
|
||||
q_radios: Query<&DemoRadio>| {
|
||||
if let Ok(radio) = q_radios.get(*value) {
|
||||
widget_states.slider_click = radio.0;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
// ui camera
|
||||
commands.spawn(Camera2d);
|
||||
commands.spawn(demo_root(&assets, on_click, on_change_value));
|
||||
commands.spawn(demo_root(
|
||||
&assets,
|
||||
on_click,
|
||||
on_change_value,
|
||||
on_change_radio,
|
||||
));
|
||||
}
|
||||
|
||||
fn demo_root(
|
||||
asset_server: &AssetServer,
|
||||
on_click: SystemId,
|
||||
on_change_value: SystemId<In<f32>>,
|
||||
on_change_radio: SystemId<In<Entity>>,
|
||||
) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
@ -135,6 +174,7 @@ fn demo_root(
|
||||
button(asset_server, on_click),
|
||||
slider(0.0, 100.0, 50.0, Some(on_change_value)),
|
||||
checkbox(asset_server, "Checkbox", None),
|
||||
radio_group(asset_server, Some(on_change_radio)),
|
||||
Text::new("Press 'D' to toggle widget disabled states"),
|
||||
],
|
||||
)
|
||||
@ -476,11 +516,11 @@ fn checkbox(
|
||||
}
|
||||
|
||||
// Update the checkbox's styles.
|
||||
fn update_checkbox_style(
|
||||
fn update_checkbox_or_radio_style(
|
||||
mut q_checkbox: Query<
|
||||
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
|
||||
(
|
||||
With<DemoCheckbox>,
|
||||
Or<(With<DemoCheckbox>, With<DemoRadio>)>,
|
||||
Or<(
|
||||
Added<DemoCheckbox>,
|
||||
Changed<Hovered>,
|
||||
@ -489,7 +529,10 @@ fn update_checkbox_style(
|
||||
)>,
|
||||
),
|
||||
>,
|
||||
mut q_border_color: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
|
||||
mut q_border_color: Query<
|
||||
(&mut BorderColor, &mut Children),
|
||||
(Without<DemoCheckbox>, Without<DemoRadio>),
|
||||
>,
|
||||
mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
|
||||
) {
|
||||
for (checked, Hovered(is_hovering), is_disabled, children) in q_checkbox.iter_mut() {
|
||||
@ -511,7 +554,7 @@ fn update_checkbox_style(
|
||||
continue;
|
||||
};
|
||||
|
||||
set_checkbox_style(
|
||||
set_checkbox_or_radio_style(
|
||||
is_disabled,
|
||||
*is_hovering,
|
||||
checked,
|
||||
@ -521,13 +564,19 @@ fn update_checkbox_style(
|
||||
}
|
||||
}
|
||||
|
||||
fn update_checkbox_style2(
|
||||
fn update_checkbox_or_radio_style2(
|
||||
mut q_checkbox: Query<
|
||||
(Has<Checked>, &Hovered, Has<InteractionDisabled>, &Children),
|
||||
With<DemoCheckbox>,
|
||||
Or<(With<DemoCheckbox>, With<DemoRadio>)>,
|
||||
>,
|
||||
mut q_border_color: Query<
|
||||
(&mut BorderColor, &mut Children),
|
||||
(Without<DemoCheckbox>, Without<DemoRadio>),
|
||||
>,
|
||||
mut q_bg_color: Query<
|
||||
&mut BackgroundColor,
|
||||
(Without<DemoCheckbox>, Without<DemoRadio>, Without<Children>),
|
||||
>,
|
||||
mut q_border_color: Query<(&mut BorderColor, &mut Children), Without<DemoCheckbox>>,
|
||||
mut q_bg_color: Query<&mut BackgroundColor, (Without<DemoCheckbox>, Without<Children>)>,
|
||||
mut removed_checked: RemovedComponents<Checked>,
|
||||
mut removed_disabled: RemovedComponents<InteractionDisabled>,
|
||||
) {
|
||||
@ -557,7 +606,7 @@ fn update_checkbox_style2(
|
||||
return;
|
||||
};
|
||||
|
||||
set_checkbox_style(
|
||||
set_checkbox_or_radio_style(
|
||||
is_disabled,
|
||||
*is_hovering,
|
||||
checked,
|
||||
@ -568,7 +617,7 @@ fn update_checkbox_style2(
|
||||
});
|
||||
}
|
||||
|
||||
fn set_checkbox_style(
|
||||
fn set_checkbox_or_radio_style(
|
||||
disabled: bool,
|
||||
hovering: bool,
|
||||
checked: bool,
|
||||
@ -601,11 +650,94 @@ fn set_checkbox_style(
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a demo radio group
|
||||
fn radio_group(asset_server: &AssetServer, on_change: Option<SystemId<In<Entity>>>) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Column,
|
||||
align_items: AlignItems::Start,
|
||||
column_gap: Val::Px(4.0),
|
||||
..default()
|
||||
},
|
||||
Name::new("RadioGroup"),
|
||||
CoreRadioGroup { on_change },
|
||||
TabIndex::default(),
|
||||
children![
|
||||
(radio(asset_server, TrackClick::Drag, "Slider Drag"),),
|
||||
(radio(asset_server, TrackClick::Step, "Slider Step"),),
|
||||
(radio(asset_server, TrackClick::Snap, "Slider Snap"),)
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a demo radio button
|
||||
fn radio(asset_server: &AssetServer, value: TrackClick, caption: &str) -> impl Bundle {
|
||||
(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
flex_direction: FlexDirection::Row,
|
||||
justify_content: JustifyContent::FlexStart,
|
||||
align_items: AlignItems::Center,
|
||||
align_content: AlignContent::Center,
|
||||
column_gap: Val::Px(4.0),
|
||||
..default()
|
||||
},
|
||||
Name::new("RadioButton"),
|
||||
Hovered::default(),
|
||||
DemoRadio(value),
|
||||
CoreRadio,
|
||||
Children::spawn((
|
||||
Spawn((
|
||||
// Radio outer
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
width: Val::Px(16.0),
|
||||
height: Val::Px(16.0),
|
||||
border: UiRect::all(Val::Px(2.0)),
|
||||
..default()
|
||||
},
|
||||
BorderColor::all(CHECKBOX_OUTLINE), // Border color for the checkbox
|
||||
BorderRadius::MAX,
|
||||
children![
|
||||
// Radio inner
|
||||
(
|
||||
Node {
|
||||
display: Display::Flex,
|
||||
width: Val::Px(8.0),
|
||||
height: Val::Px(8.0),
|
||||
position_type: PositionType::Absolute,
|
||||
left: Val::Px(2.0),
|
||||
top: Val::Px(2.0),
|
||||
..default()
|
||||
},
|
||||
BorderRadius::MAX,
|
||||
BackgroundColor(CHECKBOX_CHECK),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Spawn((
|
||||
Text::new(caption),
|
||||
TextFont {
|
||||
font: asset_server.load("fonts/FiraSans-Bold.ttf"),
|
||||
font_size: 20.0,
|
||||
..default()
|
||||
},
|
||||
)),
|
||||
)),
|
||||
)
|
||||
}
|
||||
|
||||
fn toggle_disabled(
|
||||
input: Res<ButtonInput<KeyCode>>,
|
||||
mut interaction_query: Query<
|
||||
(Entity, Has<InteractionDisabled>),
|
||||
Or<(With<CoreButton>, With<CoreSlider>, With<CoreCheckbox>)>,
|
||||
Or<(
|
||||
With<CoreButton>,
|
||||
With<CoreSlider>,
|
||||
With<CoreCheckbox>,
|
||||
With<CoreRadio>,
|
||||
)>,
|
||||
>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
---
|
||||
title: Compressed image saver feature
|
||||
pull_requests: [19789]
|
||||
---
|
||||
|
||||
The compressed image saver has been gated behind its own dedicated feature flag now. If you were using it, you need to enable the "compressed_image_saver" feature.
|
||||
@ -0,0 +1,73 @@
|
||||
---
|
||||
title: EntityClonerBuilder Split
|
||||
pull_requests: [19649]
|
||||
---
|
||||
|
||||
`EntityClonerBuilder` is now generic and has different methods depending on the generic.
|
||||
|
||||
To get the wanted one, `EntityCloner::build` got split too:
|
||||
|
||||
- `EntityCloner::build_opt_out` to get `EntityClonerBuilder<OptOut>`
|
||||
- `EntityCloner::build_opt_in` to get `EntityClonerBuilder<OptIn>`
|
||||
|
||||
The first is used to clone all components possible and optionally _opting out_ of some.
|
||||
The second is used to only clone components as specified by _opting in_ for them.
|
||||
|
||||
```rs
|
||||
// 0.16
|
||||
let mut builder = EntityCloner.build(&mut world);
|
||||
builder.allow_all().deny::<ComponentThatShouldNotBeCloned>();
|
||||
builder.clone_entity(source_entity, target_entity);
|
||||
|
||||
let mut builder = EntityCloner.build(&mut world);
|
||||
builder.deny_all().allow::<ComponentThatShouldBeCloned>();
|
||||
builder.clone_entity(source_entity, target_entity);
|
||||
|
||||
// 0.17
|
||||
let mut builder = EntityCloner.build_opt_out(&mut world);
|
||||
builder.deny::<ComponentThatShouldNotBeCloned>();
|
||||
builder.clone_entity(source_entity, target_entity);
|
||||
|
||||
let mut builder = EntityCloner.build_opt_in(&mut world);
|
||||
builder.allow::<ComponentThatShouldBeCloned>();
|
||||
builder.clone_entity(source_entity, target_entity);
|
||||
```
|
||||
|
||||
Still, using `EntityClonerBuilder::finish` will return a non-generic `EntityCloner`.
|
||||
This change is done because the behavior of the two is too different to share the same struct and same methods and mixing them caused bugs.
|
||||
|
||||
The methods of the two builder types are different to 0.16 and to each other now:
|
||||
|
||||
## Opt-Out variant
|
||||
|
||||
- Still offers variants of the `deny` methods which now also includes one with a `BundleId` argument.
|
||||
- No longer offers `allow` methods, you need to be exact with denying components.
|
||||
- Offers now the `insert_mode` method to configure if components are cloned if they already exist at the target.
|
||||
- Required components of denied components are no longer considered. Denying `A`, which requires `B`, does not imply `B` alone would not be useful at the target. So if you do not want to clone `B` too, you need to deny it explicitly. This also means there is no `without_required_components` method anymore as that would be redundant.
|
||||
- It is now the other way around: Denying `A`, which is required _by_ `C`, will now also deny `C`. This can be bypassed with the new `without_required_by_components` method.
|
||||
|
||||
## Opt-In variant
|
||||
|
||||
- Still offers variants of the `allow` methods which now also includes one with a `BundleId` argument.
|
||||
- No longer offers `deny` methods, you need to be exact with allowing components.
|
||||
- Offers now `allow_if_new` method variants that only clone this component if the target does not contain it. If it does, required components of it will also not be cloned, except those that are also required by one that is actually cloned.
|
||||
- Still offers the `without_required_components` method.
|
||||
|
||||
## Common methods
|
||||
|
||||
All other methods `EntityClonerBuilder` had in 0.16 are still available for both variants:
|
||||
|
||||
- `with_default_clone_fn`
|
||||
- `move_components`
|
||||
- `clone_behavior` variants
|
||||
- `linked_cloning`
|
||||
|
||||
## Other affected APIs
|
||||
|
||||
| 0.16 | 0.17 |
|
||||
| - | - |
|
||||
| `EntityWorldMut::clone_with` | `EntityWorldMut::clone_with_opt_out` `EntityWorldMut::clone_with_opt_in` |
|
||||
| `EntityWorldMut::clone_and_spawn_with` | `EntityWorldMut::clone_and_spawn_with_opt_out` `EntityWorldMut::clone_and_spawn_with_opt_in` |
|
||||
| `EntityCommands::clone_with` | `EntityCommands::clone_with_opt_out` `EntityCommands::clone_with_opt_in` |
|
||||
| `EntityCommands::clone_and_spawn_with` | `EntityCommands::clone_and_spawn_with_opt_out` `EntityCommands::clone_and_spawn_with_opt_in` |
|
||||
| `entity_command::clone_with` | `entity_command::clone_with_opt_out` `entity_command::clone_with_opt_in` |
|
||||
@ -0,0 +1,68 @@
|
||||
---
|
||||
title: `FULLSCREEN_SHADER_HANDLE` replaced with `FullscreenShader`
|
||||
pull_requests: [19426]
|
||||
---
|
||||
|
||||
`FULLSCREEN_SHADER_HANDLE` and `fullscreen_shader_vertex_state` have been replaced by the
|
||||
`FullscreenShader` resource. Users of either of these will need to call `FullscreenShader::shader`
|
||||
or `FullscreenShader::to_vertex_state` respectively. You may need to clone `FullscreenShader` out of
|
||||
the render world to store an instance that you can use later (e.g., if you are attempting to use the
|
||||
fullscreen shader inside a `SpecializedRenderPipeline` implementation).
|
||||
|
||||
For example, if your previous code looked like this:
|
||||
|
||||
```rust
|
||||
struct MyPipeline {
|
||||
some_bind_group: BindGroupLayout,
|
||||
}
|
||||
|
||||
impl FromWorld for MyPipeline {
|
||||
fn from_world(render_world: &mut World) -> Self {
|
||||
let some_bind_group = /* ... RenderDevice stuff */;
|
||||
Self {
|
||||
some_bind_group,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecializedRenderPipeline for MyPipeline {
|
||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||
RenderPipelineDescriptor {
|
||||
vertex: fullscreen_shader_vertex_state(),
|
||||
// ... other stuff
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
You can migrate your code to:
|
||||
|
||||
```rust
|
||||
struct MyPipeline {
|
||||
some_bind_group: BindGroupLayout,
|
||||
fullscreen_shader: FullscreenShader,
|
||||
}
|
||||
|
||||
impl FromWorld for MyPipeline {
|
||||
fn from_world(render_world: &mut World) -> Self {
|
||||
let some_bind_group = /* ... RenderDevice stuff */;
|
||||
Self {
|
||||
some_bind_group,
|
||||
fullscreen_shader: render_world.resource::<FullscreenShader>().clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl SpecializedRenderPipeline for MyPipeline {
|
||||
fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor {
|
||||
RenderPipelineDescriptor {
|
||||
vertex: self.fullscreen_shader.to_vertex_state(),
|
||||
// ... other stuff
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This is just one example. Pipelines may be initialized in different ways, but the primary strategy
|
||||
is clone out the `FullscreenShader` resource from the render world, and call `to_vertex_state` to
|
||||
use it as the vertex shader.
|
||||
@ -3,4 +3,4 @@ title: RelativeCursorPosition is object-centered
|
||||
pull_requests: [16615]
|
||||
---
|
||||
|
||||
`RelativeCursorPosition`'s coordinates are now object-centered with (0,0) at the the center of the node and the corners at (±0.5, ±0.5). Its `normalized_visible_node_rect` field has been removed and replaced with a new `cursor_over: bool` field which is set to true when the cursor is hovering a visible section of the UI node.
|
||||
`RelativeCursorPosition`'s coordinates are now object-centered with (0,0) at the center of the node and the corners at (±0.5, ±0.5). Its `normalized_visible_node_rect` field has been removed and replaced with a new `cursor_over: bool` field which is set to true when the cursor is hovering a visible section of the UI node.
|
||||
|
||||
@ -10,7 +10,7 @@ In Bevy 0.17, we've made the first steps towards realtime raytraced lighting in
|
||||
|
||||
For some background, lighting in video games can be split into two parts: direct and indirect lighting.
|
||||
|
||||
Direct lighting is light that that is emitted from a light source, bounces off of one surface, and then reaches the camera. Indirect lighting by contrast is light that bounces off of different surfaces many times before reaching the camera, and is often called global illumination.
|
||||
Direct lighting is light that is emitted from a light source, bounces off of one surface, and then reaches the camera. Indirect lighting by contrast is light that bounces off of different surfaces many times before reaching the camera, and is often called global illumination.
|
||||
|
||||
(TODO: Diagrams of direct vs indirect light)
|
||||
|
||||
@ -25,7 +25,7 @@ The problem with these methods is that they all have large downsides:
|
||||
|
||||
Bevy Solari is intended as a completely alternate, high-end lighting solution for Bevy that uses GPU-accelerated raytracing to fix all of the above problems. Emissive meshes will properly cast light and shadows, you will be able to have hundreds of shadow casting lights, quality will be much better, it will require no baking time, and it will support _fully_ dynamic scenes!
|
||||
|
||||
While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. Currently there is only a non-realtime path tracer intended as a reference and testbed for developing Bevy Solari. There is nothing usable yet for game developers. However, feel free to run the solari example to see the path tracer in action, and look forwards to more work on Bevy Solari in future releases! (TODO: Is this burying the lede?)
|
||||
While Bevy 0.17 adds the bevy_solari crate, it's intended as a long-term project. Currently there is only a non-realtime path tracer intended as a reference and testbed for developing Bevy Solari. There is nothing usable yet for game developers. However, feel free to run the solari example to see the path tracer in action, and look forward to more work on Bevy Solari in future releases! (TODO: Is this burying the lede?)
|
||||
|
||||
(TODO: Embed bevy_solari logo here, or somewhere else that looks good)
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Allow importing glTFs with a corrected coordinate system
|
||||
authors: ["@janhohenheim"]
|
||||
pull_requests: [19633]
|
||||
pull_requests: [19633, 19685]
|
||||
---
|
||||
|
||||
glTF uses the following coordinate system:
|
||||
@ -24,17 +24,34 @@ Long-term, we'd like to fix our glTF imports to use the correct coordinate syste
|
||||
But changing the import behavior would mean that *all* imported glTFs of *all* users would suddenly look different, breaking their scenes!
|
||||
Not to mention that any bugs in the conversion code would be incredibly frustating for users.
|
||||
|
||||
This is why we are now gradually rolling out support for corrected glTF imports. Starting now you can opt into the new behavior by setting the `GltfLoaderSettings`:
|
||||
This is why we are now gradually rolling out support for corrected glTF imports. Starting now you can opt into the new behavior by setting `convert_coordinates` on `GltfPlugin`:
|
||||
|
||||
```rust
|
||||
// old behavior, ignores glTF's coordinate system
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins)
|
||||
.run();
|
||||
|
||||
// new behavior, converts the coordinate system of all glTF assets into Bevy's coordinate system
|
||||
App::new()
|
||||
.add_plugins(DefaultPlugins.set(GltfPlugin {
|
||||
convert_coordinates: true,
|
||||
..default()
|
||||
}))
|
||||
.run();
|
||||
```
|
||||
|
||||
You can also control this on a per-asset-level:
|
||||
|
||||
```rust
|
||||
// Use the global default
|
||||
let handle = asset_server.load("fox.gltf#Scene0");
|
||||
|
||||
// new behavior, converts glTF's coordinate system into Bevy's coordinate system
|
||||
// Manually opt in or out of coordinate conversion for an individual asset
|
||||
let handle = asset_server.load_with_settings(
|
||||
"fox.gltf#Scene0",
|
||||
|settings: &mut GltfLoaderSettings| {
|
||||
settings.convert_coordinates = true;
|
||||
settings.convert_coordinates = Some(true);
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Headless Widgets
|
||||
authors: ["@viridia"]
|
||||
pull_requests: [19366, 19584, 19665]
|
||||
pull_requests: [19366, 19584, 19665, 19778]
|
||||
---
|
||||
|
||||
Bevy's `Button` and `Interaction` components have been around for a long time. Unfortunately
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user