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

Conversation

aevyrie
Copy link
Member

@aevyrie aevyrie commented Mar 11, 2021

Note: See RFC bevyengine/rfcs#12

Adds geometric primitives to Bevy. These are lightweight mathematical representations of primitives commonly used for bounding volumes and physics.

@aevyrie aevyrie marked this pull request as draft March 11, 2021 18:43
@aevyrie
Copy link
Member Author

aevyrie commented Mar 11, 2021

I've opted to fully define these primitives in world space, instead of using transforms, intentionally.

  1. These shapes are only useful if fully defined in space, I can't think of a case where you would, for example, reference the radius of a sphere without also referencing the origin
  2. Using transforms adds complexity for any operation - you now need to query the GlobalTransform as well as the primitive.
  3. If needed, they could still be used with Transforms! However, I would consider this a secondary use case, and these primitives need to be optimized for use in core functionality - frustum culling, bounding volumes, collision, etc.

I'd like to open this up for discussion. any disagreement on the above points?

Co-authored-by: MinerSebas <66798382+MinerSebas@users.noreply.github.com>
@MinerSebas
Copy link
Contributor

These additions should probebly also be re-exported by the bevy_internal crate.
Also, the crate needs to be added to the publish.sh inside the tools folder.

@alice-i-cecile
Copy link
Member

As an end user, it would be nice to have some alternate construction impls for each of these. E.g. Box by 3 points, plane by 3 points and so on.

These structs should derive at least Clone and Debug, and probably Hash, PartialEq, and Eq. Also probably Reflect?

@alice-i-cecile
Copy link
Member

The ability to construct Transform versions of these structs seems very useful in downstream users: it feels like this should be a shared trait method?

@alice-i-cecile
Copy link
Member

As a general note, these are very much needed in Bevy itself to a) avoid ecosystem fragmentation b) use in critical internal tools.

@alice-i-cecile alice-i-cecile added the A-Rendering Drawing game state to the screen label Mar 11, 2021
@aevyrie
Copy link
Member Author

aevyrie commented Mar 11, 2021

Great points. Yeah, I'd like to add some constructors and figure out what shared functionality should go into the primitive trait. I could see that trait being useful in the future for checking primitive intersections using the trait.

@aevyrie
Copy link
Member Author

aevyrie commented Mar 11, 2021

The ability to construct Transform versions of these structs seems very useful in downstream users: it feels like this should be a shared trait method?

This would be done by having a Transform component on the same entity, and any special logic would be handled in the user's systems. These primitives would be blind to this and could be used however you like.

tools/publish.sh Outdated Show resolved Hide resolved
@alice-i-cecile
Copy link
Member

Should Line, Point, Arc and Triangle also be implemented as this same sort of primitive? They're not fully 3D, but very common.

@aevyrie
Copy link
Member Author

aevyrie commented Mar 11, 2021

Should Line, Point, Arc and Triangle also be implemented as this same sort of primitive? They're not fully 3D, but very common.

Yes, I think those "2D" primitives defined in 3d space would make sense to add, as well as capsule, cylinder, and anything else I've forgotten.

I will be updating this PR sporadically, but (anyone!) please leave comments with ideas or feedback as I'm working. I want to keep the scope limited to primitive definition only. We can add helper functions in future PRs.

@alice-i-cecile
Copy link
Member

For clarity, I would impl Primitive3d for each of the structs you've created already, even before it has any behavior. It can still be used to make trait objects, and signals the intent to extend the trait.

@aevyrie
Copy link
Member Author

aevyrie commented Mar 11, 2021

For clarity, I would impl Primitive3d for each of the structs you've created already, even before it has any behavior. It can still be used to make trait objects, and signals the intent to extend the trait.

Yup, I just haven't pushed it yet. :)

@aevyrie
Copy link
Member Author

aevyrie commented Mar 13, 2021

I've opted to fully define these primitives in world space, instead of using transforms, intentionally.

I've been going back and forth on this for a while, but I'm now feeling fairly confident this is the right choice. I implemented a Primitive3d::outside_plane() function to get a feel for the impact of this choice. It's unsurprisingly much simpler to use if the primitives are fully defined internally, and maybe somewhat surprisingly, more efficient.

Cache Efficiency

  • Some primitives such as AAB and Sphere don't need a rotation to be fully defined.
  • By using a GlobalTransform, not only is this an unused Quat that fills the cache line, it would also cause redundant change detection on rotations.
  • This is especially important for AABs and Spheres, because they are fundamental to collision detection and BV(H), and as such need to be as efficient as possible.
  • I still haven't found a case where you would use a Primitive3d without needing this positional information that fully defines the primitive in space. If this holds true, it means that storing the positional data inside the primitive is not a waste of cache, which is normally why you would want to separate the transform into a separate component.

