diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 95e2bc4bfb291..4c7cd55c4281c 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -213,7 +213,7 @@ pub(crate) fn assign_lights_to_clusters( let view_inv_scale = camera_transform.compute_transform().scale.recip(); let view_inv_scale_max = view_inv_scale.abs().max_element(); let inverse_view_transform = view_transform.inverse(); - let is_orthographic = camera.projection_matrix().w_axis.w == 1.0; + let is_orthographic = camera.projection_matrix_unchecked().w_axis.w == 1.0; let far_z = match config.far_z_mode() { ClusterFarZMode::MaxLightRange => { @@ -239,7 +239,8 @@ pub(crate) fn assign_lights_to_clusters( // 3,2 = r * far and 2,2 = r where r = 1.0 / (far - near) // rearranging r = 1.0 / (far - near), r * (far - near) = 1.0, r * far - 1.0 = r * near, near = (r * far - 1.0) / r // = (3,2 - 1.0) / 2,2 - (camera.projection_matrix().w_axis.z - 1.0) / camera.projection_matrix().z_axis.z + (camera.projection_matrix_unchecked().w_axis.z - 1.0) + / camera.projection_matrix_unchecked().z_axis.z } (false, 1) => config.first_slice_depth().max(far_z), _ => config.first_slice_depth(), @@ -271,7 +272,7 @@ pub(crate) fn assign_lights_to_clusters( let (light_aabb_min, light_aabb_max) = cluster_space_light_aabb( inverse_view_transform, view_inv_scale, - camera.projection_matrix(), + camera.projection_matrix_unchecked(), &light_sphere, ); @@ -337,7 +338,7 @@ pub(crate) fn assign_lights_to_clusters( clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096 ); - let inverse_projection = camera.projection_matrix().inverse(); + let inverse_projection = camera.projection_matrix_unchecked().inverse(); for lights in &mut clusters.lights { lights.entities.clear(); @@ -434,7 +435,7 @@ pub(crate) fn assign_lights_to_clusters( cluster_space_light_aabb( inverse_view_transform, view_inv_scale, - camera.projection_matrix(), + camera.projection_matrix_unchecked(), &light_sphere, ); @@ -477,7 +478,7 @@ pub(crate) fn assign_lights_to_clusters( ) }); let light_center_clip = - camera.projection_matrix() * view_light_sphere.center.extend(1.0); + camera.projection_matrix_unchecked() * view_light_sphere.center.extend(1.0); let light_center_ndc = light_center_clip.xyz() / light_center_clip.w; let cluster_coordinates = ndc_position_to_cluster( clusters.dimensions, diff --git a/crates/bevy_pbr/src/prepass/mod.rs b/crates/bevy_pbr/src/prepass/mod.rs index ce7c3087e4983..f14ad20938424 100644 --- a/crates/bevy_pbr/src/prepass/mod.rs +++ b/crates/bevy_pbr/src/prepass/mod.rs @@ -207,7 +207,7 @@ pub fn update_previous_view_data( let inverse_view = camera_transform.compute_matrix().inverse(); commands.entity(entity).try_insert(PreviousViewData { inverse_view, - view_proj: camera.projection_matrix() * inverse_view, + view_proj: camera.projection_matrix_unchecked() * inverse_view, }); } } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index d0d9a4a6e1cd1..c0932a89f7908 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -24,7 +24,9 @@ use bevy_ecs::{ reflect::ReflectComponent, system::{Commands, Query, Res, ResMut, Resource}, }; -use bevy_math::{vec2, Dir3, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3}; +use bevy_math::{ + vec2, Dir3, InvalidDirectionError, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3, +}; use bevy_reflect::prelude::*; use bevy_render_macros::ExtractComponent; use bevy_transform::components::GlobalTransform; @@ -82,7 +84,7 @@ pub struct RenderTargetInfo { /// Holds internally computed [`Camera`] values. #[derive(Default, Debug, Clone)] pub struct ComputedCameraValues { - projection_matrix: Mat4, + projection_matrix: Option, target_info: Option, // size of the `Viewport` old_viewport_size: Option, @@ -285,7 +287,7 @@ impl Camera { /// For logic that requires the full logical size of the /// [`RenderTarget`], prefer [`Camera::logical_target_size`]. /// - /// Returns `None` if either: + /// Returns an error if either: /// - the function is called just after the `Camera` is created, before `camera_system` is executed, /// - the [`RenderTarget`] isn't correctly set: /// - it references the [`PrimaryWindow`](RenderTarget::Window) when there is none, @@ -293,11 +295,18 @@ impl Camera { /// - it references an [`Image`](RenderTarget::Image) that doesn't exist (invalid handle), /// - it references a [`TextureView`](RenderTarget::TextureView) that doesn't exist (invalid handle). #[inline] - pub fn logical_viewport_size(&self) -> Option { - self.viewport - .as_ref() - .and_then(|v| self.to_logical(v.physical_size)) - .or_else(|| self.logical_target_size()) + pub fn logical_viewport_size(&self) -> Result { + let viewport = self.viewport.as_ref(); + if let Some(size) = viewport.and_then(|v| self.to_logical(v.physical_size)) { + Ok(size) + } else if let Some(size) = self.logical_target_size() { + Ok(size) + } else { + Err(LogicalViewportSizeError { + viewport_is_set: viewport.is_some(), + target_info_is_set: self.computed.target_info.is_some(), + }) + } } /// The physical size of this camera's viewport (in physical pixels). @@ -340,8 +349,40 @@ impl Camera { /// The projection matrix computed using this camera's [`CameraProjection`]. #[inline] - pub fn projection_matrix(&self) -> Mat4 { - self.computed.projection_matrix + pub fn projection_matrix_unchecked(&self) -> Mat4 { + self.computed.projection_matrix.unwrap_or_default() + } + + fn projection_matrix(&self) -> Result { + let projection_matrix = self + .computed + .projection_matrix + .ok_or(BadProjectionMatrixError::ProjectionMatrixUndefined)?; + if projection_matrix.is_finite() { + if projection_matrix == Mat4::ZERO { + Err(BadProjectionMatrixError::BadProjectionMatrixValues( + projection_matrix, + )) + } else { + Ok(projection_matrix) + } + } else { + Err(BadProjectionMatrixError::BadProjectionMatrixValues( + projection_matrix, + )) + } + } + + #[inline] + fn finite_camera_transform_matrix( + camera_transform: &GlobalTransform, + ) -> Result { + let camera_transform_matrix = camera_transform.compute_matrix(); + if camera_transform_matrix.is_finite() { + Ok(camera_transform_matrix) + } else { + Err(CameraTransformNotFiniteError(camera_transform_matrix)) + } } /// Given a position in world space, use the camera to compute the viewport-space coordinates. @@ -349,29 +390,31 @@ impl Camera { /// To get the coordinates in Normalized Device Coordinates, you should use /// [`world_to_ndc`](Self::world_to_ndc). /// - /// Returns `None` if any of these conditions occur: - /// - The computed coordinates are beyond the near or far plane - /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) - /// - The world coordinates cannot be mapped to the Normalized Device Coordinates. See [`world_to_ndc`](Camera::world_to_ndc) - /// May also panic if `glam_assert` is enabled. See [`world_to_ndc`](Camera::world_to_ndc). + /// May panic if `glam_assert` is enabled. See [`world_to_ndc`](Camera::world_to_ndc). #[doc(alias = "world_to_screen")] pub fn world_to_viewport( &self, camera_transform: &GlobalTransform, world_position: Vec3, - ) -> Option { - let target_size = self.logical_viewport_size()?; - let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?; + ) -> Result { + let target_size = self + .logical_viewport_size() + .map_err(WorldToViewportError::LogicalViewportSize)?; + let ndc_space_coords = self + .world_to_ndc(camera_transform, world_position) + .map_err(WorldToViewportError::WorldToNdc)?; // NDC z-values outside of 0 < z < 1 are outside the (implicit) camera frustum and are thus not in viewport-space if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 { - return None; + return Err(WorldToViewportError::NdcCoordsOutsideFrustum( + ndc_space_coords, + )); } // 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; - Some(viewport_position) + Ok(viewport_position) } /// Returns a ray originating from the camera, that passes through everything beyond `viewport_position`. @@ -383,32 +426,37 @@ impl Camera { /// To get the world space coordinates with Normalized Device Coordinates, you should use /// [`ndc_to_world`](Self::ndc_to_world). /// - /// Returns `None` if any of these conditions occur: - /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) - /// - 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`. /// Panics if the projection matrix is null and `glam_assert` is enabled. pub fn viewport_to_world( &self, camera_transform: &GlobalTransform, mut viewport_position: Vec2, - ) -> Option { - let target_size = self.logical_viewport_size()?; + ) -> Result { + let target_size = self + .logical_viewport_size() + .map_err(ViewportToWorldError::LogicalViewportSize)?; // 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; - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.projection_matrix.inverse(); + let camera_transform_matrix = Self::finite_camera_transform_matrix(camera_transform) + .map_err(ViewportToWorldError::CameraTransformNotFinite)?; + let projection_matrix = self + .projection_matrix() + .map_err(ViewportToWorldError::ProjectionMatrixNotFinite)?; + + let ndc_to_world = camera_transform_matrix * projection_matrix.inverse(); let world_near_plane = ndc_to_world.project_point3(ndc.extend(1.)); // Using EPSILON because an ndc with Z = 0 returns NaNs. let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON)); // The fallible direction constructor ensures that world_near_plane and world_far_plane aren't NaN. - Dir3::new(world_far_plane - world_near_plane).map_or(None, |direction| { - Some(Ray3d { - origin: world_near_plane, - direction, - }) + let direction = Dir3::new(world_far_plane - world_near_plane) + .map_err(|e| ViewportToWorldError::InvalidDirection(e))?; + + Ok(Ray3d { + origin: world_near_plane, + direction, }) } @@ -419,23 +467,24 @@ impl Camera { /// To get the world space coordinates with Normalized Device Coordinates, you should use /// [`ndc_to_world`](Self::ndc_to_world). /// - /// Returns `None` if any of these conditions occur: - /// - The logical viewport size cannot be computed. See [`logical_viewport_size`](Camera::logical_viewport_size) - /// - The viewport position cannot be mapped to the world. See [`ndc_to_world`](Camera::ndc_to_world) /// May panic. See [`ndc_to_world`](Camera::ndc_to_world). pub fn viewport_to_world_2d( &self, camera_transform: &GlobalTransform, mut viewport_position: Vec2, - ) -> Option { - let target_size = self.logical_viewport_size()?; + ) -> Result { + let target_size = self + .logical_viewport_size() + .map_err(ViewportToWorld2DError::LogicalViewportSize)?; // 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; - let world_near_plane = self.ndc_to_world(camera_transform, ndc.extend(1.))?; + let world_near_plane = self + .ndc_to_world(camera_transform, ndc.extend(1.)) + .map_err(ViewportToWorld2DError::NdcToWorld)?; - Some(world_near_plane.truncate()) + Ok(world_near_plane.truncate()) } /// Given a position in world space, use the camera's viewport to compute the Normalized Device Coordinates. @@ -445,19 +494,28 @@ impl Camera { /// To get the coordinates in the render target's viewport dimensions, you should use /// [`world_to_viewport`](Self::world_to_viewport). /// - /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. /// Panics if the `camera_transform` contains `NAN` and the `glam_assert` feature is enabled. pub fn world_to_ndc( &self, camera_transform: &GlobalTransform, world_position: Vec3, - ) -> Option { + ) -> Result { + let camera_transform_matrix = Self::finite_camera_transform_matrix(camera_transform) + .map_err(WorldToNdcError::CameraTransformNotFinite)?; + + let projection_matrix = self + .projection_matrix() + .map_err(WorldToNdcError::ProjectionMatrixNotFinite)?; + // Build a transformation matrix to convert from world space to NDC using camera data - let world_to_ndc: Mat4 = - self.computed.projection_matrix * camera_transform.compute_matrix().inverse(); + let world_to_ndc: Mat4 = projection_matrix * camera_transform_matrix.inverse(); let ndc_space_coords: Vec3 = world_to_ndc.project_point3(world_position); - (!ndc_space_coords.is_nan()).then_some(ndc_space_coords) + if !ndc_space_coords.is_finite() { + return Err(WorldToNdcError::NdcSpaceCoordsNotFinite(ndc_space_coords)); + } + + Ok(ndc_space_coords) } /// Given a position in Normalized Device Coordinates, @@ -468,19 +526,91 @@ impl Camera { /// To get the world space coordinates with the viewport position, you should use /// [`world_to_viewport`](Self::world_to_viewport). /// - /// Returns `None` if the `camera_transform`, the `world_position`, or the projection matrix defined by [`CameraProjection`] contain `NAN`. /// Panics if the projection matrix is null and `glam_assert` is enabled. - pub fn ndc_to_world(&self, camera_transform: &GlobalTransform, ndc: Vec3) -> Option { + pub fn ndc_to_world( + &self, + camera_transform: &GlobalTransform, + ndc: Vec3, + ) -> Result { + let camera_transform_matrix = Self::finite_camera_transform_matrix(camera_transform) + .map_err(NdcToWorldError::CameraTransformNotFinite)?; + + let projection_matrix = self + .projection_matrix() + .map_err(NdcToWorldError::ProjectionMatrixNotFinite)?; + // Build a transformation matrix to convert from NDC to world space using camera data - let ndc_to_world = - camera_transform.compute_matrix() * self.computed.projection_matrix.inverse(); + let ndc_to_world = camera_transform_matrix * projection_matrix.inverse(); let world_space_coords = ndc_to_world.project_point3(ndc); + if !world_space_coords.is_finite() { + return Err(NdcToWorldError::WorldSpaceCoordsNotFinite( + world_space_coords, + )); + } - (!world_space_coords.is_nan()).then_some(world_space_coords) + Ok(world_space_coords) } } +/// Errors that occur when mapping an NDC point to an in-world point. +#[derive(Debug)] +pub enum NdcToWorldError { + WorldSpaceCoordsNotFinite(Vec3), + CameraTransformNotFinite(CameraTransformNotFiniteError), + ProjectionMatrixNotFinite(BadProjectionMatrixError), +} + +/// Errors that occur when mapping an in-world point to a NDC point. +#[derive(Debug)] +pub enum WorldToNdcError { + NdcSpaceCoordsNotFinite(Vec3), + CameraTransformNotFinite(CameraTransformNotFiniteError), + ProjectionMatrixNotFinite(BadProjectionMatrixError), +} + +/// An error that occurs when finding the logical viewport size. +#[derive(Debug)] +pub struct LogicalViewportSizeError { + pub viewport_is_set: bool, + pub target_info_is_set: bool, +} + +/// Errors that occur when mapping a viewport point to an in-world ray. +#[derive(Debug)] +pub enum ViewportToWorldError { + LogicalViewportSize(LogicalViewportSizeError), + CameraTransformNotFinite(CameraTransformNotFiniteError), + ProjectionMatrixNotFinite(BadProjectionMatrixError), + InvalidDirection(InvalidDirectionError), +} + +/// Errors that occur when mapping a viewport point to a 2D world. +#[derive(Debug)] +pub enum ViewportToWorld2DError { + NdcToWorld(NdcToWorldError), + LogicalViewportSize(LogicalViewportSizeError), +} + +/// Errors that occur when mapping a world point to a viewport point. +#[derive(Debug)] +pub enum WorldToViewportError { + WorldToNdc(WorldToNdcError), + LogicalViewportSize(LogicalViewportSizeError), + NdcCoordsOutsideFrustum(Vec3), +} + +/// Errors that occur when trying to get the projection matrix. +#[derive(Debug)] +pub enum BadProjectionMatrixError { + BadProjectionMatrixValues(Mat4), + ProjectionMatrixUndefined, +} + +/// An error for when the projection matrix is not finite. +#[derive(Debug)] +pub struct CameraTransformNotFiniteError(Mat4); + /// Control how this camera outputs once rendering is completed. #[derive(Debug, Clone, Copy)] pub enum CameraOutputMode { @@ -784,9 +914,10 @@ pub fn camera_system( } } camera.computed.target_info = new_computed_target_info; - if let Some(size) = camera.logical_viewport_size() { + if let Ok(size) = camera.logical_viewport_size() { camera_projection.update(size.x, size.y); - camera.computed.projection_matrix = camera_projection.get_projection_matrix(); + camera.computed.projection_matrix = + Some(camera_projection.get_projection_matrix()); } } } @@ -905,7 +1036,7 @@ pub fn extract_cameras( .unwrap_or_else(|| Exposure::default().exposure()), }, ExtractedView { - projection: camera.projection_matrix(), + projection: camera.projection_matrix_unchecked(), transform: *transform, view_projection: None, hdr: camera.hdr, diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index ab18b77d5a161..216f250e34ba0 100644 --- a/crates/bevy_ui/src/accessibility.rs +++ b/crates/bevy_ui/src/accessibility.rs @@ -41,7 +41,7 @@ fn calc_bounds( if let Ok((camera, camera_transform)) = camera.get_single() { for (mut accessible, node, transform) in &mut nodes { if node.is_changed() || transform.is_changed() { - if let Some(translation) = + if let Ok(translation) = camera.world_to_viewport(camera_transform, transform.translation()) { let bounds = Rect::new( diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index be0b0172182a3..5049cee68d679 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -231,7 +231,7 @@ pub fn extract_uinode_background_colors( let ui_logical_viewport_size = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.logical_viewport_size()) + .and_then(|(_, c)| c.logical_viewport_size().ok()) .unwrap_or(Vec2::ZERO) // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, // so we have to divide by `UiScale` to get the size of the UI viewport. @@ -373,7 +373,7 @@ pub fn extract_uinode_images( let ui_logical_viewport_size = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.logical_viewport_size()) + .and_then(|(_, c)| c.logical_viewport_size().ok()) .unwrap_or(Vec2::ZERO) // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, // so we have to divide by `UiScale` to get the size of the UI viewport. @@ -544,7 +544,7 @@ pub fn extract_uinode_borders( let ui_logical_viewport_size = camera_query .get(camera_entity) .ok() - .and_then(|(_, c)| c.logical_viewport_size()) + .and_then(|(_, c)| c.logical_viewport_size().ok()) .unwrap_or(Vec2::ZERO) // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, // so we have to divide by `UiScale` to get the size of the UI viewport. @@ -733,7 +733,7 @@ pub fn extract_default_ui_camera_view( } if let ( - Some(logical_size), + Ok(logical_size), Some(URect { min: physical_origin, .. diff --git a/examples/2d/2d_viewport_to_world.rs b/examples/2d/2d_viewport_to_world.rs index 788649f9793c0..2a5ffd819b4ae 100644 --- a/examples/2d/2d_viewport_to_world.rs +++ b/examples/2d/2d_viewport_to_world.rs @@ -22,7 +22,7 @@ fn draw_cursor( }; // Calculate a world position based on the cursor's position. - let Some(point) = camera.viewport_to_world_2d(camera_transform, cursor_position) else { + let Ok(point) = camera.viewport_to_world_2d(camera_transform, cursor_position) else { return; }; diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 9e4e4f73da3ed..717a96ff1d403 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -24,7 +24,7 @@ fn draw_cursor( }; // Calculate a ray pointing from the camera into the world based on the cursor's position. - let Some(ray) = camera.viewport_to_world(camera_transform, cursor_position) else { + let Ok(ray) = camera.viewport_to_world(camera_transform, cursor_position) else { return; }; diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 49d1de8fb0810..1f347d3bd896e 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -483,7 +483,7 @@ fn handle_mouse_clicks( }; // Figure out where the user clicked on the plane. - let Some(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { + let Ok(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { return; }; let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else { diff --git a/examples/games/desk_toy.rs b/examples/games/desk_toy.rs index ec9094feb2383..963ed683dc37a 100644 --- a/examples/games/desk_toy.rs +++ b/examples/games/desk_toy.rs @@ -222,9 +222,11 @@ fn get_cursor_world_pos( let primary_window = q_primary_window.single(); let (main_camera, main_camera_transform) = q_camera.single(); // Get the cursor position in the world - cursor_world_pos.0 = primary_window - .cursor_position() - .and_then(|cursor_pos| main_camera.viewport_to_world_2d(main_camera_transform, cursor_pos)); + cursor_world_pos.0 = primary_window.cursor_position().and_then(|cursor_pos| { + main_camera + .viewport_to_world_2d(main_camera_transform, cursor_pos) + .ok() + }); } /// Update whether the window is clickable or not