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