diff --git a/crates/bevy_gizmos/src/primitives/dim3.rs b/crates/bevy_gizmos/src/primitives/dim3.rs index 66924fbb3f007..f5d9d29e7fcf1 100644 --- a/crates/bevy_gizmos/src/primitives/dim3.rs +++ b/crates/bevy_gizmos/src/primitives/dim3.rs @@ -6,7 +6,7 @@ use std::f32::consts::TAU; use bevy_color::Color; use bevy_math::primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, Line3d, Plane3d, - Polyline3d, Primitive3d, Segment3d, Sphere, Tetrahedron, Torus, + Polyline3d, Primitive3d, Ramp, Segment3d, Sphere, Tetrahedron, Torus, }; use bevy_math::{Dir3, Quat, Vec3}; @@ -1079,6 +1079,53 @@ where } } +// ramp + +impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { + type Output<'a> = () where Self: 'a; + + fn primitive_3d( + &mut self, + primitive: Ramp, + position: Vec3, + rotation: Quat, + color: impl Into, + ) -> Self::Output<'_> { + if !self.enabled { + return; + } + + // transform the points from the reference unit cube-like ramp to the actual ramp coords + let [a, b, c, d, e, f] = [ + Vec3::new(-1.0, -1.0, -1.0), + Vec3::new(-1.0, -1.0, 1.0), + Vec3::new(1.0, -1.0, -1.0), + Vec3::new(1.0, -1.0, 1.0), + Vec3::new(1.0, 1.0, 1.0), + Vec3::new(-1.0, 1.0, 1.0), + ] + .map(|s| s * primitive.half_size) + .map(rotate_then_translate_3d(rotation, position)); + + let lines = vec![ + (a, b), + (b, d), + (d, c), + (c, a), + (b, f), + (d, e), + (e, f), + (a, f), + (c, e), + ]; + + let color = color.into(); + lines.into_iter().for_each(|(start, end)| { + self.line(start, end, color); + }); + } +} + // tetrahedron impl<'w, 's, T: GizmoConfigGroup> GizmoPrimitive3d for Gizmos<'w, 's, T> { diff --git a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs index 4c82b0f387321..2daba781c3fc2 100644 --- a/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs +++ b/crates/bevy_math/src/bounding/bounded3d/primitive_impls.rs @@ -4,7 +4,7 @@ use crate::{ bounding::{Bounded2d, BoundingCircle}, primitives::{ BoxedPolyline3d, Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, - Line3d, Polyline3d, Segment3d, Sphere, Torus, Triangle2d, Triangle3d, + Line3d, Polyline3d, Ramp, Segment3d, Sphere, Torus, Triangle2d, Triangle3d, }, Dir3, Mat3, Quat, Vec2, Vec3, }; @@ -299,6 +299,23 @@ impl Bounded3d for Torus { BoundingSphere::new(translation, self.outer_radius()) } } +impl Bounded3d for Ramp { + fn aabb_3d(&self, translation: Vec3, rotation: Quat) -> Aabb3d { + let points = [ + self.half_size, + self.half_size * Vec3::new(1.0, -1.0, 1.0), + self.half_size * Vec3::new(1.0, -1.0, -1.0), + self.half_size * Vec3::new(-1.0, 1.0, 1.0), + self.half_size * Vec3::new(-1.0, -1.0, 1.0), + self.half_size * Vec3::NEG_ONE, + ]; + Aabb3d::from_point_cloud(translation, rotation, points.into_iter()) + } + + fn bounding_sphere(&self, translation: Vec3, _rotation: Quat) -> BoundingSphere { + BoundingSphere::new(translation, self.half_size.length()) + } +} impl Bounded3d for Triangle3d { /// Get the bounding box of the triangle. @@ -361,7 +378,7 @@ mod tests { bounding::Bounded3d, primitives::{ Capsule3d, Cone, ConicalFrustum, Cuboid, Cylinder, InfinitePlane3d, Line3d, Polyline3d, - Segment3d, Sphere, Torus, + Ramp, Segment3d, Sphere, Torus, }, Dir3, }; @@ -607,4 +624,18 @@ mod tests { assert_eq!(bounding_sphere.center, translation.into()); assert_eq!(bounding_sphere.radius(), 1.5); } + + #[test] + fn ramp() { + let ramp = Ramp::new(1.0, 3.0, 4.0); + let translation = Vec3::new(-3.0, 1.75, 0.0); + + let aabb = ramp.aabb_3d(translation, Quat::IDENTITY); + assert_eq!(aabb.min, Vec3A::new(-3.5, 0.25, -2.0)); + assert_eq!(aabb.max, Vec3A::new(-2.5, 3.25, 2.0)); + + let bounding_sphere = ramp.bounding_sphere(translation, Quat::IDENTITY); + assert_eq!(bounding_sphere.center, translation.into()); + assert_eq!(bounding_sphere.radius(), 2.5495098); + } } diff --git a/crates/bevy_math/src/primitives/dim3.rs b/crates/bevy_math/src/primitives/dim3.rs index b3308acbf8089..6218c65224def 100644 --- a/crates/bevy_math/src/primitives/dim3.rs +++ b/crates/bevy_math/src/primitives/dim3.rs @@ -697,6 +697,75 @@ impl Torus { } } +/// A Ramp primitive. +/// The ramp will slant down along the Y axis towards the negative-Z axis. +#[derive(Clone, Copy, Debug, PartialEq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +pub struct Ramp { + /// Half of the width, height and depth of the ramp + pub half_size: Vec3, +} +impl Primitive3d for Ramp {} + +impl Default for Ramp { + /// Returns the default [`Ramp`] with a width, height, and depth of `1.0`. + fn default() -> Self { + Self { + half_size: Vec3::splat(0.5), + } + } +} + +impl Ramp { + /// Create a new `Ramp` from a full x, y, and z length + #[inline(always)] + pub fn new(x_length: f32, y_length: f32, z_length: f32) -> Self { + Self::from_size(Vec3::new(x_length, y_length, z_length)) + } + + /// Create a new `Ramp` from a given full size + #[inline(always)] + pub fn from_size(size: Vec3) -> Self { + Self { + half_size: size / 2.0, + } + } + + /// Create a new `Ramp` from a given full base size and an `incline`. + /// The `incline` is the angle between the Z axis and the slanted face of the ramp. + /// The `incline` is in radians. + pub fn from_incline(x_length: f32, z_length: f32, incline: f32) -> Self { + let y_length = incline.tan() * z_length; + Self::from_size(Vec3::new(x_length, y_length, z_length)) + } + + /// Get the surface area of the ramp. + #[inline(always)] + pub fn area(&self) -> f32 { + (self.half_size.x + * (self.half_size.z + + self.half_size.y + + (self.half_size.z.powi(2) + self.half_size.y.powi(2)).sqrt()) + + self.half_size.z * self.half_size.y) + * 4. + } + + /// Get the volume of the ramp. + #[inline(always)] + pub fn volume(&self) -> f32 { + 4.0 * self.half_size.element_product() + } + + /// Get the incline of the ramp. + /// + /// The `incline` is the angle between the Z axis and the slanted face of the ramp. + /// The `incline` is in radians. + #[inline(always)] + pub fn incline(&self) -> f32 { + self.half_size.y.atan2(self.half_size.z) + } +} + /// A 3D triangle primitive. #[derive(Clone, Copy, Debug, PartialEq)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] @@ -1120,6 +1189,29 @@ mod tests { assert_relative_eq!(Tetrahedron::default().centroid(), Vec3::ZERO); } + #[test] + fn ramp_math() { + use std::f32::consts::FRAC_PI_4; + let ramp = Ramp { + half_size: Vec3::new(1.9, 3.2, 7.45), + }; + + assert_eq!(ramp.area(), 237.92213, "incorrect area"); + assert_eq!(ramp.volume(), 181.18399, "incorrect volume"); + assert_eq!(ramp.incline(), 0.40570152, "incorrect incline"); + + assert_eq!(Ramp::default().area(), 4.4142136, "incorrect area"); + assert_eq!(Ramp::default().volume(), 0.5, "incorrect volume"); + assert_eq!(Ramp::default().incline(), FRAC_PI_4, "incorrect incline"); + + let ramp = Ramp::from_incline(2.4, 3.8, 0.12); + assert_eq!( + ramp.half_size, + Vec3::new(1.2, 0.22910073, 1.9), + "incorrect from_incline" + ); + } + #[test] fn triangle_math() { let [a, b, c] = [Vec3::ZERO, Vec3::new(1., 1., 0.5), Vec3::new(-3., 2.5, 1.)]; diff --git a/crates/bevy_reflect/src/impls/math/primitives3d.rs b/crates/bevy_reflect/src/impls/math/primitives3d.rs index 47d9ecbb60d0c..74c4a944a76e8 100644 --- a/crates/bevy_reflect/src/impls/math/primitives3d.rs +++ b/crates/bevy_reflect/src/impls/math/primitives3d.rs @@ -122,3 +122,11 @@ impl_reflect!( vertices: [Vec3; 4], } ); + +impl_reflect!( + #[reflect(Debug, PartialEq, Serialize, Deserialize)] + #[type_path = "bevy_math::primitives"] + struct Ramp { + half_size: Vec3, + } +); diff --git a/crates/bevy_render/src/mesh/primitives/dim3/mod.rs b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs index 3f98557f70a72..af9e9f48f469a 100644 --- a/crates/bevy_render/src/mesh/primitives/dim3/mod.rs +++ b/crates/bevy_render/src/mesh/primitives/dim3/mod.rs @@ -2,6 +2,7 @@ mod capsule; mod cuboid; mod cylinder; mod plane; +mod ramp; mod sphere; mod torus; pub(crate) mod triangle3d; diff --git a/crates/bevy_render/src/mesh/primitives/dim3/ramp.rs b/crates/bevy_render/src/mesh/primitives/dim3/ramp.rs new file mode 100644 index 0000000000000..19ecc43a8e596 --- /dev/null +++ b/crates/bevy_render/src/mesh/primitives/dim3/ramp.rs @@ -0,0 +1,72 @@ +use bevy_math::{primitives::Ramp, Vec3}; +use wgpu::PrimitiveTopology; + +use crate::{ + mesh::{Indices, Mesh, Meshable}, + render_asset::RenderAssetUsages, +}; + +impl Meshable for Ramp { + type Output = Mesh; + + fn mesh(&self) -> Self::Output { + let min = -self.half_size; + let max = self.half_size; + + let top_normal = Vec3::new(0.0, min.z, max.y).normalize_or_zero().to_array(); + + // Suppose Y-up right hand, and camera look from +Z to -Z + let vertices = &[ + // Slope + ([min.x, max.y, max.z], top_normal, [1.0, 0.0]), + ([max.x, max.y, max.z], top_normal, [0.0, 0.0]), + ([max.x, min.y, min.z], top_normal, [0.0, 1.0]), + ([min.x, min.y, min.z], top_normal, [1.0, 1.0]), + // Right + ([max.x, min.y, min.z], [1.0, 0.0, 0.0], [0.0, 0.0]), + ([max.x, max.y, max.z], [1.0, 0.0, 0.0], [1.0, 1.0]), + ([max.x, min.y, max.z], [1.0, 0.0, 0.0], [0.0, 1.0]), + // Left + ([min.x, min.y, max.z], [-1.0, 0.0, 0.0], [1.0, 0.0]), + ([min.x, max.y, max.z], [-1.0, 0.0, 0.0], [0.0, 0.0]), + ([min.x, min.y, min.z], [-1.0, 0.0, 0.0], [1.0, 1.0]), + // Bottom + ([max.x, min.y, max.z], [0.0, -1.0, 0.0], [0.0, 0.0]), + ([min.x, min.y, max.z], [0.0, -1.0, 0.0], [1.0, 0.0]), + ([min.x, min.y, min.z], [0.0, -1.0, 0.0], [1.0, 1.0]), + ([max.x, min.y, min.z], [0.0, -1.0, 0.0], [0.0, 1.0]), + // Front + ([min.x, max.y, max.z], [0.0, 0.0, 1.0], [0.0, 1.0]), + ([max.x, max.y, max.z], [0.0, 0.0, 1.0], [1.0, 1.0]), + ([max.x, min.y, max.z], [0.0, 0.0, 1.0], [1.0, 0.0]), + ([min.x, min.y, max.z], [0.0, 0.0, 1.0], [0.0, 0.0]), + ]; + + let positions: Vec<_> = vertices.iter().map(|(p, _, _)| *p).collect(); + let normals: Vec<_> = vertices.iter().map(|(_, n, _)| *n).collect(); + let uvs: Vec<_> = vertices.iter().map(|(_, _, uv)| *uv).collect(); + + let indices = Indices::U32(vec![ + 0, 1, 2, 2, 3, 0, // slope + 4, 5, 6, // right + 7, 8, 9, // left + 10, 11, 12, 12, 13, 10, // bottom + 14, 16, 15, 16, 14, 17, // front + ]); + + Mesh::new( + PrimitiveTopology::TriangleList, + RenderAssetUsages::default(), + ) + .with_inserted_attribute(Mesh::ATTRIBUTE_POSITION, positions) + .with_inserted_attribute(Mesh::ATTRIBUTE_NORMAL, normals) + .with_inserted_attribute(Mesh::ATTRIBUTE_UV_0, uvs) + .with_inserted_indices(indices) + } +} + +impl From for Mesh { + fn from(ramp: Ramp) -> Self { + ramp.mesh() + } +} diff --git a/examples/3d/3d_shapes.rs b/examples/3d/3d_shapes.rs index 41336571ae5e6..56e41e5692198 100644 --- a/examples/3d/3d_shapes.rs +++ b/examples/3d/3d_shapes.rs @@ -42,6 +42,7 @@ fn setup( meshes.add(Capsule3d::default()), meshes.add(Torus::default()), meshes.add(Cylinder::default()), + meshes.add(Ramp::default()), meshes.add(Sphere::default().mesh().ico(5).unwrap()), meshes.add(Sphere::default().mesh().uv(32, 18)), ];