Skip to content

Commit c620eb7

Browse files
chrisjuchemDasLixoualice-i-cecilebushrat011899
authored
Return Results from Camera's world/viewport conversion methods (#14989)
# Objective - Fixes #14593. ## Solution - Add `ViewportConversionError` and return it from viewport conversion methods on Camera. ## Testing - I successfully compiled and ran all changed examples. ## Migration Guide The following methods on `Camera` now return a `Result` instead of an `Option` so that they can provide more information about failures: - `world_to_viewport` - `world_to_viewport_with_depth` - `viewport_to_world` - `viewport_to_world_2d` Call `.ok()` on the `Result` to turn it back into an `Option`, or handle the `Result` directly. --------- Co-authored-by: Lixou <[email protected]> Co-authored-by: Alice Cecile <[email protected]> Co-authored-by: Zachary Harrold <[email protected]>
1 parent 4e9a62f commit c620eb7

File tree

11 files changed

+110
-57
lines changed

11 files changed

+110
-57
lines changed

crates/bevy_dev_tools/src/ui_debug_overlay/inset.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ impl<'w, 's> InsetGizmo<'w, 's> {
137137
let Ok(cam) = self.cam.get_single() else {
138138
return Vec2::ZERO;
139139
};
140-
if let Some(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) {
140+
if let Ok(new_position) = cam.world_to_viewport(&zero, position.extend(0.)) {
141141
position = new_position;
142142
};
143143
position.xy()

crates/bevy_picking/src/backend.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,6 @@ pub mod ray {
231231
let viewport_logical = camera.to_logical(viewport.physical_position)?;
232232
viewport_pos -= viewport_logical;
233233
}
234-
camera.viewport_to_world(camera_tfm, viewport_pos)
234+
camera.viewport_to_world(camera_tfm, viewport_pos).ok()
235235
}
236236
}

crates/bevy_render/src/camera/camera.rs

Lines changed: 93 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,34 @@ impl Default for PhysicalCameraParameters {
187187
}
188188
}
189189

