mujoco-react 8.2.1 → 8.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +75 -3
- package/dist/index.d.ts +6 -3
- package/dist/index.js +86 -60
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/components/ContactMarkers.tsx +14 -12
- package/src/components/Debug.tsx +18 -16
- package/src/components/FlexRenderer.tsx +1 -1
- package/src/components/TendonRenderer.tsx +1 -1
- package/src/core/MujocoProvider.tsx +3 -2
- package/src/core/MujocoSimProvider.tsx +15 -13
- package/src/core/SceneLoader.ts +11 -1
- package/src/hooks/useContacts.ts +20 -18
- package/src/rendering/GeomBuilder.ts +1 -1
- package/src/types.ts +22 -4
- package/src/wasm-url.d.ts +4 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
> **Beta** — This library is under active development. The API may change between minor versions until 10.0.
|
|
8
8
|
|
|
9
|
-
Composable [React Three Fiber](https://docs.pmnd.rs/react-three-fiber) wrapper around [mujoco
|
|
9
|
+
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.
|
|
10
10
|
|
|
11
11
|
[](https://www.npmjs.com/package/mujoco-react)
|
|
12
12
|
|
|
@@ -104,6 +104,78 @@ function useMyController(gain: number) {
|
|
|
104
104
|
}
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
### Applying Forces
|
|
108
|
+
|
|
109
|
+
Write directly to `xfrc_applied` in a physics callback for per-frame forces. The layout is `[torque_x, torque_y, torque_z, force_x, force_y, force_z]` per body:
|
|
110
|
+
|
|
111
|
+
```tsx
|
|
112
|
+
import { useBeforePhysicsStep } from "mujoco-react";
|
|
113
|
+
|
|
114
|
+
function useSpringForce(bodyId: number, target: [number, number, number], stiffness = 100) {
|
|
115
|
+
useBeforePhysicsStep((_model, data) => {
|
|
116
|
+
const i3 = bodyId * 3;
|
|
117
|
+
const i6 = bodyId * 6;
|
|
118
|
+
data.xfrc_applied[i6 + 3] = (target[0] - data.xpos[i3]) * stiffness;
|
|
119
|
+
data.xfrc_applied[i6 + 4] = (target[1] - data.xpos[i3 + 1]) * stiffness;
|
|
120
|
+
data.xfrc_applied[i6 + 5] = (target[2] - data.xpos[i3 + 2]) * stiffness;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
The `api.applyForce(bodyName, force)` convenience method also works for one-off interactions (e.g. button clicks) but does a name lookup each call.
|
|
126
|
+
|
|
127
|
+
### WebSocket Joint Control
|
|
128
|
+
|
|
129
|
+
Stream joint commands over a WebSocket and send simulation state back:
|
|
130
|
+
|
|
131
|
+
```tsx
|
|
132
|
+
import { useEffect, useRef } from "react";
|
|
133
|
+
import { useMujoco, useBeforePhysicsStep, useAfterPhysicsStep } from "mujoco-react";
|
|
134
|
+
|
|
135
|
+
function useWebSocketJoints(url: string) {
|
|
136
|
+
const { api } = useMujoco();
|
|
137
|
+
const wsRef = useRef<WebSocket | null>(null);
|
|
138
|
+
const latestJointsRef = useRef<number[] | null>(null);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
const ws = new WebSocket(url);
|
|
142
|
+
wsRef.current = ws;
|
|
143
|
+
|
|
144
|
+
ws.onmessage = (evt) => {
|
|
145
|
+
const msg = JSON.parse(evt.data);
|
|
146
|
+
if (msg.type === "joint_command") {
|
|
147
|
+
latestJointsRef.current = msg.qpos;
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
return () => ws.close();
|
|
152
|
+
}, [url]);
|
|
153
|
+
|
|
154
|
+
// Apply incoming joint positions each physics step
|
|
155
|
+
useBeforePhysicsStep((model, data) => {
|
|
156
|
+
const joints = latestJointsRef.current;
|
|
157
|
+
if (!joints) return;
|
|
158
|
+
for (let i = 0; i < Math.min(joints.length, model.nu); i++) {
|
|
159
|
+
data.ctrl[i] = joints[i];
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Send sensor feedback back after physics
|
|
164
|
+
useAfterPhysicsStep((model, data) => {
|
|
165
|
+
const ws = wsRef.current;
|
|
166
|
+
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
167
|
+
|
|
168
|
+
ws.send(JSON.stringify({
|
|
169
|
+
type: "feedback",
|
|
170
|
+
time: data.time,
|
|
171
|
+
qpos: Array.from(data.qpos),
|
|
172
|
+
qvel: Array.from(data.qvel),
|
|
173
|
+
sensordata: Array.from(data.sensordata),
|
|
174
|
+
}));
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
107
179
|
For reusable controllers with typed config, default merging, and children, use the `createController` factory:
|
|
108
180
|
|
|
109
181
|
```tsx
|
|
@@ -445,7 +517,7 @@ import { useMujocoWasm } from "mujoco-react";
|
|
|
445
517
|
const { mujoco, status } = useMujocoWasm();
|
|
446
518
|
|
|
447
519
|
if (mujoco) {
|
|
448
|
-
const model = mujoco.MjModel.
|
|
520
|
+
const model = mujoco.MjModel.from_xml_path("/path/to/scene.xml");
|
|
449
521
|
const data = new mujoco.MjData(model);
|
|
450
522
|
mujoco.mj_step(model, data);
|
|
451
523
|
console.log(data.qpos); // joint positions after one step
|
|
@@ -769,7 +841,7 @@ Features planned but not yet implemented:
|
|
|
769
841
|
| **Web Worker physics** | P2 | Run `mj_step` off main thread via SharedArrayBuffer |
|
|
770
842
|
| **Register codegen** | P2 | CLI to auto-generate `Register` type augmentation from MJCF XML |
|
|
771
843
|
|
|
772
|
-
### WASM Limitations (mujoco
|
|
844
|
+
### WASM Limitations (@mujoco/mujoco)
|
|
773
845
|
|
|
774
846
|
These MuJoCo features are not yet exposed in the WASM binding:
|
|
775
847
|
|
package/dist/index.d.ts
CHANGED
|
@@ -64,12 +64,13 @@ interface MujocoContact {
|
|
|
64
64
|
*/
|
|
65
65
|
interface MujocoContactArray {
|
|
66
66
|
get(i: number): MujocoContact | undefined;
|
|
67
|
+
delete?: () => void;
|
|
67
68
|
}
|
|
68
69
|
/**
|
|
69
|
-
* Read a single contact from
|
|
70
|
+
* Read a single contact from an already-acquired WASM contact array.
|
|
70
71
|
* Returns undefined if the access fails (WASM heap issue, bad index, etc.).
|
|
71
72
|
*/
|
|
72
|
-
declare function getContact(
|
|
73
|
+
declare function getContact(contacts: MujocoContactArray, i: number): MujocoContact | undefined;
|
|
73
74
|
/**
|
|
74
75
|
* Minimal interface for MuJoCo Model to avoid 'any'.
|
|
75
76
|
*/
|
|
@@ -217,7 +218,9 @@ interface MujocoData {
|
|
|
217
218
|
*/
|
|
218
219
|
interface MujocoModule {
|
|
219
220
|
MjModel: {
|
|
220
|
-
|
|
221
|
+
from_xml_path?: (path: string) => MujocoModel;
|
|
222
|
+
from_xml_string?: (xml: string, vfs?: unknown) => MujocoModel;
|
|
223
|
+
loadFromXML?: (path: string) => MujocoModel;
|
|
221
224
|
[key: string]: unknown;
|
|
222
225
|
};
|
|
223
226
|
MjData: new (model: MujocoModel) => MujocoData;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import loadMujoco from 'mujoco
|
|
1
|
+
import loadMujoco from '@mujoco/mujoco';
|
|
2
|
+
import defaultMujocoWasmUrl from '@mujoco/mujoco/mujoco.wasm?url';
|
|
2
3
|
import { createContext, forwardRef, useEffect, useContext, useState, useRef, useCallback, useMemo, useLayoutEffect } from 'react';
|
|
3
4
|
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
4
5
|
import { Canvas, useThree, useFrame } from '@react-three/fiber';
|
|
@@ -22,7 +23,7 @@ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
|
|
|
22
23
|
useEffect(() => {
|
|
23
24
|
isMounted.current = true;
|
|
24
25
|
const wasmPromise = loadMujoco({
|
|
25
|
-
|
|
26
|
+
locateFile: (path) => path.endsWith(".wasm") ? wasmUrl ?? defaultMujocoWasmUrl : path,
|
|
26
27
|
printErr: (text) => {
|
|
27
28
|
if (text.includes("Aborted") && isMounted.current) {
|
|
28
29
|
setError("Simulation crashed. Reload page.");
|
|
@@ -60,13 +61,21 @@ function MujocoProvider({ wasmUrl, timeout = 3e4, children, onError }) {
|
|
|
60
61
|
}
|
|
61
62
|
|
|
62
63
|
// src/types.ts
|
|
63
|
-
function getContact(
|
|
64
|
+
function getContact(contacts, i) {
|
|
64
65
|
try {
|
|
65
|
-
return
|
|
66
|
+
return contacts.get(i);
|
|
66
67
|
} catch {
|
|
67
68
|
return void 0;
|
|
68
69
|
}
|
|
69
70
|
}
|
|
71
|
+
function withContacts(data, read) {
|
|
72
|
+
const contacts = data.contact;
|
|
73
|
+
try {
|
|
74
|
+
return read(contacts);
|
|
75
|
+
} finally {
|
|
76
|
+
contacts.delete?.();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
70
79
|
var CapsuleGeometry = class extends THREE11.BufferGeometry {
|
|
71
80
|
parameters;
|
|
72
81
|
constructor(radius = 1, length = 1, capSegments = 4, radialSegments = 8) {
|
|
@@ -403,6 +412,15 @@ function ensureDir(mujoco, fname) {
|
|
|
403
412
|
}
|
|
404
413
|
}
|
|
405
414
|
}
|
|
415
|
+
function loadModelFromPath(mujoco, path) {
|
|
416
|
+
if (mujoco.MjModel.from_xml_path) {
|
|
417
|
+
return mujoco.MjModel.from_xml_path(path);
|
|
418
|
+
}
|
|
419
|
+
if (mujoco.MjModel.loadFromXML) {
|
|
420
|
+
return mujoco.MjModel.loadFromXML(path);
|
|
421
|
+
}
|
|
422
|
+
throw new Error("MuJoCo WASM module does not expose an XML path loader");
|
|
423
|
+
}
|
|
406
424
|
async function loadScene(mujoco, config, onProgress) {
|
|
407
425
|
try {
|
|
408
426
|
mujoco.FS.unmount("/working");
|
|
@@ -486,7 +504,7 @@ async function loadScene(mujoco, config, onProgress) {
|
|
|
486
504
|
}
|
|
487
505
|
}
|
|
488
506
|
onProgress?.("Loading model...");
|
|
489
|
-
const mjModel = mujoco
|
|
507
|
+
const mjModel = loadModelFromPath(mujoco, `/working/${config.sceneFile}`);
|
|
490
508
|
const mjData = new mujoco.MjData(mjModel);
|
|
491
509
|
if (config.homeJoints) {
|
|
492
510
|
const homeCount = Math.min(config.homeJoints.length, mjModel.nu);
|
|
@@ -1080,18 +1098,20 @@ function MujocoSimProvider({
|
|
|
1080
1098
|
if (!model || !data) return [];
|
|
1081
1099
|
const contacts = [];
|
|
1082
1100
|
const ncon = data.ncon;
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1101
|
+
withContacts(data, (contactArray) => {
|
|
1102
|
+
for (let i = 0; i < ncon; i++) {
|
|
1103
|
+
const c = getContact(contactArray, i);
|
|
1104
|
+
if (!c) break;
|
|
1105
|
+
contacts.push({
|
|
1106
|
+
geom1: c.geom1,
|
|
1107
|
+
geom1Name: getName(model, model.name_geomadr[c.geom1]),
|
|
1108
|
+
geom2: c.geom2,
|
|
1109
|
+
geom2Name: getName(model, model.name_geomadr[c.geom2]),
|
|
1110
|
+
pos: [c.pos[0], c.pos[1], c.pos[2]],
|
|
1111
|
+
depth: c.dist
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1095
1115
|
return contacts;
|
|
1096
1116
|
}, []);
|
|
1097
1117
|
const getBodies = useCallback(() => {
|
|
@@ -2255,18 +2275,20 @@ function ContactMarkers({
|
|
|
2255
2275
|
}
|
|
2256
2276
|
const ncon = data.ncon;
|
|
2257
2277
|
const count = Math.min(ncon, maxContacts);
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2278
|
+
let resolvedCount = count;
|
|
2279
|
+
withContacts(data, (contactArray) => {
|
|
2280
|
+
for (let i = 0; i < count; i++) {
|
|
2281
|
+
const c = getContact(contactArray, i);
|
|
2282
|
+
if (!c) {
|
|
2283
|
+
resolvedCount = i;
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
_dummy.position.set(c.pos[0], c.pos[1], c.pos[2]);
|
|
2287
|
+
_dummy.updateMatrix();
|
|
2288
|
+
mesh.setMatrixAt(i, _dummy.matrix);
|
|
2264
2289
|
}
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
mesh.setMatrixAt(i, _dummy.matrix);
|
|
2268
|
-
}
|
|
2269
|
-
mesh.count = count;
|
|
2290
|
+
});
|
|
2291
|
+
mesh.count = resolvedCount;
|
|
2270
2292
|
mesh.instanceMatrix.needsUpdate = true;
|
|
2271
2293
|
});
|
|
2272
2294
|
if (status !== "ready") return null;
|
|
@@ -2807,22 +2829,24 @@ function Debug({
|
|
|
2807
2829
|
if (!data || pool.length === 0) return;
|
|
2808
2830
|
const ncon = data.ncon;
|
|
2809
2831
|
let arrowIdx = 0;
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2832
|
+
withContacts(data, (contactArray) => {
|
|
2833
|
+
for (let i = 0; i < Math.min(ncon, MAX_CONTACT_ARROWS); i++) {
|
|
2834
|
+
const c = getContact(contactArray, i);
|
|
2835
|
+
if (!c) break;
|
|
2836
|
+
_contactPos.set(c.pos[0], c.pos[1], c.pos[2]);
|
|
2837
|
+
_contactNormal.set(c.frame[0], c.frame[1], c.frame[2]);
|
|
2838
|
+
const force = Math.abs(c.dist) * 100;
|
|
2839
|
+
const length = Math.min(force * 0.01, 0.1);
|
|
2840
|
+
if (length > 1e-3 && arrowIdx < pool.length) {
|
|
2841
|
+
const arrow = pool[arrowIdx];
|
|
2842
|
+
arrow.position.copy(_contactPos);
|
|
2843
|
+
arrow.setDirection(_contactNormal);
|
|
2844
|
+
arrow.setLength(length, length * 0.3, length * 0.15);
|
|
2845
|
+
arrow.visible = true;
|
|
2846
|
+
arrowIdx++;
|
|
2847
|
+
}
|
|
2824
2848
|
}
|
|
2825
|
-
}
|
|
2849
|
+
});
|
|
2826
2850
|
for (let i = arrowIdx; i < pool.length; i++) {
|
|
2827
2851
|
pool[i].visible = false;
|
|
2828
2852
|
}
|
|
@@ -3053,23 +3077,25 @@ function useContacts(bodyName, callback) {
|
|
|
3053
3077
|
}
|
|
3054
3078
|
const contacts = [];
|
|
3055
3079
|
const filterBody = bodyIdRef.current;
|
|
3056
|
-
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3080
|
+
withContacts(data, (contactArray) => {
|
|
3081
|
+
for (let i = 0; i < ncon; i++) {
|
|
3082
|
+
const c = getContact(contactArray, i);
|
|
3083
|
+
if (!c) break;
|
|
3084
|
+
if (filterBody >= 0) {
|
|
3085
|
+
const b1 = model.geom_bodyid[c.geom1];
|
|
3086
|
+
const b2 = model.geom_bodyid[c.geom2];
|
|
3087
|
+
if (b1 !== filterBody && b2 !== filterBody) continue;
|
|
3088
|
+
}
|
|
3089
|
+
contacts.push({
|
|
3090
|
+
geom1: c.geom1,
|
|
3091
|
+
geom1Name: getGeomNameCached(model, c.geom1),
|
|
3092
|
+
geom2: c.geom2,
|
|
3093
|
+
geom2Name: getGeomNameCached(model, c.geom2),
|
|
3094
|
+
pos: [c.pos[0], c.pos[1], c.pos[2]],
|
|
3095
|
+
depth: c.dist
|
|
3096
|
+
});
|
|
3063
3097
|
}
|
|
3064
|
-
|
|
3065
|
-
geom1: c.geom1,
|
|
3066
|
-
geom1Name: getGeomNameCached(model, c.geom1),
|
|
3067
|
-
geom2: c.geom2,
|
|
3068
|
-
geom2Name: getGeomNameCached(model, c.geom2),
|
|
3069
|
-
pos: [c.pos[0], c.pos[1], c.pos[2]],
|
|
3070
|
-
depth: c.dist
|
|
3071
|
-
});
|
|
3072
|
-
}
|
|
3098
|
+
});
|
|
3073
3099
|
contactsRef.current = contacts;
|
|
3074
3100
|
callbackRef.current?.(contacts);
|
|
3075
3101
|
});
|
|
@@ -4058,7 +4084,7 @@ function useCameraAnimation() {
|
|
|
4058
4084
|
* WASM fields used: model.ntendon, model.ten_wrapadr, model.ten_wrapnum
|
|
4059
4085
|
* data.wrap_xpos, data.ten_wrapadr (runtime)
|
|
4060
4086
|
*
|
|
4061
|
-
* Note: ten_rgba and ten_width
|
|
4087
|
+
* Note: ten_rgba and ten_width may not be available in all MuJoCo WASM builds.
|
|
4062
4088
|
* Tendons use a default color and width.
|
|
4063
4089
|
*/
|
|
4064
4090
|
/**
|