3D Physics Engine Pt. 6

Hi folks! It’s been roughly 6 years since the last elm-physics update. This year I managed to pick up the project and develop new exciting features and a new API. You can find all the previous posts here.

crane

In this post I am going to write about:

  1. Simulation Stability
  2. Declarative API
  3. New Features
  4. Performance
  5. Future Plans

Simulation Stability

This was a hard one that made me abandon the project for a while. It is easy to make things move when simulated, but it is notoriously hard to simulate stable stacks. Things would jitter and fall apart, which makes the engine impractical for anything.

The idea was to carry contact impulses from one frame to another frame, and use them to warm-start the solver. In order to implement this, I had already had a go at generating stable ids for contacts based on colliding bodies, shapes and features of these shapes (think body-a-body-b-shape-a-shape-b-face-a-face-b-vertex-a).

What I didn’t get right is that I needed to not just carry the impulses for the contact equations, but also pre-warm the bodies themselves. Claude helped me figure out this bit, and this was a great way to learn more about physics simulations.

The second improvement was to pre-sort bodies from the ground up (projected on the gravity axis), because the solver has an error build-up, and it makes sense for it to be biased towards the static ground at the bottom.

The third improvement was to solve contact equations first, and then frictions. This way the normal forces could be used to put bounds on frictions, instead of only relying on the gravity force like cannon.js. This idea comes from the bullet physics engine.

As a result, elm-physics is a bit more stable than cannon.js. This is how the same demo looks like after simulating 60 seconds (I had to disable the sleeping feature in cannon.js):

I’ve additionally added a test case that simulates a stack of 5 blocks for a long time and checks that the blocks are not moving.

Declarative API

A long time ago, I had a call with Evan to discuss possibilities to improve the elm-physics API. I think I managed to incorporate some of these ideas into the new release. I also took a lot of inspiration from Ian Mackenzie’s packages.

The main change was getting rid of the World data type, and stopping the Body data type from being a container of user-defined data. The new simulate function operates on the list of tuples of user-defined data and a body.

simulate :
    Config id
    -> List ( id, Body )
    -> ( List ( id, Body ), Contacts id )

On one hand this was a tricky change, because internally I had to maintain ids for the bodies and contacts between them. On the other hand, this made the demos much shorter, and let me remove list-like functions from the API in favor of Elm’s List!

The second change was reducing reliance on chained calls for body declarations, and instead requiring everything necessary to be specified upfront. Previously, to declare a dynamic body, you’d need to:

table =
    Body.compound tableShapes Table
        |> Body.withBehavior 
            (Body.dynamic (Mass.kilograms 3.58))
        |> Body.withMaterial
            (Material.custom
                { friction = 0.3
                , bounciness = 0
                })

And if you didn’t, it would be an immovable static body with some default material. I remember Martin Stewart was confused by this.

The new API is shorter, and less surprising. Plus all the simple bodies are dynamic by default. For a compound one, you’d write:

table =
    Physics.dynamic [ ( tableShape, Material.wood ) ]

The engine calculates everything, including the mass. It is possible to override the mass, but that is buried deeper in the API. Default materials are provided that include the density, friction and bounciness. Material Dense and Material Surface are used to distinguish materials with density from the surface materials, because static bodies don’t require density — they are of infinite mass.

It is also possible to add and subtract the shapes that compound bodies are made of. This is just for mass properties (mass, inertia, center of mass), unlike boolean operations on geometry.

The last API change was making constraints and collision masks functions that are passed into the simulation config. Previously, this was data stored in the World that you had to update. Now, these are dynamically evaluated rules that are much easier to supply.

New Features

The engine got numerous new features!

  • Collision masks let you control which two bodies may collide
  • New shapes: the capsule shape was pretty hard to make collide with convex shapes; the cone shape was contributed by Wolfgang Schuster
  • Locking: limit movement along or rotation around the world axes. This makes it technically possible to simulate in 2D.
  • Setting velocities: I was reluctant to add this in favour of interacting with forces, but this lets you capture and transfer physics state in case of e.g. multiplayer games.
  • Kinematic bodies: like static, but can be moved by setting velocity; they push dynamic bodies and carry them by friction. Useful for things like moving platforms.

Some of these features were used to make the character controller demo: an orientation-locked capsule that can jump around and push boxes. I’d like to turn it into a game at some point.

character

Performance

Internal id generation, transferring impulses between the frames, solving contacts and then frictions made the engine a bit slower, because of the increased reliance on immutable data structures, so I had to discover new ways to squeeze a bit more performance.

Island-based solver — finding colliding islands and solving them individually. If an island is two bodies, then we don’t even need an array of bodies, we can update both in the recursion. Some islands converge before reaching the maximum solver iterations.

Reducing contact points between colliding bodies, another idea I borrowed from bullet. The contact clipping algorithm between colliding faces of two blocks could produce up to 8 points, which is now reduced to 4 points. Fewer points — less work for the solver.

Optimized representation of convex shapes to speed up collisions. Representing blocks as half extents made collisions faster, because not all 8 points are needed to calculate projected bounds onto an axis. For arbitrary convex shapes I changed the representation to store edges and faces indexed by parallel ones — this made it possible to get the colliding feature from the separating axis, without having to iterate over the whole shape to find it.

Used eigendecomposition to store inertia as a Vec3 of principal moments, instead of a 3x3 matrix — the orientation of the principal axes is absorbed into the center-of-mass frame. This also made it possible to visualize inertia in the sandbox application!

Inlined Elm’s Dict to get a faster Dict Int that compares keys with a - b > 0 instead of the polymorphic comparison function.

Used sentinel values instead of Maybe where possible, to avoid an extra allocation for the Just.

If you wonder where elm-physics stands performance-wise — for a resting scene of 5x5x5 blocks, it is roughly 2 times slower than cannon.js. Collisions are a bit faster, but 11% of the frame is spent on GC, and the solver is too slow because of Elm’s immutable Array. Basically, each solver iteration requires getting two bodies, updating them, and writing them back. In Elm, even getting an element from Array pays the price of allocating a JavaScript object for Just. In my fast Dict Int I addressed the latter by introducing a getter with a default value in case of Nothing.

Future Plans

I recently gave a talk on the new API at FP-Matsuri in Tokyo (slides in Japanese) and I am slowly wrapping up the sleep feature. This feature lets the engine mark bodies that stopped moving as asleep, to then stop simulating islands of such bodies. This should further improve the stability and performance.

As for any new features, the best way to evolve it is by use cases. Please reach out to me if you have an idea for what to build with elm-physics!

It was fun to pair program with Wolfgang Schuster at the Elm Camp on setting up the physics for his block-topple game. Erkal Selman used AI to make some impressive demos with elm-physics.

I’ve been experimenting with making demos with AI too — it was surprisingly good at using elm-physics. For example, I prompted it to build this physically simulated clock:

clock

21 Likes

Amazing animations! Is that all made just with Elm?

All just Elm and all a lot of fun! I’m still actively working on the game I started prototyping at Elm Camp, but I’m already thinking about other things to build with it. Just trying not to distract myself too much with the other ideas :sweat_smile:

2 Likes