Skip to content
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

Closed
wants to merge 12 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions crates/bevy_geometry/Cargo.toml
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 <[email protected]>",
"Aevyrie Roessler <[email protected]>",
]
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"] }
356 changes: 356 additions & 0 deletions crates/bevy_geometry/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
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 {}
Copy link

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 FYI

Copy link
Member Author

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.

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 the sphere's origin.
pub fn origin(&self) -> Vec3 {
self.origin
}

/// Get 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 determine if it is entirely on the outside of the
/// 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 OBB {
aab: AABB,
transform: Mat4,
}
impl Primitive3d for OBB {
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 OBB {
/// 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] {
Copy link
Contributor

Choose a reason for hiding this comment

The 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?

Copy link
Member Author

Choose a reason for hiding this comment

The 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.: aabb::extents and aabb::vertices.

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])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

project_point3 performs a perspective divide which is only necessary if the transform is not a perspective projection. If the transform was a perspective projection the the vertices would no longer be a box shape, so perhaps it is OK to assume that it is not. For an affine transform transform_point3 is more efficient as it won't do an unnecessary divide.

}
vertices
}

/// Set the oriented box's aab.
pub fn set_aabb(&mut self, aab: AABB) {
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) -> AABB {
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
AABB::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 AABB {
min: Vec3,
max: Vec3,
}
impl Primitive3d for AABB {
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 AABB {
/// 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<AABB, PrimitiveError> {
if (max - min).min_element() >= 0.0 {
Ok(AABB { min, max })
} else {
Err(PrimitiveError::MinGreaterThanMax)
}
}
/// Construct an [AxisAlignedBox] from the origin at the minimum corner, and the extents - the
/// dimensions of the box in each axis.
pub fn from_extents_origin(extents: Vec3, origin: Vec3) -> Result<AABB, PrimitiveError> {
if extents.min_element() > 0.0 {
Ok(AABB {
min: origin,
max: extents + origin,
})
} else {
Err(PrimitiveError::NonPositiveExtents)
}
}
/// Computes the [AxisAlignedBox] whose extents are determined by the minimum and maximum of the points given.
pub fn from_points(points: &[Vec3]) -> AABB {
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
AABB::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] {
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 {
self.normal.dot(point) + -self.normal.dot(self.point)
}

/// Get the plane's point.
pub fn point(&self) -> Vec3 {
self.point
}

/// Get the plane's normal.
pub fn normal(&self) -> Vec3 {
self.normal
}
}
7 changes: 4 additions & 3 deletions crates/bevy_internal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ categories = ["game-engines", "graphics", "gui", "rendering"]

[features]
wgpu_trace = ["bevy_wgpu/trace"]
trace = [ "bevy_app/trace", "bevy_ecs/trace" ]
trace_chrome = [ "bevy_log/tracing-chrome" ]
trace = ["bevy_app/trace", "bevy_ecs/trace"]
trace_chrome = ["bevy_log/tracing-chrome"]

# Image format support for texture loading (PNG and HDR are enabled by default)
hdr = ["bevy_render/hdr"]
Expand Down Expand Up @@ -49,6 +49,7 @@ bevy_core = { path = "../bevy_core", version = "0.4.0" }
bevy_derive = { path = "../bevy_derive", version = "0.4.0" }
bevy_diagnostic = { path = "../bevy_diagnostic", version = "0.4.0" }
bevy_ecs = { path = "../bevy_ecs", version = "0.4.0" }
bevy_geometry = { path = "../bevy_geometry", version = "0.4.0" }
bevy_input = { path = "../bevy_input", version = "0.4.0" }
bevy_log = { path = "../bevy_log", version = "0.4.0" }
bevy_math = { path = "../bevy_math", version = "0.4.0" }
Expand All @@ -72,4 +73,4 @@ bevy_winit = { path = "../bevy_winit", optional = true, version = "0.4.0" }
bevy_gilrs = { path = "../bevy_gilrs", optional = true, version = "0.4.0" }

[target.'cfg(target_os = "android")'.dependencies]
ndk-glue = {version = "0.2", features = ["logger"]}
ndk-glue = { version = "0.2", features = ["logger"] }
Loading