Skip to content

Commit

Permalink
Split Ray into Ray2d and Ray3d and simplify plane construction (#…
Browse files Browse the repository at this point in the history
…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));
```
  • Loading branch information
Jondolf authored Dec 6, 2023
1 parent f683b80 commit d9aac88
Show file tree
Hide file tree
Showing 6 changed files with 189 additions and 37 deletions.
6 changes: 3 additions & 3 deletions crates/bevy_math/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
};
}

Expand Down
15 changes: 15 additions & 0 deletions crates/bevy_math/src/primitives/dim2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`]
Expand Down
15 changes: 15 additions & 0 deletions crates/bevy_math/src/primitives/dim3.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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`]
Expand Down
171 changes: 143 additions & 28 deletions crates/bevy_math/src/ray.rs
Original file line number Diff line number Diff line change
@@ -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<f32> {
let denominator = plane_normal.dot(self.direction);
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
}
}

/// 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<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
}
}

Expand All @@ -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());
}
}
15 changes: 10 additions & 5 deletions crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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;
Expand All @@ -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,
})
})
}

Expand Down
4 changes: 3 additions & 1 deletion examples/3d/3d_viewport_to_world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down

0 comments on commit d9aac88

Please sign in to comment.