dimforge / rapier.js

Official JavaScript bindings for the Rapier physics engine.
https://rapier.rs
Apache License 2.0
410 stars 56 forks source link

Allow asynchronous initialization of Rapier #30

Open viridia opened 3 years ago

viridia commented 3 years ago

Right now there are two ways to initialize rapier.js: The 'rapier-compat' package, or (for lack of a better name) the 'vanilla' version.

The reason that rapier-compat exists is to allow use with bundlers that have trouble loading .wasm resources. However, this compatibility comes at a price: the entire .wasm resource is encoded as text. While this works, it is not ideal.

Right now the only bundler (that I know of) that can handle the 'vanilla' version of rapier.js is Webpack 4. Webpack 5 will not work, nor will Snowpack. Rollup uses a strategy similar to rapier-compat, encoding the resource as text. I have not tried Parcel, Vite, or any of the other bundlers, but I imagine they have similar issues.

However, there is a third option, which is to not depend on a bundler encoding at all. This is in fact the recommended option for snowpack, as shown in this guide. This technique loads the .wasm resource directly using the browser fetch API, and then once the promise resolves, it manually bootstraps the JavaScript wrappers around the WASM resource.

However, doing it this way means that the .wasm resource will not be available until after the fetch promise resolves. Unfortunately, the current rapier.js TypeScript code imports the raw bundle as a static resource - that is, it assume that the WASM code is already ready at the time that the JavaScript code is imported. So this option will not work with the current rapier.js code.

To support this use case, it would be necessary to change the way that the rapier.js World object, and the objects within it, are constructed. Instead of statically importing all of the references to WASM classes, these would need to be passed in as part of the constructor call. Possibly it would make sense to define a TypeScript interface which has the same shape to "import * from 'raw'" and pass that object around to each class that is constructed.

Also, a lot of the import { symbol } statements would be changed to import type { symbol } to let the TypeScript compiler know that we're only interested in the type definitions, not the values of the imports. "import type" statements are always removed from the output JavaScript, so this prevents the .wasm resources from being accidentally dereferenced too early.

This is not a huge change, but unfortunately it is a backwards-incompatible one. The strategy proposed here is not that different from the way JavaScript clients use Rapier already - you have to have a handle to the rapier object before you can create a world, rb, collider, or anything else other than the basic math types.

andrewmunro commented 3 years ago

I second this. I've been attempting to switch from PhysX to Rapier as the API seems much cleaner and the determinism looks really promising when syncing objects between clients and a server.

Unfortunately, one of the blocking issues I've encountered is I cannot create an isomorphic build of our physics package, i.e one that runs on both the client (browser) and server (nodejs). I can get things running fine on the browser using physx-compat and esbuild, but not on the server.

This kind of thing is achievable quite easily for PhysX which loads the wasm file asynchronously at runtime.