diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 360ecb33c503e0..25ce82195d4953 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -170,7 +170,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, @@ -178,11 +178,24 @@ 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(); + let logical_viewport_size = viewport.and_then(|v| self.to_logical(v.physical_size)); + // TODO: This code sucks + if let Some(size) = logical_viewport_size { + Ok(size) + } else { + let logical_target_size = self.logical_target_size(); + if let Some(size) = logical_target_size { + Ok(size) + } else { + Err(LogicalViewportSizeError { + window_is_set: viewport.is_some(), + logical_viewport_size, + logical_target_size, + }) + } + } } /// The physical size of this camera's viewport. If the `viewport` field is set to [`Some`], this @@ -227,29 +240,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::LogicalViewportSizeError)?; + let ndc_space_coords = self + .world_to_ndc(camera_transform, world_position) + .map_err(WorldToViewportError::WorldToNdcError)?; // 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::NdcCoordsOutsideFrustumError( + 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`. @@ -261,27 +276,49 @@ 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::LogicalViewportSizeError)?; // 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 = camera_transform.compute_matrix(); + if !camera_transform_matrix.is_finite() { + return Err(ViewportToWorldError::CameraTransformNotFiniteError( + camera_transform_matrix, + )); + } + // TODO: Do some code deduplication of these two snippets.. + let projection_matrix = self.computed.projection_matrix; + if !projection_matrix.is_finite() { + return Err(ViewportToWorldError::ProjectionMatrixNotFiniteError( + projection_matrix, + )); + } + + 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)); - (!world_near_plane.is_nan() && !world_far_plane.is_nan()).then_some(Ray { + if !world_near_plane.is_finite() { + return Err(ViewportToWorldError::WorldNearPlaneNotFiniteError( + world_near_plane, + )); + } + + if world_far_plane.is_nan() { + return Err(ViewportToWorldError::WorldFarPlaneNanError(world_far_plane)); + } + + Ok(Ray { origin: world_near_plane, direction: (world_far_plane - world_near_plane).normalize(), }) @@ -294,25 +331,22 @@ 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, - ) -> Result { + ) -> Result { let target_size = self .logical_viewport_size() - .ok_or(ViewportToWorldError::LogicalViewportSizeError)?; + .map_err(ViewportToWorld2DError::LogicalViewportSizeError)?; // 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.)) - .map_err(ViewportToWorldError::NdcToWorldError)?; + .map_err(ViewportToWorld2DError::NdcToWorldError)?; Ok(world_near_plane.truncate()) } @@ -324,19 +358,37 @@ 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 = camera_transform.compute_matrix(); + if !camera_transform_matrix.is_finite() { + return Err(WorldToNdcError::CameraTransformNotFiniteError( + camera_transform_matrix, + )); + } + + let projection_matrix = self.computed.projection_matrix; + if !projection_matrix.is_finite() { + return Err(WorldToNdcError::ProjectionMatrixNotFiniteError( + projection_matrix, + )); + } + // 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::NdcSpaceCoordsNotFiniteError( + ndc_space_coords, + )); + } + + Ok(ndc_space_coords) } /// Given a position in Normalized Device Coordinates, @@ -381,15 +433,47 @@ impl Camera { } } +#[derive(Debug)] pub enum NdcToWorldError { WorldSpaceCoordsNotFiniteError(Vec3), CameraTransformNotFiniteError(Mat4), ProjectionMatrixNotFiniteError(Mat4), } +#[derive(Debug)] +pub enum WorldToNdcError { + NdcSpaceCoordsNotFiniteError(Vec3), + CameraTransformNotFiniteError(Mat4), + ProjectionMatrixNotFiniteError(Mat4), +} + +#[derive(Debug)] +pub struct LogicalViewportSizeError { + pub window_is_set: bool, + pub logical_viewport_size: Option, + pub logical_target_size: Option, +} + +#[derive(Debug)] pub enum ViewportToWorldError { + LogicalViewportSizeError(LogicalViewportSizeError), + CameraTransformNotFiniteError(Mat4), + ProjectionMatrixNotFiniteError(Mat4), + WorldNearPlaneNotFiniteError(Vec3), + WorldFarPlaneNanError(Vec3), //Presumably this being infinite is sometimes ok? +} + +#[derive(Debug)] +pub enum ViewportToWorld2DError { NdcToWorldError(NdcToWorldError), - LogicalViewportSizeError, + LogicalViewportSizeError(LogicalViewportSizeError), +} + +#[derive(Debug)] +pub enum WorldToViewportError { + WorldToNdcError(WorldToNdcError), + LogicalViewportSizeError(LogicalViewportSizeError), + NdcCoordsOutsideFrustumError(Vec3), } /// Control how this camera outputs once rendering is completed. @@ -642,7 +726,7 @@ pub fn camera_system( &images, &manual_texture_views, ); - 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(); } diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs index a1203a766d0f91..9acb65870155ce 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 c4195a6f412005..d95be0663802d9 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -474,7 +474,7 @@ pub fn extract_default_ui_camera_view( continue; } 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 8ed4f881f993f9..88b882042f56b4 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 d2c175197f57cf..c5b382834ed5c4 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; };