190+
/// Error returned when a conversion between world-space and viewport-space coordinates fails.
191+
///
192+
/// See [`world_to_viewport`][Camera::world_to_viewport] and [`viewport_to_world`][Camera::viewport_to_world].
193+
#[derive(Debug, Eq, PartialEq, Copy, Clone)]
194+
pub enum ViewportConversionError {
195+
/// The pre-computed size of the viewport was not available.
196+
///
197+
/// This may be because the `Camera` was just created and [`camera_system`] has not been executed
198+
/// yet, or because the [`RenderTarget`] is misconfigured in one of the following ways:
199+
/// - it references the [`PrimaryWindow`](RenderTarget::Window) when there is none,
200+
/// - it references a [`Window`](RenderTarget::Window) entity that doesn't exist or doesn't actually have a `Window` component,
201+
/// - it references an [`Image`](RenderTarget::Image) that doesn't exist (invalid handle),
202+
/// - it references a [`TextureView`](RenderTarget::TextureView) that doesn't exist (invalid handle).
203+
NoViewportSize,
204+
/// The computed coordinate was beyond the `Camera`'s near plane.
205+
///
206+
/// Only applicable when converting from world-space to viewport-space.
207+
PastNearPlane,
208+
/// The computed coordinate was beyond the `Camera`'s far plane.
209+
///
210+
/// Only applicable when converting from world-space to viewport-space.
211+
PastFarPlane,
212+
/// The Normalized Device Coordinates could not be computed because the `camera_transform`, the
213+
/// `world_position`, or the projection matrix defined by [`CameraProjection`] contained `NAN`
214+
/// (see [`world_to_ndc`][Camera::world_to_ndc] and [`ndc_to_world`][Camera::ndc_to_world]).
215+
InvalidData,
216+
}
217+
190218
/// The defining [`Component`] for camera entities,
191219
/// storing information about how and what to render through this camera.
192220
///
@@ -348,52 +376,64 @@ impl Camera {
348376
/// To get the coordinates in Normalized Device Coordinates, you should use
349377
/// [`world_to_ndc`](Self::world_to_ndc).
350378
///
351-
/// Returns `None` if any of these conditions occur:
352-
/// - The computed coordinates are beyond the near or far plane
353-
/// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size)
354-
/// - The world coordinates cannot be mapped to the Normalized Device Coordinates. See [`world_to_ndc`](Camera::world_to_ndc)
355-
/// May also panic if `glam_assert` is enabled. See [`world_to_ndc`](Camera::world_to_ndc).
379+
/// # Panics
380+
///
381+
/// Will panic if `glam_assert` is enabled and the `camera_transform` contains `NAN`
382+
/// (see [`world_to_ndc`][Self::world_to_ndc]).
356383
#[doc(alias = "world_to_screen")]
357384
pub fn world_to_viewport(
358385
&self,
359386
camera_transform: &GlobalTransform,
360387
world_position: Vec3,
361-
) -> Option<Vec2> {
362-
let target_size = self.logical_viewport_size()?;
363-
let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?;
388+
) -> Result<Vec2, ViewportConversionError> {
389+
let target_size = self
390+
.logical_viewport_size()
391+
.ok_or(ViewportConversionError::NoViewportSize)?;
392+
let ndc_space_coords = self
393+
.world_to_ndc(camera_transform, world_position)
394+
.ok_or(ViewportConversionError::InvalidData)?;
364395
// NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space
365-
if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 {
366-
return None;
396+
if ndc_space_coords.z < 0.0 {
397+
return Err(ViewportConversionError::PastNearPlane);
398+
}
399+
if ndc_space_coords.z > 1.0 {
400+
return Err(ViewportConversionError::PastFarPlane);
367401
}
368402

369403
// Once in NDC space, we can discard the z element and rescale x/y to fit the screen
370404
let mut viewport_position = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size;
371405
// Flip the Y co-ordinate origin from the bottom to the top.
372406
viewport_position.y = target_size.y - viewport_position.y;
373-
Some(viewport_position)
407+
Ok(viewport_position)
374408
}
375409

376410
/// Given a position in world space, use the camera to compute the viewport-space coordinates and depth.
377411
///
378412
/// To get the coordinates in Normalized Device Coordinates, you should use
379413
/// [`world_to_ndc`](Self::world_to_ndc).
380414
///
381-
/// Returns `None` if any of these conditions occur:
382-
/// - The computed coordinates are beyond the near or far plane
383-
/// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size)
384-
/// - The world coordinates cannot be mapped to the Normalized Device Coordinates. See [`world_to_ndc`](Camera::world_to_ndc)
385-
/// May also panic if `glam_assert` is enabled. See [`world_to_ndc`](Camera::world_to_ndc).
415+
/// # Panics
416+
///
417+
/// Will panic if `glam_assert` is enabled and the `camera_transform` contains `NAN`
418+
/// (see [`world_to_ndc`][Self::world_to_ndc]).
386419
#[doc(alias = "world_to_screen_with_depth")]
387420
pub fn world_to_viewport_with_depth(
388421
&self,
389422
camera_transform: &GlobalTransform,
390423
world_position: Vec3,
391-
) -> Option<Vec3> {
392-
let target_size = self.logical_viewport_size()?;
393-
let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?;
424+
) -> Result<Vec3, ViewportConversionError> {
425+
let target_size = self
426+
.logical_viewport_size()
427+
.ok_or(ViewportConversionError::NoViewportSize)?;
428+
let ndc_space_coords = self
429+
.world_to_ndc(camera_transform, world_position)
430+
.ok_or(ViewportConversionError::InvalidData)?;
394431
// NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space
395-
if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 {
396-
return None;
432+
if ndc_space_coords.z < 0.0 {
433+
return Err(ViewportConversionError::PastNearPlane);
434+
}
435+
if ndc_space_coords.z > 1.0 {
436+
return Err(ViewportConversionError::PastFarPlane);
397437
}
398438

399439
// Stretching ndc depth to value via near plane and negating result to be in positive room again.
@@ -403,7 +443,7 @@ impl Camera {
403443
let mut viewport_position = (ndc_space_coords.truncate() + Vec2::ONE) / 2.0 * target_size;
404444
// Flip the Y co-ordinate origin from the bottom to the top.
405445
viewport_position.y = target_size.y - viewport_position.y;
406-
Some(viewport_position.extend(depth))
446+
Ok(viewport_position.extend(depth))
407447
}
408448

409449
/// Returns a ray originating from the camera, that passes through everything beyond `viewport_position`.
@@ -415,16 +455,18 @@ impl Camera {
415455
/// To get the world space coordinates with Normalized Device Coordinates, you should use
416456
/// [`ndc_to_world`](Self::ndc_to_world).
417457
///
418-
/// Returns `None` if any of these conditions occur:
419-
/// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size)
420-
/// - The near or far plane cannot be computed. This can happen if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`.
421-
/// Panics if the projection matrix is null and `glam_assert` is enabled.
458+
/// # Panics
459+
///
460+
/// Will panic if the camera's projection matrix is invalid (has a determinant of 0) and
461+
/// `glam_assert` is enabled (see [`ndc_to_world`](Self::ndc_to_world).
422462
pub fn viewport_to_world(
423463
&self,
424464
camera_transform: &GlobalTransform,
425465
mut viewport_position: Vec2,
426-
) -> Option<Ray3d> {
427-
let target_size = self.logical_viewport_size()?;
466+
) -> Result<Ray3d, ViewportConversionError> {
467+
let target_size = self
468+
.logical_viewport_size()
469+
.ok_or(ViewportConversionError::NoViewportSize)?;
428470
// Flip the Y co-ordinate origin from the top to the bottom.
429471
viewport_position.y = target_size.y - viewport_position.y;
430472
let ndc = viewport_position * 2. / target_size - Vec2::ONE;
@@ -436,12 +478,12 @@ impl Camera {
436478
let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON));
437479

438480
// The fallible direction constructor ensures that world_near_plane and world_far_plane aren't NaN.
439-
Dir3::new(world_far_plane - world_near_plane).map_or(None, |direction| {
440-
Some(Ray3d {
481+
Dir3::new(world_far_plane - world_near_plane)
482+
.map_err(|_| ViewportConversionError::InvalidData)
483+
.map(|direction| Ray3d {
441484
origin: world_near_plane,
442485
direction,
443486
})
444-
})
445487
}
446488

447489
/// Returns a 2D world position computed from a position on this [`Camera`]'s viewport.
@@ -451,23 +493,27 @@ impl Camera {
451493
/// To get the world space coordinates with Normalized Device Coordinates, you should use
452494
/// [`ndc_to_world`](Self::ndc_to_world).
453495
///
454-
/// Returns `None` if any of these conditions occur:
455-
/// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size)
456-
/// - The viewport position cannot be mapped to the world. See [`ndc_to_world`](Camera::ndc_to_world)
457-
/// May panic. See [`ndc_to_world`](Camera::ndc_to_world).
496+
/// # Panics
497+
///
498+
/// Will panic if the camera's projection matrix is invalid (has a determinant of 0) and
499+
/// `glam_assert` is enabled (see [`ndc_to_world`](Self::ndc_to_world).
458500
pub fn viewport_to_world_2d(
459501
&self,
460502
camera_transform: &GlobalTransform,
461503
mut viewport_position: Vec2,
462-
) -> Option<Vec2> {
463-
let target_size = self.logical_viewport_size()?;
504+
) -> Result<Vec2, ViewportConversionError> {
505+
let target_size = self
506+
.logical_viewport_size()
507+
.ok_or(ViewportConversionError::NoViewportSize)?;
464508
// Flip the Y co-ordinate origin from the top to the bottom.
465509
viewport_position.y = target_size.y - viewport_position.y;
466510
let ndc = viewport_position * 2. / target_size - Vec2::ONE;
467511

468-
let world_near_plane = self.ndc_to_world(camera_transform, ndc.extend(1.))?;
512+
let world_near_plane = self
513+
.ndc_to_world(camera_transform, ndc.extend(1.))
514+
.ok_or(ViewportConversionError::InvalidData)?;
469515

470-
Some(world_near_plane.truncate())
516+
Ok(world_near_plane.truncate())
471517
}
472518

473519
/// Given a position in world space, use the camera's viewport to compute the Normalized Device Coordinates.
@@ -478,7 +524,10 @@ impl Camera {
478524
/// [`world_to_viewport`](Self::world_to_viewport).
479525
///
480526
/// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`.
481-
/// Panics if the `camera_transform` contains `NAN` and the `glam_assert` feature is enabled.
527+
///
528+
/// # Panics
529+
///
530+
/// Will panic if the `camera_transform` contains `NAN` and the `glam_assert` feature is enabled.
482531
pub fn world_to_ndc(
483532
&self,
484533
camera_transform: &GlobalTransform,
@@ -501,7 +550,10 @@ impl Camera {
501550
/// [`world_to_viewport`](Self::world_to_viewport).
502551
///
503552
/// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`.
504-
/// Panics if the projection matrix is null and `glam_assert` is enabled.
553+
///
554+
/// # Panics
555+
///
556+
/// Will panic if the projection matrix is invalid (has a determinant of 0) and `glam_assert` is enabled.
505557
pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option<Vec3> {
506558
// Build a transformation matrix to convert from NDC to world space using camera data
507559
let ndc_to_world =

crates/bevy_sprite/src/picking_backend.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ pub fn sprite_picking(
7070
continue;
7171
};
7272

73-
let Some(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position)
73+
let Ok(cursor_ray_world) = camera.viewport_to_world(cam_transform, location.position)
7474
else {
7575
continue;
7676
};

crates/bevy_ui/src/accessibility.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ fn calc_bounds(
4141
if let Ok((camera, camera_transform)) = camera.get_single() {
4242
for (mut accessible, node, transform) in &mut nodes {
4343
if node.is_changed() || transform.is_changed() {
44-
if let Some(translation) =
44+
if let Ok(translation) =
4545
camera.world_to_viewport(camera_transform, transform.translation())
4646
{
4747
let bounds = Rect::new(

examples/2d/2d_viewport_to_world.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ fn draw_cursor(
2626
};
2727

2828
// Calculate a world position based on the cursor's position.
29-
let Some(point) = camera.viewport_to_world_2d(camera_transform, cursor_position) else {
29+
let Ok(point) = camera.viewport_to_world_2d(camera_transform, cursor_position) else {
3030
return;
3131
};
3232

examples/3d/3d_viewport_to_world.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ fn draw_cursor(
2424
};
2525

2626
// Calculate a ray pointing from the camera into the world based on the cursor's position.
27-
let Some(ray) = camera.viewport_to_world(camera_transform, cursor_position) else {
27+
let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_position) else {
2828
return;
2929
};
3030

examples/3d/irradiance_volumes.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -483,7 +483,7 @@ fn handle_mouse_clicks(
483483
};
484484

485485
// Figure out where the user clicked on the plane.
486-
let Some(ray) = camera.viewport_to_world(camera_transform, mouse_position) else {
486+
let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else {
487487
return;
488488
};
489489
let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else {

examples/ecs/observers.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,7 @@ fn handle_click(
184184
if let Some(pos) = windows
185185
.single()
186186
.cursor_position()
187-
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor))
187+
.and_then(|cursor| camera.viewport_to_world(camera_transform, cursor).ok())
188188
.map(|ray| ray.origin.truncate())
189189
{
190190
if mouse_button_input.just_pressed(MouseButton::Left) {

examples/games/desk_toy.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -222,9 +222,11 @@ fn get_cursor_world_pos(
222222
let primary_window = q_primary_window.single();
223223
let (main_camera, main_camera_transform) = q_camera.single();
224224
// Get the cursor position in the world
225-
cursor_world_pos.0 = primary_window
226-
.cursor_position()
227-
.and_then(|cursor_pos| main_camera.viewport_to_world_2d(main_camera_transform, cursor_pos));
225+
cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| {
226+
main_camera
227+
.viewport_to_world_2d(main_camera_transform, cursor_pos)
228+
.ok()
229+
});
228230
}
229231

230232
/// Update whether the window is clickable or not

examples/math/cubic_splines.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -357,11 +357,10 @@ fn handle_mouse_press(
357357
};
358358

359359
// Convert the starting point and end point (current mouse pos) into world coords:
360-
let Some(point) = camera.viewport_to_world_2d(camera_transform, start) else {
360+
let Ok(point) = camera.viewport_to_world_2d(camera_transform, start) else {
361361
continue;
362362
};
363-
let Some(end_point) = camera.viewport_to_world_2d(camera_transform, mouse_pos)
364-
else {
363+
let Ok(end_point) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else {
365364
continue;
366365
};
367366
let tangent = end_point - point;
@@ -396,10 +395,10 @@ fn draw_edit_move(
396395

397396
// Resources store data in viewport coordinates, so we need to convert to world coordinates
398397
// to display them:
399-
let Some(start) = camera.viewport_to_world_2d(camera_transform, start) else {
398+
let Ok(start) = camera.viewport_to_world_2d(camera_transform, start) else {
400399
return;
401400
};
402-
let Some(end) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else {
401+
let Ok(end) = camera.viewport_to_world_2d(camera_transform, mouse_pos) else {
403402
return;
404403
};
405404

0 commit comments

Comments
 (0)