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

Primitive Shapes #12

Merged
merged 59 commits into from
Dec 12, 2022
Merged
Changes from 15 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
6a67443
Create geometric-primitives.md
aevyrie Apr 21, 2021
3f4c7e0
Correct typo
aevyrie Apr 21, 2021
fa1fc1c
Adding details
aevyrie Apr 22, 2021
7f5f21f
Incremental updates
aevyrie Apr 24, 2021
9a890e7
Update rfcs/geometric-primitives.md
aevyrie Apr 24, 2021
175b8a0
Update rfcs/geometric-primitives.md
aevyrie Apr 24, 2021
b43acde
Update rfcs/geometric-primitives.md
aevyrie Apr 24, 2021
3533994
Apply suggestions from code review
aevyrie Apr 24, 2021
68f5485
Update all the things
aevyrie Apr 30, 2021
08565cc
Merge branch 'patch-1' of https://github.com/aevyrie/rfcs into patch-1
aevyrie Apr 30, 2021
66d8c9d
Add meshing section.
aevyrie Apr 30, 2021
ceac06d
Added plane
aevyrie Apr 30, 2021
88af32e
Add primitives and use demonstration
aevyrie May 1, 2021
e6a7275
Update rfcs/geometric-primitives.md
aevyrie May 3, 2021
f8c8fbe
Update rfcs/geometric-primitives.md
aevyrie May 3, 2021
6c9cbfd
Update rfcs/geometric-primitives.md
aevyrie May 7, 2021
da6871e
wip updates
aevyrie May 12, 2021
47ea3b6
Update table and rename
aevyrie May 17, 2021
5e84667
Rewrite all the things.
aevyrie May 25, 2021
f663bd1
Rename primitive-shapes.md to 12-primitive-shapes.md
aevyrie May 25, 2021
cef33e9
Update 12-primitive-shapes.md
aevyrie May 25, 2021
9dd6b67
Update 12-primitive-shapes.md
aevyrie May 25, 2021
b67f30e
Update 12-primitive-shapes.md
aevyrie May 25, 2021
47bcc26
Update 12-primitive-shapes.md
aevyrie May 25, 2021
7d58f91
Correct Mat3 -> Mat2
aevyrie May 25, 2021
a2aa8cf
Fix AABB error in table
aevyrie May 25, 2021
bd9d5e2
Update 12-primitive-shapes.md
aevyrie May 26, 2021
0f25cfe
Typo
aevyrie May 26, 2021
b3fcd7d
Add notes to bounding/collision
aevyrie Jun 1, 2021
cede313
Clarify traits as reference only
aevyrie Jun 1, 2021
5e381f6
improve bounding/collision discussion
aevyrie Jun 1, 2021
019047e
Add notes about Parry types
aevyrie Jun 3, 2021
24dbd0b
Update 12-primitive-shapes.md
aevyrie Jul 26, 2021
9ef4d2e
Merge branch 'bevyengine:main' into patch-1
aevyrie Oct 10, 2021
f37352e
Fix markdown lint violations
Weibye Oct 26, 2021
c50b330
Fixing typo
Weibye Oct 27, 2021
bc99e5b
Merge pull request #1 from Weibye/primitive-shapes
aevyrie Dec 8, 2021
e164fa5
Merge pull request #3 from Weibye/fix-typo
aevyrie Dec 8, 2021
cc192cb
Explicitly naming x2d or x3d where it makes sense
Weibye Oct 26, 2021
2ff5978
Rename Box2d to Rectangle
Weibye Oct 26, 2021
e88a42a
Removing unresolved question
Weibye Oct 26, 2021
2a33756
Fix typo
Weibye Nov 6, 2021
5222ee8
Moving 2d shapes ontop
Weibye Nov 6, 2021
fbcc8da
Moving 2d shapes before 3d shapes
Weibye Nov 6, 2021
bafe5a5
Adding descriptions to table
Weibye Nov 6, 2021
3494f3a
Collecting and rewording 2d / 3d section
Weibye Nov 6, 2021
2ac6358
Update rfcs/12-primitive-shapes.md
Weibye Nov 6, 2021
5ec59ee
2d before 3d consistency
Weibye Dec 13, 2021
550e522
Merge pull request #2 from Weibye/name-consolidation
aevyrie Jan 3, 2022
46ae46c
Removing point as a primitive type
Weibye Dec 13, 2021
7a5e2c1
Remove angle as a component
Weibye Jan 23, 2022
ca533b1
merging 'bounding and collision' with 'where are the transforms'
Weibye Jan 23, 2022
957bfca
Merge pull request #4 from Weibye/dev/remove-point
aevyrie Jan 23, 2022
dc58497
Merge pull request #5 from Weibye/remove-angle
aevyrie Jan 23, 2022
be92586
Merge pull request #6 from Weibye/where-transforms
aevyrie Jan 23, 2022
7981ae8
Update rfcs/12-primitive-shapes.md
aevyrie Jan 23, 2022
f632b2b
Removing torus and placing non-convex in future work
Weibye Apr 24, 2022
69e3ab0
Merge pull request #7 from Weibye/non-convex
aevyrie Apr 25, 2022
03eaf5f
Update rfcs/12-primitive-shapes.md
aevyrie Aug 26, 2022
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
349 changes: 349 additions & 0 deletions rfcs/geometric-primitives.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,349 @@
# Feature Name: `geometric-primitives`

