-
-
Notifications
You must be signed in to change notification settings - Fork 44
Description
Summary
Queries can be easily split into two categories: ordered (nearest_, farthest_) and unordered (traverse, traverse_iterator).
Traits for supporting these categories can be defined in a way to allow simple adaptation, match shapes based on their specific geometry, and returning a byproduct produced by the query to shape interaction, while not being overly complicated for implementation. Existing traversal functions should be replaced to match the naming of the kind of query they use and have return type parity.
Current Limitations
- traverse/traverse_iterator
- doesn't match shapes wrt their geometry, only their AABB
- generic implementation does not support inverted queries without defining a separate query
- nearest_traverse_iterator/farthest_traverse_iterator
- doesn't match shapes wrt their geometry, only their AABB
- non-generic query, only rays
- ordering by AABB distance is not always the same as ordering by shape distance Nearest AABB Traversal Is Insufficient for Nearest Shape Traversal #159
- no fast, non-allocating method for only the first ray hit
- nearest_child_traverse_iterator/farthest_child_traverse_iterator
- similar issues to the methods which replaced it above
- nearest_to
- non-generic query, only points
- requires different shape structs for multiple distance queries on the same type of shape (nearest vertex, edge, or surface of a triangle for instance)
- no way to whitelist/blacklist shapes without building a different BVH and prefiltering all shapes
- doesn't support returning more than one shape (k-nearest neighbors)
- doesn't return additional information produced by the query (surface point, vertex index, etc)
Adaptation
What I mean by adaptation is having extra methods on query objects that adjust how they operate. It's similar to function chains on iterators.
For unordered queries these can be things like:
shape_filter((&Shape) -> bool) -> ShapeFilter(whitelist/blacklist shapes before adding to collection/iterator)map_byproduct<B>((&Shape, Self::Byproduct) -> B) -> MapByproduct<Byproduct=T>(allow for mutating/removing of parts of the byproduct to only what you need, less allocation/stack usage)or(UnorderedQuery) -> OrQueryand(UnorderedQuery) -> AndQueryinvert() -> InvertedQuery
And for ordered queries:
shape_filter((&Shape) -> bool) -> ShapeFilter(whitelist/blacklist shapes before adding to collection/iterator)traverse_filter(UnorderedQuery) -> TraverseFilter(limit visited nodes and returned shapes to ones matching both the OrderedQuery and UnorderedQuery)distance_in(RangeBounds<T>) -> DistanceFilter(limit visited nodes and returned shapes within a range)map_byproduct<B>((&Shape, Self::Byproduct) -> B) -> MapByproduct<Byproduct=T>(allow for mutating/removing of parts of the byproduct to only what you need, less allocation/stack usage)rev() -> ReversedQuery
Traverse Methods
I'd like to replace the current methods with ones specific to their kind of query and allow for returned types/fast paths that some are missing.
These can be reduced down to [un]ordered_collect::<FromQuery> and [un]ordered_iter with helper methods like: [un]ordered_first = [un]ordered_collect::<Option<_>>, [un]ordered_vec = [un]ordered_collect::<Vec<_>>, and [un]ordered_inline::<const N> = [un]ordered_collect::<heapless::Vec<_, N>>.
FromQuery Traits
Allows traversal functions to have one implementation for various collection type outputs.
trait FromUnorderedQuery<'shape, Shape, Byproduct> {
/// Initialize empty
fn new() -> Self;
/// Indicate to unordered traversal functions whether it should stop traversal for limited size collections like Option and heapless::Vec
fn is_full(&self) -> bool;
/// Insert an item into the collection
fn ingress(&self, shape: &'shape Shape, byproduct: Byproduct);
}trait FromOrderedQuery<'shape, T: BHValue, Shape, Byproduct> {
/// Initialize empty
fn new() -> Self;
/// Indicate to ordered traversal functions whether the distance of a shape or node is greater than or equal to the maximum contained distance for limited size collections like Option and heapless::Vec
fn is_distance_excluded(&self, distance: T) -> bool;
/// Insert an item into the collection. The collection may have to be insertion sorted to keep track of the maximum contained distance, or lazy sorted in ordered_egress for non-limited size collections. When full it should silently fail if is_distance_excluded returns true, or pop the maximum distance shape when it returns false
fn ingress(&self, shape &'shape Shape, distance: T, byproduct: Byproduct);
/// Called after traversal is finished and before the collection is returned. Modifies contained distances with the OrderedQuery::external_distance function and sorts if necessary
fn egress<const D: usize>(&self, query: &impl OrderedQuery<'shape, T, D, Shape>);
}Coverage Enum
Indicates to traversal functions whether a node is relevant to the query.
enum Coverage {
/// All descendants can't match the query, do not traverse children
Excluded,
/// Can't determine whether all descendants are included or not
Pending,
/// All descendants match the query, unordered queries don't need to perform AABB tests on descendants
Included,
}Using an enum (rather than a boolean in the IntersectsAabb trait) allows for inverting an unordered query, where Excluded and Included are inversions of each other and pending is the inversion of itself.
NodeState Trait
When a query is a composite of multiple queries it can be beneficial to keep track of coverage for each to not test more than what's necessary. Enables parent to child short-circuit evaluation for "or" type of queries.
trait NodeState {
/// Initialize to a neutral state
fn new_pending() -> Self;
/// The combined coverage of the internal state
fn coverage(&self) - Coverage;
}UnorderedQuery Trait
Would "boolean" prefixes be preferred over "unordered"?
trait UnorderedQuery<'shape, T: BHValue, const D: usize, Shape> {
/// Any additional information produced by the query
type Byproduct;
/// State tracking from parent to child nodes
type State: NodeState;
/// Determines if a node is relevant to the query
fn aabb_match(&self, state: &Self::State, aabb: &Aabb<T, D>) -> Self::State;
/// Determines if a shape is relevant to the query, returning Some when it is
fn shape_match(&self, state: &Self::State, shape: &'shape Shape, shape_index: usize) -> Option<Byproduct>
}OrderedQuery Trait
Would "distance" prefixes be preferred over "ordered"?
trait OrderedQuery<'shape, T: BHValue, const D: usize, Shape> {
/// Any additional information produced by the query
type Byproduct;
/// State tracking from parent to child nodes
type State: NodeState;
/// Determines if a node is relevant to the query and calculates its minimum and maximum distance
fn aabb_distance(&self, state: &Self::State, aabb: &Aabb<T, D>) -> (RangeInclusive<T>, Self::State)
/// Determines if a shape is relevant to the query and calculates its distance, returning Some when it is
fn shape_distance(&self, state: &Self::State, shape: &'shape Shape) -> Option<(T, Self::Byproduct)>
/// Modifies distances such as those from OrderedQuery::distance_in so they may be accurately compared against distances used during traversal
fn internal_distance(&self, external_distance: T) -> T;
/// Modifies distances before they are returned, allowing for a different distance to be used during traversal such as squared or negated
fn external_distance(&self, internal_distance: T) -> T;
}
// Ordered queries can act as unordered queries through a blanket implementation that removes distance info
impl<...> UnorderedQuery<...> for OrderedQuery<...> {
...
}Backwards Compatibility
For the most part traversal methods match the use of what they're replacing:
| Old | New |
|---|---|
traverse(&query, &shapes) |
unordered_vec(&query, &shapes) |
traverse_iterator(&query, &shapes) |
unordered_iter(&query, &shapes) |
nearest_traverse_iterator(&ray, &shapes) |
ordered_iter(&ray, &shapes) |
farthest_traverse_iterator(&ray, &shapes) |
ordered_iter(&ray.rev(), &shapes) |
nearest_to(point, &shapes) |
ordered_first(&point, &shapes) |
With the exception of no match for the nearest_child_traverse_iterator and farthest_child_traverse_iterator, which in my opinion both shouldn't have been kept when they were replaced. Ordered iterators could be implemented with heapless::BinaryHeap to keep its non-allocating behavior but without its ordering issues.
Built-in query objects could only be implemented for built-in shapes since queries would also be responsible for shape tests. Though if we want to match current non-shape-aware behavior they may be implemented to generically handle any shape, but only re-test their AABBs in UnorderedQuery::match_shape and OrderedQuery::shape_distance, which I don't recommend.
The PointDistance trait may be kept with a blanket implementation for OrderedQuery so that nalgebra::Point may still be used as a query.
Remarks
Thanks for reading! I know it's a lot to look through. Feel free to ask me to clarify anything here or post your own opinions on this.
I am willing to work on this myself. I'll wait to know if this proposal is generally accepted as is or if any tweaks should be made first.