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:
Joona Aalto 2023-12-06 16:09:04 +02:00 committed by GitHub
parent f683b802f1
commit d9aac887b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 189 additions and 37 deletions

View File

@ -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,
};
}

View File

@ -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`]

View File

@ -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`]

View File

@ -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());
}
}

View File

@ -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,
})
})
}

View File

@ -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);