## Summary

Lightweight geometric primitive types for use across bevy engine crates, and as interoperability types for external libraries.

## Motivation

This would provide a standard way to model primitives across the bevy ecosystem to prevent ecosystem fragmentation amongst plugins.

## User-Facing Explanation

Geometric primitives are lightweight representations of geometry that describe the type of geometry as well as fully defined dimensions. These primitives are *not* meshes, but the underlying precise mathematical definition. For example, a circle is:

```rust
pub struct Circle2d {
origin: Point2d,
radius: f32,
}
```

Geometric primitives have a defined shape, size, position, and orientation. Position and orientation are **not** defined using bevy's `Transform` components. This is because these are fundamental geometric primitives that must be usable and comparable as-is.

`bevy_geom` provides two main modules, `geom_2d` and `geom_3d`. Recall that the purpose of this crate is to provide lightweight types, so there are what appear to be duplicates in 2d and 3d, such as `geom_2d::Line2d` and `geom_3d::Line`. Note that the 2d version of a line is lighter weight, and is only defined in 2d. 3d geometry (or 2d with depth which is 3d) is assumed to be the default for most cases. The names of the types were chosen with this in mind, to guide you toward using Line instead of Line2d for example, unless you know why you are making this choice.

## Implementation strategy

### Helper Types

```rust
/// Stores an angle in radians, and supplies builder functions to prevent errors (from_radians, from_degrees)
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
struct Angle(f32);
```

### 3D Geometry Types

These types are fully defined in 3d space.

```rust
/// Point in 3D space
struct Point(Vec3)

/// Vector direction in 3D space that is guaranteed to be normalized through its getter/setter.
struct Direction(Vec3)
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
Copy link

@NathanSWard NathanSWard May 8, 2021

Choose a reason for hiding this comment

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

I'm curious is this is worth bringing into scope for the RFC.
But having a guaranteed to be normalized... is an invariant that a user has to remember via docs.

Is it worth having some extra type-safe wrappers for linear algebra types such as NormalizedVec3.
This way the invariant the the vector is normalized is held though the type interface and not just through docs?

I initially discussed this here with the directional light PR.


// Plane in 3d space defined by a point on the plane, and the normal direction of the plane
struct Plane {
point: Point,
normal: Direction,
}

/// Unbounded line in 3D space with direction
struct Ray {
point: Point,
normal: Direction,
}

/// Line in 3D space bounded by two points
struct Line {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
start: Point,
end: Point,
}

struct Triangle([Point; 3]);

struct Quad([Point; 4]);

struct Sphere{
origin: Point,
radius: f32,
}

struct Cylinder {
/// The bottom center of the cylinder.
origin: Point,
radius: f32,
/// The extent of the cylinder is the vector that extends from the origin to the opposite face of the cylinder.
extent: Vec3,
}

struct Cone{
/// Origin of the cone located at the center of its base.
origin: Point,
/// The extent of the cone is the vector that extends from the origin to the tip of the cone.
extent: Vec3,
base_radius: f32,
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
}

struct Torus{
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
origin: Point,
normal: Direction,
major_radius: f32,
tube_radius: f32,
}

struct Capsule {
origin: Point,
height: Vec3,

aevyrie marked this conversation as resolved.
Show resolved Hide resolved
}

// A 3d frustum used to represent the volume rendered by a camera, defined by the 6 planes that set the frustum limits.
struct Frustum{
near: Plane,
far: Plane,
top: Plane,
bottom: Plane,
left: Plane,
right: Plane,
}

// 3D Axis Aligned Bounding Box defined by its extents, useful for fast intersection checks and frustum culling.
struct AabbExtents {
min: Vec3
max: Vec3
}
impl Aabb for AabbExtents {} //...

// 3D Axis Aligned Bounding Box defined by its center and half extents, easily converted into an OBB
struct AabbCentered {
origin: Point,
half_extents: Vec3,
}
impl Aabb for AabbCentered {} //...

// 3D Axis Aligned Bounding Box defined by its eight vertices, useful for culling or drawing
struct AabbVertices([Point; 8]);
impl Aabb for AabbVertices {} //...

// 3D Oriented Bounding Box
struct ObbCentered {
origin: Point,
orthonormal_basis: Mat3,
half_extents: Vec3,
}
impl Obb for ObbCentered {} //...

struct ObbVertices([Point; 4]);
impl Obb for ObbVertices {} //...
```

