diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 9f1f38890a..025b2f74fa 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -134,18 +134,18 @@ impl Drop for SphereBuilder<'_, '_, '_, T> { // plane 3d -/// Builder for configuring the drawing options of [`Sphere`]. +/// Builder for configuring the drawing options of [`Plane3d`]. pub struct Plane3dBuilder<'a, 'w, 's, T: GizmoConfigGroup> { gizmos: &'a mut Gizmos<'w, 's, T>, // direction of the normal orthogonal to the plane normal: Dir3, - // Rotation of the sphere around the origin in 3D space + // Rotation of the plane around the origin in 3D space rotation: Quat, - // Center position of the sphere in 3D space + // Center position of the plane in 3D space position: Vec3, - // Color of the sphere + // Color of the plane color: Color, // Number of axis to hint the plane diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index 48363ee896..8dec29c23a 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -3,8 +3,8 @@ use crate::{ bounding::{Bounded2d, BoundingCircle}, primitives::{ - BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, - Polyline3d, Segment3d, Sphere, Torus, Triangle2d, + BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, + Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, }, Dir3, Mat3, Quat, Vec2, Vec3, }; @@ -21,7 +21,7 @@ impl Bounded3d for Sphere { } } -impl Bounded3d for Plane3d { +impl Bounded3d for InfinitePlane3d { fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { let normal = rotation * *self.normal; let facing_x = normal == Vec3::X || normal == Vec3::NEG_X; @@ -310,7 +310,7 @@ mod tests { use crate::{ bounding::Bounded3d, primitives::{ - Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, Polyline3d, + Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, Segment3d, Sphere, Torus, }, Dir3, @@ -334,23 +334,24 @@ mod tests { fn plane() { let translation = Vec3::new(2.0, 1.0, 0.0); - let aabb1 = Plane3d::new(Vec3::X).aabb_3d(translation, Quat::IDENTITY); + let aabb1 = InfinitePlane3d::new(Vec3::X).aabb_3d(translation, Quat::IDENTITY); assert_eq!(aabb1.min, Vec3::new(2.0, -f32::MAX / 2.0, -f32::MAX / 2.0)); assert_eq!(aabb1.max, Vec3::new(2.0, f32::MAX / 2.0, f32::MAX / 2.0)); - let aabb2 = Plane3d::new(Vec3::Y).aabb_3d(translation, Quat::IDENTITY); + let aabb2 = InfinitePlane3d::new(Vec3::Y).aabb_3d(translation, Quat::IDENTITY); assert_eq!(aabb2.min, Vec3::new(-f32::MAX / 2.0, 1.0, -f32::MAX / 2.0)); assert_eq!(aabb2.max, Vec3::new(f32::MAX / 2.0, 1.0, f32::MAX / 2.0)); - let aabb3 = Plane3d::new(Vec3::Z).aabb_3d(translation, Quat::IDENTITY); + let aabb3 = InfinitePlane3d::new(Vec3::Z).aabb_3d(translation, Quat::IDENTITY); assert_eq!(aabb3.min, Vec3::new(-f32::MAX / 2.0, -f32::MAX / 2.0, 0.0)); assert_eq!(aabb3.max, Vec3::new(f32::MAX / 2.0, f32::MAX / 2.0, 0.0)); - let aabb4 = Plane3d::new(Vec3::ONE).aabb_3d(translation, Quat::IDENTITY); + let aabb4 = InfinitePlane3d::new(Vec3::ONE).aabb_3d(translation, Quat::IDENTITY); assert_eq!(aabb4.min, Vec3::splat(-f32::MAX / 2.0)); assert_eq!(aabb4.max, Vec3::splat(f32::MAX / 2.0)); - let bounding_sphere = Plane3d::new(Vec3::Y).bounding_sphere(translation, Quat::IDENTITY); + let bounding_sphere = + InfinitePlane3d::new(Vec3::Y).bounding_sphere(translation, Quat::IDENTITY); assert_eq!(bounding_sphere.center, translation); assert_eq!(bounding_sphere.radius(), f32::MAX / 2.0); } diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index 92afa9f050..a974f61d60 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -3,7 +3,7 @@ use std::f32::consts::{FRAC_PI_3, PI}; use super::{Circle, Primitive3d}; use crate::{ bounding::{Aabb3d, Bounded3d, BoundingSphere}, - Dir3, InvalidDirectionError, Mat3, Quat, Vec3, + Dir3, InvalidDirectionError, Mat3, Quat, Vec2, Vec3, }; /// A sphere primitive @@ -67,33 +67,38 @@ impl Sphere { } } -/// An unbounded plane in 3D space. It forms a separating surface through the origin, -/// stretching infinitely far +/// A bounded plane in 3D space. It forms a surface starting from the origin with a defined height and width. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct Plane3d { /// The normal of the plane. The plane will be placed perpendicular to this direction pub normal: Dir3, + /// Half of the width and height of the plane + pub half_size: Vec2, } impl Primitive3d for Plane3d {} impl Default for Plane3d { - /// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction. + /// Returns the default [`Plane3d`] with a normal pointing in the `+Y` direction, width and height of `1.0`. fn default() -> Self { - Self { normal: Dir3::Y } + Self { + normal: Dir3::Y, + half_size: Vec2::splat(0.5), + } } } impl Plane3d { - /// Create a new `Plane3d` from a normal + /// Create a new `Plane3d` from a normal and a half size /// /// # Panics /// /// Panics if the given `normal` is zero (or very close to zero), or non-finite. #[inline(always)] - pub fn new(normal: Vec3) -> Self { + pub fn new(normal: Vec3, half_size: Vec2) -> Self { Self { normal: Dir3::new(normal).expect("normal must be nonzero and finite"), + half_size, } } @@ -109,8 +114,71 @@ impl Plane3d { /// are *collinear* and lie on the same line. #[inline(always)] pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) { - let normal = Dir3::new((b - a).cross(c - a)) - .expect("plane must be defined by three finite points that don't lie on the same line"); + let normal = Dir3::new((b - a).cross(c - a)).expect( + "finite plane must be defined by three finite points that don't lie on the same line", + ); + let translation = (a + b + c) / 3.0; + + ( + Self { + normal, + ..Default::default() + }, + translation, + ) + } +} + +/// An unbounded plane in 3D space. It forms a separating surface through the origin, +/// stretching infinitely far +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct InfinitePlane3d { + /// The normal of the plane. The plane will be placed perpendicular to this direction + pub normal: Dir3, +} +impl Primitive3d for InfinitePlane3d {} + +impl Default for InfinitePlane3d { + /// Returns the default [`InfinitePlane3d`] with a normal pointing in the `+Y` direction. + fn default() -> Self { + Self { normal: Dir3::Y } + } +} + +impl InfinitePlane3d { + /// Create a new `InfinitePlane3d` from a normal + /// + /// # Panics + /// + /// Panics if the given `normal` is zero (or very close to zero), or non-finite. + #[inline(always)] + pub fn new>(normal: T) -> Self + where + >::Error: std::fmt::Debug, + { + Self { + normal: normal + .try_into() + .expect("normal must be nonzero and finite"), + } + } + + /// Create a new `InfinitePlane3d` based on three points and compute the geometric center + /// of those points. + /// + /// The direction of the plane normal is determined by the winding order + /// of the triangular shape formed by the points. + /// + /// # Panics + /// + /// Panics if a valid normal can not be computed, for example when the points + /// are *collinear* and lie on the same line. + #[inline(always)] + pub fn from_points(a: Vec3, b: Vec3, c: Vec3) -> (Self, Vec3) { + let normal = Dir3::new((b - a).cross(c - a)).expect( + "infinite plane must be defined by three finite points that don't lie on the same line", + ); let translation = (a + b + c) / 3.0; (Self { normal }, translation) @@ -975,6 +1043,14 @@ mod tests { fn plane_from_points() { let (plane, translation) = Plane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); + assert_eq!(plane.half_size, Vec2::new(0.5, 0.5), "incorrect half size"); + assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation"); + } + + #[test] + fn infinite_plane_from_points() { + let (plane, translation) = InfinitePlane3d::from_points(Vec3::X, Vec3::Z, Vec3::NEG_X); + assert_eq!(*plane.normal, Vec3::NEG_Y, "incorrect normal"); assert_eq!(translation, Vec3::Z * 0.33333334, "incorrect translation"); } diff --git a/crates/bevy_math/src/ray.rs b/crates/bevy_math/src/ray.rs index d615fbd7ec..2e86f8c18b 100644 --- a/crates/bevy_math/src/ray.rs +++ b/crates/bevy_math/src/ray.rs @@ -1,5 +1,5 @@ use crate::{ - primitives::{Plane2d, Plane3d}, + primitives::{InfinitePlane3d, Plane2d}, Dir2, Dir3, Vec2, Vec3, }; @@ -79,7 +79,7 @@ impl Ray3d { /// Get the distance to a plane if the ray intersects it #[inline] - pub fn intersect_plane(&self, plane_origin: Vec3, plane: Plane3d) -> Option { + pub fn intersect_plane(&self, plane_origin: Vec3, plane: InfinitePlane3d) -> Option { let denominator = plane.normal.dot(*self.direction); if denominator.abs() > f32::EPSILON { let distance = (plane_origin - self.origin).dot(*plane.normal) / denominator; @@ -141,37 +141,40 @@ mod tests { // Orthogonal, and test that an inverse plane_normal has the same result assert_eq!( - ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::Z)), + ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::Z)), Some(1.0) ); assert_eq!( - ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::NEG_Z)), + ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::NEG_Z)), Some(1.0) ); assert!(ray - .intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::Z)) + .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::Z)) .is_none()); assert!(ray - .intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::NEG_Z)) + .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::NEG_Z)) .is_none()); // Diagonal assert_eq!( - ray.intersect_plane(Vec3::Z, Plane3d::new(Vec3::ONE)), + ray.intersect_plane(Vec3::Z, InfinitePlane3d::new(Vec3::ONE)), Some(1.0) ); assert!(ray - .intersect_plane(Vec3::NEG_Z, Plane3d::new(Vec3::ONE)) + .intersect_plane(Vec3::NEG_Z, InfinitePlane3d::new(Vec3::ONE)) .is_none()); // Parallel assert!(ray - .intersect_plane(Vec3::X, Plane3d::new(Vec3::X)) + .intersect_plane(Vec3::X, InfinitePlane3d::new(Vec3::X)) .is_none()); // Parallel with simulated rounding error assert!(ray - .intersect_plane(Vec3::X, Plane3d::new(Vec3::X + Vec3::Z * f32::EPSILON)) + .intersect_plane( + Vec3::X, + InfinitePlane3d::new(Vec3::X + Vec3::Z * f32::EPSILON) + ) .is_none()); } } diff --git a/crates/bevy_reflect/src/impls/math/primitives3d.rs b/crates/bevy_reflect/src/impls/math/primitives3d.rs index 6b6f773a17..47d9ecbb60 100644 --- a/crates/bevy_reflect/src/impls/math/primitives3d.rs +++ b/crates/bevy_reflect/src/impls/math/primitives3d.rs @@ -1,6 +1,6 @@ use crate as bevy_reflect; use crate::{ReflectDeserialize, ReflectSerialize}; -use bevy_math::{primitives::*, Dir3, Vec3}; +use bevy_math::{primitives::*, Dir3, Vec2, Vec3}; use bevy_reflect_derive::impl_reflect; impl_reflect!( @@ -16,6 +16,15 @@ impl_reflect!( #[type_path = "bevy_math::primitives"] struct Plane3d { normal: Dir3, + half_size: Vec2, + } +); + +impl_reflect!( + #[reflect(Debug, PartialEq, Serialize, Deserialize)] + #[type_path = "bevy_math::primitives"] + struct InfinitePlane3d { + normal: Dir3, } ); diff --git a/crates/bevy_render/src/mesh/primitives/dim3/plane.rs b/crates/bevy_render/src/mesh/primitives/dim3/plane.rs index b00b498038..7012a48446 100644 --- a/crates/bevy_render/src/mesh/primitives/dim3/plane.rs +++ b/crates/bevy_render/src/mesh/primitives/dim3/plane.rs @@ -7,21 +7,10 @@ use crate::{ }; /// A builder used for creating a [`Mesh`] with a [`Plane3d`] shape. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Default)] pub struct PlaneMeshBuilder { /// The [`Plane3d`] shape. pub plane: Plane3d, - /// Half the size of the plane mesh. - pub half_size: Vec2, -} - -impl Default for PlaneMeshBuilder { - fn default() -> Self { - Self { - plane: Plane3d::default(), - half_size: Vec2::ONE, - } - } } impl PlaneMeshBuilder { @@ -29,8 +18,10 @@ impl PlaneMeshBuilder { #[inline] pub fn new(normal: Dir3, size: Vec2) -> Self { Self { - plane: Plane3d { normal }, - half_size: size / 2.0, + plane: Plane3d { + normal, + half_size: size / 2.0, + }, } } @@ -38,8 +29,10 @@ impl PlaneMeshBuilder { #[inline] pub fn from_size(size: Vec2) -> Self { Self { - half_size: size / 2.0, - ..Default::default() + plane: Plane3d { + half_size: size / 2.0, + ..Default::default() + }, } } @@ -48,8 +41,10 @@ impl PlaneMeshBuilder { #[inline] pub fn from_length(length: f32) -> Self { Self { - half_size: Vec2::splat(length) / 2.0, - ..Default::default() + plane: Plane3d { + half_size: Vec2::splat(length) / 2.0, + ..Default::default() + }, } } @@ -57,14 +52,17 @@ impl PlaneMeshBuilder { #[inline] #[doc(alias = "facing")] pub fn normal(mut self, normal: Dir3) -> Self { - self.plane = Plane3d { normal }; + self.plane = Plane3d { + normal, + ..self.plane + }; self } /// Sets the size of the plane mesh. #[inline] pub fn size(mut self, width: f32, height: f32) -> Self { - self.half_size = Vec2::new(width, height) / 2.0; + self.plane.half_size = Vec2::new(width, height) / 2.0; self } @@ -72,10 +70,10 @@ impl PlaneMeshBuilder { pub fn build(&self) -> Mesh { let rotation = Quat::from_rotation_arc(Vec3::Y, *self.plane.normal); let positions = vec![ - rotation * Vec3::new(self.half_size.x, 0.0, -self.half_size.y), - rotation * Vec3::new(-self.half_size.x, 0.0, -self.half_size.y), - rotation * Vec3::new(-self.half_size.x, 0.0, self.half_size.y), - rotation * Vec3::new(self.half_size.x, 0.0, self.half_size.y), + rotation * Vec3::new(self.plane.half_size.x, 0.0, -self.plane.half_size.y), + rotation * Vec3::new(-self.plane.half_size.x, 0.0, -self.plane.half_size.y), + rotation * Vec3::new(-self.plane.half_size.x, 0.0, self.plane.half_size.y), + rotation * Vec3::new(self.plane.half_size.x, 0.0, self.plane.half_size.y), ]; let normals = vec![self.plane.normal.to_array(); 4]; @@ -97,10 +95,7 @@ impl Meshable for Plane3d { type Output = PlaneMeshBuilder; fn mesh(&self) -> Self::Output { - PlaneMeshBuilder { - plane: *self, - ..Default::default() - } + PlaneMeshBuilder { plane: *self } } } diff --git a/examples/3d/3d_viewport_to_world.rs b/examples/3d/3d_viewport_to_world.rs index 8a3aed6e5b..0347bc86b7 100644 --- a/examples/3d/3d_viewport_to_world.rs +++ b/examples/3d/3d_viewport_to_world.rs @@ -29,7 +29,8 @@ fn draw_cursor( }; // Calculate if and where the ray is hitting the ground plane. - let Some(distance) = ray.intersect_plane(ground.translation(), Plane3d::new(ground.up())) + let Some(distance) = + ray.intersect_plane(ground.translation(), InfinitePlane3d::new(ground.up())) else { return; }; diff --git a/examples/3d/irradiance_volumes.rs b/examples/3d/irradiance_volumes.rs index 24addb02ad..7ae029f22b 100644 --- a/examples/3d/irradiance_volumes.rs +++ b/examples/3d/irradiance_volumes.rs @@ -499,7 +499,7 @@ fn handle_mouse_clicks( let Some(ray) = camera.viewport_to_world(camera_transform, mouse_position) else { return; }; - let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, Plane3d::new(Vec3::Y)) else { + let Some(ray_distance) = ray.intersect_plane(Vec3::ZERO, InfinitePlane3d::new(Vec3::Y)) else { return; }; let plane_intersection = ray.origin + ray.direction.normalize() * ray_distance; diff --git a/examples/math/render_primitives.rs b/examples/math/render_primitives.rs index 7e460aaa0b..d189bc787a 100644 --- a/examples/math/render_primitives.rs +++ b/examples/math/render_primitives.rs @@ -166,7 +166,10 @@ const TRIANGLE: Triangle2d = Triangle2d { }; const PLANE_2D: Plane2d = Plane2d { normal: Dir2::Y }; -const PLANE_3D: Plane3d = Plane3d { normal: Dir3::Y }; +const PLANE_3D: Plane3d = Plane3d { + normal: Dir3::Y, + half_size: Vec2::new(BIG_3D, BIG_3D), +}; const LINE2D: Line2d = Line2d { direction: Dir2::X }; const LINE3D: Line3d = Line3d { direction: Dir3::X }; diff --git a/examples/stress_tests/many_cubes.rs b/examples/stress_tests/many_cubes.rs index 510a83612a..8d583d0535 100644 --- a/examples/stress_tests/many_cubes.rs +++ b/examples/stress_tests/many_cubes.rs @@ -388,6 +388,7 @@ fn init_meshes(args: &Args, assets: &mut Assets) -> Vec<(Handle, Tra assets.add( Plane3d { normal: Dir3::NEG_Z, + half_size: Vec2::splat(0.5), } .mesh() .size(radius, radius),