CPU Efficiency

  • Storing the primitive's positional information internally serves as a form of memoization.
  • Because you need the primitive to be fully defined in world space to run useful operations, this means that with a GlobalTransform you would need to apply the transform to the primitive every time you need to use it.
  • By applying this transformation only once (e.g. during transform propagation), we only need to do this computation a single time.

Ergonomics

  • As I've already mentioned a few times, primitives need to be fully defined in world space to do anything useful with them.
  • By making the primitive components fully defined and standalone, computing operations is as simple as: primitive1.some_function(primitive_2), instead of also having query and pass in 2 GlobalTransforms in the correct order.

Use with Transforms

  • For use cases such as oriented bounding boxes, a primitive should be defined relative to its parent.
  • In this case, the primitive would still be fully defined internally, but we would need to include primitive updates analogous to the transform propagation system.
  • For example, a bounding sphere entity would be a child of a mesh, with a Sphere primitive and a Transform. On updates to the parent's GlobalTransform, the bounding sphere's Transform component would be used to update the Sphere's position and radius by applying the scale and translation to a unit sphere. This could be applied to all primitives, with the update system being optimized for each primitive.

@alice-i-cecile
Copy link
Member

This was a fantastic analysis and I agree with your conclusions. A few thoughts:

I still haven't found a case where you would use a Primitive3d without needing this positional information that fully defines the primitive in space. If this holds true, it means that storing the positional data inside the primitive is not a waste of cache, which is normally why you would want to separate the transform into a separate component.

I spent some time thinking about this, and I couldn't come up with a compelling example, especially a performance-critical one.

In this case, the primitive would still be fully defined internally, but we would need to include primitive updates analogous to the transform propagation system.

This is actually an interesting use case for trait queries (cc @Davier whose exploring these) : you could have a single system that queried for a dyn Primitive component or the like, then used the appropriate method on it. This would be clearer and avoid duplication of systems for each new primitive type (which is particularly useful for user code. I'm not sure if they would come with a performance overhead though.

@alice-i-cecile alice-i-cecile mentioned this pull request Mar 14, 2021
crates/bevy_geometry/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_geometry/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_geometry/src/lib.rs Outdated Show resolved Hide resolved
crates/bevy_geometry/src/lib.rs Outdated Show resolved Hide resolved
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.

/// \ | \ |
/// (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.

crates/bevy_geometry/src/lib.rs Outdated Show resolved Hide resolved
@aevyrie
Copy link
Member Author

aevyrie commented Mar 17, 2021

Hey @bitshifter, I really appreciate this review. I'll be making an RFC where we take a closer look at the design of primitives, including looking into any alternative libraries. Would you mind if I ping you when I get that up?

@bitshifter
Copy link
Contributor

Yep, I'm happy to chime in @aevyrie .

crates/bevy_geometry/src/lib.rs Outdated Show resolved Hide resolved
pub trait Primitive3d {
/*
/// Returns true if this primitive is on the outside (normal direction) of the supplied
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.

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.

crates/bevy_geometry/src/lib.rs Show resolved Hide resolved
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.

@alice-i-cecile
Copy link
Member

I noticed I had some old pending comments stored up; sorry if anything is out of date!

@aevyrie
Copy link
Member Author

aevyrie commented Mar 19, 2021

FYI: There are a lot of changes I want to make to this PR, but I'm holding off until we have the RFC process up (next week?) so I can flesh out the details of what I'm thinking.

Edit: the RFC process PR: #1662

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.

aevyrie and others added 2 commits April 8, 2021 13:07
Co-authored-by: Grindv1k <torstein.grindvik@gmail.com>
Co-authored-by: Grindv1k <torstein.grindvik@gmail.com>
@alice-i-cecile alice-i-cecile added the S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged label Apr 23, 2021
@aevyrie
Copy link
Member Author

aevyrie commented May 25, 2021

Yep, I'm happy to chime in @aevyrie .

Hey @bitshifter, the RFC in question is up and ready for review. I'd appreciate your feedback!
bevyengine/rfcs#12

@cart cart added the S-Pre-Relicense This PR was made before Bevy added the Apache license. Cannot be merged or used for other work label Jul 23, 2021
@DJMcNab DJMcNab removed the S-Pre-Relicense This PR was made before Bevy added the Apache license. Cannot be merged or used for other work label Aug 7, 2021
@alice-i-cecile alice-i-cecile added the X-Controversial There is active debate or serious implications around merging this PR label Apr 22, 2022
@alice-i-cecile alice-i-cecile added this to the Bevy 0.8 milestone Apr 25, 2022
@alice-i-cecile alice-i-cecile removed this from the Bevy 0.8 milestone May 13, 2022
@aevyrie
Copy link
Member Author

aevyrie commented Feb 17, 2023

Closing, too stale to be useful.

@aevyrie aevyrie closed this Feb 17, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen S-Needs-Design-Doc This issue or PR is particularly complex, and needs an approved design doc before it can be merged X-Controversial There is active debate or serious implications around merging this PR
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants