diff --git a/crates/bevy_math/src/lib.rs b/crates/bevy_math/src/lib.rs index 4d44d095c8fed..4ad9dcc3731ba 100644 --- a/crates/bevy_math/src/lib.rs +++ b/crates/bevy_math/src/lib.rs @@ -13,7 +13,7 @@ mod ray; mod rects; pub use affine3::*; -pub use ray::Ray; +pub use ray::{Ray2d, Ray3d}; pub use rects::*; /// The `bevy_math` prelude. @@ -25,8 +25,8 @@ pub mod prelude { CubicSegment, }, primitives, BVec2, BVec3, BVec4, EulerRot, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4, - Quat, Ray, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4, - Vec4Swizzles, + Quat, Ray2d, Ray3d, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, + Vec3Swizzles, Vec4, Vec4Swizzles, }; } diff --git a/crates/bevy_math/src/primitives/dim2.rs b/crates/bevy_math/src/primitives/dim2.rs index bed4198538b82..4eef979cf60c1 100644 --- a/crates/bevy_math/src/primitives/dim2.rs +++ b/crates/bevy_math/src/primitives/dim2.rs @@ -3,6 +3,7 @@ use crate::Vec2; /// A normalized vector pointing in a direction in 2D space #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Direction2d(Vec2); impl Direction2d { @@ -86,6 +87,20 @@ pub struct Plane2d { } impl Primitive2d for Plane2d {} +impl Plane2d { + /// Create a new `Plane2d` from a normal + /// + /// # Panics + /// + /// Panics if the given `normal` is zero (or very close to zero), or non-finite. + #[inline] + pub fn new(normal: Vec2) -> Self { + Self { + normal: Direction2d::new(normal).expect("normal must be nonzero and finite"), + } + } +} + /// An infinite line along a direction in 2D space. /// /// For a finite line: [`Segment2d`] diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 66b3e38301259..bb3cc5427f244 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -3,6 +3,7 @@ use crate::Vec3; /// A normalized vector pointing in a direction in 3D space #[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Direction3d(Vec3); impl Direction3d { @@ -66,6 +67,20 @@ pub struct Plane3d { } impl Primitive3d for Plane3d {} +impl Plane3d { + /// Create a new `Plane3d` from a normal + /// + /// # Panics + /// + /// Panics if the given `normal` is zero (or very close to zero), or non-finite. + #[inline] + pub fn new(normal: Vec3) -> Self { + Self { + normal: Direction3d::new(normal).expect("normal must be nonzero and finite"), + } + } +} + /// An infinite line along a direction in 3D space. /// /// For a finite line: [`Segment3d`] diff --git a/crates/bevy_math/src/ray.rs b/crates/bevy_math/src/ray.rs index b4d8e7b03f5a8..bc9c45fa669dd 100644 --- a/crates/bevy_math/src/ray.rs +++ b/crates/bevy_math/src/ray.rs @@ -1,33 +1,95 @@ -use crate::Vec3; +use crate::{ + primitives::{Direction2d, Direction3d, Plane2d, Plane3d}, + Vec2, Vec3, +}; -/// A ray is an infinite line starting at `origin`, going in `direction`. -#[derive(Default, Clone, Copy, Debug, PartialEq)] +/// An infinite half-line starting at `origin` and going in `direction` in 2D space. +#[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -pub struct Ray { +pub struct Ray2d { /// The origin of the ray. - pub origin: Vec3, - /// A normalized vector representing the direction of the ray. - pub direction: Vec3, + pub origin: Vec2, + /// The direction of the ray. + pub direction: Direction2d, } -impl Ray { - /// Returns the distance to the plane if the ray intersects it. +impl Ray2d { + /// Create a new `Ray2d` from a given origin and direction + /// + /// # Panics + /// + /// Panics if the given `direction` is zero (or very close to zero), or non-finite. + #[inline] + pub fn new(origin: Vec2, direction: Vec2) -> Self { + Self { + origin, + direction: Direction2d::new(direction) + .expect("ray direction must be nonzero and finite"), + } + } + + /// Get a point at a given distance along the ray + #[inline] + pub fn get_point(&self, distance: f32) -> Vec2 { + self.origin + *self.direction * distance + } + + /// Get the distance to a plane if the ray intersects it #[inline] - pub fn intersect_plane(&self, plane_origin: Vec3, plane_normal: Vec3) -> Option { - let denominator = plane_normal.dot(self.direction); + pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option { + let denominator = plane.normal.dot(*self.direction); if denominator.abs() > f32::EPSILON { - let distance = (plane_origin - self.origin).dot(plane_normal) / denominator; + let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator; if distance > f32::EPSILON { return Some(distance); } } None } +} + +/// An infinite half-line starting at `origin` and going in `direction` in 3D space. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Ray3d { + /// The origin of the ray. + pub origin: Vec3, + /// The direction of the ray. + pub direction: Direction3d, +} + +impl Ray3d { + /// Create a new `Ray3d` from a given origin and direction + /// + /// # Panics + /// + /// Panics if the given `direction` is zero (or very close to zero), or non-finite. + #[inline] + pub fn new(origin: Vec3, direction: Vec3) -> Self { + Self { + origin, + direction: Direction3d::new(direction) + .expect("ray direction must be nonzero and finite"), + } + } - /// Retrieve a point at the given distance along the ray. + /// Get a point at a given distance along the ray #[inline] pub fn get_point(&self, distance: f32) -> Vec3 { - self.origin + self.direction * distance + self.origin + *self.direction * distance + } + + /// Get the distance to a plane if the ray intersects it + #[inline] + pub fn intersect_plane(&self, plane_origin: Vec3, plane: Plane3d) -> Option { + let denominator = plane.normal.dot(*self.direction); + if denominator.abs() > f32::EPSILON { + let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator; + if distance > f32::EPSILON { + return Some(distance); + } + } + None } } @@ -36,29 +98,82 @@ mod tests { use super::*; #[test] - fn intersect_plane() { - let ray = Ray { - origin: Vec3::ZERO, - direction: Vec3::Z, - }; + fn intersect_plane_2d() { + let ray = Ray2d::new(Vec2::ZERO, Vec2::Y); // Orthogonal, and test that an inverse plane_normal has the same result - assert_eq!(Some(1.), ray.intersect_plane(Vec3::Z, Vec3::Z)); - assert_eq!(Some(1.), ray.intersect_plane(Vec3::Z, Vec3::NEG_Z)); - assert_eq!(None, ray.intersect_plane(Vec3::NEG_Z, Vec3::Z)); - assert_eq!(None, ray.intersect_plane(Vec3::NEG_Z, Vec3::NEG_Z)); + assert_eq!( + ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::Y)), + Some(1.0) + ); + assert_eq!( + ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::NEG_Y)), + Some(1.0) + ); + assert!(ray + .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::Y)) + .is_none()); + assert!(ray + .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::NEG_Y)) + .is_none()); // Diagonal - assert_eq!(Some(1.), ray.intersect_plane(Vec3::Z, Vec3::ONE)); - assert_eq!(None, ray.intersect_plane(Vec3::NEG_Z, Vec3::ONE)); + assert_eq!( + ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::ONE)), + Some(1.0) + ); + assert!(ray + .intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::ONE)) + .is_none()); // Parallel - assert_eq!(None, ray.intersect_plane(Vec3::X, Vec3::X)); + assert!(ray + .intersect_plane(Vec2::X, Plane2d::new(Vec2::X)) + .is_none()); // Parallel with simulated rounding error + assert!(ray + .intersect_plane(Vec2::X, Plane2d::new(Vec2::X + Vec2::Y * f32::EPSILON)) + .is_none()); + } + + #[test] + fn intersect_plane_3d() { + let ray = Ray3d::new(Vec3::ZERO, Vec3::Z); + + // Orthogonal, and test that an inverse plane_normal has the same result + assert_eq!( + ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::Z)), + Some(1.0) + ); + assert_eq!( + ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::NEG_Z)), + Some(1.0) + ); + assert!(ray + .intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::Z)) + .is_none()); + assert!(ray + .intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::NEG_Z)) + .is_none()); + + // Diagonal assert_eq!( - None, - ray.intersect_plane(Vec3::X, Vec3::X + Vec3::Z * f32::EPSILON) + ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::ONE)), + Some(1.0) ); + assert!(ray + .intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::ONE)) + .is_none()); + + // Parallel + assert!(ray + .intersect_plane(Vec3::X, Plane3d::new(Vec3::X)) + .is_none()); + + // Parallel with simulated rounding error + assert!(ray + .intersect_plane(Vec3::X, Plane3d::new(Vec3::X + Vec3::Z * f32::EPSILON)) + .is_none()); } } diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index a0a39e190ac1f..11c472b28c31b 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -20,7 +20,9 @@ use bevy_ecs::{ system::{Commands, Query, Res, ResMut, Resource}, }; use bevy_log::warn; -use bevy_math::{vec2, Mat4, Ray, Rect, URect, UVec2, UVec4, Vec2, Vec3}; +use bevy_math::{ + primitives::Direction3d, vec2, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3, +}; use bevy_reflect::prelude::*; use bevy_transform::components::GlobalTransform; use bevy_utils::{HashMap, HashSet}; @@ -272,7 +274,7 @@ impl Camera { &self, camera_transform: &GlobalTransform, mut viewport_position: Vec2, - ) -> Option { + ) -> Option { let target_size = self.logical_viewport_size()?; // Flip the Y co-ordinate origin from the top to the bottom. viewport_position.y = target_size.y - viewport_position.y; @@ -284,9 +286,12 @@ impl Camera { // 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 { - origin: world_near_plane, - direction: (world_far_plane - world_near_plane).normalize(), + // The fallible direction constructor ensures that world_near_plane and world_far_plane aren't NaN. + Direction3d::new(world_far_plane - world_near_plane).map_or(None, |direction| { + Some(Ray3d { + origin: world_near_plane, + direction, + }) }) } diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index d2c175197f57c..308e379a4e113 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -29,7 +29,9 @@ fn draw_cursor( }; // Calculate if and where the ray is hitting the ground plane. - let Some(distance) = ray.intersect_plane(ground.translation(), ground.up()) else { + let Some(distance) = + ray.intersect_plane(ground.translation(), primitives::Plane3d::new(ground.up())) + else { return; }; let point = ray.get_point(distance);