Skip to content

Commit d9aac88

Browse files
authored
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)); ```
1 parent f683b80 commit d9aac88

File tree

6 files changed

+189
-37
lines changed

6 files changed

+189
-37
lines changed

crates/bevy_math/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ mod ray;
1313
mod rects;
1414

1515
pub use affine3::*;
16-
pub use ray::Ray;
16+
pub use ray::{Ray2d, Ray3d};
1717
pub use rects::*;
1818

1919
/// The `bevy_math` prelude.
@@ -25,8 +25,8 @@ pub mod prelude {
2525
CubicSegment,
2626
},
2727
primitives, BVec2, BVec3, BVec4, EulerRot, IRect, IVec2, IVec3, IVec4, Mat2, Mat3, Mat4,
28-
Quat, Ray, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3, Vec3Swizzles, Vec4,
29-
Vec4Swizzles,
28+
Quat, Ray2d, Ray3d, Rect, URect, UVec2, UVec3, UVec4, Vec2, Vec2Swizzles, Vec3,
29+
Vec3Swizzles, Vec4, Vec4Swizzles,
3030
};
3131
}
3232

crates/bevy_math/src/primitives/dim2.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::Vec2;
33

44
/// A normalized vector pointing in a direction in 2D space
55
#[derive(Clone, Copy, Debug, PartialEq)]
6+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
67
pub struct Direction2d(Vec2);
78

89
impl Direction2d {
@@ -86,6 +87,20 @@ pub struct Plane2d {
8687
}
8788
impl Primitive2d for Plane2d {}
8889

90+
impl Plane2d {
91+
/// Create a new `Plane2d` from a normal
92+
///
93+
/// # Panics
94+
///
95+
/// Panics if the given `normal` is zero (or very close to zero), or non-finite.
96+
#[inline]
97+
pub fn new(normal: Vec2) -> Self {
98+
Self {
99+
normal: Direction2d::new(normal).expect("normal must be nonzero and finite"),
100+
}
101+
}
102+
}
103+
89104
/// An infinite line along a direction in 2D space.
90105
///
91106
/// For a finite line: [`Segment2d`]

crates/bevy_math/src/primitives/dim3.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use crate::Vec3;
33

44
/// A normalized vector pointing in a direction in 3D space
55
#[derive(Clone, Copy, Debug, PartialEq)]
6+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
67
pub struct Direction3d(Vec3);
78

89
impl Direction3d {
@@ -66,6 +67,20 @@ pub struct Plane3d {
6667
}
6768
impl Primitive3d for Plane3d {}
6869

70+
impl Plane3d {
71+
/// Create a new `Plane3d` from a normal
72+
///
73+
/// # Panics
74+
///
75+
/// Panics if the given `normal` is zero (or very close to zero), or non-finite.
76+
#[inline]
77+
pub fn new(normal: Vec3) -> Self {
78+
Self {
79+
normal: Direction3d::new(normal).expect("normal must be nonzero and finite"),
80+
}
81+
}
82+
}
83+
6984
/// An infinite line along a direction in 3D space.
7085
///
7186
/// For a finite line: [`Segment3d`]

crates/bevy_math/src/ray.rs

Lines changed: 143 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,95 @@
1-
use crate::Vec3;
1+
use crate::{
2+
primitives::{Direction2d, Direction3d, Plane2d, Plane3d},
3+
Vec2, Vec3,
4+
};
25

3-
/// A ray is an infinite line starting at `origin`, going in `direction`.
4-
#[derive(Default, Clone, Copy, Debug, PartialEq)]
6+
/// An infinite half-line starting at `origin` and going in `direction` in 2D space.
7+
#[derive(Clone, Copy, Debug, PartialEq)]
58
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
6-
pub struct Ray {
9+
pub struct Ray2d {
710
/// The origin of the ray.
8-
pub origin: Vec3,
9-
/// A normalized vector representing the direction of the ray.
10-
pub direction: Vec3,
11+
pub origin: Vec2,
12+
/// The direction of the ray.
13+
pub direction: Direction2d,
1114
}
1215

13-
impl Ray {
14-
/// Returns the distance to the plane if the ray intersects it.
16+
impl Ray2d {
17+
/// Create a new `Ray2d` from a given origin and direction
18+
///
19+
/// # Panics
20+
///
21+
/// Panics if the given `direction` is zero (or very close to zero), or non-finite.
22+
#[inline]
23+
pub fn new(origin: Vec2, direction: Vec2) -> Self {
24+
Self {
25+
origin,
26+
direction: Direction2d::new(direction)
27+
.expect("ray direction must be nonzero and finite"),
28+
}
29+
}
30+
31+
/// Get a point at a given distance along the ray
32+
#[inline]
33+
pub fn get_point(&self, distance: f32) -> Vec2 {
34+
self.origin + *self.direction * distance
35+
}
36+
37+
/// Get the distance to a plane if the ray intersects it
1538
#[inline]
16-
pub fn intersect_plane(&self, plane_origin: Vec3, plane_normal: Vec3) -> Option<f32> {
17-
let denominator = plane_normal.dot(self.direction);
39+
pub fn intersect_plane(&self, plane_origin: Vec2, plane: Plane2d) -> Option<f32> {
40+
let denominator = plane.normal.dot(*self.direction);
1841
if denominator.abs() > f32::EPSILON {
19-
let distance = (plane_origin - self.origin).dot(plane_normal) / denominator;
42+
let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator;
2043
if distance > f32::EPSILON {
2144
return Some(distance);
2245
}
2346
}
2447
None
2548
}
49+
}
50+
51+
/// An infinite half-line starting at `origin` and going in `direction` in 3D space.
52+
#[derive(Clone, Copy, Debug, PartialEq)]
53+
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
54+
pub struct Ray3d {
55+
/// The origin of the ray.
56+
pub origin: Vec3,
57+
/// The direction of the ray.
58+
pub direction: Direction3d,
59+
}
60+
61+
impl Ray3d {
62+
/// Create a new `Ray3d` from a given origin and direction
63+
///
64+
/// # Panics
65+
///
66+
/// Panics if the given `direction` is zero (or very close to zero), or non-finite.
67+
#[inline]
68+
pub fn new(origin: Vec3, direction: Vec3) -> Self {
69+
Self {
70+
origin,
71+
direction: Direction3d::new(direction)
72+
.expect("ray direction must be nonzero and finite"),
73+
}
74+
}
2675

27-
/// Retrieve a point at the given distance along the ray.
76+
/// Get a point at a given distance along the ray
2877
#[inline]
2978
pub fn get_point(&self, distance: f32) -> Vec3 {
30-
self.origin + self.direction * distance
79+
self.origin + *self.direction * distance
80+
}
81+
82+
/// Get the distance to a plane if the ray intersects it
83+
#[inline]
84+
pub fn intersect_plane(&self, plane_origin: Vec3, plane: Plane3d) -> Option<f32> {
85+
let denominator = plane.normal.dot(*self.direction);
86+
if denominator.abs() > f32::EPSILON {
87+
let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator;
88+
if distance > f32::EPSILON {
89+
return Some(distance);
90+
}
91+
}
92+
None
3193
}
3294
}
3395

@@ -36,29 +98,82 @@ mod tests {
3698
use super::*;
3799

38100
#[test]
39-
fn intersect_plane() {
40-
let ray = Ray {
41-
origin: Vec3::ZERO,
42-
direction: Vec3::Z,
43-
};
101+
fn intersect_plane_2d() {
102+
let ray = Ray2d::new(Vec2::ZERO, Vec2::Y);
44103

45104
// Orthogonal, and test that an inverse plane_normal has the same result
46-
assert_eq!(Some(1.), ray.intersect_plane(Vec3::Z, Vec3::Z));
47-
assert_eq!(Some(1.), ray.intersect_plane(Vec3::Z, Vec3::NEG_Z));
48-
assert_eq!(None, ray.intersect_plane(Vec3::NEG_Z, Vec3::Z));
49-
assert_eq!(None, ray.intersect_plane(Vec3::NEG_Z, Vec3::NEG_Z));
105+
assert_eq!(
106+
ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::Y)),
107+
Some(1.0)
108+
);
109+
assert_eq!(
110+
ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::NEG_Y)),
111+
Some(1.0)
112+
);
113+
assert!(ray
114+
.intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::Y))
115+
.is_none());
116+
assert!(ray
117+
.intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::NEG_Y))
118+
.is_none());
50119

51120
// Diagonal
52-
assert_eq!(Some(1.), ray.intersect_plane(Vec3::Z, Vec3::ONE));
53-
assert_eq!(None, ray.intersect_plane(Vec3::NEG_Z, Vec3::ONE));
121+
assert_eq!(
122+
ray.intersect_plane(Vec2::Y, Plane2d::new(Vec2::ONE)),
123+
Some(1.0)
124+
);
125+
assert!(ray
126+
.intersect_plane(Vec2::NEG_Y, Plane2d::new(Vec2::ONE))
127+
.is_none());
54128

55129
// Parallel
56-
assert_eq!(None, ray.intersect_plane(Vec3::X, Vec3::X));
130+
assert!(ray
131+
.intersect_plane(Vec2::X, Plane2d::new(Vec2::X))
132+
.is_none());
57133

58134
// Parallel with simulated rounding error
135+
assert!(ray
136+
.intersect_plane(Vec2::X, Plane2d::new(Vec2::X + Vec2::Y * f32::EPSILON))
137+
.is_none());
138+
}
139+
140+
#[test]
141+
fn intersect_plane_3d() {
142+
let ray = Ray3d::new(Vec3::ZERO, Vec3::Z);
143+
144+
// Orthogonal, and test that an inverse plane_normal has the same result
145+
assert_eq!(
146+
ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::Z)),
147+
Some(1.0)
148+
);
149+
assert_eq!(
150+
ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::NEG_Z)),
151+
Some(1.0)
152+
);
153+
assert!(ray
154+
.intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::Z))
155+
.is_none());
156+
assert!(ray
157+
.intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::NEG_Z))
158+
.is_none());
159+
160+
// Diagonal
59161
assert_eq!(
60-
None,
61-
ray.intersect_plane(Vec3::X, Vec3::X + Vec3::Z * f32::EPSILON)
162+
ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::ONE)),
163+
Some(1.0)
62164
);
165+
assert!(ray
166+
.intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::ONE))
167+
.is_none());
168+
169+
// Parallel
170+
assert!(ray
171+
.intersect_plane(Vec3::X, Plane3d::new(Vec3::X))
172+
.is_none());
173+
174+
// Parallel with simulated rounding error
175+
assert!(ray
176+
.intersect_plane(Vec3::X, Plane3d::new(Vec3::X + Vec3::Z * f32::EPSILON))
177+
.is_none());
63178
}
64179
}

crates/bevy_render/src/camera/camera.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ use bevy_ecs::{
2020
system::{Commands, Query, Res, ResMut, Resource},
2121
};
2222
use bevy_log::warn;
23-
use bevy_math::{vec2, Mat4, Ray, Rect, URect, UVec2, UVec4, Vec2, Vec3};
23+
use bevy_math::{
24+
primitives::Direction3d, vec2, Mat4, Ray3d, Rect, URect, UVec2, UVec4, Vec2, Vec3,
25+
};
2426
use bevy_reflect::prelude::*;
2527
use bevy_transform::components::GlobalTransform;
2628
use bevy_utils::{HashMap, HashSet};
@@ -272,7 +274,7 @@ impl Camera {
272274
&self,
273275
camera_transform: &GlobalTransform,
274276
mut viewport_position: Vec2,
275-
) -> Option<Ray> {
277+
) -> Option<Ray3d> {
276278
let target_size = self.logical_viewport_size()?;
277279
// Flip the Y co-ordinate origin from the top to the bottom.
278280
viewport_position.y = target_size.y - viewport_position.y;
@@ -284,9 +286,12 @@ impl Camera {
284286
// Using EPSILON because an ndc with Z = 0 returns NaNs.
285287
let world_far_plane = ndc_to_world.project_point3(ndc.extend(f32::EPSILON));
286288

287-
(!world_near_plane.is_nan() && !world_far_plane.is_nan()).then_some(Ray {
288-
origin: world_near_plane,
289-
direction: (world_far_plane - world_near_plane).normalize(),
289+
// The fallible direction constructor ensures that world_near_plane and world_far_plane aren't NaN.
290+
Direction3d::new(world_far_plane - world_near_plane).map_or(None, |direction| {
291+
Some(Ray3d {
292+
origin: world_near_plane,
293+
direction,
294+
})
290295
})
291296
}
292297

examples/3d/3d_viewport_to_world.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ fn draw_cursor(
2929
};
3030

3131
// Calculate if and where the ray is hitting the ground plane.
32-
let Some(distance) = ray.intersect_plane(ground.translation(), ground.up()) else {
32+
let Some(distance) =
33+
ray.intersect_plane(ground.translation(), primitives::Plane3d::new(ground.up()))
34+
else {
3335
return;
3436
};
3537
let point = ray.get_point(distance);

0 commit comments

Comments
 (0)