-
-
Notifications
You must be signed in to change notification settings - Fork 304
Improve ClosestPointToPoint
#699
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
base: master
Are you sure you want to change the base?
Conversation
gkjohnson
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice work! Looking at the PQP_Distance function it looks like they use a similar approach to the hybrid method in that it starts a new sorted queue once the last one becomes full. I assume this has the benefit of limiting memory usage and preventing insertion time from becoming too long. I wonder if we'd see benefits from that solution here?
See the DistanceQueueRecurse function and this condition where it recurses and creates a new queue to traverse. Here is documentation from header file describing "qsize" use. Granted the function is a bit different because it's performing geometry-to-geometry so the queue is storing two nodes and a distance but fundamentally it's very similar.
| if ( leftDistance < closestDistanceSq && leftDistance < maxThresholdSq ) { | ||
|
|
||
| if ( _closestPointToPoint( leftIndex ) ) return true; | ||
| if ( rightDistance < closestDistanceSq ) return _closestPointToPoint( rightIndex ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Don't we want to check distance to maxThresholdSq here, as well?
| @@ -0,0 +1,20 @@ | |||
| export function closestDistanceSquaredPointToBox( nodeIndex32, array, point ) { | |||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm wondering how much of the 25% improvement we would see if we used this function instead of three.js' built in version in the original "shapecast" function (see #656) - similar to the box / ray test where avoiding reading the bounds to a Box3 object gave a significant speed improvement.
| if ( count >= sortedListMaxCount ) { | ||
|
|
||
| if ( _closestPointToPoint( nodeIndex32 ) ) return; | ||
|
|
||
| continue; | ||
|
|
||
| } | ||
|
|
||
| count ++; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you explain the logic and rationale of the hybrid function a bit? It looks like it will choose the "best" node from the first 16 nodes (by default) and then fall back to the basic approach of traversing to the leaves for each of those nodes in order. The count never goes down, right?
|
Regarding the use of sorted queue, I would like to do more tests to understand when it is necessary. I would also like to modify the current Edit: sorry I read later that you also recommended doing to this. I'll try these days :) |
|
Unfortunately, modifying the I believe it is correct to expose a Could we make an alternative method for internal use, which instead of passing the Or could we stop using shapecast for internal methods (as I am doing in this PR)? Or leave it as is. The overhead of shapecast is mainly callbacks and superfluous data that is passed as a parameter but not used by the caller (like |
|
Thanks for checking!
I was mostly curious as to how much of the improvement was coming from this conversion to a Box3. It's worth documenting in #656, I think, but given that it's a breaking change lets not make it for now. If we have a method that would be frequently used and benefit from the performance boost (like we're doing here) then I think it makes sense. Shapecast is just easier to implement initially. I'm curious about #699 (comment) as well as how the PQP_Distance approach compares to this hybrid approach:
|
|
@gkjohnson Sorry for the delay... Brief recap: The current algorithm recursively checks the child node with a shorter distance first and then the second. What can be improved? Solution: Using a sortedList, I noticed that on average 30% fewer nodes are checked (using the PR geometry), but the algorithm is slower. What can be improved? Solution 1 (Hybrid Approach): Solution 2 (Hybrid Approach 2): This approach might be better than the previous one, because all nodes in the list would have precisely the same depth in the tree. Solution 3 (Two separated lists): This logic could also be applied for other algorithms that traverse via score What do you think? :) |
|
Thanks for the summary! Generally this seems like a good improvement. Have you tried "solution 3", as well? Was Solution 2 still best? 3 sounds closest to what the
I know it will still add some overhead but I'm wondering if this would be best added for the shapecast function so the faster algorithm is used for other queries, as well. This should just sort based on the What do you think? |
Actually I only tried the first method. I just committed the second one.
Sure, as you wish 😄 But I will finish trying the third method first. |
|
Hi! Here I am again after a long break 😄 Small recap: I replaced the The minHeap has a fixed maximum capacity. Once this limit is reached, there are two possible strategies—both inspired by the PQP library:
Both approaches yield comparable results, with performance depending on the model used and the spatial distribution of points. Interestingly, the minHeap-based algorithm shows noticeable speed improvements only when the target point lies inside the bounding box of the object. Outside of it, the traditional recursive approach—with early pruning based on the minimum possible distance—is actually more efficient. This leads to a possible optimization: we could dynamically select the most suitable algorithm depending on whether the query point lies within the bounding box. In my dynamic BVH implementation, the minHeap significantly speeds up the process of finding the best location to insert a new node. This is because, in practice, most new instances fall within the root bounding box of their parent node. Over the next few days, I plan to continue refining the |
|
I did several tests on the Can we consider creating the shapecast methods without conversion and shapecastByScore (where we use minHeap), just for internal use? |
|
Thanks for taking a look at this and getting some more concrete numbers.
I think 5% is an okay cost to pay for the deduplicated code and flexibility of the callback system. 30-35% is a nice savings, though. It looks like you're suggesting changing to the following arguments: nodeScoreFunc( nodeIndex, fullFloat32Array );Lets implement that and get the PR cleaned up and I can add a flag to make sure the shapecast function is backwards compatible for release. After that I'm wondering if we do something like the following will we lose any significant performance gains? This would let us pass the 6-float buffer into the function instead of the whole bvh array so usage would be more clear for end users: const bb = new Float32Array( fullFloat32Array.buffer, BOUNDING_DATA_INDEX( c1 ) * 4, 6 );
const score = nodeScoreFunc( nodeIndex, bb );
// or
const _scratch = new Float32Array( 6 );
// ...
const bbStart = BOUNDING_DATA_INDEX( c1 );
for ( let i = 0; i < 6; i ++ ) _scratch[ i ] = fullFloat32Array[ i + bbStart ];
const score = nodeScoreFunc( nodeIndex, bb );Once the PR is ready I can help test the performance differences, as well. Thanks again for your work on this! |



By not using the
shapecastfunction, there is a slight improvement of about 25%.I have added a page for benchmarks to be removed later called 'testToRemove.html' (I know, I have a big imagination).
I will also add the function using the sorted list shortly (#695).
Edit: I finished 3 first versions of closestPointToPoint.