Split Ray into Ray2d and Ray3d and simplify plane construction (#10856)
				
					
				
			# Objective A better alternative version of #10843. Currently, Bevy has a single `Ray` struct for 3D. To allow better interoperability with Bevy's primitive shapes (#10572) and some third party crates (that handle e.g. spatial queries), it would be very useful to have separate versions for 2D and 3D respectively. ## Solution Separate `Ray` into `Ray2d` and `Ray3d`. These new structs also take advantage of the new primitives by using `Direction2d`/`Direction3d` for the direction: ```rust pub struct Ray2d { pub origin: Vec2, pub direction: Direction2d, } pub struct Ray3d { pub origin: Vec3, pub direction: Direction3d, } ``` and by using `Plane2d`/`Plane3d` in `intersect_plane`: ```rust impl Ray2d { // ... pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option<f32> { // ... } } ``` --- ## Changelog ### Added - `Ray2d` and `Ray3d` - `Ray2d::new` and `Ray3d::new` constructors - `Plane2d::new` and `Plane3d::new` constructors ### Removed - Removed `Ray` in favor of `Ray3d` ### Changed - `direction` is now a `Direction2d`/`Direction3d` instead of a vector, which provides guaranteed normalization - `intersect_plane` now takes a `Plane2d`/`Plane3d` instead of just a vector for the plane normal - `Direction2d` and `Direction3d` now derive `Serialize` and `Deserialize` to preserve ray (de)serialization ## Migration Guide `Ray` has been renamed to `Ray3d`. ### Ray creation Before: ```rust Ray { origin: Vec3::ZERO, direction: Vec3::new(0.5, 0.6, 0.2).normalize(), } ``` After: ```rust // Option 1: Ray3d { origin: Vec3::ZERO, direction: Direction3d::new(Vec3::new(0.5, 0.6, 0.2)).unwrap(), } // Option 2: Ray3d::new(Vec3::ZERO, Vec3::new(0.5, 0.6, 0.2)) ``` ### Plane intersections Before: ```rust let result = ray.intersect_plane(Vec2::X, Vec2::Y); ``` After: ```rust let result = ray.intersect_plane(Vec2::X, Plane2d::new(Vec2::Y)); ```
This commit is contained in:
		
							parent
							
								
									f683b802f1
								
							
						
					
					
						commit
						d9aac887b5
					
				@ -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,
 | 
			
		||||
    };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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`]
 | 
			
		||||
 | 
			
		||||
@ -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`]
 | 
			
		||||
 | 
			
		||||
@ -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 intersect_plane(&self, plane_origin: Vec3, plane_normal: Vec3) -> Option<f32> {
 | 
			
		||||
        let denominator = plane_normal.dot(self.direction);
 | 
			
		||||
    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: Vec2, plane: Plane2d) -> Option<f32> {
 | 
			
		||||
        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
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    /// Retrieve a point at the given distance along the ray.
 | 
			
		||||
/// 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"),
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// 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<f32> {
 | 
			
		||||
        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!(
 | 
			
		||||
            None,
 | 
			
		||||
            ray.intersect_plane(Vec3::X, Vec3::X + Vec3::Z * f32::EPSILON)
 | 
			
		||||
            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!(
 | 
			
		||||
            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());
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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<Ray> {
 | 
			
		||||
    ) -> Option<Ray3d> {
 | 
			
		||||
        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,
 | 
			
		||||
            })
 | 
			
		||||
        })
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -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);
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user