### 2D Geometry Types

These types only exist in 2d space: their dimensions and location are only defined in `x` and `y` unlike their 3d counterparts. These types are suffixed with "2d" to disambiguate from the 3d types in user code, guide users to using 3d types by default, and remove the need for name-spacing the 2d and 3d types when used in the same scope.

```rust
/// Point in 2D space
struct Point2d(Vec2)

/// Vector direction in 2D space that is guaranteed to be normalized through its getter/setter.
struct Direction2d(Vec2)

/// Unbounded line in 2D space with direction
struct Ray2d {
point: Point2d,
normal: Direction2d,
}

/// Line in 2D space bounded by two points
struct Line2d {
start: Point2d,
end: Point2d,
}

/// A line list represented by an ordered list of vertices in 2d space
struct LineList<const N: usize>{
points: [Point; N],
/// True if the LineList is a closed loop
closed: bool,
}

struct Triangle2d([Point2d; 3]);

struct Quad2d([Point2d; 4]);

/// A regular polygon, such as a square or hexagon.
struct RegularPolygon2d{
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
circumcircle: Circle2d,
/// Number of faces.
faces: u8,
/// Clockwise rotation of the polygon about the origin. At zero rotation, a point will always be located at the 12 o'clock position.
orientation: Angle,
}

struct Circle2d {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
origin: Point2d,
radius: f32,
}

/// Segment of a circle
struct Arc2d {
aevyrie marked this conversation as resolved.
Show resolved Hide resolved
circle: Circle2d,
/// Start of the arc, measured clockwise from the 12 o'clock position
start: Angle,
/// Angle in radians to sweep clockwise from the [start_angle]
sweep: Angle,
}

// 2D Axis Aligned Bounding Box defined by its extents, useful for fast intersection checks and culling with an axis-aligned viewport
struct AabbExtents2d {
min: Vec2
max: Vec2
}
impl Aabb2d for AabbExtents2d {} //...

// 2D Axis Aligned Bounding Box defined by its center and half extents, easily converted into an OBB.
struct AabbCentered2d {
origin: Point2d,
half_extents: Vec2,
}
impl Aabb2d for AabbCentered2d {} //...

// 2D Axis Aligned Bounding Box defined by its four vertices, useful for culling or drawing
struct AabbVertices2d([Point2d; 4]);
impl Aabb2d for AabbVertices2d {} //...

// 2D Oriented Bounding Box
struct ObbCentered2d {
origin: Point2d,
orthonormal_basis: Mat2,
half_extents: Vec2,
}
impl Obb2d for ObbCentered2d {} //...

struct ObbVertices2d([Point2d; 4]);
impl Obb2d for ObbVertices2d {} //...
```

### Meshing

While these primitives do not provide a meshing strategy, future work could build on these types so users can use something like `Sphere.mesh()` to generate meshes.

```rust
let unit_sphere = Sphere::UNIT;
// Some ways we could build meshes from primitives
let sphere_mesh = Mesh::from(unit_sphere);
let sphere_mesh = unit_sphere.mesh();
let sphere_mesh: Mesh = unit_sphere.into();
```

### Bounding Boxes/Volumes

