From 3f6300dc81e7c25767ecc8f009b6f32b5c4ff68c Mon Sep 17 00:00:00 2001 From: Andrew Date: Tue, 12 Mar 2024 18:03:41 -0400 Subject: [PATCH 01/71] low_power example: pick nits (#12437) # Objective - no-longer-extant type `WinitConfig` referenced in comments - `mouse_button_input` refers to `KeyCode` input - "spacebar" flagged as a typo by RustRover IDE ## Solution - replace `WinitConfig` with `WinitSettings` in comments - rename `mouse_button_input` to just `button_input` - change "spacebar" to "space bar" --- examples/window/low_power.rs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/examples/window/low_power.rs b/examples/window/low_power.rs index f175167130399..3782d50171c0d 100644 --- a/examples/window/low_power.rs +++ b/examples/window/low_power.rs @@ -16,7 +16,7 @@ fn main() { .insert_resource(WinitSettings::game()) // Power-saving reactive rendering for applications. .insert_resource(WinitSettings::desktop_app()) - // You can also customize update behavior with the fields of [`WinitConfig`] + // You can also customize update behavior with the fields of [`WinitSettings`] .insert_resource(WinitSettings { focused_mode: bevy::winit::UpdateMode::Continuous, unfocused_mode: bevy::winit::UpdateMode::ReactiveLowPower { @@ -61,13 +61,16 @@ fn update_winit( use ExampleMode::*; *winit_config = match *mode { Game => { - // In the default `WinitConfig::game()` mode: + // In the default `WinitSettings::game()` mode: // * When focused: the event loop runs as fast as possible - // * When not focused: the event loop runs as fast as possible + // * When not focused: the app will update when the window is directly interacted with + // (e.g. the mouse hovers over a visible part of the out of focus window), a + // [`RequestRedraw`] event is received, or one sixtieth of a second has passed + // without the app updating (60 Hz refresh rate max). WinitSettings::game() } Application => { - // While in `WinitConfig::desktop_app()` mode: + // While in `WinitSettings::desktop_app()` mode: // * When focused: the app will update any time a winit event (e.g. the window is // moved/resized, the mouse moves, a button is pressed, etc.), a [`RequestRedraw`] // event is received, or after 5 seconds if the app has not updated. @@ -80,7 +83,7 @@ fn update_winit( ApplicationWithRedraw => { // Sending a `RequestRedraw` event is useful when you want the app to update the next // frame regardless of any user input. For example, your application might use - // `WinitConfig::desktop_app()` to reduce power use, but UI animations need to play even + // `WinitSettings::desktop_app()` to reduce power use, but UI animations need to play even // when there are no inputs, so you send redraw requests while the animation is playing. event.send(RequestRedraw); WinitSettings::desktop_app() @@ -101,9 +104,9 @@ pub(crate) mod test_setup { /// Switch between update modes when the mouse is clicked. pub(crate) fn cycle_modes( mut mode: ResMut, - mouse_button_input: Res>, + button_input: Res>, ) { - if mouse_button_input.just_pressed(KeyCode::Space) { + if button_input.just_pressed(KeyCode::Space) { *mode = match *mode { ExampleMode::Game => ExampleMode::Application, ExampleMode::Application => ExampleMode::ApplicationWithRedraw, @@ -173,7 +176,7 @@ pub(crate) mod test_setup { commands.spawn(( TextBundle::from_sections([ TextSection::new( - "Press spacebar to cycle modes\n", + "Press space bar to cycle modes\n", TextStyle { font_size: 50.0, ..default() From baaf4c8c2dcf8399797cb99942e2ad474d96c543 Mon Sep 17 00:00:00 2001 From: Eira Fransham Date: Tue, 12 Mar 2024 23:11:21 +0100 Subject: [PATCH 02/71] SystemId should manually implement `Eq` (#12436) # Objective `System` currently does not implement `Eq` even though it should ## Solution Manually implement `Eq` like other traits are manually implemented --- crates/bevy_ecs/src/system/system_registry.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/bevy_ecs/src/system/system_registry.rs b/crates/bevy_ecs/src/system/system_registry.rs index fa0dd6c4c821d..bb4c2fc2d8170 100644 --- a/crates/bevy_ecs/src/system/system_registry.rs +++ b/crates/bevy_ecs/src/system/system_registry.rs @@ -38,9 +38,11 @@ impl RemovedSystem { /// /// These are opaque identifiers, keyed to a specific [`World`], /// and are created via [`World::register_system`]. -#[derive(Eq)] pub struct SystemId(Entity, std::marker::PhantomData O>); +// A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters. +impl Eq for SystemId {} + // A manual impl is used because the trait bounds should ignore the `I` and `O` phantom parameters. impl Copy for SystemId {} From 55b786c2b7bf0dd1d3122d6dca4cdc87d19f6def Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Tue, 12 Mar 2024 18:21:10 -0700 Subject: [PATCH 03/71] Fix blurry text (#12429) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Objective Fixes #12064 ## Solution Prior to #11326, the "global physical" translation of text was rounded. After #11326, only the "offset" is being rounded. This moves things around so that the "global translation" is converted to physical pixels, rounded, and then converted back to logical pixels, which is what I believe was happening before / what the comments above describe. ## Discussion This seems to work and fix an obvious mistake in some code, but I don't fully grok the ui / text pipelines / math here. ## Before / After and test example
Expand Code ```rust use std::f32::consts::FRAC_PI_2; use bevy::prelude::*; use bevy_internal::window::WindowResolution; const FONT_SIZE: f32 = 25.0; const PADDING: f32 = 5.0; fn main() { App::new() .add_plugins( DefaultPlugins.set(WindowPlugin { primary_window: Some(Window { resolution: WindowResolution::default().with_scale_factor_override(1.0), ..default() }), ..default() }), //.set(ImagePlugin::default_nearest()), ) .add_systems(Startup, setup) .run(); } fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2dBundle::default()); let font = asset_server.load("fonts/FiraSans-Bold.ttf"); for x in [20.5, 140.0] { for i in 1..10 { text( &mut commands, font.clone(), x, (FONT_SIZE + PADDING) * i as f32, i, Quat::default(), 1.0, ); } } for x in [450.5, 700.0] { for i in 1..10 { text( &mut commands, font.clone(), x, ((FONT_SIZE * 2.0) + PADDING) * i as f32, i, Quat::default(), 2.0, ); } } for y in [400.0, 600.0] { for i in 1..10 { text( &mut commands, font.clone(), (FONT_SIZE + PADDING) * i as f32, y, i, Quat::from_rotation_z(FRAC_PI_2), 1.0, ); } } } fn text( commands: &mut Commands, font: Handle, x: f32, y: f32, i: usize, rot: Quat, scale: f32, ) { let text = (65..(65 + i)).map(|a| a as u8 as char).collect::(); commands.spawn(TextBundle { style: Style { position_type: PositionType::Absolute, left: Val::Px(x), top: Val::Px(y), ..default() }, text: Text::from_section( text, TextStyle { font, font_size: FONT_SIZE, ..default() }, ), transform: Transform::from_rotation(rot).with_scale(Vec2::splat(scale).extend(1.)), ..default() }); } ```
Open both images in new tabs and swap back and forth. Pay attention to the "A" and "ABCD" lines.
Before main3
After pr3
--------- Co-authored-by: François Mockers --- crates/bevy_ui/src/render/mod.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index f2fa85a71f3d5..fcec109409d59 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -640,10 +640,13 @@ pub fn extract_uinode_text( // * Multiply by the rounded physical position by the inverse scale factor to return to logical coordinates let logical_top_left = -0.5 * uinode.size(); - let physical_nearest_pixel = (logical_top_left * scale_factor).round(); - let logical_top_left_nearest_pixel = physical_nearest_pixel * inverse_scale_factor; - let transform = Mat4::from(global_transform.affine()) - * Mat4::from_translation(logical_top_left_nearest_pixel.extend(0.)); + + let mut transform = global_transform.affine() + * bevy_math::Affine3A::from_translation(logical_top_left.extend(0.)); + + transform.translation *= scale_factor; + transform.translation = transform.translation.round(); + transform.translation *= inverse_scale_factor; let mut color = LinearRgba::WHITE; let mut current_section = usize::MAX; From e282ee1a1ceaca865b841e7eaeeaffe9463d3501 Mon Sep 17 00:00:00 2001 From: Nathaniel Bielanski <122288484+nbielans@users.noreply.github.com> Date: Tue, 12 Mar 2024 21:24:00 -0400 Subject: [PATCH 04/71] Extracting ambient light from light.rs, and creating light directory (#12369) # Objective Beginning of refactoring of light.rs in bevy_pbr, as per issue #12349 Create and move light.rs to its own directory, and extract AmbientLight struct. ## Solution - moved light.rs to light/mod.rs - extracted AmbientLight struct to light/ambient_light.rs --- crates/bevy_pbr/src/light/ambient_light.rs | 39 +++++++++++++++++ .../bevy_pbr/src/{light.rs => light/mod.rs} | 42 ++----------------- 2 files changed, 42 insertions(+), 39 deletions(-) create mode 100644 crates/bevy_pbr/src/light/ambient_light.rs rename crates/bevy_pbr/src/{light.rs => light/mod.rs} (99%) diff --git a/crates/bevy_pbr/src/light/ambient_light.rs b/crates/bevy_pbr/src/light/ambient_light.rs new file mode 100644 index 0000000000000..d3f7744bd4f5c --- /dev/null +++ b/crates/bevy_pbr/src/light/ambient_light.rs @@ -0,0 +1,39 @@ +use super::*; + +/// An ambient light, which lights the entire scene equally. +/// +/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light. +/// +/// # Examples +/// +/// Make ambient light slightly brighter: +/// +/// ``` +/// # use bevy_ecs::system::ResMut; +/// # use bevy_pbr::AmbientLight; +/// fn setup_ambient_light(mut ambient_light: ResMut) { +/// ambient_light.brightness = 100.0; +/// } +/// ``` +#[derive(Resource, Clone, Debug, ExtractResource, Reflect)] +#[reflect(Resource)] +pub struct AmbientLight { + pub color: Color, + /// A direct scale factor multiplied with `color` before being passed to the shader. + pub brightness: f32, +} + +impl Default for AmbientLight { + fn default() -> Self { + Self { + color: Color::WHITE, + brightness: 80.0, + } + } +} +impl AmbientLight { + pub const NONE: AmbientLight = AmbientLight { + color: Color::WHITE, + brightness: 0.0, + }; +} diff --git a/crates/bevy_pbr/src/light.rs b/crates/bevy_pbr/src/light/mod.rs similarity index 99% rename from crates/bevy_pbr/src/light.rs rename to crates/bevy_pbr/src/light/mod.rs index e131d92d2c3ca..5d6be4fcf48d3 100644 --- a/crates/bevy_pbr/src/light.rs +++ b/crates/bevy_pbr/src/light/mod.rs @@ -20,6 +20,9 @@ use bevy_utils::tracing::warn; use crate::*; +mod ambient_light; +pub use ambient_light::AmbientLight; + /// Constants for operating with the light units: lumens, and lux. pub mod light_consts { /// Approximations for converting the wattage of lamps to lumens. @@ -616,45 +619,6 @@ fn calculate_cascade( texel_size: cascade_texel_size, } } - -/// An ambient light, which lights the entire scene equally. -/// -/// This resource is inserted by the [`PbrPlugin`] and by default it is set to a low ambient light. -/// -/// # Examples -/// -/// Make ambient light slightly brighter: -/// -/// ``` -/// # use bevy_ecs::system::ResMut; -/// # use bevy_pbr::AmbientLight; -/// fn setup_ambient_light(mut ambient_light: ResMut) { -/// ambient_light.brightness = 100.0; -/// } -/// ``` -#[derive(Resource, Clone, Debug, ExtractResource, Reflect)] -#[reflect(Resource)] -pub struct AmbientLight { - pub color: Color, - /// A direct scale factor multiplied with `color` before being passed to the shader. - pub brightness: f32, -} - -impl Default for AmbientLight { - fn default() -> Self { - Self { - color: Color::WHITE, - brightness: 80.0, - } - } -} -impl AmbientLight { - pub const NONE: AmbientLight = AmbientLight { - color: Color::WHITE, - brightness: 0.0, - }; -} - /// Add this component to make a [`Mesh`](bevy_render::mesh::Mesh) not cast shadows. #[derive(Component, Reflect, Default)] #[reflect(Component, Default)] From a9ca8491aa3f2dc99116e63d67a0625afe6e738d Mon Sep 17 00:00:00 2001 From: Rob Parrett Date: Tue, 12 Mar 2024 18:30:43 -0700 Subject: [PATCH 05/71] Fix `z` scale being `0.0` in breakout example (#12439) # Objective Scaling `z` by anything but `1.0` in 2d can only lead to bugs and confusion. See #4149. ## Solution Use a `Vec2` for the paddle size const, and add a scale of `1.0` later. This matches the way `BRICK_SIZE` is defined. --- examples/games/breakout.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index cb99dbb00fa9a..3cc19c5a7fda5 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -10,7 +10,7 @@ mod stepping; // These constants are defined in `Transform` units. // Using the default 2D camera they correspond 1:1 with screen pixels. -const PADDLE_SIZE: Vec3 = Vec3::new(120.0, 20.0, 0.0); +const PADDLE_SIZE: Vec2 = Vec2::new(120.0, 20.0); const GAP_BETWEEN_PADDLE_AND_FLOOR: f32 = 60.0; const PADDLE_SPEED: f32 = 500.0; // How close can the paddle get to the wall @@ -202,7 +202,7 @@ fn setup( SpriteBundle { transform: Transform { translation: Vec3::new(0.0, paddle_y, 0.0), - scale: PADDLE_SIZE, + scale: PADDLE_SIZE.extend(1.0), ..default() }, sprite: Sprite { From ee0fa7d1c22adf5bf4cbac2553e1055b7f3cc579 Mon Sep 17 00:00:00 2001 From: Lynn <62256001+solis-lumine-vorago@users.noreply.github.com> Date: Wed, 13 Mar 2024 19:51:53 +0100 Subject: [PATCH 06/71] Gizmo 3d grids (#12430) # Objective - Adds 3d grids, suggestion of #9400 ## Solution - Added 3d grids (grids spanning all three dimensions, not flat grids) to bevy_gizmos --- ## Changelog - `gizmos.grid(...)` and `gizmos.grid_2d(...)` now return a `GridBuilder2d`. - Added `gizmos.grid_3d(...)` which returns a `GridBuilder3d`. - The difference between them is basically only that `GridBuilder3d` exposes some methods for configuring the z axis while the 2d version doesn't. - Allowed for drawing the outer edges along a specific axis by calling `.outer_edges_x()`, etc. on the builder. ## Additional information Please note that I have not added the 3d grid to any example as not to clutter them. Here is an image of what the 3d grid looks like: Screenshot 2024-03-12 at 02 19 55 --------- Co-authored-by: Alice Cecile --- crates/bevy_gizmos/src/grid.rs | 333 ++++++++++++++++++++++++++------- examples/gizmos/2d_gizmos.rs | 2 +- 2 files changed, 264 insertions(+), 71 deletions(-) diff --git a/crates/bevy_gizmos/src/grid.rs b/crates/bevy_gizmos/src/grid.rs index c7b007746849c..a00a8c4fa19bc 100644 --- a/crates/bevy_gizmos/src/grid.rs +++ b/crates/bevy_gizmos/src/grid.rs @@ -5,29 +5,91 @@ use crate::prelude::{GizmoConfigGroup, Gizmos}; use bevy_color::LinearRgba; -use bevy_math::{Quat, UVec2, Vec2, Vec3}; +use bevy_math::{Quat, UVec2, UVec3, Vec2, Vec3}; +/// A builder returned by [`Gizmos::grid_3d`] +pub struct GridBuilder3d<'a, 'w, 's, T: GizmoConfigGroup> { + gizmos: &'a mut Gizmos<'w, 's, T>, + position: Vec3, + rotation: Quat, + spacing: Vec3, + cell_count: UVec3, + skew: Vec3, + outer_edges: [bool; 3], + color: LinearRgba, +} /// A builder returned by [`Gizmos::grid`] and [`Gizmos::grid_2d`] -pub struct GridBuilder<'a, 'w, 's, T: GizmoConfigGroup> { +pub struct GridBuilder2d<'a, 'w, 's, T: GizmoConfigGroup> { gizmos: &'a mut Gizmos<'w, 's, T>, position: Vec3, rotation: Quat, spacing: Vec2, cell_count: UVec2, skew: Vec2, - outer_edges: bool, + outer_edges: [bool; 2], color: LinearRgba, } -impl GridBuilder<'_, '_, '_, T> { +impl GridBuilder3d<'_, '_, '_, T> { /// Skews the grid by `tan(skew)` in the x direction. /// `skew` is in radians pub fn skew_x(mut self, skew: f32) -> Self { self.skew.x = skew; self } + /// Skews the grid by `tan(skew)` in the y direction. + /// `skew` is in radians + pub fn skew_y(mut self, skew: f32) -> Self { + self.skew.y = skew; + self + } + /// Skews the grid by `tan(skew)` in the z direction. + /// `skew` is in radians + pub fn skew_z(mut self, skew: f32) -> Self { + self.skew.z = skew; + self + } + /// Skews the grid by `tan(skew)` in the x, y and z directions. + /// `skew` is in radians + pub fn skew(mut self, skew: Vec3) -> Self { + self.skew = skew; + self + } + + /// Declare that the outer edges of the grid along the x axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_x(mut self) -> Self { + self.outer_edges[0] = true; + self + } + /// Declare that the outer edges of the grid along the y axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_y(mut self) -> Self { + self.outer_edges[1] = true; + self + } + /// Declare that the outer edges of the grid along the z axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_z(mut self) -> Self { + self.outer_edges[2] = true; + self + } + /// Declare that all outer edges of the grid should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges(mut self) -> Self { + self.outer_edges.fill(true); + self + } +} +impl GridBuilder2d<'_, '_, '_, T> { /// Skews the grid by `tan(skew)` in the x direction. /// `skew` is in radians + pub fn skew_x(mut self, skew: f32) -> Self { + self.skew.x = skew; + self + } + /// Skews the grid by `tan(skew)` in the y direction. + /// `skew` is in radians pub fn skew_y(mut self, skew: f32) -> Self { self.skew.y = skew; self @@ -39,70 +101,54 @@ impl GridBuilder<'_, '_, '_, T> { self } - /// Toggle whether the outer edges of the grid should be drawn. + /// Declare that the outer edges of the grid along the x axis should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges_x(mut self) -> Self { + self.outer_edges[0] = true; + self + } + /// Declare that the outer edges of the grid along the y axis should be drawn. /// By default, the outer edges will not be drawn. - pub fn outer_edges(mut self, outer_edges: bool) -> Self { - self.outer_edges = outer_edges; + pub fn outer_edges_y(mut self) -> Self { + self.outer_edges[1] = true; + self + } + /// Declare that all outer edges of the grid should be drawn. + /// By default, the outer edges will not be drawn. + pub fn outer_edges(mut self) -> Self { + self.outer_edges.fill(true); self } } -impl Drop for GridBuilder<'_, '_, '_, T> { - /// Draws a grid, by drawing lines with the stored [`Gizmos`] +impl Drop for GridBuilder3d<'_, '_, '_, T> { fn drop(&mut self) { - if !self.gizmos.enabled { - return; - } - - // Offset between two adjacent grid cells along the x/y-axis and accounting for skew. - let dx = Vec3::new(self.spacing.x, self.spacing.x * self.skew.y.tan(), 0.); - let dy = Vec3::new(self.spacing.y * self.skew.x.tan(), self.spacing.y, 0.); - - // Bottom-left corner of the grid - let grid_start = self.position - - self.cell_count.x as f32 / 2.0 * dx - - self.cell_count.y as f32 / 2.0 * dy; - - let (line_count, vertical_start, horizontal_start) = if self.outer_edges { - (self.cell_count + UVec2::ONE, grid_start, grid_start) - } else { - ( - self.cell_count.saturating_sub(UVec2::ONE), - grid_start + dx, - grid_start + dy, - ) - }; - - // Vertical lines - let dline = dy * self.cell_count.y as f32; - for i in 0..line_count.x { - let i = i as f32; - let line_start = vertical_start + i * dx; - let line_end = line_start + dline; - - self.gizmos.line( - self.rotation * line_start, - self.rotation * line_end, - self.color, - ); - } - - // Horizontal lines - let dline = dx * self.cell_count.x as f32; - for i in 0..line_count.y { - let i = i as f32; - let line_start = horizontal_start + i * dy; - let line_end = line_start + dline; - - self.gizmos.line( - self.rotation * line_start, - self.rotation * line_end, - self.color, - ); - } + draw_grid( + self.gizmos, + self.position, + self.rotation, + self.spacing, + self.cell_count, + self.skew, + self.outer_edges, + self.color, + ); + } +} +impl Drop for GridBuilder2d<'_, '_, '_, T> { + fn drop(&mut self) { + draw_grid( + self.gizmos, + self.position, + self.rotation, + self.spacing.extend(0.), + self.cell_count.extend(0), + self.skew.extend(0.), + [self.outer_edges[0], self.outer_edges[1], true], + self.color, + ); } } - impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// Draw a 2D grid in 3D. /// @@ -119,7 +165,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # Builder methods /// /// - The skew of the grid can be adjusted using the `.skew(...)`, `.skew_x(...)` or `.skew_y(...)` methods. They behave very similar to their CSS equivalents. - /// - The outer edges can be toggled on or off using `.outer_edges(...)`. + /// - All outer edges can be toggled on or off using `.outer_edges(...)`. Alternatively you can use `.outer_edges_x(...)` or `.outer_edges_y(...)` to toggle the outer edges along an axis. /// /// # Example /// ``` @@ -136,7 +182,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// GREEN /// ) /// .skew_x(0.25) - /// .outer_edges(true); + /// .outer_edges(); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` @@ -147,15 +193,71 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { cell_count: UVec2, spacing: Vec2, color: impl Into, - ) -> GridBuilder<'_, 'w, 's, T> { - GridBuilder { + ) -> GridBuilder2d<'_, 'w, 's, T> { + GridBuilder2d { gizmos: self, position, rotation, spacing, cell_count, skew: Vec2::ZERO, - outer_edges: false, + outer_edges: [false, false], + color: color.into(), + } + } + + /// Draw a 3D grid of voxel-like cells. + /// + /// This should be called for each frame the grid needs to be rendered. + /// + /// # Arguments + /// + /// - `position`: The center point of the grid. + /// - `rotation`: defines the orientation of the grid, by default we assume the grid is contained in a plane parallel to the XY plane. + /// - `cell_count`: defines the amount of cells in the x, y and z axes + /// - `spacing`: defines the distance between cells along the x, y and z axes + /// - `color`: color of the grid + /// + /// # Builder methods + /// + /// - The skew of the grid can be adjusted using the `.skew(...)`, `.skew_x(...)`, `.skew_y(...)` or `.skew_z(...)` methods. They behave very similar to their CSS equivalents. + /// - All outer edges can be toggled on or off using `.outer_edges(...)`. Alternatively you can use `.outer_edges_x(...)`, `.outer_edges_y(...)` or `.outer_edges_z(...)` to toggle the outer edges along an axis. + /// + /// # Example + /// ``` + /// # use bevy_gizmos::prelude::*; + /// # use bevy_render::prelude::*; + /// # use bevy_math::prelude::*; + /// # use bevy_color::palettes::basic::GREEN; + /// fn system(mut gizmos: Gizmos) { + /// gizmos.grid_3d( + /// Vec3::ZERO, + /// Quat::IDENTITY, + /// UVec3::new(10, 2, 10), + /// Vec3::splat(2.), + /// GREEN + /// ) + /// .skew_x(0.25) + /// .outer_edges(); + /// } + /// # bevy_ecs::system::assert_is_system(system); + /// ``` + pub fn grid_3d( + &mut self, + position: Vec3, + rotation: Quat, + cell_count: UVec3, + spacing: Vec3, + color: impl Into, + ) -> GridBuilder3d<'_, 'w, 's, T> { + GridBuilder3d { + gizmos: self, + position, + rotation, + spacing, + cell_count, + skew: Vec3::ZERO, + outer_edges: [false, false, false], color: color.into(), } } @@ -175,7 +277,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// # Builder methods /// /// - The skew of the grid can be adjusted using the `.skew(...)`, `.skew_x(...)` or `.skew_y(...)` methods. They behave very similar to their CSS equivalents. - /// - The outer edges can be toggled on or off using `.outer_edges(...)`. + /// - All outer edges can be toggled on or off using `.outer_edges(...)`. Alternatively you can use `.outer_edges_x(...)` or `.outer_edges_y(...)` to toggle the outer edges along an axis. /// /// # Example /// ``` @@ -192,7 +294,7 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { /// GREEN /// ) /// .skew_x(0.25) - /// .outer_edges(true); + /// .outer_edges(); /// } /// # bevy_ecs::system::assert_is_system(system); /// ``` @@ -203,16 +305,107 @@ impl<'w, 's, T: GizmoConfigGroup> Gizmos<'w, 's, T> { cell_count: UVec2, spacing: Vec2, color: impl Into, - ) -> GridBuilder<'_, 'w, 's, T> { - GridBuilder { + ) -> GridBuilder2d<'_, 'w, 's, T> { + GridBuilder2d { gizmos: self, position: position.extend(0.), rotation: Quat::from_rotation_z(rotation), spacing, cell_count, skew: Vec2::ZERO, - outer_edges: false, + outer_edges: [false, false], color: color.into(), } } } + +#[allow(clippy::too_many_arguments)] +fn draw_grid( + gizmos: &mut Gizmos<'_, '_, T>, + position: Vec3, + rotation: Quat, + spacing: Vec3, + cell_count: UVec3, + skew: Vec3, + outer_edges: [bool; 3], + color: LinearRgba, +) { + if !gizmos.enabled { + return; + } + + // Offset between two adjacent grid cells along the x/y-axis and accounting for skew. + let dx = spacing.x + * Vec3::new(1., skew.y.tan(), skew.z.tan()) + * if cell_count.x != 0 { 1. } else { 0. }; + let dy = spacing.y + * Vec3::new(skew.x.tan(), 1., skew.z.tan()) + * if cell_count.y != 0 { 1. } else { 0. }; + let dz = spacing.z + * Vec3::new(skew.x.tan(), skew.y.tan(), 1.) + * if cell_count.z != 0 { 1. } else { 0. }; + + // Bottom-left-front corner of the grid + let grid_start = position + - cell_count.x as f32 / 2.0 * dx + - cell_count.y as f32 / 2.0 * dy + - cell_count.z as f32 / 2.0 * dz; + + let line_count = UVec3::new( + if outer_edges[0] { + cell_count.x + 1 + } else { + cell_count.x.saturating_sub(1) + }, + if outer_edges[1] { + cell_count.y + 1 + } else { + cell_count.y.saturating_sub(1) + }, + if outer_edges[2] { + cell_count.z + 1 + } else { + cell_count.z.saturating_sub(1) + }, + ); + let x_start = grid_start + if outer_edges[0] { Vec3::ZERO } else { dy + dz }; + let y_start = grid_start + if outer_edges[1] { Vec3::ZERO } else { dx + dz }; + let z_start = grid_start + if outer_edges[2] { Vec3::ZERO } else { dx + dy }; + + // Lines along the x direction + let dline = dx * cell_count.x as f32; + for iy in 0..line_count.y { + let iy = iy as f32; + for iz in 0..line_count.z { + let iz = iz as f32; + let line_start = x_start + iy * dy + iz * dz; + let line_end = line_start + dline; + + gizmos.line(rotation * line_start, rotation * line_end, color); + } + } + // Lines along the y direction + let dline = dy * cell_count.y as f32; + for ix in 0..line_count.x { + let ix = ix as f32; + for iz in 0..line_count.z { + let iz = iz as f32; + let line_start = y_start + ix * dx + iz * dz; + let line_end = line_start + dline; + + gizmos.line(rotation * line_start, rotation * line_end, color); + } + } + // Lines along the z direction + let dline = dz * cell_count.z as f32; + for ix in 0..line_count.x { + let ix = ix as f32; + for iy in 0..line_count.y { + let iy = iy as f32; + let line_start = z_start + ix * dx + iy * dy; + let line_end = line_start + dline; + + gizmos.line(rotation * line_start, rotation * line_end, color); + } + } +} diff --git a/examples/gizmos/2d_gizmos.rs b/examples/gizmos/2d_gizmos.rs index ccd4b1171f699..c4aa2c03838ff 100644 --- a/examples/gizmos/2d_gizmos.rs +++ b/examples/gizmos/2d_gizmos.rs @@ -51,7 +51,7 @@ fn draw_example_collection( // Light gray LinearRgba::gray(0.65), ) - .outer_edges(true); + .outer_edges(); // Triangle gizmos.linestrip_gradient_2d([ From 4b64d1d1d721a6974a6a06e57749227806f6835c Mon Sep 17 00:00:00 2001 From: James Liu Date: Wed, 13 Mar 2024 18:36:03 -0700 Subject: [PATCH 07/71] Make a note about the performance of Query::is_empty (#12466) # Objective `Query::is_empty` does not mention the potential performance footgun of using it with non-archetypal filters. ## Solution Document it. --- crates/bevy_ecs/src/query/state.rs | 8 ++++++++ crates/bevy_ecs/src/system/query.rs | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/crates/bevy_ecs/src/query/state.rs b/crates/bevy_ecs/src/query/state.rs index 1a97e2bd70b4e..e211e3bd078b9 100644 --- a/crates/bevy_ecs/src/query/state.rs +++ b/crates/bevy_ecs/src/query/state.rs @@ -188,9 +188,17 @@ impl QueryState { /// Checks if the query is empty for the given [`World`], where the last change and current tick are given. /// + /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` + /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely + /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query + /// result for a match. + /// /// # Panics /// /// If `world` does not match the one used to call `QueryState::new` for this instance. + /// + /// [`Added`]: crate::query::Added + /// [`Changed`]: crate::query::Changed #[inline] pub fn is_empty(&self, world: &World, last_run: Tick, this_run: Tick) -> bool { self.validate_world(world.id()); diff --git a/crates/bevy_ecs/src/system/query.rs b/crates/bevy_ecs/src/system/query.rs index e34557dbda268..fb87bb2e018ef 100644 --- a/crates/bevy_ecs/src/system/query.rs +++ b/crates/bevy_ecs/src/system/query.rs @@ -1208,6 +1208,11 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// Returns `true` if there are no query items. /// + /// This is equivalent to `self.iter().next().is_none()`, and thus the worst case runtime will be `O(n)` + /// where `n` is the number of *potential* matches. This can be notably expensive for queries that rely + /// on non-archetypal filters such as [`Added`] or [`Changed`] which must individually check each query + /// result for a match. + /// /// # Example /// /// Here, the score is increased only if an entity with a `Player` component is present in the world: @@ -1226,6 +1231,9 @@ impl<'w, 's, D: QueryData, F: QueryFilter> Query<'w, 's, D, F> { /// } /// # bevy_ecs::system::assert_is_system(update_score_system); /// ``` + /// + /// [`Added`]: crate::query::Added + /// [`Changed`]: crate::query::Changed #[inline] pub fn is_empty(&self) -> bool { // SAFETY: From d3e44325b45daf13087a6fe7a425b1744b5f2379 Mon Sep 17 00:00:00 2001 From: Peter Hayman Date: Thu, 14 Mar 2024 16:15:20 +1100 Subject: [PATCH 08/71] Fix: deserialize `DynamicEnum` using index (#12464) # Objective - Addresses #12462 - When we serialize an enum, deserialize it, then reserialize it, the correct variant should be selected. ## Solution - Change `dynamic_enum.set_variant` to `dynamic_enum.set_variant_with_index` in `EnumVisitor` --- crates/bevy_reflect/src/serde/de.rs | 109 +++++++++------------------ crates/bevy_reflect/src/serde/ser.rs | 79 +++---------------- 2 files changed, 47 insertions(+), 141 deletions(-) diff --git a/crates/bevy_reflect/src/serde/de.rs b/crates/bevy_reflect/src/serde/de.rs index 0949085bc4976..a121a7831c188 100644 --- a/crates/bevy_reflect/src/serde/de.rs +++ b/crates/bevy_reflect/src/serde/de.rs @@ -753,8 +753,12 @@ impl<'a, 'de> Visitor<'de> for EnumVisitor<'a> { )? .into(), }; - - dynamic_enum.set_variant(variant_info.name(), value); + let variant_name = variant_info.name(); + let variant_index = self + .enum_info + .index_of(variant_name) + .expect("variant should exist"); + dynamic_enum.set_variant_with_index(variant_index, variant_name, value); Ok(dynamic_enum) } } @@ -1058,7 +1062,7 @@ mod tests { use bevy_utils::HashMap; use crate as bevy_reflect; - use crate::serde::{TypedReflectDeserializer, UntypedReflectDeserializer}; + use crate::serde::{ReflectSerializer, TypedReflectDeserializer, UntypedReflectDeserializer}; use crate::{DynamicEnum, FromReflect, Reflect, ReflectDeserialize, TypeRegistry}; #[derive(Reflect, Debug, PartialEq)] @@ -1116,7 +1120,7 @@ mod tests { #[reflect(Deserialize)] struct CustomDeserialize { value: usize, - #[serde(rename = "renamed")] + #[serde(alias = "renamed")] inner_struct: SomeDeserializableStruct, } @@ -1166,12 +1170,11 @@ mod tests { registry } - #[test] - fn should_deserialize() { + fn get_my_struct() -> MyStruct { let mut map = HashMap::new(); map.insert(64, 32); - let expected = MyStruct { + MyStruct { primitive_value: 123, option_value: Some(String::from("Hello world!")), option_value_complex: Some(SomeStruct { foo: 123 }), @@ -1198,7 +1201,13 @@ mod tests { value: 100, inner_struct: SomeDeserializableStruct { foo: 101 }, }, - }; + } + } + + #[test] + fn should_deserialize() { + let expected = get_my_struct(); + let registry = get_registry(); let input = r#"{ "bevy_reflect::serde::de::tests::MyStruct": ( @@ -1243,7 +1252,6 @@ mod tests { ), }"#; - let registry = get_registry(); let reflect_deserializer = UntypedReflectDeserializer::new(®istry); let mut ron_deserializer = ron::de::Deserializer::from_str(input).unwrap(); let dynamic_output = reflect_deserializer @@ -1425,40 +1433,28 @@ mod tests { assert!(expected.reflect_partial_eq(output.as_ref()).unwrap()); } + // Regression test for https://github.com/bevyengine/bevy/issues/12462 #[test] - fn should_deserialize_non_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); + fn should_reserialize() { + let registry = get_registry(); + let input1 = get_my_struct(); - let expected = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 0 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(0), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::default(), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(0.0, 0.0), - custom_deserialize: CustomDeserialize { - value: 100, - inner_struct: SomeDeserializableStruct { foo: 101 }, - }, - }; + let serializer1 = ReflectSerializer::new(&input1, ®istry); + let serialized1 = ron::ser::to_string(&serializer1).unwrap(); + + let mut deserializer = ron::de::Deserializer::from_str(&serialized1).unwrap(); + let reflect_deserializer = UntypedReflectDeserializer::new(®istry); + let input2 = reflect_deserializer.deserialize(&mut deserializer).unwrap(); + + let serializer2 = ReflectSerializer::new(&*input2, ®istry); + let serialized2 = ron::ser::to_string(&serializer2).unwrap(); + assert_eq!(serialized1, serialized2); + } + + #[test] + fn should_deserialize_non_self_describing_binary() { + let expected = get_my_struct(); let registry = get_registry(); let input = vec![ @@ -1490,38 +1486,7 @@ mod tests { #[test] fn should_deserialize_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); - - let expected = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 0 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(0), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::default(), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(0.0, 0.0), - custom_deserialize: CustomDeserialize { - value: 100, - inner_struct: SomeDeserializableStruct { foo: 101 }, - }, - }; - + let expected = get_my_struct(); let registry = get_registry(); let input = vec![ diff --git a/crates/bevy_reflect/src/serde/ser.rs b/crates/bevy_reflect/src/serde/ser.rs index 2823f2f811617..c67b81e8cc2e2 100644 --- a/crates/bevy_reflect/src/serde/ser.rs +++ b/crates/bevy_reflect/src/serde/ser.rs @@ -574,12 +574,10 @@ mod tests { registry } - #[test] - fn should_serialize() { + fn get_my_struct() -> MyStruct { let mut map = HashMap::new(); map.insert(64, 32); - - let input = MyStruct { + MyStruct { primitive_value: 123, option_value: Some(String::from("Hello world!")), option_value_complex: Some(SomeStruct { foo: 123 }), @@ -606,9 +604,14 @@ mod tests { value: 100, inner_struct: SomeSerializableStruct { foo: 101 }, }, - }; + } + } + #[test] + fn should_serialize() { + let input = get_my_struct(); let registry = get_registry(); + let serializer = ReflectSerializer::new(&input, ®istry); let config = PrettyConfig::default() @@ -776,38 +779,7 @@ mod tests { #[test] fn should_serialize_non_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); - - let input = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 123 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(123), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::from("Struct Variant"), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(1.23, 3.45), - custom_serialize: CustomSerialize { - value: 100, - inner_struct: SomeSerializableStruct { foo: 101 }, - }, - }; - + let input = get_my_struct(); let registry = get_registry(); let serializer = ReflectSerializer::new(&input, ®istry); @@ -834,38 +806,7 @@ mod tests { #[test] fn should_serialize_self_describing_binary() { - let mut map = HashMap::new(); - map.insert(64, 32); - - let input = MyStruct { - primitive_value: 123, - option_value: Some(String::from("Hello world!")), - option_value_complex: Some(SomeStruct { foo: 123 }), - tuple_value: (PI, 1337), - list_value: vec![-2, -1, 0, 1, 2], - array_value: [-2, -1, 0, 1, 2], - map_value: map, - struct_value: SomeStruct { foo: 999999999 }, - tuple_struct_value: SomeTupleStruct(String::from("Tuple Struct")), - unit_struct: SomeUnitStruct, - unit_enum: SomeEnum::Unit, - newtype_enum: SomeEnum::NewType(123), - tuple_enum: SomeEnum::Tuple(1.23, 3.21), - struct_enum: SomeEnum::Struct { - foo: String::from("Struct variant value"), - }, - ignored_struct: SomeIgnoredStruct { ignored: 123 }, - ignored_tuple_struct: SomeIgnoredTupleStruct(123), - ignored_struct_variant: SomeIgnoredEnum::Struct { - foo: String::from("Struct Variant"), - }, - ignored_tuple_variant: SomeIgnoredEnum::Tuple(1.23, 3.45), - custom_serialize: CustomSerialize { - value: 100, - inner_struct: SomeSerializableStruct { foo: 101 }, - }, - }; - + let input = get_my_struct(); let registry = get_registry(); let serializer = ReflectSerializer::new(&input, ®istry); From 325f0fd98284e88f73e29ebcbb09a041dad04aeb Mon Sep 17 00:00:00 2001 From: Matty Date: Thu, 14 Mar 2024 10:55:55 -0400 Subject: [PATCH 09/71] Alignment API for Transforms (#12187) # Objective - Closes #11793 - Introduces a general API for aligning local coordinates of Transforms with given vectors. ## Solution - We introduce `Transform::align`, which allows a rotation to be specified by four pieces of alignment data, as explained by the documentation: ````rust /// Rotates this [`Transform`] so that the `main_axis` vector, reinterpreted in local coordinates, points /// in the given `main_direction`, while `secondary_axis` points towards `secondary_direction`. /// /// For example, if a spaceship model has its nose pointing in the X-direction in its own local coordinates /// and its dorsal fin pointing in the Y-direction, then `align(Vec3::X, v, Vec3::Y, w)` will make the spaceship's /// nose point in the direction of `v`, while the dorsal fin does its best to point in the direction `w`. /// /// More precisely, the [`Transform::rotation`] produced will be such that: /// * applying it to `main_axis` results in `main_direction` /// * applying it to `secondary_axis` produces a vector that lies in the half-plane generated by `main_direction` and /// `secondary_direction` (with positive contribution by `secondary_direction`) /// /// [`Transform::look_to`] is recovered, for instance, when `main_axis` is `Vec3::NEG_Z` (the [`Transform::forward`] /// direction in the default orientation) and `secondary_axis` is `Vec3::Y` (the [`Transform::up`] direction in the default /// orientation). (Failure cases may differ somewhat.) /// /// In some cases a rotation cannot be constructed. Another axis will be picked in those cases: /// * if `main_axis` or `main_direction` is zero, `Vec3::X` takes its place /// * if `secondary_axis` or `secondary_direction` is zero, `Vec3::Y` takes its place /// * if `main_axis` is parallel with `secondary_axis` or `main_direction` is parallel with `secondary_direction`, /// a rotation is constructed which takes `main_axis` to `main_direction` along a great circle, ignoring the secondary /// counterparts /// /// Example /// ``` /// # use bevy_math::{Vec3, Quat}; /// # use bevy_transform::components::Transform; /// let mut t1 = Transform::IDENTITY; /// let mut t2 = Transform::IDENTITY; /// t1.align(Vec3::ZERO, Vec3::Z, Vec3::ZERO, Vec3::X); /// t2.align(Vec3::X, Vec3::Z, Vec3::Y, Vec3::X); /// assert_eq!(t1.rotation, t2.rotation); /// /// t1.align(Vec3::X, Vec3::Z, Vec3::X, Vec3::Y); /// assert_eq!(t1.rotation, Quat::from_rotation_arc(Vec3::X, Vec3::Z)); /// ``` pub fn align( &mut self, main_axis: Vec3, main_direction: Vec3, secondary_axis: Vec3, secondary_direction: Vec3, ) { //... } ```` - We introduce `Transform::aligned_by`, the returning-Self version of `align`: ````rust pub fn aligned_by( mut self, main_axis: Vec3, main_direction: Vec3, secondary_axis: Vec3, secondary_direction: Vec3, ) -> Self { //... } ```` - We introduce an example (examples/transforms/align.rs) that shows the usage of this API. It is likely to be mathier than most other `Transform` APIs, so when run, the example demonstrates what the API does in space: Screenshot 2024-03-12 at 11 01 19 AM --- ## Changelog - Added methods `align`, `aligned_by` to `Transform`. - Added transforms/align.rs to examples. --- ## Discussion ### On the form of `align` The original issue linked above suggests an API similar to that of the existing `Transform::look_to` method: ````rust pub fn align_to(&mut self, direction: Vec3, up: Vec3) { //... } ```` Not allowing an input axis of some sort that is to be aligned with `direction` would not really solve the problem in the issue, since the user could easily be in a scenario where they have to compose with another rotation on their own (undesirable). This leads to something like: ````rust pub fn align_to(&mut self, axis: Vec3, direction: Vec3, up: Vec3) { //... } ```` However, this still has two problems: - If the vector that the user wants to align is parallel to the Y-axis, then the API basically does not work (we cannot fully specify a rotation) - More generally, it does not give the user the freedom to specify which direction is to be treated as the local "up" direction, so it fails as a general alignment API Specifying both leads us to the present situation, with two local axis inputs (`main_axis` and `secondary_axis`) and two target directions (`main_direction` and `secondary_direction`). This might seem a little cumbersome for general use, but for the time being I stand by the decision not to expand further without prompting from users. I'll expand on this below. ### Additional APIs? Presently, this PR introduces only `align` and `aligned_by`. Other potentially useful bundles of API surface arrange into a few different categories: 1. Inferring direction from position, a la `Transform::look_at`, which might look something like this: ````rust pub fn align_at(&mut self, axis: Vec3, target: Vec3, up: Vec3) { self.align(axis, target - self.translation, Vec3::Y, up); } ```` (This is simple but still runs into issues when the user wants to point the local Y-axis somewhere.) 2. Filling in some data for the user for common use-cases; e.g.: ````rust pub fn align_x(&mut self, direction: Vec3, up: Vec3) { self.align(Vec3::X, direction, Vec3::Y, up); } ```` (Here, use of the `up` vector doesn't lose any generality, but it might be less convenient to specify than something else. This does naturally leave open the question of what `align_y` would look like if we provided it.) Morally speaking, I do think that the `up` business is more pertinent when the intention is to work with cameras, which the `look_at` and `look_to` APIs seem to cover pretty well. If that's the case, then I'm not sure what the ideal shape for these API functions would be, since it seems like a lot of input would have to be baked into the function definitions. For some cases, this might not be the end of the world: ````rust pub fn align_x_z(&mut self, direction: Vec3, weak_direction: Vec3) { self.align(Vec3::X, direction, Vec3::Z, weak_direction); } ```` (However, this is not symmetrical in x and z, so you'd still need six API functions just to support the standard positive coordinate axes, and if you support negative axes then things really start to balloon.) The reasons that these are not actually produced in this PR are as follows: 1. Without prompting from actual users in the wild, it is unknown to me whether these additional APIs would actually see a lot of use. Extending these to our users in the future would be trivial if we see there is a demand for something specific from the above-mentioned categories. 2. As discussed above, there are so many permutations of these that could be provided that trying to do so looks like it risks unduly ballooning the API surface for this feature. 3. Finally, and most importantly, creating these helper functions in user-space is trivial, since they all just involve specializing `align` to particular inputs; e.g.: ````rust fn align_ship(ship_transform: &mut Transform, nose_direction: Vec3, dorsal_direction: Vec3) { ship_transform.align(Ship::NOSE, nose_direction, Ship::DORSAL, dorsal_direction); } ```` With that in mind, I would prefer instead to focus on making the documentation and examples for a thin API as clear as possible, so that users can get a grip on the tool and specialize it for their own needs when they feel the desire to do so. ### `Dir3`? As in the case of `Transform::look_to` and `Transform::look_at`, the inputs to this function are, morally speaking, *directions* rather than vectors (actually, if we're being pedantic, the input is *really really* a pair of orthonormal frames), so it's worth asking whether we should really be using `Dir3` as inputs instead of `Vec3`. I opted for `Vec3` for the following reasons: 1. Specifying a `Dir3` in user-space is just more annoying than providing a `Vec3`. Even in the most basic cases (e.g. providing a vector literal), you still have to do error handling or call an unsafe unwrap in your function invocations. 2. The existing API mentioned above uses `Vec3`, so we are just adhering to the same thing. Of course, the use of `Vec3` has its own downsides; it can be argued that the replacement of zero-vectors with fixed ones (which we do in `Transform::align` as well as `Transform::look_to`) more-or-less amounts to failing silently. ### Future steps The question of additional APIs was addressed above. For me, the main thing here to handle more immediately is actually just upstreaming this API (or something similar and slightly mathier) to `glam::Quat`. The reason that this would be desirable for users is that this API currently only works with `Transform`s even though all it's actually doing is specifying a rotation. Upstreaming to `glam::Quat`, properly done, could buy a lot basically for free, since a number of `Transform` methods take a rotation as an input. Using these together would require a little bit of mathematical savvy, but it opens up some good things (e.g. `Transform::rotate_around`). --- Cargo.toml | 11 + .../src/components/transform.rs | 114 ++++++++ examples/README.md | 1 + examples/transforms/align.rs | 260 ++++++++++++++++++ 4 files changed, 386 insertions(+) create mode 100644 examples/transforms/align.rs diff --git a/Cargo.toml b/Cargo.toml index ab9da009fd4d5..9c43da5a15bd1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2284,6 +2284,17 @@ description = "Illustrates how to (constantly) rotate an object around an axis" category = "Transforms" wasm = true +[[example]] +name = "align" +path = "examples/transforms/align.rs" +doc-scrape-examples = true + +[package.metadata.example.align] +name = "Alignment" +description = "A demonstration of Transform's axis-alignment feature" +category = "Transforms" +wasm = true + [[example]] name = "scale" path = "examples/transforms/scale.rs" diff --git a/crates/bevy_transform/src/components/transform.rs b/crates/bevy_transform/src/components/transform.rs index ed8b004074f9a..ae58c50d850fb 100644 --- a/crates/bevy_transform/src/components/transform.rs +++ b/crates/bevy_transform/src/components/transform.rs @@ -145,6 +145,39 @@ impl Transform { self } + /// Returns this [`Transform`] with a rotation so that the `handle` vector, reinterpreted in local coordinates, + /// points in the given `direction`, while `weak_handle` points towards `weak_direction`. + /// + /// For example, if a spaceship model has its nose pointing in the X-direction in its own local coordinates + /// and its dorsal fin pointing in the Y-direction, then `Transform::aligned_by(Vec3::X, v, Vec3::Y, w)` will + /// make the spaceship's nose point in the direction of `v`, while the dorsal fin does its best to point in the + /// direction `w`. + /// + /// In some cases a rotation cannot be constructed. Another axis will be picked in those cases: + /// * if `handle` or `direction` is zero, `Vec3::X` takes its place + /// * if `weak_handle` or `weak_direction` is zero, `Vec3::Y` takes its place + /// * if `handle` is parallel with `weak_handle` or `direction` is parallel with `weak_direction`, a rotation is + /// constructed which takes `handle` to `direction` but ignores the weak counterparts (i.e. is otherwise unspecified) + /// + /// See [`Transform::align`] for additional details. + #[inline] + #[must_use] + pub fn aligned_by( + mut self, + main_axis: Vec3, + main_direction: Vec3, + secondary_axis: Vec3, + secondary_direction: Vec3, + ) -> Self { + self.align( + main_axis, + main_direction, + secondary_axis, + secondary_direction, + ); + self + } + /// Returns this [`Transform`] with a new translation. #[inline] #[must_use] @@ -366,6 +399,87 @@ impl Transform { self.rotation = Quat::from_mat3(&Mat3::from_cols(right, up, back)); } + /// Rotates this [`Transform`] so that the `main_axis` vector, reinterpreted in local coordinates, points + /// in the given `main_direction`, while `secondary_axis` points towards `secondary_direction`. + /// + /// For example, if a spaceship model has its nose pointing in the X-direction in its own local coordinates + /// and its dorsal fin pointing in the Y-direction, then `align(Vec3::X, v, Vec3::Y, w)` will make the spaceship's + /// nose point in the direction of `v`, while the dorsal fin does its best to point in the direction `w`. + /// + /// More precisely, the [`Transform::rotation`] produced will be such that: + /// * applying it to `main_axis` results in `main_direction` + /// * applying it to `secondary_axis` produces a vector that lies in the half-plane generated by `main_direction` and + /// `secondary_direction` (with positive contribution by `secondary_direction`) + /// + /// [`Transform::look_to`] is recovered, for instance, when `main_axis` is `Vec3::NEG_Z` (the [`Transform::forward`] + /// direction in the default orientation) and `secondary_axis` is `Vec3::Y` (the [`Transform::up`] direction in the default + /// orientation). (Failure cases may differ somewhat.) + /// + /// In some cases a rotation cannot be constructed. Another axis will be picked in those cases: + /// * if `main_axis` or `main_direction` is zero, `Vec3::X` takes its place + /// * if `secondary_axis` or `secondary_direction` is zero, `Vec3::Y` takes its place + /// * if `main_axis` is parallel with `secondary_axis` or `main_direction` is parallel with `secondary_direction`, + /// a rotation is constructed which takes `main_axis` to `main_direction` along a great circle, ignoring the secondary + /// counterparts + /// + /// Example + /// ``` + /// # use bevy_math::{Vec3, Quat}; + /// # use bevy_transform::components::Transform; + /// # let mut t1 = Transform::IDENTITY; + /// # let mut t2 = Transform::IDENTITY; + /// t1.align(Vec3::X, Vec3::Y, Vec3::new(1., 1., 0.), Vec3::Z); + /// let main_axis_image = t1.rotation * Vec3::X; + /// let secondary_axis_image = t1.rotation * Vec3::new(1., 1., 0.); + /// assert!(main_axis_image.abs_diff_eq(Vec3::Y, 1e-5)); + /// assert!(secondary_axis_image.abs_diff_eq(Vec3::new(0., 1., 1.), 1e-5)); + /// + /// t1.align(Vec3::ZERO, Vec3::Z, Vec3::ZERO, Vec3::X); + /// t2.align(Vec3::X, Vec3::Z, Vec3::Y, Vec3::X); + /// assert_eq!(t1.rotation, t2.rotation); + /// + /// t1.align(Vec3::X, Vec3::Z, Vec3::X, Vec3::Y); + /// assert_eq!(t1.rotation, Quat::from_rotation_arc(Vec3::X, Vec3::Z)); + /// ``` + #[inline] + pub fn align( + &mut self, + main_axis: Vec3, + main_direction: Vec3, + secondary_axis: Vec3, + secondary_direction: Vec3, + ) { + let main_axis = main_axis.try_normalize().unwrap_or(Vec3::X); + let main_direction = main_direction.try_normalize().unwrap_or(Vec3::X); + let secondary_axis = secondary_axis.try_normalize().unwrap_or(Vec3::Y); + let secondary_direction = secondary_direction.try_normalize().unwrap_or(Vec3::Y); + + // The solution quaternion will be constructed in two steps. + // First, we start with a rotation that takes `main_axis` to `main_direction`. + let first_rotation = Quat::from_rotation_arc(main_axis, main_direction); + + // Let's follow by rotating about the `main_direction` axis so that the image of `secondary_axis` + // is taken to something that lies in the plane of `main_direction` and `secondary_direction`. Since + // `main_direction` is fixed by this rotation, the first criterion is still satisfied. + let secondary_image = first_rotation * secondary_axis; + let secondary_image_ortho = secondary_image + .reject_from_normalized(main_direction) + .try_normalize(); + let secondary_direction_ortho = secondary_direction + .reject_from_normalized(main_direction) + .try_normalize(); + + // If one of the two weak vectors was parallel to `main_direction`, then we just do the first part + self.rotation = match (secondary_image_ortho, secondary_direction_ortho) { + (Some(secondary_img_ortho), Some(secondary_dir_ortho)) => { + let second_rotation = + Quat::from_rotation_arc(secondary_img_ortho, secondary_dir_ortho); + second_rotation * first_rotation + } + _ => first_rotation, + }; + } + /// Multiplies `self` with `transform` component by component, returning the /// resulting [`Transform`] #[inline] diff --git a/examples/README.md b/examples/README.md index 47c59cb9069d0..74795cbaeffe0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -388,6 +388,7 @@ Example | Description Example | Description --- | --- [3D Rotation](../examples/transforms/3d_rotation.rs) | Illustrates how to (constantly) rotate an object around an axis +[Alignment](../examples/transforms/align.rs) | A demonstration of Transform's axis-alignment feature [Scale](../examples/transforms/scale.rs) | Illustrates how to scale an object in each direction [Transform](../examples/transforms/transform.rs) | Shows multiple transformations of objects [Translation](../examples/transforms/translation.rs) | Illustrates how to move an object along an axis diff --git a/examples/transforms/align.rs b/examples/transforms/align.rs new file mode 100644 index 0000000000000..1879868da0d6e --- /dev/null +++ b/examples/transforms/align.rs @@ -0,0 +1,260 @@ +//! This example shows how to align the orientations of objects in 3D space along two axes using the `Transform::align` API. + +use bevy::color::{ + palettes::basic::{GRAY, RED, WHITE}, + Color, +}; +use bevy::input::mouse::{MouseButton, MouseButtonInput, MouseMotion}; +use bevy::prelude::*; +use rand::random; +use std::f32::consts::PI; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .add_systems(Update, (draw_cube_axes, draw_random_axes)) + .add_systems(Update, (handle_keypress, handle_mouse, rotate_cube).chain()) + .run(); +} + +/// This struct stores metadata for a single rotational move of the cube +#[derive(Component, Default)] +struct Cube { + /// The initial transform of the cube move, the starting point of interpolation + initial_transform: Transform, + + /// The target transform of the cube move, the endpoint of interpolation + target_transform: Transform, + + /// The progress of the cube move in percentage points + progress: u16, + + /// Whether the cube is currently in motion; allows motion to be paused + in_motion: bool, +} + +#[derive(Component)] +struct RandomAxes(Vec3, Vec3); + +#[derive(Component)] +struct Instructions; + +#[derive(Resource)] +struct MousePressed(bool); + +// Setup + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, +) { + // A camera looking at the origin + commands.spawn(Camera3dBundle { + transform: Transform::from_xyz(3., 2.5, 4.).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }); + + // A plane that we can sit on top of + commands.spawn(PbrBundle { + transform: Transform::from_xyz(0., -2., 0.), + mesh: meshes.add(Plane3d::default().mesh().size(100.0, 100.0)), + material: materials.add(Color::srgb(0.3, 0.5, 0.3)), + ..default() + }); + + // A light source + commands.spawn(PointLightBundle { + point_light: PointLight { + shadows_enabled: true, + ..default() + }, + transform: Transform::from_xyz(4.0, 7.0, -4.0), + ..default() + }); + + // Initialize random axes + let first = random_direction(); + let second = random_direction(); + commands.spawn(RandomAxes(first, second)); + + // Finally, our cube that is going to rotate + commands.spawn(( + PbrBundle { + mesh: meshes.add(Cuboid::new(1.0, 1.0, 1.0)), + material: materials.add(Color::srgb(0.5, 0.5, 0.5)), + ..default() + }, + Cube { + initial_transform: Transform::IDENTITY, + target_transform: random_axes_target_alignment(&RandomAxes(first, second)), + ..default() + }, + )); + + // Instructions for the example + commands.spawn(( + TextBundle::from_section( + "The bright red axis is the primary alignment axis, and it will always be\n\ + made to coincide with the primary target direction (white) exactly.\n\ + The fainter red axis is the secondary alignment axis, and it is made to\n\ + line up with the secondary target direction (gray) as closely as possible.\n\ + Press 'R' to generate random target directions.\n\ + Press 'T' to align the cube to those directions.\n\ + Click and drag the mouse to rotate the camera.\n\ + Press 'H' to hide/show these instructions.", + TextStyle { + font_size: 20., + ..default() + }, + ) + .with_style(Style { + position_type: PositionType::Absolute, + top: Val::Px(12.0), + left: Val::Px(12.0), + ..default() + }), + Instructions, + )); + + commands.insert_resource(MousePressed(false)); +} + +// Update systems + +// Draw the main and secondary axes on the rotating cube +fn draw_cube_axes(mut gizmos: Gizmos, query: Query<&Transform, With>) { + let cube_transform = query.single(); + + // Local X-axis arrow + let x_ends = arrow_ends(cube_transform, Vec3::X, 1.5); + gizmos.arrow(x_ends.0, x_ends.1, RED); + + // local Y-axis arrow + let y_ends = arrow_ends(cube_transform, Vec3::Y, 1.5); + gizmos.arrow(y_ends.0, y_ends.1, Color::srgb(0.65, 0., 0.)); +} + +// Draw the randomly generated axes +fn draw_random_axes(mut gizmos: Gizmos, query: Query<&RandomAxes>) { + let RandomAxes(v1, v2) = query.single(); + gizmos.arrow(Vec3::ZERO, 1.5 * *v1, WHITE); + gizmos.arrow(Vec3::ZERO, 1.5 * *v2, GRAY); +} + +// Actually update the cube's transform according to its initial source and target +fn rotate_cube(mut cube: Query<(&mut Cube, &mut Transform)>) { + let (mut cube, mut cube_transform) = cube.single_mut(); + + if !cube.in_motion { + return; + } + + let start = cube.initial_transform.rotation; + let end = cube.target_transform.rotation; + + let p: f32 = cube.progress.into(); + let t = p / 100.; + + *cube_transform = Transform::from_rotation(start.slerp(end, t)); + + if cube.progress == 100 { + cube.in_motion = false; + } else { + cube.progress += 1; + } +} + +// Handle user inputs from the keyboard for dynamically altering the scenario +fn handle_keypress( + mut cube: Query<(&mut Cube, &Transform)>, + mut random_axes: Query<&mut RandomAxes>, + mut instructions: Query<&mut Visibility, With>, + keyboard: Res>, +) { + let (mut cube, cube_transform) = cube.single_mut(); + let mut random_axes = random_axes.single_mut(); + + if keyboard.just_pressed(KeyCode::KeyR) { + // Randomize the target axes + let first = random_direction(); + let second = random_direction(); + *random_axes = RandomAxes(first, second); + + // Stop the cube and set it up to transform from its present orientation to the new one + cube.in_motion = false; + cube.initial_transform = *cube_transform; + cube.target_transform = random_axes_target_alignment(&random_axes); + cube.progress = 0; + } + + if keyboard.just_pressed(KeyCode::KeyT) { + cube.in_motion ^= true; + } + + if keyboard.just_pressed(KeyCode::KeyH) { + let mut instructions_viz = instructions.single_mut(); + if *instructions_viz == Visibility::Hidden { + *instructions_viz = Visibility::Visible; + } else { + *instructions_viz = Visibility::Hidden; + } + } +} + +// Handle user mouse input for panning the camera around +fn handle_mouse( + mut button_events: EventReader, + mut motion_events: EventReader, + mut camera: Query<&mut Transform, With>, + mut mouse_pressed: ResMut, +) { + // Store left-pressed state in the MousePressed resource + for button_event in button_events.read() { + if button_event.button != MouseButton::Left { + continue; + } + *mouse_pressed = MousePressed(button_event.state.is_pressed()); + } + + // If the mouse is not pressed, just ignore motion events + if !mouse_pressed.0 { + return; + } + let displacement = motion_events + .read() + .fold(0., |acc, mouse_motion| acc + mouse_motion.delta.x); + let mut camera_transform = camera.single_mut(); + camera_transform.rotate_around(Vec3::ZERO, Quat::from_rotation_y(-displacement / 75.)); +} + +// Helper functions (i.e. non-system functions) + +fn arrow_ends(transform: &Transform, axis: Vec3, length: f32) -> (Vec3, Vec3) { + let local_vector = length * (transform.rotation * axis); + (transform.translation, transform.translation + local_vector) +} + +fn random_direction() -> Vec3 { + let height = random::() * 2. - 1.; + let theta = random::() * 2. * PI; + + build_direction(height, theta) +} + +fn build_direction(height: f32, theta: f32) -> Vec3 { + let z = height; + let m = f32::acos(z).sin(); + let x = theta.cos() * m; + let y = theta.sin() * m; + + Vec3::new(x, y, z) +} + +// This is where `Transform::align` is actually used! +// Note that the choice of `Vec3::X` and `Vec3::Y` here matches the use of those in `draw_cube_axes`. +fn random_axes_target_alignment(random_axes: &RandomAxes) -> Transform { + let RandomAxes(first, second) = random_axes; + Transform::IDENTITY.aligned_by(Vec3::X, *first, Vec3::Y, *second) +} From 7d816aab048098c686e427d68e524b597ae62c29 Mon Sep 17 00:00:00 2001 From: Zeenobit Date: Thu, 14 Mar 2024 10:57:22 -0400 Subject: [PATCH 10/71] Fix inconsistency between Debug and serialized representation of Entity (#12469) # Objective Fixes #12139 ## Solution - Derive `Debug` impl for `Entity` - Add impl `Display` for `Entity` - Add `entity_display` test to check the output contains all required info I decided to go with `0v0|1234` format as opposed to the `0v0[1234]` which was initially discussed in the issue. My rationale for this is that `[1234]` may be confused for index values, which may be common in logs, and so searching for entities by text would become harder. I figured `|1234` would help the entity IDs stand out more. Additionally, I'm a little concerned that this change is gonna break existing logging for projects because `Debug` is now going to be a multi-line output. But maybe this is ok. We could implement `Debug` to be a single-line output, but then I don't see why it would be different from `Display` at all. @alice-i-cecile Let me know if we'd like to make any changes based on these points. --- crates/bevy_ecs/src/entity/mod.rs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ecs/src/entity/mod.rs b/crates/bevy_ecs/src/entity/mod.rs index 9dbc26ac1a8cc..3013b9bb8a292 100644 --- a/crates/bevy_ecs/src/entity/mod.rs +++ b/crates/bevy_ecs/src/entity/mod.rs @@ -139,7 +139,7 @@ type IdCursor = isize; /// [`Query::get`]: crate::system::Query::get /// [`World`]: crate::world::World /// [SemVer]: https://semver.org/ -#[derive(Clone, Copy)] +#[derive(Clone, Copy, Debug)] #[cfg_attr(feature = "bevy_reflect", derive(Reflect))] #[cfg_attr( feature = "bevy_reflect", @@ -384,9 +384,15 @@ impl<'de> Deserialize<'de> for Entity { } } -impl fmt::Debug for Entity { +impl fmt::Display for Entity { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}v{}", self.index(), self.generation()) + write!( + f, + "{}v{}|{}", + self.index(), + self.generation(), + self.to_bits() + ) } } @@ -1147,4 +1153,14 @@ mod tests { assert_ne!(hash, first_hash); } } + + #[test] + fn entity_display() { + let entity = Entity::from_raw(42); + let string = format!("{}", entity); + let bits = entity.to_bits().to_string(); + assert!(string.contains("42")); + assert!(string.contains("v1")); + assert!(string.contains(&bits)); + } } From d3d9cab30c17a8acab08873bbf708e3c2e078748 Mon Sep 17 00:00:00 2001 From: Tolki Date: Fri, 15 Mar 2024 02:32:05 +0900 Subject: [PATCH 11/71] Breakout refactor (#12477) # Objective - Improve the code quality of the breakout example - As a newcomer to `bevy` I was pointed to the breakout example after the "Getting Started" tutorial - I'm making this PR because it had a few wrong comments + some inconsistency in used patterns ## Solution - Remove references to `wall` in all the collision code as it also handles bricks and the paddle - Use the newtype pattern with `bevy::prelude::Deref` for resources - It was already used for `Velocity` before this PR - `Scoreboard` is a resource only containing `score`, so it's simpler as a newtype `Score` resource - `CollisionSound` is already a newtype, so might as well unify the access pattern for it - Added docstrings for `WallLocation::position` and `WallLocation::size` to explain what they represent --- examples/games/breakout.rs | 51 +++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/examples/games/breakout.rs b/examples/games/breakout.rs index 3cc19c5a7fda5..5b62d81a9252f 100644 --- a/examples/games/breakout.rs +++ b/examples/games/breakout.rs @@ -58,7 +58,7 @@ fn main() { .add_schedule(FixedUpdate) .at(Val::Percent(35.0), Val::Percent(50.0)), ) - .insert_resource(Scoreboard { score: 0 }) + .insert_resource(Score(0)) .insert_resource(ClearColor(BACKGROUND_COLOR)) .add_event::() .add_systems(Startup, setup) @@ -97,7 +97,7 @@ struct CollisionEvent; #[derive(Component)] struct Brick; -#[derive(Resource)] +#[derive(Resource, Deref)] struct CollisionSound(Handle); // This bundle is a collection of the components that define a "wall" in our game @@ -118,6 +118,7 @@ enum WallLocation { } impl WallLocation { + /// Location of the *center* of the wall, used in `transform.translation()` fn position(&self) -> Vec2 { match self { WallLocation::Left => Vec2::new(LEFT_WALL, 0.), @@ -127,6 +128,7 @@ impl WallLocation { } } + /// (x, y) dimensions of the wall, used in `transform.scale()` fn size(&self) -> Vec2 { let arena_height = TOP_WALL - BOTTOM_WALL; let arena_width = RIGHT_WALL - LEFT_WALL; @@ -173,10 +175,8 @@ impl WallBundle { } // This resource tracks the game's score -#[derive(Resource)] -struct Scoreboard { - score: usize, -} +#[derive(Resource, Deref, DerefMut)] +struct Score(usize); #[derive(Component)] struct ScoreboardUi; @@ -350,27 +350,26 @@ fn apply_velocity(mut query: Query<(&mut Transform, &Velocity)>, time: Res