Respect viewport position in coordinate conversion functions (#17633)
# Objective - In `Camera::viewport_to_world_2d`, `Camera::viewport_to_world`, `Camera::world_to_viewport` and `Camera::world_to_viewport_with_depth`, the results were incorrect when the `Camera::viewport` field was configured with a viewport position that was non-zero. This PR attempts to correct that. - Fixes #16200 ## Solution - This PR now takes the viewport position into account in the functions mentioned above. - Extended `2d_viewport_to_world` example to test the functions with a dynamic viewport position and size, camera positions and zoom levels. It is probably worth discussing whether to change the example, add a new one or just completely skip touching the examples. ## Testing Used the modified example to test the functions with dynamic camera transform as well as dynamic viewport size and position.
This commit is contained in:
parent
b574599444
commit
683b08fec9
@ -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
|
||||
|
||||
|
@ -472,10 +472,10 @@ impl Camera {
|
||||
camera_transform: &GlobalTransform,
|
||||
world_position: Vec3,
|
||||
) -> Result<Vec2, ViewportConversionError> {
|
||||
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<Vec3, ViewportConversionError> {
|
||||
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<Ray3d, ViewportConversionError> {
|
||||
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<Vec2, ViewportConversionError> {
|
||||
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.))
|
||||
|
@ -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<ButtonInput<KeyCode>>,
|
||||
time: Res<Time<Fixed>>,
|
||||
) {
|
||||
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<Assets<Mesh>>,
|
||||
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||
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)),
|
||||
));
|
||||
}
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user