This section is based off of prototyping work done in [bevy_mod_bounding](https://github.com/aevyrie/bevy_mod_bounding).

A number of bounding box types are provided for 2d and 3d use. Some representations of a bounding box are more efficient depending on the use case. Instead of storing all possible values in a component (wasted space) or computing them with a helper function every time (wasted cpu), each representation is provided as a distinct component that can be updated independently. This gives users of Bevy the flexibility to optimize for their use case without needing to write their own incompatible types from scratch. Consider the functionality built on top of bounding such as physics or collisions - because they are all built on the same fundamental types, they can interoperate.

Note that the bounding types presented in this RFC implement their respective `Obb` or `Aabb` trait. Bounding boxes can be represented by a number of different underlying types; by making these traits instead of structs, systems can easily be made generic over these types depending on the situation.

Because bounding boxes are fully defined in world space, this leads to the natural question of how they are kept in sync with their parent. The plan would be to provide a system similar to transform propagation, that would apply an OBB's precomputed `Transform` to its parent's `GlobalTransform`. Further details are more appropriate for a subsequent bounding RFC/PR. The important point to consider is how this proposal provides common types that can be used for this purpose in the future.

### Frustum Culling

This section is based off of prototyping work done in [bevy_frustum_culling](https://github.com/aevyrie/bevy_frustum_culling).

The provided `Frustum` type is useful for frustum culling, which is generally done on the CPU by comparing each frustum plane with each entity's bounding volume.

```rust
// What frustum culling might look like:
for bounding_volume in bound_vol_query.iter() {
for plane in camera_frustum.planes().iter() {
if bounding_volume.outside(plane) {
// Cull me!
return;
}
}
}
```

This data structure alone does not ensure the representation is valid; planes could be placed in nonsensical positions. To prevent this, the struct's fields should be made private, and constructors and setters should be provided to ensure `Frustum`s can only be initialized or mutated into valid arrangements.

In addition, by defining the frustum as a set of planes, it is also trivial to support oblique frustums.
aevyrie marked this conversation as resolved.
Show resolved Hide resolved

### Ray Casting

This section is based off of prototyping work done in [bevy_mod_picking](https://github.com/aevyrie/bevy_mod_picking).

The bounding volumes section covers how these types would be used for the bounding volumes which are used for accelerating ray casting. In addition, the `Ray` component can be used to represent rays. Applicable 3d types could implement a `RayIntersection` trait to extend their functionality.

```rust
let ray = Ray::X;
let sphere = Sphere::new(Point::x(5.0), 1.0);
let intersection = ray.cast(sphere);
```

## Drawbacks

An argument could be made to use an external crate for this, however these types are so fundamental I think it's important that they are optimized for the engine's uses, and are not from a generalized solution.

This is also a technically simple addition that shouldn't present maintenance burden. The real challenge is upfront in ensuring the API is designed well, and the primitives are performant for their most common use cases. If anything, this furthers the argument for not using an external crate.

## Rationale and alternatives

### Lack of `Transform`s

Primitives are fully defined in space, and do not use `Transform` or `GlobalTransform`. This is an intentional decision.

It's unsurprisingly much simpler to use these types when the primitives are fully defined internally, but maybe somewhat surprisingly, more efficient.

### Cache Efficiency

- Some primitives such as AABB 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 AABBs 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 `GlobalTransform`s 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.

## Prior art

- Unity `PrimitiveObjects`: https://docs.unity3d.com/Manual/PrimitiveObjects.html
- Godot `PrimitiveMesh`: https://docs.godotengine.org/en/stable/classes/class_primitivemesh.html#class-primitivemesh

These examples intermingle primitive geometry with the meshes themselves. This RFC makes these distinct.

aevyrie marked this conversation as resolved.
Show resolved Hide resolved

## Unresolved questions

aevyrie marked this conversation as resolved.
Show resolved Hide resolved
What is the best naming scheme, e.g., `2d::Line`/`3d::Line` vs. `Line2d`/`Line3d` vs. `Line2d`/`Line`.
Copy link
Member

Choose a reason for hiding this comment

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

1 > 2 >> 3 for me. However, option 1 is challenging as crate names can't start with a number IIRC.

Copy link
Member Author

Choose a reason for hiding this comment

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

crate names can't start with a number

Right, the point here is namespacing vs. unique symmetric names vs. unique asymmetric names.

Choose a reason for hiding this comment

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

I think a combination of 1 and 2, breaking them into their own crates is a good idea, but at the same time, to remove any confusion over what of the 2 types it is, they should have the 2d/3d suffix


How will fully defined primitives interact with `Transforms`? Will this confuse users? How can the API be shaped to prevent this?

### Out of Scope

- Value types, e.g. float vs. fixed is out of scope. This RFC is focused on the core geometry types and is intended to use Bevy's common rendering value types such as `f32`.

## Future possibilities
aevyrie marked this conversation as resolved.
Show resolved Hide resolved

- Bounding boxes
- Collisions
- Frustum Culling
- Ray casting
- Physics
- SDF rendering
- Debug Rendering