diff --git a/.github/workflows/publish-wasm.yml b/.github/workflows/publish-wasm.yml new file mode 100644 index 00000000..5ca7e78d --- /dev/null +++ b/.github/workflows/publish-wasm.yml @@ -0,0 +1,69 @@ +name: publish-wasm + +on: + push: + tags: + - '[0-9]*.[0-9]*.[0-9]*' + +jobs: + publish: + name: Publish MuJoCo WASM package + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js for WASM bindings + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Prepare Linux + run: bash ./.github/workflows/build_steps.sh prepare_linux + + - name: Install NPM Dependencies for WASM bindings + run: bash ./.github/workflows/build_steps.sh npm_ci + + - name: Setup Emscripten for WASM bindings + run: bash ./.github/workflows/build_steps.sh setup_emsdk + + - name: Build WASM bindings + run: bash ./.github/workflows/build_steps.sh build_test_wasm + + - name: Prepare clean package manifest in dist + run: | + if [ -f wasm/package.npm.json ]; then + mkdir -p wasm/dist + cp wasm/package.npm.json wasm/dist/package.json + echo "Copied wasm/package.npm.json -> wasm/dist/package.json" + else + echo "No wasm/package.npm.json found; aborting" + exit 1 + fi + + - name: Prepare README for dist + run: | + if [ -f wasm/README.npm.md ]; then + mkdir -p wasm/dist + cp wasm/README.npm.md wasm/dist/README.md + echo "Copied wasm/README.npm.md -> wasm/dist/README.md" + else + echo "No wasm/README.npm.md found; skipping README copy" + fi + + - name: Sync version with tag (dist) + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "Setting package version to: ${VERSION}" + npm --prefix wasm/dist version "${VERSION}" --no-git-tag-version + + - name: Configure npm auth + env: + NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }} + run: | + echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > ~/.npmrc + + - name: Preview package (dist) + run: npm pack --dry-run ./wasm/dist + + - name: Publish package (dist) + run: npm publish ./wasm/dist --access public diff --git a/wasm/README.npm.md b/wasm/README.npm.md new file mode 100644 index 00000000..2bb96153 --- /dev/null +++ b/wasm/README.npm.md @@ -0,0 +1,209 @@ +# MuJoCo WASM bindings + +Official WebAssembly (WASM) bindings for the MuJoCo physics engine, compiled from the MuJoCo C/C++ sources into WASM with JS glue (Emscripten + Embind). The package ships prebuilt ESM-ready JS/WASM artifacts and TypeScript types for immediate use. + +**Important**:_These bindings are still a WIP_. + +## Install +```sh +npm install mujoco_wasm +``` + +## Quick start (ESM / TypeScript) + +```ts +import loadMujoco from 'mujoco_wasm'; + +const mujoco = await loadMujoco(); + +const model = mujoco.MjModel.fromXMLString(` + + + + + +`); + +const data = new mujoco.MjData(model); +mujoco.mj_step(model, data); +``` + +## Usage Guide +When interacting with MuJoCo objects through the WASM bindings, it's important to understand how data is accessed. Properties on objects like `MjModel` and `MjData` can expose data in two ways: by copy or by reference. + +### Copy vs. Reference + +#### 1. By Copy (Value-based access) + +Some properties return a copy of the data at the time of access. This is common for complex data structures that need to be marshalled from C++ to JavaScript. + +A key example is `MjData.contact`. When you access `data.contact`, you get a new array containing the contacts at that specific moment in the simulation. If you step the simulation forward, this array will not be updated. You must access `data.contact` again to get the new contact information. + +Example: +```typescript +// Gets contacts at the current time. +const contacts = data.contact; + +// Step the simulation +mujoco.mj_step(model, data); + +// `contacts` is now stale. To get the new contacts, you must access the property again: +const newContacts = data.contact; +``` + +#### 2. By Reference (View-based access) +Many properties, especially large numerical arrays, return a live view directly into the WebAssembly memory. This is highly efficient as it avoids copying large amounts of data. + +A key example is `MjData.qpos` (joint positions). When you get a reference to this array, it points directly to the simulation's state data. Any changes in the simulation (e.g., after a call to `mj_step`) will be immediately reflected in this array. + +```typescript +// `qpos` is a live view into the simulation state. +const qpos = data.qpos; + +console.log(qpos[0]); // Print initial position + +// Step the simulation +mujoco.mj_step(model, data); + +// `qpos` is automatically updated. +console.log(qpos[0]); // Print new position +``` + +### Data Layout: Row-Major Matrices +When a function from the MuJoCo C API returns a matrix (or needs a matrix as input), these are represented in the JavaScript bindings as flat, one-dimensional `TypedArray`'s. The elements are stored in row-major order. + +For example, a 3x10 matrix will be returned as a flat array with 30 elements. The first 10 elements represent the first row, the next 10 represent the second row, and so on. + +Example: Accessing an element at `(row, col)` +```typescript +// A 3x10 matrix stored as a flat array. +const matrix: Float64Array = ...; +const nRows = 3; +const nCols = 10; + +// To access the element at row `i` and column `j`: +const element = matrix[i * nCols + j]; +``` + +### Working with Out Parameters +Many functions in the MuJoCo C API use "out parameters" to return data. This means instead of returning a value, they write the result into one of the arguments passed to them by reference (using pointers). In our JavaScript bindings, you'll need to handle these cases specifically. + +There are two main scenarios you'll encounter: + +#### 1. Array-like Out Parameters +When a function expects a pointer to a primitive type (like `mjtNum*` or `int*`) to write an array of values, you need to pre-allocate memory for the result on the JavaScript side. We provide helper classes for this: `mujoco.Uint8Buffer`, `mujoco.DoubleBuffer`, `mujoco.FloatBuffer`, and `mujoco.IntBuffer`. + +Here's how to use them: + +1. Create a buffer: Instantiate the appropriate buffer class with an initial array of the correct size (e.g., an array of zeros). +2. Call the function: Pass the buffer instance to the function as the out parameter. +3. Access the result: Use the `.getView()` method on the buffer to get a `TypedArray` view of the data written by the C++ function. +4. Free the memory: When you are done with the buffer, you must call the `.delete()` method to free the underlying memory and prevent memory leaks. + +Example: Rotating a vector + +The function `mju_rotVecQuat` rotates a vector `vec` by a quaternion `quat` and stores the result in the `res` out parameter. + +```typescript +// Create a buffer to hold the 3D vector result. +const res = new mujoco.DoubleBuffer([0, 0, 0]); + +const vec = [1, 0, 0]; +const quat = [0.707, 0, 0, 0.707]; // 90-degree rotation around z-axis + +try { + // Call the function with the buffer as the out parameter. + mujoco.mju_rotVecQuat(res, vec, quat); + + // Get the result as a Float64Array. + const resultView = res.getView(); + console.log(resultView); // Expected: approximately [0, 1, 0] + +} finally { + // IMPORTANT: Free the memory allocated for the buffer. + res.delete(); +} +``` + +#### 2. Struct Out Parameters (e.g., mjvCamera*, mjvScene*) +When a function modifies a struct passed by pointer, you should pass an instance of the corresponding JavaScript wrapper class. The underlying C++ struct will be modified in place. + +Example: Updating a scene + +The function `mjv_updateScene` populates an `mjvScene` object with information from `mjModel` and `mjData`. +```typescript +// Create instances of the necessary structs. +const model = mujoco.MjModel.loadFromXML(xmlContent); +const data = new mujoco.MjData(model); +const scene = new mujoco.MjvScene(model, 1000); +const option = new mujoco.MjvOption(); +const perturb = new mujoco.MjvPerturb(); +const camera = new mujoco.MjvCamera(); + +// ... (step simulation, etc.) + +// Update the scene. The 'scene' object is modified by the function. +mujoco.mjv_updateScene( + model, + data, + option, + perturb, + camera, + mujoco.mjtCatBit.mjCAT_ALL.value, + scene +); + +console.log('Number of geoms in scene:', scene.ngeom); + +// Remember to delete all created objects when they are no longer needed. +scene.delete(); +camera.delete(); +perturb.delete(); +option.delete(); +data.delete(); +model.delete(); +``` + +As with buffers, you are responsible for managing the memory of these struct instances and must call `.delete()` on them when you are finished. + +### Enums +Access via `.value`: +```javascript +mujoco.mjtDisableBit.mjDSBL_CLAMPCTRL.value +``` + +### Constants +Scalar constants will be accessed the same way they are on python, simply: + +```javascript +mujoco.mjNEQDATA +``` + +Due to Embind limitations, more complex constants that are not scalar, but are represented in more dimensions are exposed as functions. E.g to use `mujoco.mjFRAMESTRING` you will need to call a function: + +```javascript +mujoco.get_mjFRAMESTRING() +``` + +This will return a javascript array representation of the values in MuJoCo `mjFRAMESTRING`. + +## Notes +The package is ESM (`type: module`) and ships TypeScript types. + +Ensure your bundler or dev server serves the `.wasm` asset from +`node_modules/mujoco_wasm/` at runtime. + +### Development +For detailed build instructions see the repository [README](https://github.com/google-deepmind/mujoco/tree/main/wasm#mujoco-javascript-bindings) (Emscripten toolchain, Embind bindings, producing the .wasm artifact, and targets for Node and browser). + +## Versioning +Package versions follow the official MuJoCo release versions. +For example: + +| npm version | MuJoCo version | +|-------------|----------------| +| 3.5.0 | 3.5.0 | + +--- + +For full engine documentation, see: https://mujoco.readthedocs.io diff --git a/wasm/package.json b/wasm/package.json index 6384da1b..609a5178 100644 --- a/wasm/package.json +++ b/wasm/package.json @@ -7,6 +7,9 @@ "lib": "lib", "test": "tests" }, + "files": [ + "dist" + ], "scripts": { "test": "node --loader ts-node/esm --trace-warnings tests/run_tests.mjs", "benchmark": "node --loader ts-node/esm --trace-warnings tests/run_benchmarks.mjs", diff --git a/wasm/package.npm.json b/wasm/package.npm.json new file mode 100644 index 00000000..75f8d9a0 --- /dev/null +++ b/wasm/package.npm.json @@ -0,0 +1,38 @@ +{ + "name": "mujoco_wasm", + "version": "0.0.0", + "description": "MuJoCo WASM bindings", + "repository": { + "type": "git", + "url": "https://github.com/google-deepmind/mujoco.git", + "directory": "wasm" + }, + "homepage": "https://github.com/google-deepmind/mujoco/tree/main/wasm", + "bugs": { + "url": "https://github.com/google-deepmind/mujoco/issues" + }, + "keywords": [ + "mujoco", + "physics", + "wasm", + "webassembly", + "bindings", + "simulation", + "robotics", + "emscripten", + "javascript", + "typescript" + ], + "main": "mujoco_wasm.js", + "types": "mujoco_wasm.d.ts", + "files": [ + "mujoco_wasm.js", + "mujoco_wasm.d.ts", + "mujoco_wasm.wasm", + "mujoco_wasm.wasm.map", + "README.md" + ], + "author": "Google DeepMind", + "license": "Apache-2.0", + "type": "module" +}