Skip to content

New plane solver#905

Merged
Jondolf merged 26 commits into
avianphysics:mainfrom
unpairedbracket:faster_normal_projection
Dec 31, 2025
Merged

New plane solver#905
Jondolf merged 26 commits into
avianphysics:mainfrom
unpairedbracket:faster_normal_projection

Conversation

@unpairedbracket

@unpairedbracket unpairedbracket commented Dec 6, 2025

Copy link
Copy Markdown
Contributor

Opening as a draft because it still needs a docs pass, benchmarking to ensure it's actually faster/scales better than the existing algorithm and removing the old code if so.

Objective

The current plane solver is O(n^3) in the number of collision planes. It's not likely to break the bank but it'd be preferable to bring this down to O(n^2) or lower

Solution

  • I figured out a GJK-like algorithm for solving the underlying quadratic programming problem, and implemented it. Steps of the algorithm are O(n), and it should terminate in at most n steps, though I've never seen the step number exceed 4. I've tried to construct sets of normals that would take 5+ steps to converge and the arrangements of planes it would take seem very specific. Blog post fully explaining the details is in progress

Testing

  • Tested so far in the 3d move-and-slide example, still need to test in 2d as well
  • I'm intending to benchmark the new method against the existing one for some arrangements of maybe 1 to 10 normals? to try and establish scalings empirically
  • I think there are a few more micro-optimisations I could squeeze out, like imposing a specific ordering of the normals in SimplicialCone::Wedge(n1, n2) relative to the new search direction, which would save a few comparisons and dot products in the relevant branch of SimplicialCone::project_point.
  • The SimplicialCone enum exists to keep the algorithm allocation-free (as opposed to keeping the current simplex points in a vec. Alternatively the first couple of iterations of the loop could be manually unrolled, which would get rid of the match and might speed things up a bit

@Jondolf Jondolf added the A-Character-Controller Relates to character controllers label Dec 6, 2025
This uses a quasirandom sphere cover generator to sample input velocities,
because the number of iterations of the new implementation is dependent on
the direction of the input velocity. The existing one can early-out for some
input directions (cases where no projection is required) as well.
@unpairedbracket

Copy link
Copy Markdown
Contributor Author

Ok, I've managed to get a decent benchmark set up for this. Performance of both methods vary based on where the velocity is relative to the normal cone (the old one can early-out if all constraints are fine, the new one takes varying numbers of steps depending on whether the initial velocity is allowed, or the end result is constrained by one plane or two). Because of that I came up with a thing for sampling an endless sequence of points on the sphere evenly (based on a quasirandom sequence) to use a different velocity direction for each iteration. Uniform sampling around the whole sphere of directions might not be the most realistic distribution of directions for real-world use, but it's better than nothing.

Scaling for the new method looks better than the old one on a linear scale:

lines

On log-log it appears that the old one is actually closer to quadratic than cubic (the brighter lines without markers are power laws, n for red and n^2 for green), and the new one is roughly linear (in case anyone reading this is unfamiliar with log-log plots, power laws with exponent α on linear plots translate to lines with gradient α on log-log plots: f(x) = x^α -> log f(x) = α log x):

lines_powerlaws

@Jondolf Jondolf added C-Performance Improvements or questions related to performance D-Modest A moderate level of difficulty: suitable for simple features or challenging fixes labels Dec 9, 2025
@unpairedbracket

Copy link
Copy Markdown
Contributor Author

Explainer up at https://benspiers.co.uk/Games/Velocity-Projection in case it's useful to anyone

…eren't accurate

Gotta account for the cross product being unnormalised when computing distances, or they're meaningless
Guaranteeing that the triple product x0 . (n1 x n2) is nonnegative for a
SimplicialCone::Wedge(n1,n2) allows us to skip some sign checks when handling it later.
The alpha == 0.0 case when handling Rays gets picked up on the next iteration
because the resulting search vector is zero
Not sure if this is too much tbh, but can figure that out in review
This should maybe be equal to normals.len(), or I think we could fairly safely just cap it at 4?
This one has perfect agreement \o/
…ion functions from move_and_slide

Not sure if this is the best structure long-term but it gets me out of having to write module docs for now
This means doing a bunch of feature-gating and using Scalar/Vector/Dir and .adjust_precision() around the place
@unpairedbracket

Copy link
Copy Markdown
Contributor Author

Fixed a maths error, and hopefully got CI passing again? Tried to run the steps locally but the tests fill up the memory and then the storage of the laptop I have with me while I'm home for the holidays, then fail because the disk is full 😅

I've added a "test" (it can't fail) that finds the worst agreement between the old method and the new for each of the sets of normals used in the benchmark, and prints what those worst cases are. It (after some searching and head-scratching) pointed me to a problem with my maths, which I've now fixed. The "test" now reports good agreement between the two methods. I also did that optimisation around the ordering in SimplicialCone::Wedge that I think I mentioned before, because it made the code a bit cleaner.

Still needs a pass for docs and cleanup before ready for merging, but it's getting towards a good place now I think. Not sure if we'll want to keep the benchmark around - I see that avian has its own bench system but not sure if it this kind of benchmark would fit into it. It might not be of much continued value now I've demonstrated the performance and scaling anyway.

The test isn't any use in an automated suite so it'll either need redoing to make it fallible or removing. Checking that the solutions it finds is optimal might be tricky (it'd basically involve running the old brute-force method to check against, and keeping two implementations in the code seems like it might be weird?) but checking that they're at least feasible (n_j . v_out > 0 for all j) would be straightforward, so maybe just do that in a proper test?

 - if one of the constraints is not satisfied (to within DOT_EPSILON)
 - if the quantity we're supposed to be minimising (|v_out - v_in|) is worse (by more than DOT_EPSILON) for the new method than bruteforce

Not sure if the DOT_EPSILON is the right tolerance to use for the second criterion, but it seems to work
…her than me to understand what's going on here
Make the version in velocity_projection pub so it can be used in the benchmark as well,
document it because it's pub now, and add the "test" feature to the [[bench]] so that
the struct is included when compiling the benchmark
@unpairedbracket unpairedbracket marked this pull request as ready for review December 29, 2025 20:15

@Jondolf Jondolf left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Huge thanks for this work! The perf win is small in absolute terms, but a win is a win 😄 and the algorithm itself is super cool!

I did a quick pass to clean up some of the docs and naming. This looks good to me now, so I'll merge tomorrow unless something pops up

@Jondolf Jondolf added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Dec 30, 2025
@Jondolf Jondolf merged commit 5bef382 into avianphysics:main Dec 31, 2025
6 checks passed
slyedoc added a commit to slyedoc/avian that referenced this pull request Jan 16, 2026
- Rename shadows_enabled to shadow_maps_enabled in examples
- Replace EarthlikeAtmosphere resource with Atmosphere::earthlike() method
- Rebase on upstream main (new plane solver avianphysics#905)
slyedoc added a commit to slyedoc/avian that referenced this pull request Feb 8, 2026
- Rename shadows_enabled to shadow_maps_enabled in examples
- Replace EarthlikeAtmosphere resource with Atmosphere::earthlike() method
- Rebase on upstream main (new plane solver avianphysics#905)
slyedoc added a commit to slyedoc/avian that referenced this pull request Mar 31, 2026
- Rename shadows_enabled to shadow_maps_enabled in examples
- Replace EarthlikeAtmosphere resource with Atmosphere::earthlike() method
- Rebase on upstream main (new plane solver avianphysics#905)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

A-Character-Controller Relates to character controllers C-Performance Improvements or questions related to performance D-Modest A moderate level of difficulty: suitable for simple features or challenging fixes S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants