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]
|
[package.metadata.example.2d_viewport_to_world]
|
||||||
name = "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"
|
category = "2D Rendering"
|
||||||
wasm = true
|
wasm = true
|
||||||
|
|
||||||
|
@ -472,10 +472,10 @@ impl Camera {
|
|||||||
camera_transform: &GlobalTransform,
|
camera_transform: &GlobalTransform,
|
||||||
world_position: Vec3,
|
world_position: Vec3,
|
||||||
) -> Result<Vec2, ViewportConversionError> {
|
) -> Result<Vec2, ViewportConversionError> {
|
||||||
let target_size = self
|
let target_rect = self
|
||||||
.logical_viewport_size()
|
.logical_viewport_rect()
|
||||||
.ok_or(ViewportConversionError::NoViewportSize)?;
|
.ok_or(ViewportConversionError::NoViewportSize)?;
|
||||||
let ndc_space_coords = self
|
let mut ndc_space_coords = self
|
||||||
.world_to_ndc(camera_transform, world_position)
|
.world_to_ndc(camera_transform, world_position)
|
||||||
.ok_or(ViewportConversionError::InvalidData)?;
|
.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
|
// 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);
|
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.
|
// 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)
|
Ok(viewport_position)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -508,10 +510,10 @@ impl Camera {
|
|||||||
camera_transform: &GlobalTransform,
|
camera_transform: &GlobalTransform,
|
||||||
world_position: Vec3,
|
world_position: Vec3,
|
||||||
) -> Result<Vec3, ViewportConversionError> {
|
) -> Result<Vec3, ViewportConversionError> {
|
||||||
let target_size = self
|
let target_rect = self
|
||||||
.logical_viewport_size()
|
.logical_viewport_rect()
|
||||||
.ok_or(ViewportConversionError::NoViewportSize)?;
|
.ok_or(ViewportConversionError::NoViewportSize)?;
|
||||||
let ndc_space_coords = self
|
let mut ndc_space_coords = self
|
||||||
.world_to_ndc(camera_transform, world_position)
|
.world_to_ndc(camera_transform, world_position)
|
||||||
.ok_or(ViewportConversionError::InvalidData)?;
|
.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
|
// 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.
|
// 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);
|
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.
|
// 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))
|
Ok(viewport_position.extend(depth))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,15 +552,16 @@ impl Camera {
|
|||||||
pub fn viewport_to_world(
|
pub fn viewport_to_world(
|
||||||
&self,
|
&self,
|
||||||
camera_transform: &GlobalTransform,
|
camera_transform: &GlobalTransform,
|
||||||
mut viewport_position: Vec2,
|
viewport_position: Vec2,
|
||||||
) -> Result<Ray3d, ViewportConversionError> {
|
) -> Result<Ray3d, ViewportConversionError> {
|
||||||
let target_size = self
|
let target_rect = self
|
||||||
.logical_viewport_size()
|
.logical_viewport_rect()
|
||||||
.ok_or(ViewportConversionError::NoViewportSize)?;
|
.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.
|
// Flip the Y co-ordinate origin from the top to the bottom.
|
||||||
viewport_position.y = target_size.y - viewport_position.y;
|
rect_relative.y = 1.0 - rect_relative.y;
|
||||||
let ndc = viewport_position * 2. / target_size - Vec2::ONE;
|
|
||||||
|
|
||||||
|
let ndc = rect_relative * 2. - Vec2::ONE;
|
||||||
let ndc_to_world =
|
let ndc_to_world =
|
||||||
camera_transform.compute_matrix() * self.computed.clip_from_view.inverse();
|
camera_transform.compute_matrix() * self.computed.clip_from_view.inverse();
|
||||||
let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.));
|
let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.));
|
||||||
@ -586,14 +591,17 @@ impl Camera {
|
|||||||
pub fn viewport_to_world_2d(
|
pub fn viewport_to_world_2d(
|
||||||
&self,
|
&self,
|
||||||
camera_transform: &GlobalTransform,
|
camera_transform: &GlobalTransform,
|
||||||
mut viewport_position: Vec2,
|
viewport_position: Vec2,
|
||||||
) -> Result<Vec2, ViewportConversionError> {
|
) -> Result<Vec2, ViewportConversionError> {
|
||||||
let target_size = self
|
let target_rect = self
|
||||||
.logical_viewport_size()
|
.logical_viewport_rect()
|
||||||
.ok_or(ViewportConversionError::NoViewportSize)?;
|
.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.
|
// Flip the Y co-ordinate origin from the top to the bottom.
|
||||||
viewport_position.y = target_size.y - viewport_position.y;
|
rect_relative.y = 1.0 - rect_relative.y;
|
||||||
let ndc = viewport_position * 2. / target_size - Vec2::ONE;
|
|
||||||
|
let ndc = rect_relative * 2. - Vec2::ONE;
|
||||||
|
|
||||||
let world_near_plane = self
|
let world_near_plane = self
|
||||||
.ndc_to_world(camera_transform, ndc.extend(1.))
|
.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() {
|
fn main() {
|
||||||
App::new()
|
App::new()
|
||||||
.add_plugins(DefaultPlugins)
|
.add_plugins(DefaultPlugins)
|
||||||
.add_systems(Startup, setup)
|
.add_systems(Startup, setup)
|
||||||
.add_systems(Update, draw_cursor)
|
.add_systems(FixedUpdate, controls)
|
||||||
|
.add_systems(
|
||||||
|
PostUpdate,
|
||||||
|
draw_cursor.after(TransformSystem::TransformPropagate),
|
||||||
|
)
|
||||||
.run();
|
.run();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -15,30 +27,147 @@ fn draw_cursor(
|
|||||||
window: Query<&Window>,
|
window: Query<&Window>,
|
||||||
mut gizmos: Gizmos,
|
mut gizmos: Gizmos,
|
||||||
) {
|
) {
|
||||||
|
let (camera, camera_transform) = *camera_query;
|
||||||
let Ok(window) = window.single() else {
|
let Ok(window) = window.single() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let (camera, camera_transform) = *camera_query;
|
|
||||||
|
|
||||||
let Some(cursor_position) = window.cursor_position() else {
|
let Some(cursor_position) = window.cursor_position() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Calculate a world position based on the cursor's position.
|
// 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;
|
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) {
|
fn controls(
|
||||||
commands.spawn(Camera2d);
|
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
|
// Create a minimal UI explaining how to interact with the example
|
||||||
commands.spawn((
|
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 {
|
Node {
|
||||||
position_type: PositionType::Absolute,
|
position_type: PositionType::Absolute,
|
||||||
top: Val::Px(12.0),
|
top: Val::Px(12.0),
|
||||||
@ -46,4 +175,17 @@ fn setup(mut commands: Commands) {
|
|||||||
..default()
|
..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 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 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 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
|
[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
|
[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
|
[CPU Drawing](../examples/2d/cpu_draw.rs) | Manually read/write the pixels of a texture
|
||||||
|
Loading…
Reference in New Issue
Block a user