Merge branch 'main' into prestartup-window

This commit is contained in:
Jan Hohenheim 2025-07-07 22:48:36 +02:00 committed by GitHub
commit 8e73cbf9c8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
60 changed files with 1015 additions and 422 deletions

View File

@ -868,6 +868,7 @@ doc-scrape-examples = true
name = "Texture Atlas"
description = "Generates a texture atlas (sprite sheet) from individual sprites"
category = "2D Rendering"
# Loading asset folders is not supported in Wasm, but required to create the atlas.
wasm = false
[[example]]
@ -945,6 +946,7 @@ doc-scrape-examples = true
name = "2D Wireframe"
description = "Showcases wireframes for 2d meshes"
category = "2D Rendering"
# PolygonMode::Line wireframes are not supported by WebGL
wasm = false
# 3D Rendering
@ -1012,6 +1014,7 @@ doc-scrape-examples = true
name = "Anti-aliasing"
description = "Compares different anti-aliasing methods"
category = "3D Rendering"
# TAA not supported by WebGL
wasm = false
[[example]]
@ -1056,6 +1059,7 @@ doc-scrape-examples = true
name = "Auto Exposure"
description = "A scene showcasing auto exposure"
category = "3D Rendering"
# Requires compute shaders, which are not supported by WebGL.
wasm = false
[[example]]
@ -1123,6 +1127,7 @@ doc-scrape-examples = true
name = "Screen Space Ambient Occlusion"
description = "A scene showcasing screen space ambient occlusion"
category = "3D Rendering"
# Requires compute shaders, which are not supported by WebGL.
wasm = false
[[example]]
@ -1222,6 +1227,7 @@ doc-scrape-examples = true
name = "Order Independent Transparency"
description = "Demonstrates how to use OIT"
category = "3D Rendering"
# Not supported by WebGL
wasm = false
[[example]]
@ -1321,7 +1327,7 @@ doc-scrape-examples = true
name = "Skybox"
description = "Load a cubemap texture onto a cube like a skybox and cycle through different compressed texture formats."
category = "3D Rendering"
wasm = false
wasm = true
[[example]]
name = "solari"
@ -1432,6 +1438,7 @@ doc-scrape-examples = true
name = "Wireframe"
description = "Showcases wireframe rendering"
category = "3D Rendering"
# Not supported on WebGL
wasm = false
[[example]]
@ -1443,6 +1450,8 @@ doc-scrape-examples = true
name = "Irradiance Volumes"
description = "Demonstrates irradiance volumes"
category = "3D Rendering"
# On WebGL and WebGPU, the number of texture bindings is too low
# See <https://github.com/bevyengine/bevy/issues/11885>
wasm = false
[[example]]
@ -1455,6 +1464,7 @@ required-features = ["meshlet"]
name = "Meshlet"
description = "Meshlet rendering for dense high-poly scenes (experimental)"
category = "3D Rendering"
# Requires compute shaders and WGPU extensions, not supported by WebGL nor WebGPU.
wasm = false
setup = [
[
@ -1490,7 +1500,7 @@ doc-scrape-examples = true
name = "Lightmaps"
description = "Rendering a scene with baked lightmaps"
category = "3D Rendering"
wasm = false
wasm = true
[[example]]
name = "no_prepass"
@ -1643,6 +1653,7 @@ doc-scrape-examples = true
name = "Custom Loop"
description = "Demonstrates how to create a custom runner (to update an app manually)"
category = "Application"
# Doesn't render anything, doesn't create a canvas
wasm = false
[[example]]
@ -1654,6 +1665,7 @@ doc-scrape-examples = true
name = "Drag and Drop"
description = "An example that shows how to handle drag and drop in an app"
category = "Application"
# Browser drag and drop is not supported
wasm = false
[[example]]
@ -1665,6 +1677,7 @@ doc-scrape-examples = true
name = "Empty"
description = "An empty application (does nothing)"
category = "Application"
# Doesn't render anything, doesn't create a canvas
wasm = false
[[example]]
@ -1688,6 +1701,7 @@ required-features = ["bevy_log"]
name = "Headless"
description = "An application that runs without default plugins"
category = "Application"
# Doesn't render anything, doesn't create a canvas
wasm = false
[[example]]
@ -1710,6 +1724,8 @@ doc-scrape-examples = true
name = "Log layers"
description = "Illustrate how to add custom log layers"
category = "Application"
# Accesses `time`, which is not available on the web
# Also doesn't render anything
wasm = false
[[example]]
@ -1721,6 +1737,7 @@ doc-scrape-examples = true
name = "Advanced log layers"
description = "Illustrate how to transfer data between log layers and Bevy's ECS"
category = "Application"
# Doesn't render anything, doesn't create a canvas
wasm = false
[[example]]
@ -4325,6 +4342,14 @@ description = "Demonstrates specular tints and maps"
category = "3D Rendering"
wasm = true
[[example]]
name = "test_invalid_skinned_mesh"
path = "tests/3d/test_invalid_skinned_mesh.rs"
doc-scrape-examples = true
[package.metadata.example.test_invalid_skinned_mesh]
hidden = true
[profile.wasm-release]
inherits = "release"
opt-level = "z"

View File

@ -1,3 +1,5 @@
// For 2d replace `bevy_pbr::mesh_functions` with `bevy_sprite::mesh2d_functions`
// and `mesh_position_local_to_clip` with `mesh2d_position_local_to_clip`.
#import bevy_pbr::mesh_functions::{get_world_from_local, mesh_position_local_to_clip}
struct CustomMaterial {

View File

@ -17,6 +17,12 @@
struct MyExtendedMaterial {
quantize_steps: u32,
#ifdef SIXTEEN_BYTE_ALIGNMENT
// Web examples WebGL2 support: structs must be 16 byte aligned.
_webgl2_padding_8b: u32,
_webgl2_padding_12b: u32,
_webgl2_padding_16b: u32,
#endif
}
@group(3) @binding(100)

View File

@ -14,7 +14,6 @@ bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_mesh = { path = "../bevy_mesh", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [

View File

@ -16,7 +16,6 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
# other
# TODO: Remove `coreaudio-sys` dep below when updating `cpal`.

View File

@ -3,6 +3,7 @@ use bevy_asset::{Asset, Handle};
use bevy_ecs::prelude::*;
use bevy_math::Vec3;
use bevy_reflect::prelude::*;
use bevy_transform::components::Transform;
/// The way Bevy manages the sound playback.
#[derive(Debug, Clone, Copy, Reflect)]
@ -10,10 +11,10 @@ use bevy_reflect::prelude::*;
pub enum PlaybackMode {
/// Play the sound once. Do nothing when it ends.
///
/// Note: It is not possible to reuse an `AudioPlayer` after it has finished playing and
/// the underlying `AudioSink` or `SpatialAudioSink` has been drained.
/// Note: It is not possible to reuse an [`AudioPlayer`] after it has finished playing and
/// the underlying [`AudioSink`](crate::AudioSink) or [`SpatialAudioSink`](crate::SpatialAudioSink) has been drained.
///
/// To replay a sound, the audio components provided by `AudioPlayer` must be removed and
/// To replay a sound, the audio components provided by [`AudioPlayer`] must be removed and
/// added again.
Once,
/// Repeat the sound forever.
@ -27,7 +28,7 @@ pub enum PlaybackMode {
/// Initial settings to be used when audio starts playing.
///
/// If you would like to control the audio while it is playing, query for the
/// [`AudioSink`][crate::AudioSink] or [`SpatialAudioSink`][crate::SpatialAudioSink]
/// [`AudioSink`](crate::AudioSink) or [`SpatialAudioSink`](crate::SpatialAudioSink)
/// components. Changes to this component will *not* be applied to already-playing audio.
#[derive(Component, Clone, Copy, Debug, Reflect)]
#[reflect(Clone, Default, Component, Debug)]
@ -78,10 +79,10 @@ impl Default for PlaybackSettings {
impl PlaybackSettings {
/// Will play the associated audio source once.
///
/// Note: It is not possible to reuse an `AudioPlayer` after it has finished playing and
/// the underlying `AudioSink` or `SpatialAudioSink` has been drained.
/// Note: It is not possible to reuse an [`AudioPlayer`] after it has finished playing and
/// the underlying [`AudioSink`](crate::AudioSink) or [`SpatialAudioSink`](crate::SpatialAudioSink) has been drained.
///
/// To replay a sound, the audio components provided by `AudioPlayer` must be removed and
/// To replay a sound, the audio components provided by [`AudioPlayer`] must be removed and
/// added again.
pub const ONCE: PlaybackSettings = PlaybackSettings {
mode: PlaybackMode::Once,
@ -164,14 +165,15 @@ impl PlaybackSettings {
/// Settings for the listener for spatial audio sources.
///
/// This must be accompanied by `Transform` and `GlobalTransform`.
/// Only one entity with a `SpatialListener` should be present at any given time.
/// This is accompanied by [`Transform`] and [`GlobalTransform`](bevy_transform::prelude::GlobalTransform).
/// Only one entity with a [`SpatialListener`] should be present at any given time.
#[derive(Component, Clone, Debug, Reflect)]
#[require(Transform)]
#[reflect(Clone, Default, Component, Debug)]
pub struct SpatialListener {
/// Left ear position relative to the `GlobalTransform`.
/// Left ear position relative to the [`GlobalTransform`](bevy_transform::prelude::GlobalTransform).
pub left_ear_offset: Vec3,
/// Right ear position relative to the `GlobalTransform`.
/// Right ear position relative to the [`GlobalTransform`](bevy_transform::prelude::GlobalTransform).
pub right_ear_offset: Vec3,
}
@ -182,7 +184,7 @@ impl Default for SpatialListener {
}
impl SpatialListener {
/// Creates a new `SpatialListener` component.
/// Creates a new [`SpatialListener`] component.
///
/// `gap` is the distance between the left and right "ears" of the listener. Ears are
/// positioned on the x axis.
@ -203,12 +205,12 @@ impl SpatialListener {
pub struct SpatialScale(pub Vec3);
impl SpatialScale {
/// Create a new `SpatialScale` with the same value for all 3 dimensions.
/// Create a new [`SpatialScale`] with the same value for all 3 dimensions.
pub const fn new(scale: f32) -> Self {
Self(Vec3::splat(scale))
}
/// Create a new `SpatialScale` with the same value for `x` and `y`, and `0.0`
/// Create a new [`SpatialScale`] with the same value for `x` and `y`, and `0.0`
/// for `z`.
pub const fn new_2d(scale: f32) -> Self {
Self(Vec3::new(scale, scale, 0.0))
@ -238,11 +240,11 @@ pub struct DefaultSpatialScale(pub SpatialScale);
/// If the handle refers to an unavailable asset (such as if it has not finished loading yet),
/// the audio will not begin playing immediately. The audio will play when the asset is ready.
///
/// When Bevy begins the audio playback, an [`AudioSink`][crate::AudioSink] component will be
/// When Bevy begins the audio playback, an [`AudioSink`](crate::AudioSink) component will be
/// added to the entity. You can use that component to control the audio settings during playback.
///
/// Playback can be configured using the [`PlaybackSettings`] component. Note that changes to the
/// `PlaybackSettings` component will *not* affect already-playing audio.
/// [`PlaybackSettings`] component will *not* affect already-playing audio.
#[derive(Component, Reflect)]
#[reflect(Component, Clone)]
#[require(PlaybackSettings)]

View File

@ -103,7 +103,7 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
Entity,
&AudioPlayer<Source>,
&PlaybackSettings,
Option<&GlobalTransform>,
&GlobalTransform,
),
(Without<AudioSink>, Without<SpatialAudioSink>),
>,
@ -118,7 +118,7 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
return;
};
for (entity, source_handle, settings, maybe_emitter_transform) in &query_nonplaying {
for (entity, source_handle, settings, emitter_transform) in &query_nonplaying {
let Some(audio_source) = audio_sources.get(&source_handle.0) else {
continue;
};
@ -136,14 +136,7 @@ pub(crate) fn play_queued_audio_system<Source: Asset + Decodable>(
}
let scale = settings.spatial_scale.unwrap_or(default_spatial_scale.0).0;
let emitter_translation = if let Some(emitter_transform) = maybe_emitter_transform {
(emitter_transform.translation() * scale).into()
} else {
warn!("Spatial AudioPlayer with no GlobalTransform component. Using zero.");
Vec3::ZERO.into()
};
let emitter_translation = (emitter_transform.translation() * scale).into();
let sink = match SpatialSink::try_new(
stream_handle,
emitter_translation,

View File

@ -24,7 +24,6 @@ bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_image = { path = "../bevy_image", version = "0.17.0-dev" }
bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" }
@ -39,14 +38,12 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea
"serialize",
] }
serde = { version = "1", features = ["derive"] }
bitflags = "2.3"
radsort = "0.1"
nonmax = "0.5"
smallvec = { version = "1", default-features = false }
thiserror = { version = "2", default-features = false }
tracing = { version = "0.1", default-features = false, features = ["std"] }
bytemuck = { version = "1" }
[lints]
workspace = true

View File

@ -18,8 +18,6 @@ bevy_input_focus = { path = "../bevy_input_focus", version = "0.17.0-dev" }
bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev", features = [
"bevy_ui_picking_backend",
] }

View File

@ -18,14 +18,12 @@ bevy_asset = { path = "../bevy_asset", version = "0.17.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.17.0-dev" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.17.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.17.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.17.0-dev" }
bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
bevy_state = { path = "../bevy_state", version = "0.17.0-dev" }

View File

@ -56,7 +56,6 @@ critical-section = [
bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false }
bevy_time = { path = "../bevy_time", version = "0.17.0-dev", default-features = false }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false }
bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", default-features = false }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"alloc",

View File

@ -795,47 +795,34 @@ impl<'w> EntityClonerBuilder<'w, OptOut> {
/// this behavior.
pub fn deny<T: Bundle>(&mut self) -> &mut Self {
let bundle_id = self.world.register_bundle::<T>().id();
self.deny_by_bundle_id(bundle_id)
}
/// Disallows all components of the bundle ID from being cloned.
///
/// If component `A` is denied here and component `B` requires `A`, then `A`
/// is denied as well. See [`Self::without_required_by_components`] to alter
/// this behavior.
pub fn deny_by_bundle_id(&mut self, bundle_id: BundleId) -> &mut Self {
if let Some(bundle) = self.world.bundles().get(bundle_id) {
let ids = bundle.explicit_components().iter();
for &id in ids {
self.filter.filter_deny(id, self.world);
}
}
self
self.deny_by_ids(bundle_id)
}
/// Extends the list of components that shouldn't be cloned.
/// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these.
///
/// If component `A` is denied here and component `B` requires `A`, then `A`
/// is denied as well. See [`Self::without_required_by_components`] to alter
/// this behavior.
pub fn deny_by_ids(&mut self, ids: impl IntoIterator<Item = ComponentId>) -> &mut Self {
for id in ids {
self.filter.filter_deny(id, self.world);
}
self
}
/// Extends the list of components that shouldn't be cloned by type ids.
///
/// If component `A` is denied here and component `B` requires `A`, then `A`
/// is denied as well. See [`Self::without_required_by_components`] to alter
/// this behavior.
pub fn deny_by_type_ids(&mut self, ids: impl IntoIterator<Item = TypeId>) -> &mut Self {
for type_id in ids {
if let Some(id) = self.world.components().get_valid_id(type_id) {
self.filter.filter_deny(id, self.world);
pub fn deny_by_ids<M: Marker>(&mut self, ids: impl FilterableIds<M>) -> &mut Self {
ids.filter_ids(&mut |ids| match ids {
FilterableId::Type(type_id) => {
if let Some(id) = self.world.components().get_valid_id(type_id) {
self.filter.filter_deny(id, self.world);
}
}
}
FilterableId::Component(component_id) => {
self.filter.filter_deny(component_id, self.world);
}
FilterableId::Bundle(bundle_id) => {
if let Some(bundle) = self.world.bundles().get(bundle_id) {
let ids = bundle.explicit_components().iter();
for &id in ids {
self.filter.filter_deny(id, self.world);
}
}
}
});
self
}
}
@ -865,7 +852,7 @@ impl<'w> EntityClonerBuilder<'w, OptIn> {
/// to alter this behavior.
pub fn allow<T: Bundle>(&mut self) -> &mut Self {
let bundle_id = self.world.register_bundle::<T>().id();
self.allow_by_bundle_id(bundle_id)
self.allow_by_ids(bundle_id)
}
/// Adds all components of the bundle to the list of components to clone if
@ -876,94 +863,55 @@ impl<'w> EntityClonerBuilder<'w, OptIn> {
/// to alter this behavior.
pub fn allow_if_new<T: Bundle>(&mut self) -> &mut Self {
let bundle_id = self.world.register_bundle::<T>().id();
self.allow_by_bundle_id_if_new(bundle_id)
}
/// Adds all components of the bundle ID to the list of components to clone.
///
/// If component `A` is allowed here and requires component `B`, then `B`
/// is allowed as well. See [`Self::without_required_components`]
/// to alter this behavior.
pub fn allow_by_bundle_id(&mut self, bundle_id: BundleId) -> &mut Self {
if let Some(bundle) = self.world.bundles().get(bundle_id) {
let ids = bundle.explicit_components().iter();
for &id in ids {
self.filter
.filter_allow(id, self.world, InsertMode::Replace);
}
}
self
}
/// Adds all components of the bundle ID to the list of components to clone
/// if the target does not contain them.
///
/// If component `A` is allowed here and requires component `B`, then `B`
/// is allowed as well. See [`Self::without_required_components`]
/// to alter this behavior.
pub fn allow_by_bundle_id_if_new(&mut self, bundle_id: BundleId) -> &mut Self {
if let Some(bundle) = self.world.bundles().get(bundle_id) {
let ids = bundle.explicit_components().iter();
for &id in ids {
self.filter.filter_allow(id, self.world, InsertMode::Keep);
}
}
self
self.allow_by_ids_if_new(bundle_id)
}
/// Extends the list of components to clone.
/// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these.
///
/// If component `A` is allowed here and requires component `B`, then `B`
/// is allowed as well. See [`Self::without_required_components`]
/// to alter this behavior.
pub fn allow_by_ids(&mut self, ids: impl IntoIterator<Item = ComponentId>) -> &mut Self {
for id in ids {
self.filter
.filter_allow(id, self.world, InsertMode::Replace);
}
pub fn allow_by_ids<M: Marker>(&mut self, ids: impl FilterableIds<M>) -> &mut Self {
self.allow_by_ids_inner(ids, InsertMode::Replace);
self
}
/// Extends the list of components to clone if the target does not contain them.
/// Supports filtering by [`TypeId`], [`ComponentId`], [`BundleId`], and [`IntoIterator`] yielding one of these.
///
/// If component `A` is allowed here and requires component `B`, then `B`
/// is allowed as well. See [`Self::without_required_components`]
/// to alter this behavior.
pub fn allow_by_ids_if_new(&mut self, ids: impl IntoIterator<Item = ComponentId>) -> &mut Self {
for id in ids {
self.filter.filter_allow(id, self.world, InsertMode::Keep);
}
pub fn allow_by_ids_if_new<M: Marker>(&mut self, ids: impl FilterableIds<M>) -> &mut Self {
self.allow_by_ids_inner(ids, InsertMode::Keep);
self
}
/// Extends the list of components to clone using [`TypeId`]s.
///
/// If component `A` is allowed here and requires component `B`, then `B`
/// is allowed as well. See [`Self::without_required_components`]
/// to alter this behavior.
pub fn allow_by_type_ids(&mut self, ids: impl IntoIterator<Item = TypeId>) -> &mut Self {
for type_id in ids {
if let Some(id) = self.world.components().get_valid_id(type_id) {
fn allow_by_ids_inner<M: Marker>(
&mut self,
ids: impl FilterableIds<M>,
insert_mode: InsertMode,
) {
ids.filter_ids(&mut |id| match id {
FilterableId::Type(type_id) => {
if let Some(id) = self.world.components().get_valid_id(type_id) {
self.filter.filter_allow(id, self.world, insert_mode);
}
}
FilterableId::Component(component_id) => {
self.filter
.filter_allow(id, self.world, InsertMode::Replace);
.filter_allow(component_id, self.world, insert_mode);
}
}
self
}
/// Extends the list of components to clone using [`TypeId`]s if the target
/// does not contain them.
///
/// If component `A` is allowed here and requires component `B`, then `B`
/// is allowed as well. See [`Self::without_required_components`]
/// to alter this behavior.
pub fn allow_by_type_ids_if_new(&mut self, ids: impl IntoIterator<Item = TypeId>) -> &mut Self {
for type_id in ids {
if let Some(id) = self.world.components().get_valid_id(type_id) {
self.filter.filter_allow(id, self.world, InsertMode::Keep);
FilterableId::Bundle(bundle_id) => {
if let Some(bundle) = self.world.bundles().get(bundle_id) {
let ids = bundle.explicit_components().iter();
for &id in ids {
self.filter.filter_allow(id, self.world, insert_mode);
}
}
}
}
self
});
}
}
@ -1309,6 +1257,77 @@ impl Required {
}
}
mod private {
use super::*;
/// Marker trait to allow multiple blanket implementations for [`FilterableIds`].
pub trait Marker {}
/// Marker struct for [`FilterableIds`] implementation for single-value types.
pub struct ScalarType {}
impl Marker for ScalarType {}
/// Marker struct for [`FilterableIds`] implementation for [`IntoIterator`] types.
pub struct VectorType {}
impl Marker for VectorType {}
/// Defines types of ids that [`EntityClonerBuilder`] can filter components by.
#[derive(From)]
pub enum FilterableId {
Type(TypeId),
Component(ComponentId),
Bundle(BundleId),
}
impl<'a, T> From<&'a T> for FilterableId
where
T: Into<FilterableId> + Copy,
{
#[inline]
fn from(value: &'a T) -> Self {
(*value).into()
}
}
/// A trait to allow [`EntityClonerBuilder`] filter by any supported id type and their iterators,
/// reducing the number of method permutations required for all id types.
///
/// The supported id types that can be used to filter components are defined by [`FilterableId`], which allows following types: [`TypeId`], [`ComponentId`] and [`BundleId`].
///
/// `M` is a generic marker to allow multiple blanket implementations of this trait.
/// This works because `FilterableId<M1>` is a different trait from `FilterableId<M2>`, so multiple blanket implementations for different `M` are allowed.
/// The reason this is required is because supporting `IntoIterator` requires blanket implementation, but that will conflict with implementation for `TypeId`
/// since `IntoIterator` can technically be implemented for `TypeId` in the future.
/// Functions like `allow_by_ids` rely on type inference to automatically select proper type for `M` at call site.
pub trait FilterableIds<M: Marker> {
/// Takes in a function that processes all types of [`FilterableId`] one-by-one.
fn filter_ids(self, ids: &mut impl FnMut(FilterableId));
}
impl<I, T> FilterableIds<VectorType> for I
where
I: IntoIterator<Item = T>,
T: Into<FilterableId>,
{
#[inline]
fn filter_ids(self, ids: &mut impl FnMut(FilterableId)) {
for id in self.into_iter() {
ids(id.into());
}
}
}
impl<T> FilterableIds<ScalarType> for T
where
T: Into<FilterableId>,
{
#[inline]
fn filter_ids(self, ids: &mut impl FnMut(FilterableId)) {
ids(self.into());
}
}
}
use private::{FilterableId, FilterableIds, Marker};
#[cfg(test)]
mod tests {
use super::*;

View File

@ -455,15 +455,13 @@ pub fn validate_parent_has_component<C: Component>(
let Some(child_of) = entity_ref.get::<ChildOf>() else {
return;
};
if !world
.get_entity(child_of.parent())
.is_ok_and(|e| e.contains::<C>())
{
let parent = child_of.parent();
if !world.get_entity(parent).is_ok_and(|e| e.contains::<C>()) {
// TODO: print name here once Name lives in bevy_ecs
let name: Option<String> = None;
let debug_name = DebugName::type_name::<C>();
warn!(
"warning[B0004]: {}{name} with the {ty_name} component has a parent without {ty_name}.\n\
"warning[B0004]: {}{name} with the {ty_name} component has a parent ({parent}) without {ty_name}.\n\
This will cause inconsistent behaviors! See: https://bevy.org/learn/errors/b0004",
caller.map(|c| format!("{c}: ")).unwrap_or_default(),
ty_name = debug_name.shortname(),

View File

@ -133,7 +133,7 @@ pub trait Relationship: Component + Sized {
.and_modify(move |mut relationship_target| {
relationship_target.collection_mut_risky().add(entity);
})
.or_insert_with(|| {
.or_insert_with(move || {
let mut target = Self::RelationshipTarget::with_capacity(1);
target.collection_mut_risky().add(entity);
target

View File

@ -143,12 +143,38 @@ pub unsafe fn insert_by_id<T: Send + 'static>(
/// An [`EntityCommand`] that adds a component to an entity using
/// the component's [`FromWorld`] implementation.
///
/// `T::from_world` will only be invoked if the component will actually be inserted.
/// In other words, `T::from_world` will *not* be invoked if `mode` is [`InsertMode::Keep`]
/// and the entity already has the component.
#[track_caller]
pub fn insert_from_world<T: Component + FromWorld>(mode: InsertMode) -> impl EntityCommand {
let caller = MaybeLocation::caller();
move |mut entity: EntityWorldMut| {
let value = entity.world_scope(|world| T::from_world(world));
entity.insert_with_caller(value, mode, caller, RelationshipHookMode::Run);
if !(mode == InsertMode::Keep && entity.contains::<T>()) {
let value = entity.world_scope(|world| T::from_world(world));
entity.insert_with_caller(value, mode, caller, RelationshipHookMode::Run);
}
}
}
/// An [`EntityCommand`] that adds a component to an entity using
/// some function that returns the component.
///
/// The function will only be invoked if the component will actually be inserted.
/// In other words, the function will *not* be invoked if `mode` is [`InsertMode::Keep`]
/// and the entity already has the component.
#[track_caller]
pub fn insert_with<T: Component, F>(component_fn: F, mode: InsertMode) -> impl EntityCommand
where
F: FnOnce() -> T + Send + 'static,
{
let caller = MaybeLocation::caller();
move |mut entity: EntityWorldMut| {
if !(mode == InsertMode::Keep && entity.contains::<T>()) {
let value = component_fn();
entity.insert_with_caller(value, mode, caller, RelationshipHookMode::Run);
}
}
}

View File

@ -2273,35 +2273,53 @@ impl<'a, T: Component> EntityEntryCommands<'a, T> {
/// [Insert](EntityCommands::insert) the value returned from `default` into this entity,
/// if `T` is not already present.
///
/// `default` will only be invoked if the component will actually be inserted.
#[track_caller]
pub fn or_insert_with(&mut self, default: impl Fn() -> T) -> &mut Self {
self.or_insert(default())
pub fn or_insert_with<F>(&mut self, default: F) -> &mut Self
where
F: FnOnce() -> T + Send + 'static,
{
self.entity_commands
.queue(entity_command::insert_with(default, InsertMode::Keep));
self
}
/// [Insert](EntityCommands::insert) the value returned from `default` into this entity,
/// if `T` is not already present.
///
/// `default` will only be invoked if the component will actually be inserted.
///
/// # Note
///
/// If the entity does not exist when this command is executed,
/// the resulting error will be ignored.
#[track_caller]
pub fn or_try_insert_with(&mut self, default: impl Fn() -> T) -> &mut Self {
self.or_try_insert(default())
pub fn or_try_insert_with<F>(&mut self, default: F) -> &mut Self
where
F: FnOnce() -> T + Send + 'static,
{
self.entity_commands
.queue_silenced(entity_command::insert_with(default, InsertMode::Keep));
self
}
/// [Insert](EntityCommands::insert) `T::default` into this entity,
/// if `T` is not already present.
///
/// `T::default` will only be invoked if the component will actually be inserted.
#[track_caller]
pub fn or_default(&mut self) -> &mut Self
where
T: Default,
{
self.or_insert(T::default())
self.or_insert_with(T::default)
}
/// [Insert](EntityCommands::insert) `T::from_world` into this entity,
/// if `T` is not already present.
///
/// `T::from_world` will only be invoked if the component will actually be inserted.
#[track_caller]
pub fn or_from_world(&mut self) -> &mut Self
where
@ -2396,6 +2414,12 @@ mod tests {
}
}
impl Default for W<u8> {
fn default() -> Self {
unreachable!()
}
}
#[test]
fn entity_commands_entry() {
let mut world = World::default();
@ -2435,6 +2459,17 @@ mod tests {
let id = commands.entity(entity).entry::<W<u64>>().entity().id();
queue.apply(&mut world);
assert_eq!(id, entity);
let mut commands = Commands::new(&mut queue, &world);
commands
.entity(entity)
.entry::<W<u8>>()
.or_insert_with(|| W(5))
.or_insert_with(|| unreachable!())
.or_try_insert_with(|| unreachable!())
.or_default()
.or_from_world();
queue.apply(&mut world);
assert_eq!(5, world.get::<W<u8>>(entity).unwrap().0);
}
#[test]

View File

@ -13,7 +13,6 @@ keywords = ["bevy"]
bevy_app = { path = "../bevy_app", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_input = { path = "../bevy_input", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",

View File

@ -16,7 +16,6 @@ proc-macro = true
bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" }
syn = "2.0"
proc-macro2 = "1.0"
quote = "1.0"
[lints]

View File

@ -36,7 +36,6 @@ bevy_scene = { path = "../bevy_scene", version = "0.17.0-dev", features = [
] }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",
"serialize",

View File

@ -63,7 +63,6 @@ libm = ["bevy_math/libm"]
bevy_app = { path = "../bevy_app", version = "0.17.0-dev", default-features = false }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev", default-features = false }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", features = [
"glam",
], default-features = false, optional = true }

View File

@ -19,7 +19,6 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev" }
bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [
@ -27,7 +26,6 @@ bevy_color = { path = "../bevy_color", version = "0.17.0-dev", features = [
] }
# other
serde = { version = "1", default-features = false, features = ["derive"] }
tracing = { version = "0.1", default-features = false }
[features]

View File

@ -4,6 +4,7 @@ use crate::{
};
use core::f32::consts::FRAC_1_SQRT_2;
use core::fmt;
use derive_more::derive::Into;
#[cfg(feature = "bevy_reflect")]
@ -325,6 +326,12 @@ impl core::ops::Mul<Dir2> for Rot2 {
}
}
impl fmt::Display for Dir2 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(any(feature = "approx", test))]
impl approx::AbsDiffEq for Dir2 {
type Epsilon = f32;
@ -587,6 +594,12 @@ impl core::ops::Mul<Dir3> for Quat {
}
}
impl fmt::Display for Dir3 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(feature = "approx")]
impl approx::AbsDiffEq for Dir3 {
type Epsilon = f32;
@ -834,6 +847,12 @@ impl core::ops::Mul<Dir3A> for Quat {
}
}
impl fmt::Display for Dir3A {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(feature = "approx")]
impl approx::AbsDiffEq for Dir3A {
type Epsilon = f32;
@ -1022,6 +1041,12 @@ impl core::ops::Mul<Dir4> for f32 {
}
}
impl fmt::Display for Dir4 {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
#[cfg(feature = "approx")]
impl approx::AbsDiffEq for Dir4 {
type Epsilon = f32;

View File

@ -18,7 +18,6 @@ bevy_ecs = { path = "../bevy_ecs", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_mikktspace = { path = "../bevy_mikktspace", version = "0.17.0-dev" }
bevy_derive = { path = "../bevy_derive", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",
"serialize",

View File

@ -21,7 +21,7 @@ pbr_light_textures = []
shader_format_glsl = ["bevy_render/shader_format_glsl"]
trace = ["bevy_render/trace"]
# Enables the meshlet renderer for dense high-poly scenes (experimental)
meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:half", "dep:bevy_tasks"]
meshlet = ["dep:lz4_flex", "dep:range-alloc", "dep:bevy_tasks"]
# Enables processing meshes into meshlet meshes
meshlet_processor = [
"meshlet",
@ -51,7 +51,6 @@ bevy_camera = { path = "../bevy_camera", version = "0.17.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev", optional = true }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",
] }
@ -66,14 +65,12 @@ lz4_flex = { version = "0.11", default-features = false, features = [
"frame",
], optional = true }
range-alloc = { version = "0.1.3", optional = true }
half = { version = "2", features = ["bytemuck"], optional = true }
meshopt = { version = "0.4.1", optional = true }
metis = { version = "0.2", optional = true }
itertools = { version = "0.14", optional = true }
bitvec = { version = "1", optional = true }
# direct dependency required for derive macro
bytemuck = { version = "1", features = ["derive", "must_cast"] }
radsort = "0.1"
smallvec = { version = "1", default-features = false }
nonmax = "0.5"
static_assertions = "1"

View File

@ -24,7 +24,6 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_time = { path = "../bevy_time", version = "0.17.0-dev" }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",

View File

@ -39,24 +39,30 @@ pub mod prelude {
pub use crate::input::PointerInputPlugin;
}
/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin,
/// that you can replace with your own plugin as needed.
///
/// [`crate::PickingPlugin::is_input_enabled`] can be used to toggle whether
/// the core picking plugin processes the inputs sent by this, or other input plugins, in one place.
///
/// This plugin contains several settings, and is added to the world as a resource after initialization.
/// You can configure pointer input settings at runtime by accessing the resource.
#[derive(Copy, Clone, Resource, Debug, Reflect)]
#[reflect(Resource, Default, Clone)]
pub struct PointerInputPlugin {
/// Settings for enabling and disabling updating mouse and touch inputs for picking
///
/// ## Custom initialization
/// ```
/// # use bevy_app::App;
/// # use bevy_picking::input::{PointerInputSettings,PointerInputPlugin};
/// App::new()
/// .insert_resource(PointerInputSettings {
/// is_touch_enabled: false,
/// is_mouse_enabled: true,
/// })
/// // or DefaultPlugins
/// .add_plugins(PointerInputPlugin);
/// ```
pub struct PointerInputSettings {
/// Should touch inputs be updated?
pub is_touch_enabled: bool,
/// Should mouse inputs be updated?
pub is_mouse_enabled: bool,
}
impl PointerInputPlugin {
impl PointerInputSettings {
fn is_mouse_enabled(state: Res<Self>) -> bool {
state.is_mouse_enabled
}
@ -66,7 +72,7 @@ impl PointerInputPlugin {
}
}
impl Default for PointerInputPlugin {
impl Default for PointerInputSettings {
fn default() -> Self {
Self {
is_touch_enabled: true,
@ -75,25 +81,35 @@ impl Default for PointerInputPlugin {
}
}
/// Adds mouse and touch inputs for picking pointers to your app. This is a default input plugin,
/// that you can replace with your own plugin as needed.
///
/// Toggling mouse input or touch input can be done at runtime by modifying
/// [`PointerInputSettings`] resource.
///
/// [`PointerInputSettings`] can be initialized with custom values, but will be
/// initialized with default values if it is not present at the moment this is
/// added to the app.
pub struct PointerInputPlugin;
impl Plugin for PointerInputPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(*self)
app.init_resource::<PointerInputSettings>()
.register_type::<PointerInputSettings>()
.add_systems(Startup, spawn_mouse_pointer)
.add_systems(
First,
(
mouse_pick_events.run_if(PointerInputPlugin::is_mouse_enabled),
touch_pick_events.run_if(PointerInputPlugin::is_touch_enabled),
mouse_pick_events.run_if(PointerInputSettings::is_mouse_enabled),
touch_pick_events.run_if(PointerInputSettings::is_touch_enabled),
)
.chain()
.in_set(PickingSystems::Input),
)
.add_systems(
Last,
deactivate_touch_pointers.run_if(PointerInputPlugin::is_touch_enabled),
)
.register_type::<Self>()
.register_type::<PointerInputPlugin>();
deactivate_touch_pointers.run_if(PointerInputSettings::is_touch_enabled),
);
}
}

View File

@ -293,20 +293,31 @@ pub struct DefaultPickingPlugins;
impl PluginGroup for DefaultPickingPlugins {
fn build(self) -> PluginGroupBuilder {
PluginGroupBuilder::start::<Self>()
.add(input::PointerInputPlugin::default())
.add(PickingPlugin::default())
.add(input::PointerInputPlugin)
.add(PickingPlugin)
.add(InteractionPlugin)
}
}
/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared
/// types used by other picking plugins.
///
/// This plugin contains several settings, and is added to the world as a resource after initialization. You
/// can configure picking settings at runtime through the resource.
#[derive(Copy, Clone, Debug, Resource, Reflect)]
#[reflect(Resource, Default, Debug, Clone)]
pub struct PickingPlugin {
/// Controls the behavior of picking
///
/// ## Custom initialization
/// ```
/// # use bevy_app::App;
/// # use bevy_picking::{PickingSettings, PickingPlugin};
/// App::new()
/// .insert_resource(PickingSettings {
/// is_enabled: true,
/// is_input_enabled: false,
/// is_hover_enabled: true,
/// is_window_picking_enabled: false,
/// })
/// // or DefaultPlugins
/// .add_plugins(PickingPlugin);
/// ```
pub struct PickingSettings {
/// Enables and disables all picking features.
pub is_enabled: bool,
/// Enables and disables input collection.
@ -317,7 +328,7 @@ pub struct PickingPlugin {
pub is_window_picking_enabled: bool,
}
impl PickingPlugin {
impl PickingSettings {
/// Whether or not input collection systems should be running.
pub fn input_should_run(state: Res<Self>) -> bool {
state.is_input_enabled && state.is_enabled
@ -335,7 +346,7 @@ impl PickingPlugin {
}
}
impl Default for PickingPlugin {
impl Default for PickingSettings {
fn default() -> Self {
Self {
is_enabled: true,
@ -346,9 +357,18 @@ impl Default for PickingPlugin {
}
}
/// This plugin sets up the core picking infrastructure. It receives input events, and provides the shared
/// types used by other picking plugins.
///
/// Behavior of picking can be controlled by modifying [`PickingSettings`].
///
/// [`PickingSettings`] will be initialized with default values if it
/// is not present at the moment this is added to the app.
pub struct PickingPlugin;
impl Plugin for PickingPlugin {
fn build(&self, app: &mut App) {
app.insert_resource(*self)
app.init_resource::<PickingSettings>()
.init_resource::<pointer::PointerMap>()
.init_resource::<backend::ray::RayMap>()
.add_event::<pointer::PointerInput>()
@ -369,7 +389,7 @@ impl Plugin for PickingPlugin {
.add_systems(
PreUpdate,
window::update_window_hits
.run_if(Self::window_picking_should_run)
.run_if(PickingSettings::window_picking_should_run)
.in_set(PickingSystems::Backend),
)
.configure_sets(
@ -382,15 +402,15 @@ impl Plugin for PickingPlugin {
.configure_sets(
PreUpdate,
(
PickingSystems::ProcessInput.run_if(Self::input_should_run),
PickingSystems::ProcessInput.run_if(PickingSettings::input_should_run),
PickingSystems::Backend,
PickingSystems::Hover.run_if(Self::hover_should_run),
PickingSystems::Hover.run_if(PickingSettings::hover_should_run),
PickingSystems::PostHover,
PickingSystems::Last,
)
.chain(),
)
.register_type::<Self>()
.register_type::<PickingSettings>()
.register_type::<Pickable>()
.register_type::<hover::PickingInteraction>()
.register_type::<hover::Hovered>()

View File

@ -9,6 +9,7 @@ mod structs {
#[reflect_remote(external_crate::TheirStruct)]
//~^ ERROR: `?` operator has incompatible types
//~| ERROR: mismatched types
struct MyStruct {
// Reason: Should be `u32`
pub value: bool,
@ -25,6 +26,7 @@ mod tuple_structs {
#[reflect_remote(external_crate::TheirStruct)]
//~^ ERROR: `?` operator has incompatible types
//~| ERROR: mismatched types
struct MyStruct(
// Reason: Should be `u32`
pub bool,
@ -48,6 +50,7 @@ mod enums {
//~| ERROR: variant `enums::external_crate::TheirStruct::Unit` has no field named `0`
//~| ERROR: `?` operator has incompatible types
//~| ERROR: `?` operator has incompatible types
//~| ERROR: mismatched types
enum MyStruct {
// Reason: Should be unit variant
Unit(i32),
@ -57,6 +60,7 @@ mod enums {
// Reason: Should be `usize`
Struct { value: String },
//~^ ERROR: mismatched types
//~| ERROR: mismatched types
}
}

View File

@ -26,8 +26,8 @@ mod incorrect_inner_type {
//~| ERROR: `TheirInner<T>` does not implement `PartialReflect` so cannot be introspected
//~| ERROR: `TheirInner<T>` does not implement `PartialReflect` so cannot be introspected
//~| ERROR: `TheirInner<T>` does not implement `TypePath` so cannot provide dynamic type path information
//~| ERROR: `TheirInner<T>` does not implement `TypePath` so cannot provide dynamic type path information
//~| ERROR: `?` operator has incompatible types
//~| ERROR: mismatched types
struct MyOuter<T: FromReflect + GetTypeRegistration> {
// Reason: Should not use `MyInner<T>` directly
pub inner: MyInner<T>,

View File

@ -77,6 +77,7 @@ mod enums {
#[reflect_remote(external_crate::TheirBar)]
//~^ ERROR: `?` operator has incompatible types
//~| ERROR: mismatched types
enum MyBar {
// Reason: Should use `i32`
Value(u32),

View File

@ -24,7 +24,6 @@ indexmap = "2.0"
proc-macro2 = "1.0"
quote = "1.0"
syn = { version = "2.0", features = ["full", "extra-traits"] }
uuid = { version = "1.13.1", features = ["v4"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
# TODO: Assuming all wasm builds are for the browser. Require `no_std` support to break assumption.

View File

@ -722,18 +722,7 @@ impl<'a> ReflectStruct<'a> {
}
} else {
quote! {
#bevy_reflect_path::PartialReflect::reflect_clone(#accessor)?
.take()
.map_err(|value| #bevy_reflect_path::ReflectCloneError::FailedDowncast {
expected: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed(
<#field_ty as #bevy_reflect_path::TypePath>::type_path()
),
received: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Owned(
#bevy_reflect_path::__macro_exports::alloc_utils::ToString::to_string(
#bevy_reflect_path::DynamicTypePath::reflect_type_path(&*value)
)
),
})?
<#field_ty as #bevy_reflect_path::PartialReflect>::reflect_clone_and_take(#accessor)?
}
};

View File

@ -316,9 +316,7 @@ impl<'a> VariantBuilder for ReflectCloneVariantBuilder<'a> {
fn construct_field(&self, field: VariantField) -> TokenStream {
let bevy_reflect_path = self.reflect_enum.meta().bevy_reflect_path();
let field_ty = field.field.reflected_type();
let alias = field.alias;
let alias = match &field.field.attrs.remote {
Some(wrapper_ty) => {
@ -332,18 +330,7 @@ impl<'a> VariantBuilder for ReflectCloneVariantBuilder<'a> {
match &field.field.attrs.clone {
CloneBehavior::Default => {
quote! {
#bevy_reflect_path::PartialReflect::reflect_clone(#alias)?
.take()
.map_err(|value| #bevy_reflect_path::ReflectCloneError::FailedDowncast {
expected: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Borrowed(
<#field_ty as #bevy_reflect_path::TypePath>::type_path()
),
received: #bevy_reflect_path::__macro_exports::alloc_utils::Cow::Owned(
#bevy_reflect_path::__macro_exports::alloc_utils::ToString::to_string(
#bevy_reflect_path::DynamicTypePath::reflect_type_path(&*value)
)
),
})?
<#field_ty as #bevy_reflect_path::PartialReflect>::reflect_clone_and_take(#alias)?
}
}
CloneBehavior::Trait => {

View File

@ -231,11 +231,11 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre
/// // Generates a where clause like:
/// // impl bevy_reflect::Reflect for Foo
/// // where
/// // Self: Any + Send + Sync,
/// // Vec<Foo>: FromReflect + TypePath,
/// // Foo: Any + Send + Sync,
/// // Vec<Foo>: FromReflect + TypePath + MaybeTyped + RegisterForReflection,
/// ```
///
/// In this case, `Foo` is given the bounds `Vec<Foo>: FromReflect + TypePath`,
/// In this case, `Foo` is given the bounds `Vec<Foo>: FromReflect + ...`,
/// which requires that `Foo` implements `FromReflect`,
/// which requires that `Vec<Foo>` implements `FromReflect`,
/// and so on, resulting in the error.
@ -283,10 +283,10 @@ fn match_reflect_impls(ast: DeriveInput, source: ReflectImplSource) -> TokenStre
/// //
/// // impl<T: Trait> bevy_reflect::Reflect for Foo<T>
/// // where
/// // Self: Any + Send + Sync,
/// // Foo<T>: Any + Send + Sync,
/// // T::Assoc: Default,
/// // T: TypePath,
/// // T::Assoc: FromReflect + TypePath,
/// // T::Assoc: FromReflect + TypePath + MaybeTyped + RegisterForReflection,
/// // T::Assoc: List,
/// // {/* ... */}
/// ```

View File

@ -1,8 +1,8 @@
use crate::derive_data::ReflectMeta;
use bevy_macro_utils::fq_std::{FQAny, FQSend, FQSync};
use proc_macro2::TokenStream;
use proc_macro2::{TokenStream, TokenTree};
use quote::{quote, ToTokens};
use syn::{punctuated::Punctuated, Token, Type, WhereClause};
use syn::{punctuated::Punctuated, Ident, Token, Type, WhereClause};
/// Options defining how to extend the `where` clause for reflection.
pub(crate) struct WhereClauseOptions<'a, 'b> {
@ -29,15 +29,24 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> {
self.meta
}
/// Extends the `where` clause for a type with additional bounds needed for the reflection impls.
/// Extends the `where` clause for a type with additional bounds needed for the reflection
/// impls.
///
/// The default bounds added are as follows:
/// - `Self` has the bounds `Any + Send + Sync`
/// - Type parameters have the bound `TypePath` unless `#[reflect(type_path = false)]` is present
/// - Active fields have the bounds `TypePath` and either `PartialReflect` if `#[reflect(from_reflect = false)]` is present
/// or `FromReflect` otherwise (or no bounds at all if `#[reflect(no_field_bounds)]` is present)
/// - `Self` has:
/// - `Any + Send + Sync` bounds, if generic over types
/// - An `Any` bound, if generic over lifetimes but not types
/// - No bounds, if generic over neither types nor lifetimes
/// - Any given bounds in a `where` clause on the type
/// - Type parameters have the bound `TypePath` unless `#[reflect(type_path = false)]` is
/// present
/// - Active fields with non-generic types have the bounds `TypePath`, either `PartialReflect`
/// if `#[reflect(from_reflect = false)]` is present or `FromReflect` otherwise,
/// `MaybeTyped`, and `RegisterForReflection` (or no bounds at all if
/// `#[reflect(no_field_bounds)]` is present)
///
/// When the derive is used with `#[reflect(where)]`, the bounds specified in the attribute are added as well.
/// When the derive is used with `#[reflect(where)]`, the bounds specified in the attribute are
/// added as well.
///
/// # Example
///
@ -55,57 +64,69 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> {
/// ```ignore (bevy_reflect is not accessible from this crate)
/// where
/// // `Self` bounds:
/// Self: Any + Send + Sync,
/// Foo<T, U>: Any + Send + Sync,
/// // Type parameter bounds:
/// T: TypePath,
/// U: TypePath,
/// // Field bounds
/// T: FromReflect + TypePath,
/// // Active non-generic field bounds
/// T: FromReflect + TypePath + MaybeTyped + RegisterForReflection,
///
/// ```
///
/// If we had added `#[reflect(where T: MyTrait)]` to the type, it would instead generate:
/// If we add various things to the type:
///
/// ```ignore (bevy_reflect is not accessible from this crate)
/// #[derive(Reflect)]
/// #[reflect(where T: MyTrait)]
/// #[reflect(no_field_bounds)]
/// struct Foo<T, U>
/// where T: Clone
/// {
/// a: T,
/// #[reflect(ignore)]
/// b: U
/// }
/// ```
///
/// It will instead generate the following where clause:
///
/// ```ignore (bevy_reflect is not accessible from this crate)
/// where
/// // `Self` bounds:
/// Self: Any + Send + Sync,
/// // Type parameter bounds:
/// T: TypePath,
/// U: TypePath,
/// // Field bounds
/// T: FromReflect + TypePath,
/// // Custom bounds
/// T: MyTrait,
/// ```
///
/// And if we also added `#[reflect(no_field_bounds)]` to the type, it would instead generate:
///
/// ```ignore (bevy_reflect is not accessible from this crate)
/// where
/// // `Self` bounds:
/// Self: Any + Send + Sync,
/// Foo<T, U>: Any + Send + Sync,
/// // Given bounds:
/// T: Clone,
/// // Type parameter bounds:
/// T: TypePath,
/// U: TypePath,
/// // No active non-generic field bounds
/// // Custom bounds
/// T: MyTrait,
/// ```
pub fn extend_where_clause(&self, where_clause: Option<&WhereClause>) -> TokenStream {
// We would normally just use `Self`, but that won't work for generating things like assertion functions
// and trait impls for a type's reference (e.g. `impl FromArg for &MyType`)
let this = self.meta.type_path().true_type();
let mut generic_where_clause = quote! { where };
let required_bounds = self.required_bounds();
// Bounds on `Self`. We would normally just use `Self`, but that won't work for generating
// things like assertion functions and trait impls for a type's reference (e.g. `impl
// FromArg for &MyType`).
let generics = self.meta.type_path().generics();
if generics.type_params().next().is_some() {
// Generic over types? We need `Any + Send + Sync`.
let this = self.meta.type_path().true_type();
generic_where_clause.extend(quote! { #this: #FQAny + #FQSend + #FQSync, });
} else if generics.lifetimes().next().is_some() {
// Generic only over lifetimes? We need `'static`.
let this = self.meta.type_path().true_type();
generic_where_clause.extend(quote! { #this: 'static, });
}
// Maintain existing where clause, if any.
let mut generic_where_clause = if let Some(where_clause) = where_clause {
// Maintain existing where clause bounds, if any.
if let Some(where_clause) = where_clause {
let predicates = where_clause.predicates.iter();
quote! {where #this: #required_bounds, #(#predicates,)*}
} else {
quote!(where #this: #required_bounds,)
};
generic_where_clause.extend(quote! { #(#predicates,)* });
}
// Add additional reflection trait bounds
// Add additional reflection trait bounds.
let predicates = self.predicates();
generic_where_clause.extend(quote! {
#predicates
@ -157,19 +178,57 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> {
let bevy_reflect_path = self.meta.bevy_reflect_path();
let reflect_bound = self.reflect_bound();
// `TypePath` is always required for active fields since they are used to
// construct `NamedField` and `UnnamedField` instances for the `Typed` impl.
// Likewise, `GetTypeRegistration` is always required for active fields since
// they are used to register the type's dependencies.
Some(self.active_fields.iter().map(move |ty| {
quote!(
#ty : #reflect_bound
+ #bevy_reflect_path::TypePath
// Needed for `Typed` impls
+ #bevy_reflect_path::MaybeTyped
// Needed for `GetTypeRegistration` impls
+ #bevy_reflect_path::__macro_exports::RegisterForReflection
)
// Get the identifiers of all type parameters.
let type_param_idents = self
.meta
.type_path()
.generics()
.type_params()
.map(|type_param| type_param.ident.clone())
.collect::<Vec<Ident>>();
// Do any of the identifiers in `idents` appear in `token_stream`?
fn is_any_ident_in_token_stream(idents: &[Ident], token_stream: TokenStream) -> bool {
for token_tree in token_stream {
match token_tree {
TokenTree::Ident(ident) => {
if idents.contains(&ident) {
return true;
}
}
TokenTree::Group(group) => {
if is_any_ident_in_token_stream(idents, group.stream()) {
return true;
}
}
TokenTree::Punct(_) | TokenTree::Literal(_) => {}
}
}
false
}
Some(self.active_fields.iter().filter_map(move |ty| {
// Field type bounds are only required if `ty` is generic. How to determine that?
// Search `ty`s token stream for identifiers that match the identifiers from the
// function's type params. E.g. if `T` and `U` are the type param identifiers and
// `ty` is `Vec<[T; 4]>` then the `T` identifiers match. This is a bit hacky, but
// it works.
let is_generic =
is_any_ident_in_token_stream(&type_param_idents, ty.to_token_stream());
is_generic.then(|| {
quote!(
#ty: #reflect_bound
// Needed to construct `NamedField` and `UnnamedField` instances for
// the `Typed` impl.
+ #bevy_reflect_path::TypePath
// Needed for `Typed` impls
+ #bevy_reflect_path::MaybeTyped
// Needed for registering type dependencies in the
// `GetTypeRegistration` impl.
+ #bevy_reflect_path::__macro_exports::RegisterForReflection
)
})
}))
}
}
@ -194,9 +253,4 @@ impl<'a, 'b> WhereClauseOptions<'a, 'b> {
None
}
}
/// The minimum required bounds for a type to be reflected.
fn required_bounds(&self) -> TokenStream {
quote!(#FQAny + #FQSend + #FQSync)
}
}

View File

@ -9,7 +9,6 @@ use crate::{
type_registry::{FromType, GetTypeRegistration, ReflectFromPtr, TypeRegistration},
utility::GenericTypeInfoCell,
};
use alloc::borrow::Cow;
use alloc::vec::Vec;
use bevy_platform::prelude::*;
use bevy_reflect_derive::impl_type_path;
@ -144,21 +143,8 @@ where
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
let mut map = Self::new();
for (key, value) in self.iter() {
let key =
key.reflect_clone()?
.take()
.map_err(|_| ReflectCloneError::FailedDowncast {
expected: Cow::Borrowed(<Self as TypePath>::type_path()),
received: Cow::Owned(key.reflect_type_path().to_string()),
})?;
let value =
value
.reflect_clone()?
.take()
.map_err(|_| ReflectCloneError::FailedDowncast {
expected: Cow::Borrowed(<Self as TypePath>::type_path()),
received: Cow::Owned(value.reflect_type_path().to_string()),
})?;
let key = key.reflect_clone_and_take()?;
let value = value.reflect_clone_and_take()?;
map.insert(key, value);
}

View File

@ -10,7 +10,6 @@ use crate::{
};
use bevy_platform::prelude::*;
use bevy_reflect_derive::impl_type_path;
use core::any::Any;
use core::fmt;
macro_rules! impl_reflect_for_atomic {
@ -21,10 +20,7 @@ macro_rules! impl_reflect_for_atomic {
#[cfg(feature = "functions")]
crate::func::macros::impl_function_traits!($ty);
impl GetTypeRegistration for $ty
where
$ty: Any + Send + Sync,
{
impl GetTypeRegistration for $ty {
fn get_type_registration() -> TypeRegistration {
let mut registration = TypeRegistration::of::<Self>();
registration.insert::<ReflectFromPtr>(FromType::<Self>::from_type());
@ -42,10 +38,7 @@ macro_rules! impl_reflect_for_atomic {
}
}
impl Typed for $ty
where
$ty: Any + Send + Sync,
{
impl Typed for $ty {
fn type_info() -> &'static TypeInfo {
static CELL: NonGenericTypeInfoCell = NonGenericTypeInfoCell::new();
CELL.get_or_set(|| {
@ -55,10 +48,7 @@ macro_rules! impl_reflect_for_atomic {
}
}
impl PartialReflect for $ty
where
$ty: Any + Send + Sync,
{
impl PartialReflect for $ty {
#[inline]
fn get_represented_type_info(&self) -> Option<&'static TypeInfo> {
Some(<Self as Typed>::type_info())
@ -128,10 +118,7 @@ macro_rules! impl_reflect_for_atomic {
}
}
impl FromReflect for $ty
where
$ty: Any + Send + Sync,
{
impl FromReflect for $ty {
fn from_reflect(reflect: &dyn PartialReflect) -> Option<Self> {
Some(<$ty>::new(
reflect.try_downcast_ref::<$ty>()?.load($ordering),
@ -140,7 +127,7 @@ macro_rules! impl_reflect_for_atomic {
}
};
impl_full_reflect!(for $ty where $ty: Any + Send + Sync);
impl_full_reflect!(for $ty);
};
}

View File

@ -113,14 +113,7 @@ macro_rules! impl_reflect_for_veclike {
fn reflect_clone(&self) -> Result<bevy_platform::prelude::Box<dyn $crate::reflect::Reflect>, $crate::error::ReflectCloneError> {
Ok(bevy_platform::prelude::Box::new(
self.iter()
.map(|value| {
value.reflect_clone()?.take().map_err(|_| {
$crate::error::ReflectCloneError::FailedDowncast {
expected: alloc::borrow::Cow::Borrowed(<T as $crate::type_path::TypePath>::type_path()),
received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(value.reflect_type_path())),
}
})
})
.map(|value| value.reflect_clone_and_take())
.collect::<Result<Self, $crate::error::ReflectCloneError>>()?,
))
}

View File

@ -146,18 +146,8 @@ macro_rules! impl_reflect_for_hashmap {
fn reflect_clone(&self) -> Result<bevy_platform::prelude::Box<dyn $crate::reflect::Reflect>, $crate::error::ReflectCloneError> {
let mut map = Self::with_capacity_and_hasher(self.len(), S::default());
for (key, value) in self.iter() {
let key = key.reflect_clone()?.take().map_err(|_| {
$crate::error::ReflectCloneError::FailedDowncast {
expected: alloc::borrow::Cow::Borrowed(<K as $crate::type_path::TypePath>::type_path()),
received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(key.reflect_type_path())),
}
})?;
let value = value.reflect_clone()?.take().map_err(|_| {
$crate::error::ReflectCloneError::FailedDowncast {
expected: alloc::borrow::Cow::Borrowed(<V as $crate::type_path::TypePath>::type_path()),
received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(value.reflect_type_path())),
}
})?;
let key = key.reflect_clone_and_take()?;
let value = value.reflect_clone_and_take()?;
map.insert(key, value);
}

View File

@ -129,12 +129,7 @@ macro_rules! impl_reflect_for_hashset {
fn reflect_clone(&self) -> Result<bevy_platform::prelude::Box<dyn $crate::reflect::Reflect>, $crate::error::ReflectCloneError> {
let mut set = Self::with_capacity_and_hasher(self.len(), S::default());
for value in self.iter() {
let value = value.reflect_clone()?.take().map_err(|_| {
$crate::error::ReflectCloneError::FailedDowncast {
expected: alloc::borrow::Cow::Borrowed(<V as $crate::type_path::TypePath>::type_path()),
received: alloc::borrow::Cow::Owned(alloc::string::ToString::to_string(value.reflect_type_path())),
}
})?;
let value = value.reflect_clone_and_take()?;
set.insert(value);
}

View File

@ -4,7 +4,7 @@ use crate::{
ReflectMut, ReflectOwned, ReflectRef, TypeInfo, TypeParamInfo, TypePath, TypeRegistration,
Typed,
};
use alloc::{borrow::Cow, boxed::Box, string::ToString, vec::Vec};
use alloc::{boxed::Box, vec::Vec};
use bevy_reflect::ReflectCloneError;
use bevy_reflect_derive::impl_type_path;
use core::any::Any;
@ -137,16 +137,11 @@ where
fn reflect_clone(&self) -> Result<Box<dyn Reflect>, ReflectCloneError> {
Ok(Box::new(
self.iter()
.map(|value| {
value
.reflect_clone()?
.take()
.map_err(|_| ReflectCloneError::FailedDowncast {
expected: Cow::Borrowed(<T::Item as TypePath>::type_path()),
received: Cow::Owned(value.reflect_type_path().to_string()),
})
})
// `(**self)` avoids getting `SmallVec<T> as List::iter`, which
// would give us the wrong item type.
(**self)
.iter()
.map(PartialReflect::reflect_clone_and_take)
.collect::<Result<Self, ReflectCloneError>>()?,
))
}

View File

@ -2615,9 +2615,11 @@ bevy_reflect::tests::Test {
#[reflect(where T: Default)]
struct Foo<T>(String, #[reflect(ignore)] PhantomData<T>);
#[expect(dead_code, reason = "Bar is never constructed")]
#[derive(Default, TypePath)]
struct Bar;
#[expect(dead_code, reason = "Baz is never constructed")]
#[derive(TypePath)]
struct Baz;
@ -2631,6 +2633,7 @@ bevy_reflect::tests::Test {
#[reflect(where)]
struct Foo<T>(String, #[reflect(ignore)] PhantomData<T>);
#[expect(dead_code, reason = "Bar is never constructed")]
#[derive(TypePath)]
struct Bar;
@ -2665,6 +2668,7 @@ bevy_reflect::tests::Test {
#[reflect(where T::Assoc: core::fmt::Display)]
struct Foo<T: Trait>(T::Assoc);
#[expect(dead_code, reason = "Bar is never constructed")]
#[derive(TypePath)]
struct Bar;
@ -2672,6 +2676,7 @@ bevy_reflect::tests::Test {
type Assoc = usize;
}
#[expect(dead_code, reason = "Baz is never constructed")]
#[derive(TypePath)]
struct Baz;

View File

@ -313,6 +313,24 @@ where
})
}
/// For a type implementing [`PartialReflect`], combines `reflect_clone` and
/// `take` in a useful fashion, automatically constructing an appropriate
/// [`ReflectCloneError`] if the downcast fails.
///
/// This is an associated function, rather than a method, because methods
/// with generic types prevent dyn-compatibility.
fn reflect_clone_and_take<T: 'static>(&self) -> Result<T, ReflectCloneError>
where
Self: TypePath + Sized,
{
self.reflect_clone()?
.take()
.map_err(|_| ReflectCloneError::FailedDowncast {
expected: Cow::Borrowed(<Self as TypePath>::type_path()),
received: Cow::Owned(self.reflect_type_path().to_string()),
})
}
/// Returns a hash of the value (which includes the type).
///
/// If the underlying type does not support hashing, returns `None`.

View File

@ -13,7 +13,6 @@ bevy_macro_utils = { path = "../../bevy_macro_utils", version = "0.17.0-dev" }
syn = { version = "2.0", features = ["full"] }
quote = "1.0"
proc-macro2 = "1.0"
[lints]
workspace = true

View File

@ -50,6 +50,8 @@ pub struct OnTransition<S: States> {
/// }
/// ```
///
/// This schedule is split up into four phases, as described in [`StateTransitionSteps`].
///
/// [`PreStartup`]: https://docs.rs/bevy/latest/bevy/prelude/struct.PreStartup.html
/// [`PreUpdate`]: https://docs.rs/bevy/latest/bevy/prelude/struct.PreUpdate.html
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash, Default)]

View File

@ -36,10 +36,8 @@ bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-fea
taffy = { version = "0.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 = "2", default-features = false, features = ["from"] }
nonmax = "0.5"
smallvec = { version = "1", default-features = false }
accesskit = "0.19"
tracing = { version = "0.1", default-features = false, features = ["std"] }

View File

@ -631,6 +631,14 @@ pub enum InterpolationColorSpace {
Srgb,
/// Interpolates in linear sRGB space.
LinearRgb,
/// Interpolates in HSL space, taking the shortest hue path.
Hsl,
/// Interpolates in HSL space, taking the longest hue path.
HslLong,
/// Interpolates in HSV space, taking the shortest hue path.
Hsv,
/// Interpolates in HSV space, taking the longest hue path.
HsvLong,
}
/// Set the color space used for interpolation.

View File

@ -23,7 +23,6 @@ bevy_render = { path = "../bevy_render", version = "0.17.0-dev" }
bevy_sprite = { path = "../bevy_sprite", version = "0.17.0-dev" }
bevy_picking = { path = "../bevy_picking", version = "0.17.0-dev", optional = true }
bevy_transform = { path = "../bevy_transform", version = "0.17.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",
@ -32,23 +31,13 @@ bevy_ui = { path = "../bevy_ui", version = "0.17.0-dev" }
bevy_text = { path = "../bevy_text", version = "0.17.0-dev", default-features = false }
# other
serde = { version = "1", features = ["derive"], optional = true }
bytemuck = { version = "1.5", features = ["derive"] }
thiserror = { version = "2", default-features = false }
derive_more = { version = "1", default-features = false, features = ["from"] }
nonmax = "0.5"
smallvec = { version = "1", default-features = false }
accesskit = "0.18"
tracing = { version = "0.1", default-features = false, features = ["std"] }
[features]
default = []
serialize = [
"serde",
"smallvec/serde",
"bevy_math/serialize",
"bevy_platform/serialize",
]
serialize = ["bevy_math/serialize", "bevy_platform/serialize"]
bevy_ui_picking_backend = ["bevy_picking"]
bevy_ui_debug = []

View File

@ -186,6 +186,10 @@ impl SpecializedRenderPipeline for GradientPipeline {
InterpolationColorSpace::OkLchLong => "IN_OKLCH_LONG",
InterpolationColorSpace::Srgb => "IN_SRGB",
InterpolationColorSpace::LinearRgb => "IN_LINEAR_RGB",
InterpolationColorSpace::Hsl => "IN_HSL",
InterpolationColorSpace::HslLong => "IN_HSL_LONG",
InterpolationColorSpace::Hsv => "IN_HSV",
InterpolationColorSpace::HsvLong => "IN_HSV_LONG",
};
let shader_defs = if key.anti_alias {

View File

@ -31,7 +31,7 @@ struct GradientVertexOutput {
@location(0) uv: vec2<f32>,
@location(1) @interpolate(flat) size: vec2<f32>,
@location(2) @interpolate(flat) flags: u32,
@location(3) @interpolate(flat) radius: vec4<f32>,
@location(3) @interpolate(flat) radius: vec4<f32>,
@location(4) @interpolate(flat) border: vec4<f32>,
// Position relative to the center of the rectangle.
@ -114,27 +114,27 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4<f32> {
}
}
// This function converts two linear rgb colors to srgb space, mixes them, and then converts the result back to linear rgb space.
fn mix_linear_rgb_in_srgb_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space.
fn mix_linear_rgba_in_srgba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let a_srgb = pow(a.rgb, vec3(1. / 2.2));
let b_srgb = pow(b.rgb, vec3(1. / 2.2));
let mixed_srgb = mix(a_srgb, b_srgb, t);
return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t));
}
fn linear_rgb_to_oklab(c: vec4<f32>) -> vec4<f32> {
fn linear_rgba_to_oklaba(c: vec4<f32>) -> vec4<f32> {
let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.);
let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.);
let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.);
return vec4(
0.21045426 * l + 0.7936178 * m - 0.004072047 * s,
1.9779985 * l - 2.4285922 * m + 0.4505937 * s,
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
c.w
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
c.a
);
}
fn oklab_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z;
let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z;
let s_ = c.x - 0.08948418 * c.y - 1.2914855 * c.z;
@ -145,26 +145,127 @@ fn oklab_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
4.0767417 * l - 3.3077116 * m + 0.23096994 * s,
-1.268438 * l + 2.6097574 * m - 0.34131938 * s,
-0.0041960863 * l - 0.7034186 * m + 1.7076147 * s,
c.w
c.a
);
}
fn mix_linear_rgb_in_oklab_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklab_to_linear_rgba(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t));
fn mix_linear_rgba_in_oklaba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t));
}
fn linear_rgba_to_hsla(c: vec4<f32>) -> vec4<f32> {
let maxc = max(max(c.r, c.g), c.b);
let minc = min(min(c.r, c.g), c.b);
let delta = maxc - minc;
let l = (maxc + minc) * 0.5;
var h: f32 = 0.0;
var s: f32 = 0.0;
if delta != 0.0 {
s = delta / (1.0 - abs(2.0 * l - 1.0));
if maxc == c.r {
h = ((c.g - c.b) / delta) % 6.0;
} else if maxc == c.g {
h = ((c.b - c.r) / delta) + 2.0;
} else {
h = ((c.r - c.g) / delta) + 4.0;
}
h = h / 6.0;
if h < 0.0 {
h = h + 1.0;
}
}
return vec4<f32>(h, s, l, c.a);
}
fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
let h = hsl.x;
let s = hsl.y;
let l = hsl.z;
let c = (1.0 - abs(2.0 * l - 1.0)) * s;
let hp = h * 6.0;
let x = c * (1.0 - abs(hp % 2.0 - 1.0));
var r: f32 = 0.0;
var g: f32 = 0.0;
var b: f32 = 0.0;
if 0.0 <= hp && hp < 1.0 {
r = c; g = x; b = 0.0;
} else if 1.0 <= hp && hp < 2.0 {
r = x; g = c; b = 0.0;
} else if 2.0 <= hp && hp < 3.0 {
r = 0.0; g = c; b = x;
} else if 3.0 <= hp && hp < 4.0 {
r = 0.0; g = x; b = c;
} else if 4.0 <= hp && hp < 5.0 {
r = x; g = 0.0; b = c;
} else if 5.0 <= hp && hp < 6.0 {
r = c; g = 0.0; b = x;
}
let m = l - 0.5 * c;
return vec4<f32>(r + m, g + m, b + m, hsl.a);
}
fn linear_rgba_to_hsva(c: vec4<f32>) -> vec4<f32> {
let maxc = max(max(c.r, c.g), c.b);
let minc = min(min(c.r, c.g), c.b);
let delta = maxc - minc;
var h: f32 = 0.0;
var s: f32 = 0.0;
let v: f32 = maxc;
if delta != 0.0 {
s = delta / maxc;
if maxc == c.r {
h = ((c.g - c.b) / delta) % 6.0;
} else if maxc == c.g {
h = ((c.b - c.r) / delta) + 2.0;
} else {
h = ((c.r - c.g) / delta) + 4.0;
}
h = h / 6.0;
if h < 0.0 {
h = h + 1.0;
}
}
return vec4<f32>(h, s, v, c.a);
}
fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
let h = hsva.x * 6.0;
let s = hsva.y;
let v = hsva.z;
let c = v * s;
let x = c * (1.0 - abs(h % 2.0 - 1.0));
let m = v - c;
var r: f32 = 0.0;
var g: f32 = 0.0;
var b: f32 = 0.0;
if 0.0 <= h && h < 1.0 {
r = c; g = x; b = 0.0;
} else if 1.0 <= h && h < 2.0 {
r = x; g = c; b = 0.0;
} else if 2.0 <= h && h < 3.0 {
r = 0.0; g = c; b = x;
} else if 3.0 <= h && h < 4.0 {
r = 0.0; g = x; b = c;
} else if 4.0 <= h && h < 5.0 {
r = x; g = 0.0; b = c;
} else if 5.0 <= h && h < 6.0 {
r = c; g = 0.0; b = x;
}
return vec4<f32>(r + m, g + m, b + m, hsva.a);
}
/// hue is left in radians and not converted to degrees
fn linear_rgb_to_oklch(c: vec4<f32>) -> vec4<f32> {
let o = linear_rgb_to_oklab(c);
fn linear_rgba_to_oklcha(c: vec4<f32>) -> vec4<f32> {
let o = linear_rgba_to_oklaba(c);
let chroma = sqrt(o.y * o.y + o.z * o.z);
let hue = atan2(o.z, o.y);
return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.w);
return vec4(o.x, chroma, select(hue + TAU, hue, hue < 0.0), o.a);
}
fn oklch_to_linear_rgb(c: vec4<f32>) -> vec4<f32> {
fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
let a = c.y * cos(c.z);
let b = c.y * sin(c.z);
return oklab_to_linear_rgba(vec4(c.x, a, b, c.w));
return oklaba_to_linear_rgba(vec4(c.x, a, b, c.a));
}
fn rem_euclid(a: f32, b: f32) -> f32 {
@ -181,28 +282,75 @@ fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 {
return rem_euclid(a + select(diff - TAU, diff + TAU, 0. < diff) * t, TAU);
}
fn mix_oklch(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return vec4(
mix(a.xy, b.xy, t),
lerp_hue(a.z, b.z, t),
mix(a.w, b.w, t)
mix(a.a, b.a, t)
);
}
fn mix_oklch_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return vec4(
mix(a.xy, b.xy, t),
lerp_hue_long(a.z, b.z, t),
mix(a.w, b.w, t)
mix(a.a, b.a, t)
);
}
fn mix_linear_rgb_in_oklch_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t));
fn mix_linear_rgba_in_oklcha_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgb_in_oklch_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t));
fn mix_linear_rgba_in_oklcha_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgba_in_hsva_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
var h: f32;
if ha.y == 0. {
h = hb.x;
} else if hb.y == 0. {
h = ha.x;
} else {
h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
}
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsva_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsla_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
fn mix_linear_rgba_in_hsla_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
// These functions are used to calculate the distance in gradient space from the start of the gradient to the point.
@ -277,13 +425,21 @@ fn interpolate_gradient(
}
#ifdef IN_SRGB
return mix_linear_rgb_in_srgb_space(start_color, end_color, t);
return mix_linear_rgba_in_srgba_space(start_color, end_color, t);
#else ifdef IN_OKLAB
return mix_linear_rgb_in_oklab_space(start_color, end_color, t);
return mix_linear_rgba_in_oklaba_space(start_color, end_color, t);
#else ifdef IN_OKLCH
return mix_linear_rgb_in_oklch_space(start_color, end_color, t);
return mix_linear_rgba_in_oklcha_space(start_color, end_color, t);
#else ifdef IN_OKLCH_LONG
return mix_linear_rgb_in_oklch_space_long(start_color, end_color, t);
return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t);
#else ifdef IN_HSV
return mix_linear_rgba_in_hsva_space(start_color, end_color, t);
#else ifdef IN_HSV_LONG
return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t);
#else ifdef IN_HSL
return mix_linear_rgba_in_hsla_space(start_color, end_color, t);
#else ifdef IN_HSL_LONG
return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t);
#else
return mix(start_color, end_color, t);
#endif

View File

@ -22,12 +22,7 @@ bevy_reflect = [
]
## Adds serialization support through `serde`.
serialize = [
"serde",
"smol_str/serde",
"bevy_ecs/serialize",
"bevy_input/serialize",
]
serialize = ["serde", "bevy_ecs/serialize", "bevy_input/serialize"]
# Platform Compatibility
@ -56,9 +51,7 @@ bevy_input = { path = "../bevy_input", version = "0.17.0-dev", default-features
bevy_math = { path = "../bevy_math", version = "0.17.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev", default-features = false, features = [
"glam",
"smol_str",
], optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev", default-features = false }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false }
# other
@ -69,7 +62,6 @@ serde = { version = "1.0", features = [
raw-window-handle = { version = "0.6", features = [
"alloc",
], default-features = false }
smol_str = { version = "0.2", default-features = false }
log = { version = "0.4", default-features = false }
[target.'cfg(target_os = "android")'.dependencies]

View File

@ -16,7 +16,6 @@ x11 = ["winit/x11"]
accesskit_unix = ["accesskit_winit/accesskit_unix", "accesskit_winit/async-io"]
serialize = [
"serde",
"bevy_input/serialize",
"bevy_window/serialize",
"bevy_platform/serialize",
@ -38,7 +37,6 @@ bevy_log = { path = "../bevy_log", version = "0.17.0-dev" }
bevy_math = { path = "../bevy_math", version = "0.17.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.17.0-dev" }
bevy_window = { path = "../bevy_window", version = "0.17.0-dev" }
bevy_utils = { path = "../bevy_utils", version = "0.17.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.17.0-dev" }
bevy_platform = { path = "../bevy_platform", version = "0.17.0-dev", default-features = false, features = [
"std",
@ -57,7 +55,6 @@ accesskit_winit = { version = "0.27", default-features = false, features = [
approx = { version = "0.5", default-features = false }
cfg-if = "1.0"
raw-window-handle = "0.6"
serde = { version = "1.0", features = ["derive"], optional = true }
bytemuck = { version = "1.5", optional = true }
wgpu-types = { version = "25", optional = true }
accesskit = "0.19"

View File

@ -1,5 +1,14 @@
//! This example demonstrates the built-in 3d shapes in Bevy.
//! The scene includes a patterned texture and a rotation for visualizing the normals and UVs.
//! Here we use shape primitives to generate meshes for 3d objects as well as attaching a runtime-generated patterned texture to each 3d object.
//!
//! "Shape primitives" here are just the mathematical definition of certain shapes, they're not meshes on their own! A sphere with radius `1.0` can be defined with [`Sphere::new(1.0)`][Sphere::new] but all this does is store the radius. So we need to turn these descriptions of shapes into meshes.
//!
//! While a shape is not a mesh, turning it into one in Bevy is easy. In this example we call [`meshes.add(/* Shape here! */)`][Assets<A>::add] on the shape, which works because the [`Assets<A>::add`] method takes anything that can be turned into the asset type it stores. There's an implementation for [`From`] on shape primitives into [`Mesh`], so that will get called internally by [`Assets<A>::add`].
//!
//! [`Extrusion`] lets us turn 2D shape primitives into versions of those shapes that have volume by extruding them. A 1x1 square that gets wrapped in this with an extrusion depth of 2 will give us a rectangular prism of size 1x1x2, but here we're just extruding these 2d shapes by depth 1.
//!
//! The material applied to these shapes is a texture that we generate at run time by looping through a "palette" of RGBA values (stored adjacent to each other in the array) and writing values to positions in another array that represents the buffer for an 8x8 texture. This texture is then registered with the assets system just one time, with that [`Handle<StandardMaterial>`] then applied to all the shapes in this example.
//!
//! The mesh and material are [`Handle<Mesh>`] and [`Handle<StandardMaterial>`] at the moment, neither of which implement `Component` on their own. Handles are put behind "newtypes" to prevent ambiguity, as some entities might want to have handles to meshes (or images, or materials etc.) for different purposes! All we need to do to make them rendering-relevant components is wrap the mesh handle and the material handle in [`Mesh3d`] and [`MeshMaterial3d`] respectively.
//!
//! You can toggle wireframes with the space bar except on wasm. Wasm does not support
//! `POLYGON_MODE_LINE` on the gpu.

View File

@ -41,7 +41,7 @@ fn setup(
// change the above to `OpaqueRendererMethod::Deferred` or add the `DefaultOpaqueRendererMethod` resource.
..Default::default()
},
extension: MyExtension { quantize_steps: 3 },
extension: MyExtension::new(1),
})),
Transform::from_xyz(0.0, 0.5, 0.0),
));
@ -69,12 +69,30 @@ fn rotate_things(mut q: Query<&mut Transform, With<Rotate>>, time: Res<Time>) {
}
}
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone)]
#[derive(Asset, AsBindGroup, Reflect, Debug, Clone, Default)]
struct MyExtension {
// We need to ensure that the bindings of the base material and the extension do not conflict,
// so we start from binding slot 100, leaving slots 0-99 for the base material.
#[uniform(100)]
quantize_steps: u32,
// Web examples WebGL2 support: structs must be 16 byte aligned.
#[cfg(feature = "webgl2")]
#[uniform(100)]
_webgl2_padding_8b: u32,
#[cfg(feature = "webgl2")]
#[uniform(100)]
_webgl2_padding_12b: u32,
#[cfg(feature = "webgl2")]
#[uniform(100)]
_webgl2_padding_16b: u32,
}
impl MyExtension {
fn new(quantize_steps: u32) -> Self {
Self {
quantize_steps,
..default()
}
}
}
impl MaterialExtension for MyExtension {

View File

@ -245,6 +245,18 @@ fn setup(mut commands: Commands) {
InterpolationColorSpace::LinearRgb
}
InterpolationColorSpace::LinearRgb => {
InterpolationColorSpace::Hsl
}
InterpolationColorSpace::Hsl => {
InterpolationColorSpace::HslLong
}
InterpolationColorSpace::HslLong => {
InterpolationColorSpace::Hsv
}
InterpolationColorSpace::Hsv => {
InterpolationColorSpace::HsvLong
}
InterpolationColorSpace::HsvLong => {
InterpolationColorSpace::OkLab
}
};

View File

@ -1,6 +1,6 @@
---
title: EntityClonerBuilder Split
pull_requests: [19649]
pull_requests: [19649, 19977]
---
`EntityClonerBuilder` is now generic and has different methods depending on the generic.
@ -40,7 +40,7 @@ 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.
- Still offers variants of the `deny` methods.
- 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.
@ -48,7 +48,7 @@ The methods of the two builder types are different to 0.16 and to each other now
## Opt-In variant
- Still offers variants of the `allow` methods which now also includes one with a `BundleId` argument.
- Still offers variants of the `allow` methods.
- 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.
@ -62,6 +62,13 @@ All other methods `EntityClonerBuilder` had in 0.16 are still available for both
- `clone_behavior` variants
- `linked_cloning`
## Unified id filtering
Previously `EntityClonerBuilder` supported filtering by 2 types of ids: `ComponentId` and `TypeId`, the functions taking in `IntoIterator` for them.
Since now `EntityClonerBuilder` supports filtering by `BundleId` as well, the number of method variations would become a bit too unwieldy.
Instead, all id filtering methods were unified into generic `deny_by_ids/allow_by_ids(_if_new)` methods, which allow to filter components by
`TypeId`, `ComponentId`, `BundleId` and their `IntoIterator` variations.
## Other affected APIs
| 0.16 | 0.17 |

View File

@ -0,0 +1,10 @@
---
title: Extract `PickingPlugin` members into `PickingSettings`
pull_requests: [19078]
---
Controlling the behavior of picking should be done through
the `PickingSettings` resource instead of `PickingPlugin`.
To initialize `PickingSettings` with non-default values, simply add
the resource to the app using `insert_resource` with the desired value.

View File

@ -0,0 +1,10 @@
---
title: Extract `PointerInputPlugin` members into `PointerInputSettings`
pull_requests: [19078]
---
Toggling mouse and touch input update for picking should be done through
the `PointerInputSettings` resource instead of `PointerInputPlugin`.
To initialize `PointerInputSettings` with non-default values, simply add
the resource to the app using `insert_resource` with the desired value.

View File

@ -1,7 +1,7 @@
---
title: UI Gradients
authors: ["@Ickshonpe"]
pull_requests: [18139, 19330]
pull_requests: [18139, 19330, 19992]
---
Support for UI node's that display a gradient that transitions smoothly between two or more colors.
@ -14,12 +14,12 @@ Each gradient type consists of the geometric properties for that gradient, a lis
Color stops consist of a color, a position or angle and an optional hint. If no position is specified for a stop, it's evenly spaced between the previous and following stops. Color stop positions are absolute. With the list of stops:
```rust
vec![vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(Color::GREEN, Val::Percent(10.))
vec![ColorStop::new(RED, Val::Percent(90.), ColorStop::new(GREEN), Val::Percent(10.))]
```
the colors will be reordered and the gradient will transition from green at 10% to red at 90%.
Colors can be interpolated between the stops in OKLab, OKLCH, SRGB and linear RGB color spaces. The hint is a normalized value that can be used to shift the mid-point where the colors are mixed 50-50 between the stop with the hint and the following stop.
Colors can be interpolated between the stops in OKLab, OKLCH, SRGB, HSL, HSV and linear RGB color spaces. The hint is a normalized value that can be used to shift the mid-point where the colors are mixed 50-50 between the stop with the hint and the following stop. Cylindrical color spaces support interpolation along both short and long hue paths.
For sharp stops with no interpolated transition, place two stops at the same point.

View File

@ -0,0 +1,233 @@
//! Test that the renderer can handle various invalid skinned meshes
use bevy::{
core_pipeline::motion_blur::MotionBlur,
math::ops,
prelude::*,
render::{
camera::ScalingMode,
mesh::{
skinning::{SkinnedMesh, SkinnedMeshInverseBindposes},
Indices, PrimitiveTopology, VertexAttributeValues,
},
render_asset::RenderAssetUsages,
},
};
use core::f32::consts::TAU;
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.insert_resource(AmbientLight {
brightness: 20_000.0,
..default()
})
.add_systems(Startup, (setup_environment, setup_meshes))
.add_systems(Update, update_animated_joints)
.run();
}
fn setup_environment(
mut commands: Commands,
mut mesh_assets: ResMut<Assets<Mesh>>,
mut material_assets: ResMut<Assets<StandardMaterial>>,
) {
let description = "(left to right)\n\
0: Normal skinned mesh.\n\
1: Mesh asset is missing skinning attributes.\n\
2: One joint entity is missing.\n\
3: Mesh entity is missing SkinnedMesh component.";
commands.spawn((
Text::new(description),
Node {
position_type: PositionType::Absolute,
top: Val::Px(12.0),
left: Val::Px(12.0),
..default()
},
));
commands.spawn((
Camera3d::default(),
Transform::from_xyz(0.0, 0.0, 1.0).looking_at(Vec3::new(0.0, 0.0, 0.0), Vec3::Y),
Projection::Orthographic(OrthographicProjection {
scaling_mode: ScalingMode::AutoMin {
min_width: 19.0,
min_height: 6.0,
},
..OrthographicProjection::default_3d()
}),
// Add motion blur so we can check if it's working for skinned meshes.
// This also exercises the renderer's prepass path.
MotionBlur {
// Use an unrealistically large shutter angle so that motion blur is clearly visible.
shutter_angle: 3.0,
samples: 2,
},
// MSAA and MotionBlur together are not compatible on WebGL.
#[cfg(all(feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu")))]
Msaa::Off,
));
// Add a directional light to make sure we exercise the renderer's shadow path.
commands.spawn((
Transform::from_xyz(1.0, 1.0, 3.0).looking_at(Vec3::ZERO, Vec3::Y),
DirectionalLight {
shadows_enabled: true,
..default()
},
));
// Add a plane behind the meshes so we can see the shadows.
commands.spawn((
Transform::from_xyz(0.0, 0.0, -1.0),
Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(100.0, 100.0).normal(Dir3::Z))),
MeshMaterial3d(material_assets.add(StandardMaterial {
base_color: Color::srgb(0.05, 0.05, 0.15),
reflectance: 0.2,
..default()
})),
));
}
fn setup_meshes(
mut commands: Commands,
mut mesh_assets: ResMut<Assets<Mesh>>,
mut material_assets: ResMut<Assets<StandardMaterial>>,
mut inverse_bindposes_assets: ResMut<Assets<SkinnedMeshInverseBindposes>>,
) {
// Create a mesh with two rectangles.
let unskinned_mesh = Mesh::new(
PrimitiveTopology::TriangleList,
RenderAssetUsages::default(),
)
.with_inserted_attribute(
Mesh::ATTRIBUTE_POSITION,
vec![
[-0.3, -0.3, 0.0],
[0.3, -0.3, 0.0],
[-0.3, 0.3, 0.0],
[0.3, 0.3, 0.0],
[-0.4, 0.8, 0.0],
[0.4, 0.8, 0.0],
[-0.4, 1.8, 0.0],
[0.4, 1.8, 0.0],
],
)
.with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, vec![[0.0, 0.0, 1.0]; 8])
.with_inserted_indices(Indices::U16(vec![0, 1, 3, 0, 3, 2, 4, 5, 7, 4, 7, 6]));
// Copy the mesh and add skinning attributes that bind each rectangle to a joint.
let skinned_mesh = unskinned_mesh
.clone()
.with_inserted_attribute(
Mesh::ATTRIBUTE_JOINT_INDEX,
VertexAttributeValues::Uint16x4(vec![
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[0, 0, 0, 0],
[1, 0, 0, 0],
[1, 0, 0, 0],
[1, 0, 0, 0],
[1, 0, 0, 0],
]),
)
.with_inserted_attribute(
Mesh::ATTRIBUTE_JOINT_WEIGHT,
vec![[1.00, 0.00, 0.0, 0.0]; 8],
);
let unskinned_mesh_handle = mesh_assets.add(unskinned_mesh);
let skinned_mesh_handle = mesh_assets.add(skinned_mesh);
let inverse_bindposes_handle = inverse_bindposes_assets.add(vec![
Mat4::IDENTITY,
Mat4::from_translation(Vec3::new(0.0, -1.3, 0.0)),
]);
let mesh_material_handle = material_assets.add(StandardMaterial::default());
let background_material_handle = material_assets.add(StandardMaterial {
base_color: Color::srgb(0.05, 0.15, 0.05),
reflectance: 0.2,
..default()
});
#[derive(PartialEq)]
enum Variation {
Normal,
MissingMeshAttributes,
MissingJointEntity,
MissingSkinnedMeshComponent,
}
for (index, variation) in [
Variation::Normal,
Variation::MissingMeshAttributes,
Variation::MissingJointEntity,
Variation::MissingSkinnedMeshComponent,
]
.into_iter()
.enumerate()
{
// Skip variations that are currently broken. See https://github.com/bevyengine/bevy/issues/16929,
// https://github.com/bevyengine/bevy/pull/18074.
if (variation == Variation::MissingSkinnedMeshComponent)
|| (variation == Variation::MissingMeshAttributes)
{
continue;
}
let transform = Transform::from_xyz(((index as f32) - 1.5) * 4.5, 0.0, 0.0);
let joint_0 = commands.spawn(transform).id();
let joint_1 = commands
.spawn((ChildOf(joint_0), AnimatedJoint, Transform::IDENTITY))
.id();
if variation == Variation::MissingJointEntity {
commands.entity(joint_1).despawn();
}
let mesh_handle = match variation {
Variation::MissingMeshAttributes => &unskinned_mesh_handle,
_ => &skinned_mesh_handle,
};
let mut entity_commands = commands.spawn((
Mesh3d(mesh_handle.clone()),
MeshMaterial3d(mesh_material_handle.clone()),
transform,
));
if variation != Variation::MissingSkinnedMeshComponent {
entity_commands.insert(SkinnedMesh {
inverse_bindposes: inverse_bindposes_handle.clone(),
joints: vec![joint_0, joint_1],
});
}
// Add a square behind the mesh to distinguish it from the other meshes.
commands.spawn((
Transform::from_xyz(transform.translation.x, transform.translation.y, -0.8),
Mesh3d(mesh_assets.add(Plane3d::default().mesh().size(4.3, 4.3).normal(Dir3::Z))),
MeshMaterial3d(background_material_handle.clone()),
));
}
}
#[derive(Component)]
struct AnimatedJoint;
fn update_animated_joints(time: Res<Time>, query: Query<&mut Transform, With<AnimatedJoint>>) {
for mut transform in query {
let angle = TAU * 4.0 * ops::cos((time.elapsed_secs() / 8.0) * TAU);
let rotation = Quat::from_rotation_z(angle);
transform.rotation = rotation;
transform.translation = rotation.mul_vec3(Vec3::new(0.0, 1.3, 0.0));
}
}