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 1 commit
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
3 changes: 2 additions & 1 deletion crates/bevy_geometry/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@ 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" }
bevy_reflect = { path = "../bevy_reflect", version = "0.4.0", features = ["bevy"] }
179 changes: 167 additions & 12 deletions crates/bevy_geometry/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,28 +1,183 @@
use bevy_math::{Quat, Vec3};
use bevy_math::*;
use bevy_reflect::Reflect;
//use bevy_transform::components::GlobalTransform;
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
use std::error::Error;
use std::fmt;

pub trait Primitive3d {}
pub trait Primitive3d {
/*
/// Returns true if this primitive is on the outside (normal direction) of the supplied
Copy link
Member

Choose a reason for hiding this comment

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

Can we even define this for lower dimensional shapes embedded in 3D?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, all 3d primitives can be bounded by a finite volume. Lower dimensional shapes may have a bounding volume of zero, but it still makes sense to check if they are fully in the outer halfspace of a plane.

Copy link
Member Author

Choose a reason for hiding this comment

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

Even a plane, with an infinite area, can be fully "outside" another plane if both planes are parallel.

fn outside_plane(
Copy link
Member

Choose a reason for hiding this comment

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

Is this the standard name for this function / property? Seems a bit confusing for non-planar shapes?

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 see how this might be confusing. It's checking if a 3d objects is fully on the outer side of the plane - i.e. fully contained by the halfspace in the direction of the plane's normal. I'll poke around and see if I can find a standard name.

&self,
primitive_transform: GlobalTransform,
plane: Plane,
plane_transform: GlobalTransform,
) -> bool;*/
}

#[derive(Copy, Clone, PartialEq, Debug)]
#[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 than maximums")
}
PrimitiveError::NonPositiveExtents => {
write!(f, "AxisAlignedBox extents must be greater than zero")
}
}
}
}

#[derive(Copy, Clone, PartialEq, Debug, Reflect)]
pub struct Sphere {
pub origin: Vec3,
pub radius: f32,
}
impl Primitive3d for Sphere {}

#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Box {
pub maximums: Vec3,
pub minimums: Vec3,
/// 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
pub aab: AxisAlignedBox,
pub orientation: Quat,
}
impl Primitive3d for OrientedBox {}

#[derive(Copy, Clone, PartialEq, Debug)]
/// 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 {
pub maximums: Vec3,
pub minimums: Vec3,
minimums: Vec3,
maximums: Vec3,
}
impl Primitive3d for AxisAlignedBox {}
impl AxisAlignedBox {
pub fn from_min_max(minimums: Vec3, maximums: Vec3) -> Result<AxisAlignedBox, PrimitiveError> {
if (maximums - minimums).min_element() > 0.0 {
Ok(AxisAlignedBox { minimums, maximums })
} else {
Err(PrimitiveError::MinGreaterThanMax)
}
}
pub fn from_extents_origin(
extents: Vec3,
origin: Vec3,
) -> Result<AxisAlignedBox, PrimitiveError> {
if extents.min_element() > 0.0 {
Ok(AxisAlignedBox {
minimums: origin,
maximums: extents + origin,
})
} else {
Err(PrimitiveError::NonPositiveExtents)
}
}
}

/// 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],
}
impl Primitive3d for Frustum {}
impl Frustum {
pub fn from_camera_properties(
&self,
camera_position: Mat4,
projection_matrix: Mat4,
) -> Frustum {
let ndc_to_world: Mat4 = camera_position * projection_matrix.inverse();
// Near/Far, Top/Bottom, Left/Right
Copy link
Member

Choose a reason for hiding this comment

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

It might be nice to construct this whole thing in a for loop for clarity? The actual logic is hard to follow.

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 don't see a way to do this in a loop yet, without making the logic harder to follow. Everything is "unrolled" and named so it's easier to follow which points are being used to take cross products and compute normals. I could probably make the code shorter and less repetitive with a well-constructed loop, but I worry it will be harder to follow and debug.

let nbl_world = ndc_to_world.project_point3(Vec3::new(-1.0, -1.0, -1.0));
let nbr_world = ndc_to_world.project_point3(Vec3::new(1.0, -1.0, -1.0));
let ntl_world = ndc_to_world.project_point3(Vec3::new(-1.0, 1.0, -1.0));
let fbl_world = ndc_to_world.project_point3(Vec3::new(-1.0, -1.0, 1.0));
let ftr_world = ndc_to_world.project_point3(Vec3::new(1.0, 1.0, 1.0));
let ftl_world = ndc_to_world.project_point3(Vec3::new(-1.0, 1.0, 1.0));
let fbr_world = ndc_to_world.project_point3(Vec3::new(1.0, -1.0, 1.0));
let ntr_world = ndc_to_world.project_point3(Vec3::new(1.0, 1.0, -1.0));

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,
};
Frustum {
planes: [left, right, top, bottom, near, far],
}
}
}

/// A plane is defined by a point in space and a normal vector at that point.
#[derive(Copy, Clone, PartialEq, Debug)]
pub struct Plane {
pub point: Vec3,
pub normal: Vec3,
point: Vec3,
normal: Vec3,
}
impl Primitive3d for Plane {}
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(),
}
}
}
2 changes: 1 addition & 1 deletion tools/publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ crates=(
bevy_utils
bevy_derive
bevy_math
bevy_geometry
bevy_tasks
bevy_ecs/macros
bevy_ecs
Expand All @@ -17,6 +16,7 @@ crates=(
bevy_core
bevy_diagnostic
bevy_transform
bevy_geometry
bevy_window
bevy_render
bevy_input
Expand Down