<p align="center">
  <img width="941" height="598" alt="Screenshot 2026-06-24 at 5 44 13 PM" src="https://github.com/user-attachments/assets/09999f02-c093-473e-aaee-519fe2e4cdd4" />
</p>

# mujoco-react

Composable [React Three Fiber](https://docs.pmnd.rs/react-three-fiber) wrapper around the official [@mujoco/mujoco](https://www.npmjs.com/package/@mujoco/mujoco) WASM bindings. Load any MuJoCo model, step physics, render bodies, and write controllers as React components.

[![npm](https://img.shields.io/npm/v/mujoco-react)](https://www.npmjs.com/package/mujoco-react)

**[Demo](https://mujoco-react-example.pages.dev)** | **[Docs](https://dadd.mintlify.app)** | **[npm](https://www.npmjs.com/package/mujoco-react)** | **[Example Source](https://github.com/noah-wardlow/mujoco-react-example)** | **[llms.txt](https://dadd.mintlify.app/llms.txt)**

## Install

```bash
npm install mujoco-react three @react-three/fiber @react-three/drei
```

## Vite Plugin and Type-Safe Names

Use the Vite plugin to generate declaration merging for actuator, sensor, body, joint, site, geom, and keyframe names:

```ts
// vite.config.ts
import { defineConfig } from "vite";
import { mujocoReact } from "mujoco-react/vite";

export default defineConfig({
  plugins: [
    mujocoReact({
      models: {
        franka: "models/panda/scene.xml",
      },
    }),
  ],
});
```

The plugin writes `src/mujoco-register.gen.ts` during dev and build. Commit that generated file. Vite auto-loads it, so app code imports generated values from `mujoco-react`, not from the generated file:

```ts
// src/mujoco-register.gen.ts
// Auto-generated by mujoco-react. Do not edit.

import { registerModelResources } from "mujoco-react";

const generatedModelResources = {
  franka: {
    actuators: { joint1: "joint1", joint2: "joint2", joint3: "joint3", gripper: "gripper" },
    sensors: { force_sensor: "force_sensor", torque_sensor: "torque_sensor" },
    bodies: { link0: "link0", link1: "link1", hand: "hand" },
    joints: { joint1: "joint1", joint2: "joint2", joint3: "joint3" },
    sites: { tcp: "tcp" },
    geoms: { floor: "floor" },
    keyframes: { home: "home" },
  },
};

registerModelResources(generatedModelResources);

declare module "mujoco-react" {
  interface Register {
    models: {
      franka: {
        actuators: "joint1" | "joint2" | "joint3" | "gripper";
        sensors: "force_sensor" | "torque_sensor";
        bodies: "link0" | "link1" | "hand";
        joints: "joint1" | "joint2" | "joint3";
        sites: "tcp";
        geoms: "floor";
        keyframes: "home";
      };
    };
    actuators: "joint1" | "joint2" | "joint3" | "gripper";
    sensors: "force_sensor" | "torque_sensor";
    bodies: "link0" | "link1" | "hand";
  }
}
```

Once generated, hooks like `useCtrl`, `useSensor`, `useBodyState`, and API methods like `setCtrl`, `applyForce`, `getSensorData` accept the global union. For model-scoped code, use package exports such as `ModelActuators.franka.gripper`, `ModelSites.franka.tcp`, and `ModelBodies.franka.hand`. Generic type helpers like `ModelActuators<"franka">` are still available for reusable library code. When no `Register` augmentation is present, names fall back to `string`.

Non-Vite projects can generate the same file with:

```bash
npx mujoco-react codegen franka=models/panda/scene.xml
```

## Load a Model

```tsx
import { MujocoProvider, MujocoCanvas } from "mujoco-react";
import type { SceneConfig } from "mujoco-react";

const panda: SceneConfig = {
  src: "https://raw.githubusercontent.com/google-deepmind/mujoco_menagerie/main/franka_emika_panda/",
  sceneFile: "scene.xml",
  homeJoints: [1.707, -1.754, 0.003, -2.702, 0.003, 0.951, 2.490],
};

export function App() {
  return (
    <MujocoProvider>
      <MujocoCanvas
        config={panda}
        camera={{ position: [2, -1.5, 2.5], up: [0, 0, 1], fov: 45 }}
        style={{ width: "100%", height: "100vh" }}
      />
    </MujocoProvider>
  );
}
```

## Add Scene Tools

```tsx
import { OrbitControls } from "@react-three/drei";
import { IkGizmo, ModelSites, useIkController } from "mujoco-react";

function PandaTools() {
  const ik = useIkController({ siteName: ModelSites.franka.tcp });

  return (
    <>
      <OrbitControls makeDefault />
      {ik && <IkGizmo controller={ik} />}
      <ambientLight intensity={0.7} />
      <directionalLight position={[1, 2, 5]} intensity={1.2} />
    </>
  );
}
```

Use it as a child of `<MujocoCanvas>`:

```tsx
<MujocoCanvas config={panda}>
  <PandaTools />
</MujocoCanvas>
```

## Gaussian Splat Environments

Gaussian splats are visual context; MuJoCo XML remains the source of physics, contacts, and task fixtures. Use visual-only splats when you only need rendered environment context, and add collision proxy metadata when a workflow needs simplified contact geometry.

Use `VisualScenarioEffects` when the same MuJoCo task should render under
different camera exposure, fog/background, and deterministic material variants:

```tsx
import { ScenarioLighting, VisualScenarioEffects } from "mujoco-react";

<MujocoCanvas config={sceneConfig}>
  <VisualScenarioEffects
    scenario={scenario}
    materialFilter={({ object }) => object.name.startsWith("prop_")}
  />
  <ScenarioLighting preset={scenario.lighting} />
</MujocoCanvas>;
```

Use the renderer-agnostic boundary from the main package. If your app stores
visual scenarios as data, pass the scenario directly; the component resolves the
splat asset and any paired MJCF collision proxy metadata for you. Visual-only
splats are valid, and readiness tells you whether a collision proxy is required
for your training/physics handoff.

```tsx
import { MujocoCanvas, SplatEnvironment, useSplatSceneConfig } from "mujoco-react";

const splat = useSplatSceneConfig({ sceneConfig, scenario });

<MujocoCanvas config={splat.sceneConfig}>
  {splat.environment ? (
    <SplatEnvironment environment={splat.environment} renderer="custom">
      <MySplatRenderer src={splat.environment.splat.src} />
    </SplatEnvironment>
  ) : null}
</MujocoCanvas>;
```

When a splat scenario includes paired MJCF collision proxy metadata, render a
generic wireframe preview from that XML with `SplatCollisionProxyPreview`. The
component parses MJCF primitives such as planes, boxes, spheres, capsules, and
mesh placeholders from any fetchable proxy XML; the tabletop example is just one
possible environment.

```tsx
import {
  SplatCollisionProxyPreview,
  SplatEnvironment,
  useSplatCollisionProxyGeoms,
  useSplatSceneConfig,
} from "mujoco-react";

const splat = useSplatSceneConfig({ sceneConfig, scenario });
const proxy = splat.environment?.collisionProxy;

<SplatEnvironment
  environment={splat.environment}
  collisionProxy={
    proxy ? <SplatCollisionProxyPreview collisionProxy={proxy} /> : undefined
  }
/>;
```

Use `useSplatCollisionProxyGeoms()` when your app wants to inspect or style the
proxy primitives itself:

```tsx
const proxyPreview = useSplatCollisionProxyGeoms({
  collisionProxy: splat.environment?.collisionProxy,
});

proxyPreview.geoms.map((geom) => geom.type);
```

Use `splat.readiness` or `getSplatEnvironmentReadiness(scenario)` to gate
authoring and import flows. The status distinguishes disabled scenarios,
missing visual assets, missing collision proxies, unsupported renderer formats,
and ready environments.

Use `createSplatSceneConfig()` when the same scene composition needs to run
outside React, such as codegen, import validation, backend handoff metadata, or
tests:

```tsx
import { createSplatSceneConfig } from "mujoco-react";

const splat = createSplatSceneConfig({
  sceneConfig,
  scenario,
  renderer: "spark",
});
```

Use `createVisualScenarioExecutionContext()` or
`useVisualScenarioExecutionContext()` when recording rollouts or exporting
LeRobot/HF Jobs handoff artifacts. It resolves the scenario seed, camera
exposure/noise/blur/jitter, material randomization, splat source, collision
proxy, and readiness into one serializable object.

```tsx
import { createVisualScenarioExecutionContext } from "mujoco-react";

const visualContext = createVisualScenarioExecutionContext({
  scenario,
  renderer: "spark",
  variantId,
});

writeEpisodeManifest({
  task,
  visualExecutionContext: visualContext,
});
```

For MuJoCo + 3DGS composition, derive the optional collision environment from
the same splat metadata and pass the resulting config to `<MujocoCanvas>`:

```tsx
const sceneConfig = withSplatEnvironment(
  {
    src: "/models/xlerobot/",
    sceneFile: "xlerobot.xml",
  },
  kitchenScenario
);
```

For first-class Spark rendering, install Spark and import the optional adapter:

```bash
npm install @sparkjsdev/spark
```

```tsx
import {
  SparkSplatEnvironment,
  useSparkSplatEnvironment,
} from "mujoco-react/spark";

function Scene() {
  const splat = useSparkSplatEnvironment({ sceneConfig, scenario });

  return (
    <MujocoCanvas config={splat.sceneConfig} gl={{ preserveDrawingBuffer: true }}>
      {splat.environment ? (
        <SparkSplatEnvironment hideGroundMeshes {...splat.props} />
      ) : null}
      <StatusBadge status={splat.lifecycle.status} error={splat.lifecycle.error} />
    </MujocoCanvas>
  );
}
```

`SparkSplatEnvironment` currently renders `.spz` assets. Use the renderer-agnostic
`SplatEnvironment` for `.ply`/`.splat` metadata or when wiring a different renderer.
Tune live rendering and snapshots separately with `renderTuning` and
`captureTuning`:

```tsx
<SparkSplatEnvironment
  {...splat.props}
  renderTuning={{ lodSplatScale: 0.75, minSortIntervalMs: 50 }}
  captureTuning={{ lodSplatScale: 1.4, lodRenderScale: 0.45, maxWarmupFrames: 6 }}
/>
```

## Write Controllers

```tsx
import { ModelActuators, useBeforePhysicsStep, useCtrl } from "mujoco-react";

function MyController() {
  const shoulder = useCtrl(ModelActuators.franka.actuator1);

  useBeforePhysicsStep(({ data }) => {
    shoulder.write(Math.sin(data.time));
  });

  return null;
}
```

Controllers are just React children that read sensors, write named controls, apply forces, or call the `MujocoSimAPI` at physics-step time.

With generated resource values, reusable controllers can be scoped to one robot without hand-typing names:

```tsx
import { ModelActuators, ModelJoints, ModelSites, useCtrl, useIkController } from "mujoco-react";

function FrankaTypedControls() {
  const gripper = useCtrl(ModelActuators.franka.gripper);
  const ik = useIkController({
    siteName: ModelSites.franka.tcp,
    joints: [
      ModelJoints.franka.joint1,
      ModelJoints.franka.joint2,
      ModelJoints.franka.joint3,
      ModelJoints.franka.joint4,
      ModelJoints.franka.joint5,
      ModelJoints.franka.joint6,
      ModelJoints.franka.joint7,
    ],
    actuators: [
      ModelActuators.franka.actuator1,
      ModelActuators.franka.actuator2,
      ModelActuators.franka.actuator3,
      ModelActuators.franka.actuator4,
      ModelActuators.franka.actuator5,
      ModelActuators.franka.actuator6,
      ModelActuators.franka.actuator7,
    ],
  });

  return null;
}
```

## Use the Sim API

```tsx
import { useMujoco } from "mujoco-react";

function ResetButton() {
  const sim = useMujoco();
  if (!sim.isReady) return null;

  return <button onClick={() => sim.api.reset()}>Reset</button>;
}
```

## Map Controls to Joints

Use named controls when a controller should write actuator commands without
hard-coding `data.ctrl` offsets:

```tsx
import {
  ModelActuators,
  defineControls,
  useBeforePhysicsStep,
  useControls,
} from "mujoco-react";

const arm = defineControls({
  shoulder: ModelActuators.so101.shoulder_pan,
  lift: ModelActuators.so101.shoulder_lift,
  elbow: ModelActuators.so101.elbow_flex,
});

function PolicyDriver({ action }: { action: number[] }) {
  const controls = useControls(arm);

  useBeforePhysicsStep(() => {
    controls.write(action);
    controls.patch({ elbow: 1.2 });
  });

  return null;
}
```

`defineControls()` preserves the alias keys for `set()`, `patch()`, `get()`,
and `read()`, while `write()` accepts an ordered vector in the same order as the
object. The Vite plugin injects the generated register module automatically, so
application code imports generated names from `mujoco-react`, not from
`src/mujoco-register.gen.ts`.

Use lower-level control groups when a robot's actuator order does not match a
simple `qpos[0..n]` layout or you want actuator-name keys directly:

```tsx
import { controlGroup, ModelActuators, useControlGroup } from "mujoco-react";

const gripper = controlGroup([ModelActuators.so101.gripper]);

function HoldGripper() {
  const controls = useControlGroup(gripper);
  controls.set(ModelActuators.so101.gripper, -0.17453);
  return null;
}
```

For selector-based introspection, `resolveControlGroup()` accepts `{ siteName }`,
`{ bodyName }`, `{ joints }`, or `{ actuators }`. Selectors can be a name,
ordered name array, regex, or predicate.

## Build Observations

Build policy-ready observation vectors from common MuJoCo state without hard-coding offsets:

```tsx
import { buildObservation, useBeforePhysicsStep } from "mujoco-react";

function PolicyDriver() {
  useBeforePhysicsStep(({ model, data }) => {
    const obs = buildObservation(model, data, {
      qpos: true,
      qvel: true,
      ctrl: true,
      sensors: ["imu_gyro", "imu_accel"],
      sites: ["tcp"],
      projectedGravity: "torso",
    });

    runPolicy(obs.values, obs.layout);
  });

  return null;
}
```

Use `output: "float64"` when a downstream model expects double precision. Named resources are skipped when absent, so `obs.layout` is the source of truth for the current model.

## WebSocket Control

Stream actuator commands over a WebSocket and send simulation state back. Use a schema validator such as Zod at this boundary because socket messages are untrusted app input (`npm install zod` for this example):

```tsx
import { useEffect, useRef } from "react";
import { z } from "zod";
import { useMujoco, useBeforePhysicsStep, useAfterPhysicsStep } from "mujoco-react";

const CtrlCommand = z.preprocess((data) => {
  try {
    return typeof data === "string" ? JSON.parse(data) : data;
  } catch {
    return undefined;
  }
}, z.object({
  type: z.literal("ctrl_command"),
  ctrl: z.array(z.number()),
}));

function useWebSocketControls(url: string) {
  const wsRef = useRef<WebSocket | null>(null);
  const latestCtrlRef = useRef<number[] | null>(null);

  useEffect(() => {
    const ws = new WebSocket(url);
    wsRef.current = ws;

    ws.onmessage = (evt) => {
      const command = CtrlCommand.safeParse(evt.data);
      if (command.success) latestCtrlRef.current = command.data.ctrl;
    };

    return () => ws.close();
  }, [url]);

  // Apply incoming actuator controls each physics step.
  useBeforePhysicsStep(({ model, data }) => {
    const ctrl = latestCtrlRef.current;
    if (!ctrl) return;
    for (let i = 0; i < Math.min(ctrl.length, model.nu); i++) {
      data.ctrl[i] = ctrl[i];
    }
  });

  // Send simulation feedback back after physics.
  useAfterPhysicsStep(({ data }) => {
    const ws = wsRef.current;
    if (!ws || ws.readyState !== WebSocket.OPEN) return;

    ws.send(JSON.stringify({
      type: "feedback",
      time: data.time,
      qpos: Array.from(data.qpos),
      qvel: Array.from(data.qvel),
      sensordata: Array.from(data.sensordata),
    }));
  });
}
```

For reusable controllers with typed config, default merging, and children, use the `createController` factory:

```tsx
import {
  ModelActuators,
  createController,
  useBeforePhysicsStep,
  useCtrl,
} from "mujoco-react";

export const MyController = createController<{ gain: number }>(
  { name: "MyController", defaultConfig: { gain: 1.0 } },
  ({ config, children }) => {
    const shoulder = useCtrl(ModelActuators.franka.actuator1);

    useBeforePhysicsStep(({ data }) => {
      shoulder.write(config.gain * Math.sin(data.time));
    });

    return <>{children}</>;
  },
);

// <MyController config={{ gain: 2.0 }}>
//   <Debug showJoints />
// </MyController>
```

A `createControllerHook` factory is also available for the hook equivalent — see the [Building Controllers](https://dadd.mintlify.app/guides/building-controllers) guide.

## Architecture

`<MujocoCanvas>` wraps R3F `<Canvas>` and forwards all Canvas props (`camera`, `shadows`, `gl`, etc.). For full control over the Canvas, use `<MujocoPhysics>` inside your own:

```
<MujocoProvider>                           <MujocoProvider>
  <MujocoCanvas config={...}>               <Canvas shadows gl={...}>
    <Scene />                                  <MujocoPhysics config={...}>
    <MyController />                             <MyController />
  </MujocoCanvas>                              </MujocoPhysics>
</MujocoProvider>                              <EffectComposer>...</EffectComposer>
                                             </Canvas>
                                           </MujocoProvider>
```

### Custom IK Solvers

The built-in `useIkController()` uses Damped Least-Squares. Pass `ikSolveFn` to swap in your own solver (analytical, learned, etc.):

```tsx
import { ModelSites } from "mujoco-react";
import type { IKSolveFn } from "mujoco-react";

const myIK: IKSolveFn = ({ position, currentQ }) => {
  return myAnalyticalSolver(position, currentQ); // return joint angles or null
};

const ik = useIkController({ siteName: ModelSites.franka.tcp, ikSolveFn: myIK });
```

### `useIkController(config | null)`

Hook for interactive end-effector control. Pass `null` to disable IK (safe to call unconditionally):

```tsx
import { IkGizmo, ModelSites, useIkController } from "mujoco-react";

const ik = useIkController({ siteName: ModelSites.franka.tcp });
return ik ? <IkGizmo controller={ik} /> : null;
```

| Config | Type | Default | Description |
|--------|------|---------|-------------|
| `siteName` | `string` | **required** | MuJoCo site to track |
| `joints` | `string \| string[] \| RegExp \| (joint) => boolean` | inferred | Explicit hinge/slide joints for IK |
| `actuators` | `string \| string[] \| RegExp \| (actuator) => boolean` | inferred | Explicit actuators for IK output |
| `numJoints` | `number` | legacy only | Contiguous qpos/ctrl count from older examples |
| `ikSolveFn` | `IKSolveFn` | built-in DLS | Custom solver function |
| `damping` | `number` | `0.01` | DLS damping |
| `maxIterations` | `number` | `50` | Max solver iterations |

Returns `IkContextValue | null` with methods like `setIkEnabled`, `moveTarget`, `syncTargetToSite`, `solveIK`, and `getGizmoStats`.

Pass the returned value to `<IkGizmo controller={ik} />` or to your own controller as a prop.

By default the controller infers scalar hinge/slide joints by walking from the site body toward the model root. For robots where the MJCF control layout is not a simple chain, pass explicit names:

```tsx
import { ModelActuators, ModelJoints, ModelSites } from "mujoco-react";

const leftArmIk = useIkController({
  siteName: ModelSites.franka.tcp,
  joints: [
    ModelJoints.franka.joint1,
    ModelJoints.franka.joint2,
    ModelJoints.franka.joint3,
    ModelJoints.franka.joint4,
    ModelJoints.franka.joint5,
    ModelJoints.franka.joint6,
    ModelJoints.franka.joint7,
  ],
});

const gripperIk = useIkController({
  siteName: ModelSites.franka.tcp,
  actuators: [
    ModelActuators.franka.actuator1,
    ModelActuators.franka.actuator2,
    ModelActuators.franka.actuator3,
    ModelActuators.franka.actuator4,
    ModelActuators.franka.actuator5,
    ModelActuators.franka.actuator6,
    ModelActuators.franka.actuator7,
  ],
});
```

## Loading Models

The loader fetches `src + sceneFile`, parses the XML for dependencies (meshes, textures, includes), recursively fetches those too, and writes everything to MuJoCo's in-memory WASM filesystem.

```tsx
// MuJoCo Menagerie
const franka: SceneConfig = {
  src: "https://raw.githubusercontent.com/google-deepmind/mujoco_menagerie/main/franka_emika_panda/",
  sceneFile: "scene.xml",
};

// Any URL
const custom: SceneConfig = {
  src: "http://localhost:3000/models/my_model/",
  sceneFile: "model.xml",
};
```

## SceneConfig

```ts
interface SceneConfig {
  src: string;                      // Base URL for model files
  sceneFile: string;                // Entry XML/URDF file, e.g. "scene.xml"
  files?: File[];                   // Local files for browser upload workflows
  environmentFiles?: string[];      // Static MJCF environment XMLs merged before compile
  sceneObjects?: SceneObject[];     // Objects injected into scene XML at load time
  homeJoints?: number[];            // Initial joint positions
  xmlPatches?: XmlPatch[];          // Patches applied to XML files during loading
  onReset?: ({ model, data }) => void;  // Called during reset after mj_resetData
}
```

Use `environmentFiles` to compose reusable physics/collision layers with a robot model. For Gaussian splat scenes, keep the `.spz` as a parallel visual layer and point `environmentFiles` at a paired MJCF proxy scene only when contact geometry is needed:

```tsx
const kitchenRobot: SceneConfig = {
  src: "/models/xlerobot/",
  sceneFile: "xlerobot.xml",
  environmentFiles: ["splats/tabletop/scene.xml"],
};
```

### Local Files and URDF

Load browser-selected MJCF or URDF files directly. Folder uploads preserve `webkitRelativePath`, and flat uploads fall back to matching referenced mesh/texture assets by basename:

```tsx
import { useEffect, useRef } from "react";
import { useMujoco } from "mujoco-react";

function ModelUpload() {
  const sim = useMujoco();
  const inputRef = useRef<HTMLInputElement>(null);

  useEffect(() => {
    inputRef.current?.setAttribute("webkitdirectory", "");
  }, []);

  return (
    <input
      ref={inputRef}
      type="file"
      multiple
      onChange={(event) => {
        if (sim.isReady && event.currentTarget.files) {
          sim.api.loadFromFiles(event.currentTarget.files);
        }
      }}
    />
  );
}
```

### Adding Objects to Any Scene

```tsx
const config: SceneConfig = {
  src: "https://raw.githubusercontent.com/google-deepmind/mujoco_menagerie/main/franka_emika_panda/",
  sceneFile: "scene.xml",
  sceneObjects: [
    { name: "ball", type: "sphere", size: [0.03, 0.03, 0.03],
      position: [0.5, 0, 0.1], rgba: [1, 0, 0, 1], mass: 0.1, freejoint: true },
    { name: "platform", type: "box", size: [0.2, 0.2, 0.01],
      position: [0.4, 0.3, 0], rgba: [0.5, 0.5, 0.5, 1] },
  ],
};
```

### XML Patching

```tsx
xmlPatches: [{
  target: "panda.xml",
  replace: ["name=\"actuator8\"", "name=\"gripper\""],
  inject: "<site name=\"tcp\" pos=\"0 0 0.1\" size=\"0.01\"/>",
  injectAfter: "<body name=\"hand\"",
}]
```

## Components

### `<MujocoProvider>`

Loads the MuJoCo WASM module. Wrap your entire app in this.

| Prop | Type | Description |
|------|------|-------------|
| `wasmUrl` | `string?` | Custom WASM URL override |
| `mtWasmUrl` | `string?` | Custom multi-threaded WASM URL override |
| `threadedLoader` | `(options?) => Promise<unknown>` | Optional loader imported from `@mujoco/mujoco/mt` |
| `wasmVariant` | `"single" \| "threaded" \| "auto"` | MuJoCo WASM build. Defaults to `"single"` |
| `timeout` | `number` | WASM load timeout in ms |
| `onError` | `(error: Error) => void` | Called if WASM fails to load |

The official `@mujoco/mujoco` package also ships a multi-threaded WASM build. Import it only in apps that opt into it:

```tsx
import loadMujocoMt from "@mujoco/mujoco/mt";
import mtWasmUrl from "@mujoco/mujoco/mt/mujoco.wasm?url";

<MujocoProvider
  wasmVariant="auto"
  threadedLoader={loadMujocoMt}
  mtWasmUrl={mtWasmUrl}
>
  <App />
</MujocoProvider>
```

`"auto"` uses the threaded build only when `threadedLoader` and `mtWasmUrl` are provided and `globalThis.crossOriginIsolated` is true. Forced `"threaded"` mode requires `threadedLoader`, `mtWasmUrl`, `Cross-Origin-Opener-Policy: same-origin` and `Cross-Origin-Embedder-Policy: require-corp`.

### `<MujocoCanvas>`

Thin wrapper around R3F `<Canvas>`. Accepts all R3F Canvas props plus:

| Prop | Type | Description |
|------|------|-------------|
| `config` | `SceneConfig` | **Required.** Scene/robot configuration |
| `onReady` | `({ api }) => void` | Fires when model is loaded |
| `onError` | `(error: Error) => void` | Fires on scene load failure |
| `onStep` | `({ time, model, data }) => void` | Called each physics step |
| `onSelection` | `({ bodyId, name }) => void` | Called on double-click |
| `gravity` | `[number, number, number]` | Override model gravity |
| `timestep` | `number` | Override model.opt.timestep |
| `substeps` | `number` | mj_step calls per frame |
| `paused` | `boolean` | Declarative pause |
| `speed` | `number` | Simulation speed multiplier |
| `renderOptions` | `MujocoRenderOptions` | Optional render-time geometry settings such as `meshNormalSmoothing` |
| `hiddenBodies` | `readonly string[]` | Names of model bodies whose meshes are skipped at build time. The bodies still simulate (indices unchanged); only rendering is skipped. Reapplied on every rebuild, so toggling names can't be lost to a scene rebuild |

### `<MujocoPhysics>`

Physics provider for use inside your own R3F `<Canvas>`. Same physics props as `<MujocoCanvas>` without the Canvas wrapper. Accepts a `ref` for the `MujocoSimAPI`.

```tsx
<MujocoProvider>
  <Canvas shadows camera={{ position: [2, 2, 2] }}>
    <MujocoPhysics ref={apiRef} config={config} paused={paused}>
      <MyController />
    </MujocoPhysics>
    <OrbitControls />
  </Canvas>
</MujocoProvider>
```

| Prop | Type | Description |
|------|------|-------------|
| `config` | `SceneConfig` | **Required.** Scene/robot configuration |
| `onReady` | `({ api }) => void` | Fires when model is loaded |
| `onError` | `(error: Error) => void` | Fires on scene load failure |
| `onStep` | `({ time, model, data }) => void` | Called each physics step |
| `onSelection` | `({ bodyId, name }) => void` | Called on double-click |
| `gravity` | `[number, number, number]` | Override model gravity |
| `timestep` | `number` | Override model.opt.timestep |
| `substeps` | `number` | mj_step calls per frame |
| `paused` | `boolean` | Declarative pause |
| `speed` | `number` | Simulation speed multiplier |

### `<Body />`

Declaratively add physics bodies to the simulation as JSX. Bodies are injected into the MJCF XML before model compilation.

```tsx
<Body name="cube" type="box" size={[0.05, 0.05, 0.05]}
      position={[0.5, 0, 0.05]} rgba={[1, 0, 0, 1]}
      mass={0.1} freejoint />

// With custom Three.js visuals
<Body name="ball" type="sphere" size={[0.03, 0, 0]}
      position={[0, 0.3, 0.1]} mass={0.5} freejoint>
  <mesh>
    <sphereGeometry args={[0.03]} />
    <meshPhysicalMaterial color="gold" metalness={0.8} />
  </mesh>
</Body>
```

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `name` | `string` | **required** | Unique body name |
| `type` | `'box' \| 'sphere' \| 'cylinder'` | **required** | Geom type |
| `size` | `[number, number, number]` | **required** | Geom size |
| `position` | `[number, number, number]` | `[0,0,0]` | Initial position |
| `rgba` | `[number, number, number, number]` | `[0.5,0.5,0.5,1]` | Color (ignored with children) |
| `mass` | `number?` | -- | Body mass in kg |
| `freejoint` | `boolean?` | -- | Add freejoint for free movement |
| `friction` | `string?` | -- | MuJoCo friction params |
| `condim` | `number?` | -- | Contact dimensionality (4-6 for grasping) |
| `children` | `ReactNode?` | -- | Custom Three.js visuals |

### `<IkGizmo />`

drei PivotControls gizmo that tracks a MuJoCo site and drives IK on drag. Requires a `controller` from `useIkController()`.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `controller` | `IkContextValue` | **required** | Controller from `useIkController()` |
| `siteName` | `string?` | controller's site | MuJoCo site to track |
| `scale` | `number?` | `0.18` | Gizmo handle scale |
| `onDrag` | `({ position, quaternion }) => void` | -- | Custom drag handler (disables auto-IK) |

### `<DragInteraction />`

Click-drag to apply spring forces to bodies. Raycasts to find bodies, applies `F = (mouseWorld - grabWorld) * body_mass * stiffness` via `mj_applyFT`.

### R3F Group Props

All visual components (`DragInteraction`, `ContactMarkers`, `Debug`, `TendonRenderer`, `FlexRenderer`) accept standard R3F group props like `position`, `rotation`, `scale`, `visible`.

```tsx
<ContactMarkers visible={showContacts} />
<Debug showJoints scale={0.5} />
```

### `<ContactMarkers />`

InstancedMesh showing MuJoCo contact points for debugging.

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `maxContacts` | `number?` | `100` | Max contacts to display |
| `radius` | `number?` | `0.005` | Marker sphere radius |
| `color` | `string?` | `"#4f46e5"` | Marker color |
| `visible` | `boolean?` | `true` | Toggle visibility |

### `<SceneLights />`

Auto-creates Three.js lights from MJCF `<light>` elements. Also available as `useSceneLights(intensity?)` hook.

### `<Debug />`

Visualization overlays:

| Prop | Type | Default | Description |
|------|------|---------|-------------|
| `showGeoms` | `boolean?` | `false` | Wireframe collision geoms |
| `showSites` | `boolean?` | `false` | Site markers |
| `showJoints` | `boolean?` | `false` | Joint axes |
| `showContacts` | `boolean?` | `false` | Contact force vectors |
| `showCameras` | `boolean?` | `false` | MuJoCo camera positions, frustums, and forward rays |
| `virtualCameras` | `DebugVirtualCamera[]?` | `[]` | Explicit virtual policy/offscreen render camera poses to draw alongside MuJoCo cameras |
| `showCOM` | `boolean?` | `false` | Center of mass markers |
| `showInertia` | `boolean?` | `false` | Inertia ellipsoids |
| `showTendons` | `boolean?` | `false` | Tendon paths |
| `geomColor` | `string?` | `"#00ff00"` | Color for wireframe geoms |
| `siteColor` | `string?` | `"#ff00ff"` | Color for site markers |
| `contactColor` | `string?` | `"#ff4444"` | Color for contact force arrows |
| `comColor` | `string?` | `"#ff0000"` | Color for COM markers |

Camera debug overlays use the live MuJoCo `cam_xpos` / `cam_xmat` frame, so the frustum matches mounted camera captures and follows parent body motion. Use `virtualCameras` for synthetic fixed policy/offscreen render viewpoints that are not declared as MJCF `<camera>` elements. Debug camera overlays are excluded from camera captures.

### `<TendonRenderer />`

Renders tendons as tube geometry from wrap paths.

### `<FlexRenderer />`

Renders deformable flex bodies from `flexvert_xpos`.

### `<InstancedGeomRenderer />`

Opt-in renderer for repeated compatible geoms. It batches matching geom shape/material signatures into Three.js `InstancedMesh` objects and syncs instance transforms from `data.geom_xpos` / `data.geom_xmat`.

### `<ContactListener />`

Component wrapper for contact events:

```tsx
<ContactListener
  body="block_1"
  onContactEnter={(info) => console.log("contact!", info)}
  onContactExit={(info) => console.log("released", info)}
/>
```

### `<TrajectoryPlayer />`

Plays back recorded qpos trajectories with scrubbing.

## Hooks

### `useMujoco()`

Access the simulation API (must be inside `<MujocoCanvas>` or `<MujocoPhysics>`). Narrow on `isReady`, `isPending`, or `isError`:

```tsx
const sim = useMujoco();
if (sim.isReady) {
  sim.api.reset(); // fully typed
}
```

### `useMujocoWasm()`

Access the raw WASM module lifecycle from any child of `<MujocoProvider>`. Most users won't need this — `useMujoco()` and hooks like `useBeforePhysicsStep` handle the model/data lifecycle for you.

```tsx
import { useMujocoWasm } from "mujoco-react";

const { mujoco, status } = useMujocoWasm();

if (mujoco) {
  const model = mujoco.MjModel.from_xml_path("/path/to/scene.xml");
  const data = new mujoco.MjData(model);
  mujoco.mj_step(model, data);
  console.log(data.qpos);  // joint positions after one step
}
```

### `useBeforePhysicsStep(callback)`

Run logic **before** `mj_step` each frame. Write to `data.ctrl`, apply forces, drive automation.

```tsx
import { ModelActuators, useBeforePhysicsStep, useCtrl } from "mujoco-react";

const shoulder = useCtrl(ModelActuators.franka.actuator1);

useBeforePhysicsStep(({ data }) => {
  shoulder.write(Math.sin(data.time));
});
```

### `useAfterPhysicsStep(callback)`

Run logic **after** `mj_step` each frame. Read results, compute rewards, log telemetry.

### `useIkController(config | null)`

Set up IK control for a MuJoCo site. Pass `null` to disable. Returns `IkContextValue | null`.

### `useCameraAnimation()`

Standalone camera animation hook:

```tsx
const { getCameraState, moveCameraTo } = useCameraAnimation();

// Animate camera over 1 second
await moveCameraTo(
  new THREE.Vector3(3, 0, 2),
  new THREE.Vector3(0, 0, 0.5),
  1000
);
```

### `useSensor(name)` / `useSensors()`

Read sensor values by name. Returns a `SensorHandle` with `read()`, `dim`, and `name`:

```tsx
import { ModelSensors, useSensor } from "mujoco-react";

const imu = useSensor(ModelSensors.g1["imu-torso-angular-velocity"]);
// imu.read() -> Float64Array, imu.dim -> number
```

### `useBodyState(name)`

Position, quaternion, linear/angular velocity of a body (ref-based):

```tsx
const { position, quaternion, linearVelocity, angularVelocity } = useBodyState("block_1");
```

### `useJointState(name)`

Joint position and velocity:

```tsx
const { position, velocity } = useJointState("joint1");
```

### `useCtrl(name)`

Read/write actuator control by name. Returns a `CtrlHandle` with `read()`, `write()`, `name`, and `range`:

```tsx
import { ModelActuators, useCtrl } from "mujoco-react";

const gripper = useCtrl(ModelActuators.franka.gripper);
// gripper.read() -> number, gripper.write(0.04), gripper.range -> [min, max]
```

### `useContacts(bodyName?)` / `useContactEvents(bodyName, handlers)`

Query contacts or subscribe to enter/exit events:

```tsx
useContactEvents("block_1", {
  onEnter: (info) => console.log("contact!", info),
  onExit: (info) => console.log("released", info),
});
```

### `useKeyboardTeleop(config)`

Map keyboard keys to actuators:

```tsx
import { ModelActuators, useKeyboardTeleop } from "mujoco-react";

useKeyboardTeleop({
  bindings: {
    "w": { actuator: "forward", delta: 0.1 },
    "s": { actuator: "forward", delta: -0.1 },
    "v": { actuator: ModelActuators.franka.gripper, toggle: [0, 0.04] },
  },
});
```

### `useGamepad(config)`

Map gamepad axes/buttons to actuators:

```tsx
useGamepad({
  axes: { 0: "joint1", 1: "joint2" },
  buttons: { 0: "gripper" },
  deadzone: 0.1,
});
```

### `usePolicy(config)`

Framework-agnostic decimation loop for RL policies:

```tsx
const obs = useObservation({ qpos: true, qvel: true, projectedGravity: "torso" });

const policy = usePolicy({
  frequency: 50,
  onObservation: () => obs.readValues(),
  infer: ({ observation }) => policySession.run(observation),
  onAction: ({ action, data }) => applyAction(action, data),
});
```

### `buildObservation(model, data, config)` / `useObservation(config)`

Build a flat `Float32Array` or `Float64Array` plus a layout map from qpos, qvel, ctrl, actuator activations, sensordata, named sensors, named site positions, and projected gravity.

### `useTrajectoryRecorder(config)` / `useTrajectoryPlayer(trajectory, config)`

Record and play back simulation trajectories:

```tsx
// Record
const recorder = useTrajectoryRecorder({ fields: ["qpos", "ctrl"] });
recorder.start();
// ... interact with simulation ...
recorder.stop();

// Play back recorded frames directly (no conversion needed)
const player = useTrajectoryPlayer(recorder.frames, {
  fps: 30,
  speed: 1.0,        // 0.5x, 1x, 2x, etc.
  loop: true,
  mode: "kinematic",  // or "physics" to replay ctrl through the sim
  onComplete: () => console.log("done"),
});
// player.play(), player.pause(), player.seek(42), player.setSpeed(2)
// player.state → "idle" | "playing" | "paused" | "completed"
// player.progress → 0-1
```

### `useVideoRecorder(config)`

Record the canvas as video:

```tsx
const video = useVideoRecorder({ fps: 30, mimeType: "video/webm" });
// video.start(), video.stop() -> returns Blob
```

### Camera Frames, Streams, and Tensors

A camera pose — the viewer camera, a MuJoCo `cameraName`/`siteName`/`bodyName`, or
an explicit `position`+`lookAt` — can be turned into three outputs: a **snapshot**
(PNG/JPEG), a **live on-screen stream**, or a **policy tensor**.

**Snapshots.** `captureFrame()`/`captureFrameBlob()` grab the live canvas (or use
`useFrameCapture()` / the standalone `captureFrame()` when you own the canvas).
`captureCameraFrame()`/`captureCameraFrameBlob()` render a chosen camera offscreen
at a stable resolution without moving the viewport; pass `cameraName`/`siteName`/
`bodyName` for true mounted frames (`source.kind` lets dataset pipelines reject
fallback/synthetic poses). Set `mujocoCameraCompatibility` to inherit a MJCF
camera's `resolution`, `fovy`, clipping, and calibrated intrinsics; use
`visualOverrides`/`renderIsolation` for canonical training captures.

```tsx
const frame = await apiRef.current?.captureCameraFrame({
  cameraName: "front_camera",
  width: 640,
  type: "image/jpeg",
  mujocoCameraCompatibility: true,
});
```

**Live streams.** `useCameraStream(canvasRef, options)` renders a camera into a
DOM `<canvas>` every frame (offscreen render → blit: no PNG round-trip, no render-
loop takeover, splat scenes stream safely). Call it inside `<MujocoCanvas>`; the
canvas can live anywhere in your DOM:

```tsx
function WristStream({ canvasRef }) {
  useCameraStream(canvasRef, { cameraName: "wrist_cam", width: 256, height: 192 });
  return null;
}
```

For a transparent picture-in-picture overlay, use `<CameraView>` (or
`useCameraViewport` to track your own element). It scissors into the main canvas —
cheaper, but it takes over the render loop while mounted (incompatible with
postprocessing, occluded by opaque DOM), so prefer `useCameraStream` for panel
tiles:

```tsx
<MujocoCanvas config={config}>
  <CameraView cameraName="wrist_cam" style={{ right: 16, bottom: 16, width: 240, height: 180 }} />
</MujocoCanvas>
```

**Policy tensors.** For in-browser inference, capture straight into a
`Float32Array` — no canvas, no PNG. `usePolicyCameraTensors` keeps one reusable
session per camera and re-aims it to the live pose each step:

```tsx
const cams = usePolicyCameraTensors({
  streams: [
    { key: "wrist", cameraName: "wrist_cam", width: 96, height: 96, layout: "CHW" },
    { key: "front", cameraName: "front",     width: 96, height: 96, layout: "CHW" },
  ],
});

useAfterPhysicsStep(() => {
  const { tensors } = cams.capture();
  const wrist = new ort.Tensor("float32", tensors.wrist.data, [1, ...tensors.wrist.shape]);
});
```

Use `usePolicyCameraTensorsFromMountedStreams` for dataset-name resolution,
`captureCameraFrameTensor()` for one-offs, or `createCameraFrameCaptureSession()`
+ `pixelsToPolicyImageTensor()` for lower-level control. The optional
`mujoco-react/onnx` entry point wraps ONNX Runtime Web: `createOnnxPolicySession()`
loads a manifest plus model, and `onnxTensorToPolicyActionChunk()` decodes the
output into action chunks.

**Dataset recording.** `recordMountedCameraFrameSequence()` steps a rollout and
captures synchronized per-camera frames, resolving LeRobot-style task camera keys
to MuJoCo cameras/sites/bodies. It requires every requested `cameraKey` by default
(set `requireAll: false` for exploratory tooling) and returns a plan, readiness,
and per-camera source provenance:

```tsx
const sequence = await recordMountedCameraFrameSequence(api, {
  cameraKeys: ["head", "left_wrist", "right_wrist"],
  aliases: {
    head: [{ siteName: "head_camera_rgb_optical_frame" }],
    left_wrist: [{ siteName: "left_wrist_camera_optical_frame" }],
    right_wrist: [{ siteName: "right_wrist_camera_optical_frame" }],
  },
  defaults: { width: 640, height: 480, type: "image/png", fov: 45 },
  frames: 16,
  onFrame: ({ frameIndex, cameras }) => queueLeRobotImages(frameIndex, cameras),
});

const manifest = createMountedCameraFrameSequenceManifest(sequence); // dataset-facing manifest
```

Inside `<MujocoCanvas>`, `useMountedCameraSequenceRecorder()` exposes the same
planning/recording with React status and a `checkReadiness()` preflight gate.
`resolveMountedCameraFrameSource()` maps a single dataset feature name to a MuJoCo
source (aliases first, then camera > site > body, then normalized prefix/suffix
matches) when you need the selector before recording. The lower-level
`recordCameraSequence()` / `useCameraSequenceRecorder()` take explicit camera
configs.

### `useCtrlNoise(config)`

Apply Gaussian noise to controls for robustness testing:

```tsx
useCtrlNoise({ rate: 0.01, std: 0.05 });
```

### `useGravityCompensation(enabled?)`

Applies `qfrc_bias` to `qfrc_applied` so joints hold position against gravity.

### `useActuators()`

Returns actuator metadata for building control UIs.

### `useSitePosition(siteName)`

Ref-based site position/quaternion tracking.

### `useBodyMeshes(bodyId)`

Returns the Three.js meshes belonging to a MuJoCo body. Use for custom selection visuals, outlines, postprocessing, or any per-body mesh manipulation:

```tsx
const meshes = useBodyMeshes(selectedBodyId);

// Use with drei Outline, or manipulate materials directly
```

### `useSelectionHighlight(bodyId, options?)`

Convenience wrapper around `useBodyMeshes` that applies an emissive highlight:

```tsx
useSelectionHighlight(selectedBodyId, { color: "#00ff00", emissiveIntensity: 0.5 });
```

### `useSceneLights(intensity?)`

Hook form of `<SceneLights>`. Create Three.js lights from MJCF definitions imperatively:

```tsx
useSceneLights(1.5);
```

## MujocoSimAPI

The full API object available via `ref` or `useMujoco()` (when `isReady`):

### Simulation Control

| Method | Description |
|--------|-------------|
| `reset()` | Reset sim, re-apply home joints |
| `setPaused(paused)` | Set pause state |
| `togglePause()` | Toggle pause, returns new state |
| `setSpeed(multiplier)` | Set simulation speed |
| `step(n?)` | Advance exactly n steps while paused |
| `getTime()` | Current simulation time |
| `getTimestep()` | Current timestep |

### State Management

| Method | Description |
|--------|-------------|
| `saveState()` | Snapshot qpos, qvel, ctrl, time, act |
| `restoreState(snapshot)` | Restore from snapshot |
| `setQpos(values)` / `getQpos()` | Direct qpos access |
| `setQvel(values)` / `getQvel()` | Direct qvel access |
| `setCtrl(nameOrValues, value?)` | Set control by name or batch |
| `getCtrl()` | Get all control values |
| `getControlMap()` | Map directly actuated scalar joints to qpos/dof/ctrl addresses |
| `getActuatedJoints()` | List scalar hinge/slide joints with matching actuators |
| `resolveControlGroup(selector)` | Resolve qpos/ctrl mapping from a site, body, joint selector, or actuator selector |
| `buildObservation(model, data, config)` | Build policy-ready observation vectors with layout metadata |
| `applyKeyframe(nameOrIndex)` | Apply a keyframe |
| `getKeyframeNames()` / `getKeyframeCount()` | Keyframe introspection |

### Forces

| Method | Description |
|--------|-------------|
| `applyForce(bodyName, force, point?)` | Apply force via `mj_applyFT` |
| `applyTorque(bodyName, torque)` | Apply torque via `mj_applyFT` |
| `setExternalForce(bodyName, force, torque)` | Write to `xfrc_applied` |
| `applyGeneralizedForce(values)` | Write to `qfrc_applied` |

### Model Introspection

| Method | Description |
|--------|-------------|
| `getBodies()` | All bodies with id, name, mass, parentId |
| `getJoints()` | All joints with id, name, type, range, bodyId |
| `getGeoms()` | All geoms with id, name, type, size, bodyId |
| `getSites()` | All sites with id, name, bodyId |
| `getActuators()` | All actuators with id, name, range |
| `getSensors()` | All sensors with id, name, type, dim |
| `getSensorData(name)` | Read sensor value by name |
| `getContacts()` | All active contacts |
| `getModelOption()` | Timestep, gravity, integrator |

### Model Mutation

| Method | Description |
|--------|-------------|
| `setGravity(g)` | Set gravity vector |
| `setTimestep(dt)` | Set timestep |
| `addBody(body)` | Add a `SceneObject` and recompile the scene |
| `removeBody(name)` | Remove a generated `SceneObject` and recompile |
| `recompile(patches?)` | Recompile current scene, optionally appending XML patches |
| `setBodyMass(name, mass)` | Domain randomization |
| `setGeomFriction(name, friction)` | Domain randomization |
| `setGeomSize(name, size)` | Domain randomization |

### Spatial Queries

| Method | Description |
|--------|-------------|
| `raycast(origin, direction, maxDist?)` | Physics raycast via `mj_ray` |
| `project2DTo3D(x, y, camPos, lookAt)` | Screen-to-world raycast (returns bodyId + geomId) |
| `getCanvas()` | Return the underlying R3F canvas element |
| `getCanvasSnapshot(w?, h?, mime?)` | Base64 screenshot |
| `captureFrame(options?)` | Capture the canvas as a data URL |
| `captureFrameBlob(options?)` | Capture the canvas as a Blob |

### Scene Management

| Method | Description |
|--------|-------------|
| `loadScene(newConfig)` | Runtime model swap |
| `loadFromFiles(files, options?)` | Load MJCF/URDF from a browser `FileList` |

## Guides

### Building Controllers

See [Building Controllers](https://dadd.mintlify.app/guides/building-controllers) for full patterns including config-driven controllers, IK gizmo coexistence, multi-arm support, and the `createControllerHook`/`createController` factories.

### Contact Parameters

Objects that need stable contact (grasping, stacking, etc.) require tuned MuJoCo solver parameters — `friction`, `solref`, `solimp`, and `condim`. See [Contact Parameters](https://dadd.mintlify.app/guides/graspable-objects) for details.

### Click-to-Select

Combine R3F raycasting with `useSelectionHighlight` for body selection:

```tsx
function ClickSelectOverlay() {
  const selectedBodyId = useClickSelect(); // your raycasting hook
  useSelectionHighlight(selectedBodyId);
  return null;
}
```

See [Click-to-Select](https://dadd.mintlify.app/guides/click-to-select) for the full implementation.

## useFrame Priority

| Priority | Owner | Purpose |
|----------|-------|---------|
| -1 | MujocoSimProvider | beforeStep, mj_step, afterStep |
| 0 (default) | SceneRenderer (internal), useIkController, your code | Body mesh sync, IK, rendering |

## Roadmap

Features planned but not yet implemented:

| Feature | Priority | Description |
|---------|----------|-------------|
| **Web Worker physics** | P2 | Run `mj_step` off main thread via SharedArrayBuffer |

### WASM Limitations (@mujoco/mujoco)

These MuJoCo features are not yet exposed in the WASM binding:

- `flex_faceadr` / `flex_facenum` / `flex_face` -- FlexRenderer renders vertices without face indices
- `ten_rgba` / `ten_width` -- TendonRenderer uses default color/width

## License

Apache-2.0
