-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add geometric primitives #1621
Add geometric primitives #1621
Changes from 8 commits
f87cc5c
5e1e472
5917041
557c31a
e2fd3d2
a9b5072
432138f
45ba723
feab0c8
f369c50
924cc5a
e967a83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[package] | ||
name = "bevy_geometry" | ||
version = "0.4.0" | ||
authors = [ | ||
"Bevy Contributors <bevyengine@gmail.com>", | ||
"Aevyrie Roessler <aevyrie@gmail.com>", | ||
] | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
bevy_transform = { path = "../bevy_transform", version = "0.4.0" } | ||
bevy_math = { path = "../bevy_math", version = "0.4.0" } | ||
bevy_reflect = { path = "../bevy_reflect", version = "0.4.0", features = ["bevy"] } |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,359 @@ | ||
use bevy_math::*; | ||
use bevy_reflect::Reflect; | ||
use std::error::Error; | ||
use std::fmt; | ||
|
||
pub trait Primitive3d { | ||
/// Returns true if this primitive is entirely on the outside (in the normal direction) of the | ||
/// supplied plane. | ||
fn outside_plane(&self, plane: Plane) -> bool; | ||
} | ||
|
||
#[derive(Debug, Clone)] | ||
pub enum PrimitiveError { | ||
MinGreaterThanMax, | ||
NonPositiveExtents, | ||
} | ||
impl Error for PrimitiveError {} | ||
impl fmt::Display for PrimitiveError { | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
match self { | ||
PrimitiveError::MinGreaterThanMax => { | ||
write!( | ||
f, | ||
"AxisAlignedBox minimums must be smaller or equal to the maximums" | ||
) | ||
} | ||
PrimitiveError::NonPositiveExtents => { | ||
write!(f, "AxisAlignedBox extents must be greater than zero") | ||
} | ||
} | ||
} | ||
} | ||
|
||
#[derive(Copy, Clone, PartialEq, Debug, Reflect)] | ||
pub struct Sphere { | ||
origin: Vec3, | ||
radius: f32, | ||
} | ||
|
||
impl Sphere { | ||
/// Get a reference to the sphere's origin. | ||
pub fn origin(&self) -> &Vec3 { | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
&self.origin | ||
} | ||
|
||
/// Get a reference to the sphere's radius. | ||
pub fn radius(&self) -> &f32 { | ||
&self.radius | ||
} | ||
|
||
/// Set the sphere's origin. | ||
pub fn set_origin(&mut self, origin: Vec3) { | ||
self.origin = origin; | ||
} | ||
|
||
/// Set the sphere's radius. | ||
pub fn set_radius(&mut self, radius: f32) { | ||
self.radius = radius; | ||
} | ||
} | ||
impl Primitive3d for Sphere { | ||
/// Use the sphere's position and radius to determin eif it is entirely on the outside of the | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// the supplied plane. | ||
fn outside_plane(&self, plane: Plane) -> bool { | ||
plane.distance_to_point(&self.origin) > self.radius | ||
} | ||
} | ||
|
||
/// An oriented box, unlike an axis aligned box, can be rotated and is not constrained to match the | ||
/// orientation of the coordinate system it is defined in. Internally, this is represented as an | ||
/// axis aligned box with some rotation ([Quat]) applied. | ||
#[derive(Copy, Clone, PartialEq, Debug, Reflect)] | ||
pub struct OrientedBox { | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
aab: AxisAlignedBox, | ||
transform: Mat4, | ||
} | ||
impl Primitive3d for OrientedBox { | ||
fn outside_plane(&self, plane: Plane) -> bool { | ||
for vertex in self.vertices().iter() { | ||
if plane.distance_to_point(vertex) <= 0.0 { | ||
return false; | ||
} | ||
} | ||
true | ||
} | ||
} | ||
impl OrientedBox { | ||
/// An ordered list of the vertices that form the 8 corners of the [AxisAlignedBox]. | ||
/// ```none | ||
/// (5)------(1) | ||
/// | \ | \ | ||
/// | (4)------(0) | ||
/// | | | | | ||
/// (7)--|---(3) | | ||
/// \ | \ | | ||
/// (6)------(2) | ||
/// ``` | ||
pub fn vertices(&self) -> [Vec3; 8] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It seems quite expensive calculating these every time this is called. It may be preferable to store the transformed vertices instead of storing it as an aabb and transform? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm really not sure. I've been deliberating how to make the structs as small as possible, while also retaining information needed to update the primitives. Some algorithms call for AABB box extents, while others need all vertices. Storing only box extents is the most memory efficient, while storing all vertices (might) be more CPU efficient. I don't want to store both in the struct, because that would be even worse for memory use. The ECS idiomatic way to handle this would be to create a component for each style of struct, and only query the one I need. My hesitation with this approach is this could quickly lead to an explosion in number of types, and I'd like to keep primitives as simple as possible. Maybe namespacing them would be enough, e.g.: I'm really uncertain. I'll create an RFC once that process has been finalized, and that will hopefully allow a nuanced discussion on the topic. |
||
let mut vertices = [Vec3::ZERO; 8]; | ||
let aab_vertices = self.aab.vertices(); | ||
for i in 0..vertices.len() { | ||
vertices[i] = self.transform.project_point3(aab_vertices[i]) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
} | ||
vertices | ||
} | ||
|
||
/// Set the oriented box's aab. | ||
pub fn set_aab(&mut self, aab: AxisAlignedBox) { | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.aab = aab; | ||
} | ||
|
||
/// Set the oriented box's transform. | ||
pub fn set_transform(&mut self, transform: Mat4) { | ||
self.transform = transform; | ||
} | ||
pub fn fast_aabb(&self) -> AxisAlignedBox { | ||
let vertices = self.vertices(); | ||
let mut max = Vec3::splat(f32::MIN); | ||
let mut min = Vec3::splat(f32::MAX); | ||
for vertex in vertices.iter() { | ||
max = vertex.max(max); | ||
min = vertex.min(min); | ||
} | ||
// Unwrap is okay here because min < max | ||
AxisAlignedBox::from_min_max(min, max).unwrap() | ||
} | ||
} | ||
|
||
/// An axis aligned box is a box whose axes lie in the x/y/z directions of the coordinate system | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// the box is defined in. | ||
#[derive(Copy, Clone, PartialEq, Debug, Reflect)] | ||
pub struct AxisAlignedBox { | ||
min: Vec3, | ||
max: Vec3, | ||
} | ||
impl Primitive3d for AxisAlignedBox { | ||
fn outside_plane(&self, plane: Plane) -> bool { | ||
for vertex in self.vertices().iter() { | ||
if plane.distance_to_point(vertex) <= 0.0 { | ||
return false; | ||
} | ||
} | ||
true | ||
} | ||
} | ||
impl AxisAlignedBox { | ||
/// An ordered list of the vertices that form the 8 corners of the [AxisAlignedBox]. | ||
/// ```none | ||
/// (5)------(1) Y | ||
/// | \ | \ | | ||
/// | (4)------(0) MAX o---X | ||
/// | | | | \ | ||
/// MIN (7)--|---(3) | Z | ||
/// \ | \ | | ||
/// (6)------(2) | ||
/// ``` | ||
pub fn vertices(&self) -> [Vec3; 8] { | ||
let min = self.min; | ||
let max = self.max; | ||
[ | ||
Vec3::new(max.x, max.y, max.z), | ||
Vec3::new(max.x, max.y, min.z), | ||
Vec3::new(max.x, min.y, max.z), | ||
Vec3::new(max.x, min.y, min.z), | ||
Vec3::new(min.x, max.y, max.z), | ||
Vec3::new(min.x, max.y, min.z), | ||
Vec3::new(min.x, min.y, max.z), | ||
Vec3::new(min.x, min.y, min.z), | ||
] | ||
} | ||
/// Construct an [AxisAlignedBox] given the coordinates of the minimum and maximum corners. | ||
pub fn from_min_max(min: Vec3, max: Vec3) -> Result<AxisAlignedBox, PrimitiveError> { | ||
if (max - min).min_element() >= 0.0 { | ||
Ok(AxisAlignedBox { min, max }) | ||
} else { | ||
Err(PrimitiveError::MinGreaterThanMax) | ||
} | ||
} | ||
/// Construct an [AxisALignedBox] from the origin at the minimum corner, and the extents - the | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
/// dimensions of the box in each axis. | ||
pub fn from_extents_origin( | ||
extents: Vec3, | ||
origin: Vec3, | ||
) -> Result<AxisAlignedBox, PrimitiveError> { | ||
if extents.min_element() > 0.0 { | ||
Ok(AxisAlignedBox { | ||
min: origin, | ||
max: extents + origin, | ||
}) | ||
} else { | ||
Err(PrimitiveError::NonPositiveExtents) | ||
} | ||
} | ||
/// Computes the AAB that | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
pub fn from_points(points: Vec<Vec3>) -> AxisAlignedBox { | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let mut max = Vec3::splat(f32::MIN); | ||
let mut min = Vec3::splat(f32::MAX); | ||
for &point in points.iter() { | ||
max = point.max(max); | ||
min = point.min(min); | ||
} | ||
// Unwrap is okay here because min < max | ||
AxisAlignedBox::from_min_max(min, max).unwrap() | ||
} | ||
} | ||
|
||
/// A frustum is a truncated pyramid that is used to represent the volume of world space that is | ||
/// visible to the camera. | ||
#[derive(Copy, Clone, PartialEq, Debug, Reflect)] | ||
#[reflect_value(PartialEq)] | ||
pub struct Frustum { | ||
planes: [Plane; 6], | ||
vertices: [Vec3; 8], | ||
} | ||
impl Primitive3d for Frustum { | ||
fn outside_plane(&self, plane: Plane) -> bool { | ||
for vertex in self.vertices().iter() { | ||
if plane.distance_to_point(vertex) <= 0.0 { | ||
return false; | ||
} | ||
} | ||
true | ||
} | ||
} | ||
impl Frustum { | ||
fn compute_vertices(camera_position: Mat4, projection_matrix: Mat4) -> [Vec3; 8] { | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
let ndc_to_world: Mat4 = camera_position * projection_matrix.inverse(); | ||
[ | ||
ndc_to_world.project_point3(Vec3::new(-1.0, -1.0, -1.0)), | ||
ndc_to_world.project_point3(Vec3::new(1.0, -1.0, -1.0)), | ||
ndc_to_world.project_point3(Vec3::new(-1.0, 1.0, -1.0)), | ||
ndc_to_world.project_point3(Vec3::new(1.0, 1.0, -1.0)), | ||
ndc_to_world.project_point3(Vec3::new(-1.0, -1.0, 1.0)), | ||
ndc_to_world.project_point3(Vec3::new(1.0, -1.0, 1.0)), | ||
ndc_to_world.project_point3(Vec3::new(-1.0, 1.0, 1.0)), | ||
ndc_to_world.project_point3(Vec3::new(1.0, 1.0, 1.0)), | ||
] | ||
} | ||
|
||
pub fn from_camera_properties(camera_position: Mat4, projection_matrix: Mat4) -> Frustum { | ||
let vertices = Frustum::compute_vertices(camera_position, projection_matrix); | ||
let [nbl_world, nbr_world, ntl_world, ntr_world, fbl_world, fbr_world, ftl_world, ftr_world] = | ||
vertices; | ||
|
||
let near_normal = (nbr_world - nbl_world) | ||
.cross(ntl_world - nbl_world) | ||
.normalize(); | ||
let far_normal = (fbr_world - ftr_world) | ||
.cross(ftl_world - ftr_world) | ||
.normalize(); | ||
let top_normal = (ftl_world - ftr_world) | ||
.cross(ntr_world - ftr_world) | ||
.normalize(); | ||
let bottom_normal = (fbl_world - nbl_world) | ||
.cross(nbr_world - nbl_world) | ||
.normalize(); | ||
let right_normal = (ntr_world - ftr_world) | ||
.cross(fbr_world - ftr_world) | ||
.normalize(); | ||
let left_normal = (ntl_world - nbl_world) | ||
.cross(fbl_world - nbl_world) | ||
.normalize(); | ||
|
||
let left = Plane { | ||
point: nbl_world, | ||
normal: left_normal, | ||
}; | ||
let right = Plane { | ||
point: ftr_world, | ||
normal: right_normal, | ||
}; | ||
let bottom = Plane { | ||
point: nbl_world, | ||
normal: bottom_normal, | ||
}; | ||
let top = Plane { | ||
point: ftr_world, | ||
normal: top_normal, | ||
}; | ||
let near = Plane { | ||
point: nbl_world, | ||
normal: near_normal, | ||
}; | ||
let far = Plane { | ||
point: ftr_world, | ||
normal: far_normal, | ||
}; | ||
|
||
let planes = [left, right, top, bottom, near, far]; | ||
|
||
Frustum { planes, vertices } | ||
} | ||
|
||
/// Get a reference to the frustum's vertices. These are given as an ordered list of vertices | ||
/// that form the 8 corners of a [Frustum]. | ||
/// ```none | ||
/// (6)--------------(7) | ||
/// | \ TOP / | | ||
/// | (2)------(3) | | ||
/// | L | | R | | ||
/// (4) | NEAR | (5) | ||
/// \ | | / | ||
/// (0)------(1) | ||
/// ``` | ||
pub fn vertices(&self) -> &[Vec3; 8] { | ||
&self.vertices | ||
} | ||
|
||
/// Get a reference to the frustum's planes. | ||
pub fn planes(&self) -> &[Plane; 6] { | ||
&self.planes | ||
} | ||
} | ||
|
||
/// A plane is defined by a point in space and a normal vector at that point. | ||
#[derive(Copy, Clone, PartialEq, Debug)] | ||
pub struct Plane { | ||
point: Vec3, | ||
normal: Vec3, | ||
} | ||
impl Primitive3d for Plane { | ||
fn outside_plane(&self, plane: Plane) -> bool { | ||
self.normal == plane.normal && self.distance_to_point(plane.point()) > 0.0 | ||
} | ||
} | ||
impl Plane { | ||
/// Generate a plane from three points that lie on the plane. | ||
pub fn from_points(points: [Vec3; 3]) -> Plane { | ||
let point = points[1]; | ||
let arm_1 = points[0] - point; | ||
let arm_2 = points[2] - point; | ||
let normal = arm_1.cross(arm_2).normalize(); | ||
Plane { point, normal } | ||
} | ||
/// Generate a plane from a point on that plane and the normal direction of the plane. The | ||
/// normal vector does not need to be normalized (length can be != 1). | ||
pub fn from_point_normal(point: Vec3, normal: Vec3) -> Plane { | ||
Plane { | ||
point, | ||
normal: normal.normalize(), | ||
} | ||
} | ||
/// Returns the nearest distance from the supplied point to this plane. Positive values are in | ||
/// the direction of the plane's normal (outside), negative values are opposite the direction | ||
/// of the planes normal (inside). | ||
pub fn distance_to_point(&self, point: &Vec3) -> f32 { | ||
aevyrie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
self.normal.dot(*point) + -self.normal.dot(self.point) | ||
} | ||
|
||
/// Get a reference to the plane's point. | ||
pub fn point(&self) -> &Vec3 { | ||
&self.point | ||
} | ||
|
||
/// Get a reference to the plane's normal. | ||
pub fn normal(&self) -> &Vec3 { | ||
&self.normal | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/bevyengine/bevy/search?q=thiserror
I see lots of use of
thiserror
in other bevy crates FYIThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the heads up. For some reason I didn't think this was the case.