Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Respect viewport position in coordinate conversion functions #17633

Merged
merged 3 commits into from
Mar 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
52 changes: 30 additions & 22 deletions crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}

Expand All @@ -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
Expand All @@ -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))
}

Expand All @@ -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.));
Expand Down Expand Up @@ -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.))
Expand Down
162 changes: 152 additions & 10 deletions examples/2d/2d_viewport_to_world.rs
Original file line number Diff line number Diff line change
@@ -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();
}

Expand All @@ -15,35 +27,165 @@ 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;
};

// 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 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();

gizmos.circle_2d(point, 10., WHITE);
// 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) {
commands.spawn(Camera2d);
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),
left: Val::Px(12.0),
..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)),
));
}
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down