Physics: rapier3d.js + three.js

So, I’ve managed to get the rapier3d physics engine working together with three.js. Things are in the very early stages so I don’t have a lot to report yet. I was able to bounce an object off the terrain :slight_smile:

The API for rapier is pretty lean compared to some of the other engines I looked at, which means that the learning curve is not too bad. I was particularly drawn to the fact that the same people who are writing the engine in Rust are also maintaining the JavaScript bindings - when you have two separate teams doing this there’s a nontrivial chance of incompatibilities and impedance mismatches.

One big challenge was just getting the WASM bundle to load. I was using Snowpack to bundle my app, but this doesn’t support WASM resources currently. Webpack 5 also has serious difficulties with WASM at the moment. I ended up having to downgrade to Webpack 4 to get everything to work, which made me sad. (I believe that it may also be possible to get it working with Rollup as well, but I haven’t tried.)

Eventually what I may do - since my project is a large monorepo - is have one subpackage that builds a library bundle with webpack 4 or rollup, and then import that library into a Snowpack build for the whole app.

One other thing I wanted to mention - the rapier TypeScript APIs are pretty flexible about the data types they accept. So for example, their Vector type doesn’t have to be specifically their vector class, it can be any object that has an x/y/z coordinate. That means you can pass in a three.js Vector3 without having to convert it to another data type.

Unfortunately, it doesn’t work in the reverse direction. One thing that would be a nice improvement for the three.js TypeScript type defininitions is to define Vector3Like, or IVector3 interface types, and then make any method that accepts a Vector3 accept these interfaces - so for example Vector3.copy() would take a Readonly<IVector3> as input. This would then allow you to things like meshObject.position.copy(rigidBody.translation()) and not have to convert or typecast anything. Similar interfaces would be defined for Eulers, Quaternions and other simple object types that are used as parameters.

1 Like

I don’t think this is a priority for TypeScript maintainers, since it’s not Three.js’ responsibility to know how other third-party libraries work. It would be impossible to maintain and keep up with all the new packages that come out. You may need to create your own custom type definitions for this specific case.

I’m not proposing to make changes to three.js for a specific third-party library. The changes I propose are much more general than that - they would work with many different third-party libraries.

Just about any 3D library is going to have a representation for a 3d vector, but there are really only two good choices for representation: either [x, y, z] or { x, y, z }. For the former case, we can do Vector3.set(..arrayVector), but what about the latter case?

In TypeScript/JavaScript programming we have this concept of structural vs nominal typing - TypeScript is structural, whereas Flow is nominal. Structural typing simply means that any type that has the right “shape” should work - it doesn’t have to be a specific class.

So even though the expression { x: 1, y: 2, z: 3 } is not a Vector3, if you were to call (in JavaScript) Vector3.copy({ x: 1, y: 2, z: 3 }) it will work exactly as you expect - it will copy the values of the x, y and z properties and ignore everything else about the object. The fact that it wasn’t constructed with the Vector3 constructor doesn’t matter

Therefore, if Vector3.copy() will work with a looser type than Vector3, the type definitions should reflect that. This is fairly standard practice in TypeScript - don’t restrict types in ways you don’t care about. It’s why PromiseLike exists - because there are multiple implementations of Promise, the PromiseLike interface represents an object that may not actually be a Promise, but has the same interface as one.

Also, if you don’t plan on mutating the argument, it’s good practice to wrap it in Readonly. This allows you to pass in immutable/frozen objects without getting a compiler error.

I see what you mean now. So for instance, in this line instead of
copy(v: Vector3): this;

it could read

type Vector3Like = {x: number, y: number, z: number};
copy(v: Vector3Like): this;

to make it more versatile? I kinda like the idea!

Yes, exactly! In effect, you are making the type definitions more accurate - by accurately reflecting what the API will accept.