diff --git a/Cargo.toml b/Cargo.toml index 697cf8dada..e9e593b607 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -605,7 +605,7 @@ doc-scrape-examples = true [package.metadata.example.2d_viewport_to_world] name = "2D Viewport To World" -description = "Demonstrates how to use the `Camera::viewport_to_world_2d` method" +description = "Demonstrates how to use the `Camera::viewport_to_world_2d` method with a dynamic viewport and camera." category = "2D Rendering" wasm = true diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 0c67f0ea04..a1f4918f74 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -472,10 +472,10 @@ impl Camera { camera_transform: &GlobalTransform, world_position: Vec3, ) -> Result { - let target_size = self - .logical_viewport_size() + let target_rect = self + .logical_viewport_rect() .ok_or(ViewportConversionError::NoViewportSize)?; - let ndc_space_coords = self + let mut ndc_space_coords = self .world_to_ndc(camera_transform, world_position) .ok_or(ViewportConversionError::InvalidData)?; // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space @@ -486,10 +486,12 @@ impl Camera { return Err(ViewportConversionError::PastFarPlane); } - // Once in NDC space, we can discard the z element and rescale x/y to fit the screen - let mut viewport_position = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size; // Flip the Y co-ordinate origin from the bottom to the top. - viewport_position.y = target_size.y - viewport_position.y; + ndc_space_coords.y = -ndc_space_coords.y; + + // Once in NDC space, we can discard the z element and map x/y to the viewport rect + let viewport_position = + (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_rect.size() + target_rect.min; Ok(viewport_position) } @@ -508,10 +510,10 @@ impl Camera { camera_transform: &GlobalTransform, world_position: Vec3, ) -> Result { - let target_size = self - .logical_viewport_size() + let target_rect = self + .logical_viewport_rect() .ok_or(ViewportConversionError::NoViewportSize)?; - let ndc_space_coords = self + let mut ndc_space_coords = self .world_to_ndc(camera_transform, world_position) .ok_or(ViewportConversionError::InvalidData)?; // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space @@ -525,10 +527,12 @@ impl Camera { // Stretching ndc depth to value via near plane and negating result to be in positive room again. let depth = -self.depth_ndc_to_view_z(ndc_space_coords.z); - // Once in NDC space, we can discard the z element and rescale x/y to fit the screen - let mut viewport_position = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size; // Flip the Y co-ordinate origin from the bottom to the top. - viewport_position.y = target_size.y - viewport_position.y; + ndc_space_coords.y = -ndc_space_coords.y; + + // Once in NDC space, we can discard the z element and map x/y to the viewport rect + let viewport_position = + (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_rect.size() + target_rect.min; Ok(viewport_position.extend(depth)) } @@ -548,15 +552,16 @@ impl Camera { pub fn viewport_to_world( &self, camera_transform: &GlobalTransform, - mut viewport_position: Vec2, + viewport_position: Vec2, ) -> Result { - let target_size = self - .logical_viewport_size() + let target_rect = self + .logical_viewport_rect() .ok_or(ViewportConversionError::NoViewportSize)?; + let mut rect_relative = (viewport_position - target_rect.min) / target_rect.size(); // Flip the Y co-ordinate origin from the top to the bottom. - viewport_position.y = target_size.y - viewport_position.y; - let ndc = viewport_position * 2. / target_size - Vec2::ONE; + rect_relative.y = 1.0 - rect_relative.y; + let ndc = rect_relative * 2. - Vec2::ONE; let ndc_to_world = camera_transform.compute_matrix() * self.computed.clip_from_view.inverse(); let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.)); @@ -586,14 +591,17 @@ impl Camera { pub fn viewport_to_world_2d( &self, camera_transform: &GlobalTransform, - mut viewport_position: Vec2, + viewport_position: Vec2, ) -> Result { - let target_size = self - .logical_viewport_size() + let target_rect = self + .logical_viewport_rect() .ok_or(ViewportConversionError::NoViewportSize)?; + let mut rect_relative = (viewport_position - target_rect.min) / target_rect.size(); + // Flip the Y co-ordinate origin from the top to the bottom. - viewport_position.y = target_size.y - viewport_position.y; - let ndc = viewport_position * 2. / target_size - Vec2::ONE; + rect_relative.y = 1.0 - rect_relative.y; + + let ndc = rect_relative * 2. - Vec2::ONE; let world_near_plane = self .ndc_to_world(camera_transform, ndc.extend(1.)) diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index 3169b93bba..9e58816cee 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -1,12 +1,24 @@ -//! This example demonstrates how to use the `Camera::viewport_to_world_2d` method. +//! This example demonstrates how to use the `Camera::viewport_to_world_2d` method with a dynamic viewport and camera. -use bevy::{color::palettes::basic::WHITE, prelude::*}; +use bevy::{ + color::palettes::{ + basic::WHITE, + css::{GREEN, RED}, + }, + math::ops::powf, + prelude::*, + render::camera::Viewport, +}; fn main() { App::new() .add_plugins(DefaultPlugins) .add_systems(Startup, setup) - .add_systems(Update, draw_cursor) + .add_systems(FixedUpdate, controls) + .add_systems( + PostUpdate, + draw_cursor.after(TransformSystem::TransformPropagate), + ) .run(); } @@ -15,30 +27,147 @@ fn draw_cursor( window: Query<&Window>, mut gizmos: Gizmos, ) { + let (camera, camera_transform) = *camera_query; let Ok(window) = window.single() else { return; }; - let (camera, camera_transform) = *camera_query; - let Some(cursor_position) = window.cursor_position() else { return; }; // Calculate a world position based on the cursor's position. - let Ok(point) = camera.viewport_to_world_2d(camera_transform, cursor_position) else { + let Ok(world_pos) = camera.viewport_to_world_2d(camera_transform, cursor_position) else { return; }; - gizmos.circle_2d(point, 10., WHITE); + // To test Camera::world_to_viewport, convert result back to viewport space and then back to world space. + let Ok(viewport_check) = camera.world_to_viewport(camera_transform, world_pos.extend(0.0)) + else { + return; + }; + let Ok(world_check) = camera.viewport_to_world_2d(camera_transform, viewport_check.xy()) else { + return; + }; + + gizmos.circle_2d(world_pos, 10., WHITE); + // Should be the same as world_pos + gizmos.circle_2d(world_check, 8., RED); } -fn setup(mut commands: Commands) { - commands.spawn(Camera2d); +fn controls( + mut camera_query: Query<(&mut Camera, &mut Transform, &mut Projection)>, + window: Query<&Window>, + input: Res>, + time: Res>, +) { + let Ok(window) = window.single() else { + return; + }; + let Ok((mut camera, mut transform, mut projection)) = camera_query.single_mut() else { + return; + }; + let fspeed = 600.0 * time.delta_secs(); + let uspeed = fspeed as u32; + let window_size = window.resolution.physical_size(); + + // Camera movement controls + if input.pressed(KeyCode::ArrowUp) { + transform.translation.y += fspeed; + } + if input.pressed(KeyCode::ArrowDown) { + transform.translation.y -= fspeed; + } + if input.pressed(KeyCode::ArrowLeft) { + transform.translation.x -= fspeed; + } + if input.pressed(KeyCode::ArrowRight) { + transform.translation.x += fspeed; + } + + // Camera zoom controls + if let Projection::Orthographic(projection2d) = &mut *projection { + if input.pressed(KeyCode::Comma) { + projection2d.scale *= powf(4.0f32, time.delta_secs()); + } + + if input.pressed(KeyCode::Period) { + projection2d.scale *= powf(0.25f32, time.delta_secs()); + } + } + + if let Some(viewport) = camera.viewport.as_mut() { + // Viewport movement controls + if input.pressed(KeyCode::KeyW) { + viewport.physical_position.y = viewport.physical_position.y.saturating_sub(uspeed); + } + if input.pressed(KeyCode::KeyS) { + viewport.physical_position.y += uspeed; + } + if input.pressed(KeyCode::KeyA) { + viewport.physical_position.x = viewport.physical_position.x.saturating_sub(uspeed); + } + if input.pressed(KeyCode::KeyD) { + viewport.physical_position.x += uspeed; + } + + // Bound viewport position so it doesn't go off-screen + viewport.physical_position = viewport + .physical_position + .min(window_size - viewport.physical_size); + + // Viewport size controls + if input.pressed(KeyCode::KeyI) { + viewport.physical_size.y = viewport.physical_size.y.saturating_sub(uspeed); + } + if input.pressed(KeyCode::KeyK) { + viewport.physical_size.y += uspeed; + } + if input.pressed(KeyCode::KeyJ) { + viewport.physical_size.x = viewport.physical_size.x.saturating_sub(uspeed); + } + if input.pressed(KeyCode::KeyL) { + viewport.physical_size.x += uspeed; + } + + // Bound viewport size so it doesn't go off-screen + viewport.physical_size = viewport + .physical_size + .min(window_size - viewport.physical_position) + .max(UVec2::new(20, 20)); + } +} + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + window: Single<&Window>, +) { + let window_size = window.resolution.physical_size().as_vec2(); + + // Initialize centered, non-window-filling viewport + commands.spawn(( + Camera2d, + Camera { + viewport: Some(Viewport { + physical_position: (window_size * 0.125).as_uvec2(), + physical_size: (window_size * 0.75).as_uvec2(), + ..default() + }), + ..default() + }, + )); // Create a minimal UI explaining how to interact with the example commands.spawn(( - Text::new("Move the mouse to see the circle follow your cursor."), + Text::new( + "Move the mouse to see the circle follow your cursor.\n\ + Use the arrow keys to move the camera.\n\ + Use the comma and period keys to zoom in and out.\n\ + Use the WASD keys to move the viewport.\n\ + Use the IJKL keys to resize the viewport.", + ), Node { position_type: PositionType::Absolute, top: Val::Px(12.0), @@ -46,4 +175,17 @@ fn setup(mut commands: Commands) { ..default() }, )); + + // Add mesh to make camera movement visible + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(40.0, 20.0))), + MeshMaterial2d(materials.add(Color::from(GREEN))), + )); + + // Add background to visualize viewport bounds + commands.spawn(( + Mesh2d(meshes.add(Rectangle::new(50000.0, 50000.0))), + MeshMaterial2d(materials.add(Color::linear_rgb(0.01, 0.01, 0.01))), + Transform::from_translation(Vec3::new(0.0, 0.0, -200.0)), + )); } diff --git a/examples/README.md b/examples/README.md index ba1850d231..1cf4d14070 100644 --- a/examples/README.md +++ b/examples/README.md @@ -105,7 +105,7 @@ Example | Description [2D Bloom](../examples/2d/bloom_2d.rs) | Illustrates bloom post-processing in 2d [2D Rotation](../examples/2d/rotation.rs) | Demonstrates rotating entities in 2D with quaternions [2D Shapes](../examples/2d/2d_shapes.rs) | Renders simple 2D primitive shapes like circles and polygons -[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method +[2D Viewport To World](../examples/2d/2d_viewport_to_world.rs) | Demonstrates how to use the `Camera::viewport_to_world_2d` method with a dynamic viewport and camera. [2D Wireframe](../examples/2d/wireframe_2d.rs) | Showcases wireframes for 2d meshes [Arc 2D Meshes](../examples/2d/mesh2d_arcs.rs) | Demonstrates UV-mapping of the circular segment and sector primitives [CPU Drawing](../examples/2d/cpu_draw.rs) | Manually read/write the pixels of a texture