mujoco-react 9.4.0 → 9.6.0

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.
@@ -11,6 +11,7 @@ import type { ThreeElements } from '@react-three/fiber';
11
11
  import * as THREE from 'three';
12
12
  import { useMujocoContext } from '../core/MujocoSimProvider';
13
13
  import { getName } from '../core/SceneLoader';
14
+ import { CAPTURE_EXCLUDE_KEY } from '../rendering/cameraFrameCapture';
14
15
  import { getContact, withContacts } from '../types';
15
16
  import type { DebugProps } from '../types';
16
17
 
@@ -25,9 +26,20 @@ const JOINT_COLORS: Record<number, number> = {
25
26
  const _v3a = new THREE.Vector3();
26
27
  const _v3b = new THREE.Vector3();
27
28
  const _quat = new THREE.Quaternion();
29
+ const _cameraMatrix = new THREE.Matrix4();
28
30
  const _contactPos = new THREE.Vector3();
29
31
  const _contactNormal = new THREE.Vector3();
30
32
  const MAX_CONTACT_ARROWS = 50;
33
+ const CAMERA_DEBUG_LENGTH = 0.12;
34
+ const CAMERA_DEBUG_FRUSTUM_DEPTH = 0.08;
35
+
36
+ type CameraDebugObject = THREE.Group & {
37
+ userData: {
38
+ cameraId: number;
39
+ frustum: THREE.LineSegments;
40
+ label?: THREE.Sprite;
41
+ };
42
+ };
31
43
 
32
44
  /**
33
45
  * Declarative debug visualization component.
@@ -37,6 +49,7 @@ export function Debug({
37
49
  showGeoms = false,
38
50
  showSites = false,
39
51
  showJoints = false,
52
+ showCameras = false,
40
53
  showContacts = false,
41
54
  showCOM = false,
42
55
  showInertia = false,
@@ -55,6 +68,7 @@ export function Debug({
55
68
  const geoms: THREE.Object3D[] = [];
56
69
  const sites: THREE.Object3D[] = [];
57
70
  const joints: THREE.Object3D[] = [];
71
+ const cameras: CameraDebugObject[] = [];
58
72
  const comMarkers: THREE.Object3D[] = [];
59
73
 
60
74
  // Wireframe geoms
@@ -179,6 +193,83 @@ export function Debug({
179
193
  }
180
194
  }
181
195
 
196
+ if (showCameras && model.ncam && model.name_camadr) {
197
+ for (let i = 0; i < model.ncam; i++) {
198
+ const group = new THREE.Group() as CameraDebugObject;
199
+ group.userData.cameraId = i;
200
+ group.renderOrder = 999;
201
+ group.frustumCulled = false;
202
+
203
+ const marker = new THREE.Mesh(
204
+ new THREE.BoxGeometry(0.014, 0.009, 0.006),
205
+ new THREE.MeshBasicMaterial({ color: 0x38bdf8, depthTest: false })
206
+ );
207
+ marker.renderOrder = 999;
208
+ marker.frustumCulled = false;
209
+ group.add(marker);
210
+
211
+ const forward = new THREE.ArrowHelper(
212
+ new THREE.Vector3(0, 0, -1),
213
+ new THREE.Vector3(),
214
+ CAMERA_DEBUG_LENGTH,
215
+ 0x38bdf8,
216
+ CAMERA_DEBUG_LENGTH * 0.24,
217
+ CAMERA_DEBUG_LENGTH * 0.11
218
+ );
219
+ forward.renderOrder = 999;
220
+ forward.frustumCulled = false;
221
+ forward.line.material = new THREE.LineBasicMaterial({
222
+ color: 0x38bdf8,
223
+ depthTest: false,
224
+ });
225
+ (forward.cone.material as THREE.MeshBasicMaterial).depthTest = false;
226
+ group.add(forward);
227
+
228
+ const frustumGeometry = new THREE.BufferGeometry();
229
+ frustumGeometry.setAttribute(
230
+ 'position',
231
+ new THREE.Float32BufferAttribute(new Float32Array(8 * 2 * 3), 3)
232
+ );
233
+ const frustum = new THREE.LineSegments(
234
+ frustumGeometry,
235
+ new THREE.LineBasicMaterial({
236
+ color: 0x38bdf8,
237
+ transparent: true,
238
+ opacity: 0.8,
239
+ depthTest: false,
240
+ })
241
+ );
242
+ frustum.renderOrder = 999;
243
+ frustum.frustumCulled = false;
244
+ group.userData.frustum = frustum;
245
+ group.add(frustum);
246
+
247
+ const canvas = document.createElement('canvas');
248
+ canvas.width = 256;
249
+ canvas.height = 64;
250
+ const ctx = canvas.getContext('2d')!;
251
+ ctx.fillStyle = '#38bdf8';
252
+ ctx.font = 'bold 32px monospace';
253
+ ctx.textAlign = 'center';
254
+ ctx.fillText(getName(model, model.name_camadr[i]), 128, 42);
255
+ const texture = new THREE.CanvasTexture(canvas);
256
+ const sprite = new THREE.Sprite(
257
+ new THREE.SpriteMaterial({
258
+ map: texture,
259
+ depthTest: false,
260
+ transparent: true,
261
+ })
262
+ );
263
+ sprite.position.set(0, 0.014, 0.01);
264
+ sprite.scale.set(0.04, 0.01, 1);
265
+ sprite.renderOrder = 999;
266
+ group.userData.label = sprite;
267
+ group.add(sprite);
268
+
269
+ cameras.push(group);
270
+ }
271
+ }
272
+
182
273
  // COM markers
183
274
  if (showCOM) {
184
275
  for (let i = 1; i < model.nbody; i++) {
@@ -190,8 +281,8 @@ export function Debug({
190
281
  }
191
282
  }
192
283
 
193
- return { geoms, sites, joints, comMarkers };
194
- }, [status, mjModelRef, showGeoms, showSites, showJoints, showCOM]);
284
+ return { geoms, sites, joints, cameras, comMarkers };
285
+ }, [status, mjModelRef, showGeoms, showSites, showJoints, showCameras, showCOM]);
195
286
 
196
287
  // Add/remove debug objects from scene
197
288
  useEffect(() => {
@@ -202,6 +293,7 @@ export function Debug({
202
293
  ...debugGeometry.geoms,
203
294
  ...debugGeometry.sites,
204
295
  ...debugGeometry.joints,
296
+ ...debugGeometry.cameras,
205
297
  ...debugGeometry.comMarkers,
206
298
  ];
207
299
  for (const obj of allObjects) group.add(obj);
@@ -281,6 +373,59 @@ export function Debug({
281
373
  }
282
374
  }
283
375
 
376
+ const camXpos = data.cam_xpos;
377
+ const camXmat = data.cam_xmat;
378
+ if (camXpos && camXmat) {
379
+ for (const group of debugGeometry.cameras) {
380
+ const cameraId = group.userData.cameraId;
381
+ const i3 = cameraId * 3;
382
+ const i9 = cameraId * 9;
383
+ group.position.set(
384
+ camXpos[i3],
385
+ camXpos[i3 + 1],
386
+ camXpos[i3 + 2]
387
+ );
388
+ _cameraMatrix.set(
389
+ camXmat[i9], camXmat[i9 + 1], camXmat[i9 + 2], 0,
390
+ camXmat[i9 + 3], camXmat[i9 + 4], camXmat[i9 + 5], 0,
391
+ camXmat[i9 + 6], camXmat[i9 + 7], camXmat[i9 + 8], 0,
392
+ 0, 0, 0, 1
393
+ );
394
+ group.quaternion.setFromRotationMatrix(_cameraMatrix);
395
+
396
+ const fovy = model.cam_fovy?.[cameraId] ?? 45;
397
+ const halfHeight = Math.tan(THREE.MathUtils.degToRad(fovy) / 2) *
398
+ CAMERA_DEBUG_FRUSTUM_DEPTH;
399
+ const halfWidth = halfHeight * 4 / 3;
400
+ const positions = group.userData.frustum.geometry.attributes.position;
401
+ const array = positions.array as Float32Array;
402
+ const points = [
403
+ [0, 0, 0],
404
+ [-halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
405
+ [0, 0, 0],
406
+ [halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
407
+ [0, 0, 0],
408
+ [halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
409
+ [0, 0, 0],
410
+ [-halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
411
+ [-halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
412
+ [halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
413
+ [halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
414
+ [halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
415
+ [halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
416
+ [-halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
417
+ [-halfWidth, -halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
418
+ [-halfWidth, halfHeight, -CAMERA_DEBUG_FRUSTUM_DEPTH],
419
+ ];
420
+ for (let i = 0; i < points.length; i += 1) {
421
+ array[i * 3] = points[i][0];
422
+ array[i * 3 + 1] = points[i][1];
423
+ array[i * 3 + 2] = points[i][2];
424
+ }
425
+ positions.needsUpdate = true;
426
+ }
427
+ }
428
+
284
429
  // Update COM markers
285
430
  for (const mesh of debugGeometry.comMarkers) {
286
431
  const bid = mesh.userData.bodyId;
@@ -358,7 +503,13 @@ export function Debug({
358
503
  if (status !== 'ready') return null;
359
504
 
360
505
  return (
361
- <group {...groupProps}>
506
+ <group
507
+ {...groupProps}
508
+ userData={{
509
+ ...groupProps.userData,
510
+ [CAPTURE_EXCLUDE_KEY]: true,
511
+ }}
512
+ >
362
513
  <group ref={groupRef} />
363
514
  {showContacts && <group ref={contactGroupRef} />}
364
515
  </group>
@@ -8,6 +8,7 @@ import type { ThreeElements } from '@react-three/fiber';
8
8
  import { useEffect, useRef } from 'react';
9
9
  import * as THREE from 'three';
10
10
  import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
11
+ import { CAPTURE_EXCLUDE_KEY } from '../rendering/cameraFrameCapture';
11
12
  import type { DragInteractionProps } from '../types';
12
13
 
13
14
  // Preallocated temps to avoid GC pressure
@@ -60,6 +61,7 @@ export function DragInteraction({
60
61
  0.1,
61
62
  0xff4444,
62
63
  );
64
+ arrow.userData[CAPTURE_EXCLUDE_KEY] = true;
63
65
  arrow.visible = false;
64
66
  // Make arrow semi-transparent
65
67
  (arrow.line.material as THREE.LineBasicMaterial).transparent = true;
@@ -9,6 +9,7 @@ import { useEffect, useRef } from 'react';
9
9
  import * as THREE from 'three';
10
10
  import { useMujocoContext } from '../core/MujocoSimProvider';
11
11
  import { findSiteByName } from '../core/SceneLoader';
12
+ import { CAPTURE_EXCLUDE_KEY } from '../rendering/cameraFrameCapture';
12
13
  import type { IkGizmoProps } from '../types';
13
14
 
14
15
  // Preallocated temps to avoid GC pressure in useFrame
@@ -83,7 +84,10 @@ export function IkGizmo({ controller, siteName, scale = 0.18, onDrag }: IkGizmoP
83
84
  if (status !== 'ready') return null;
84
85
 
85
86
  return (
86
- <group ref={wrapperRef}>
87
+ <group
88
+ ref={wrapperRef}
89
+ userData={{ [CAPTURE_EXCLUDE_KEY]: true }}
90
+ >
87
91
  <PivotControls
88
92
  ref={pivotRef}
89
93
  autoTransform
@@ -148,12 +148,12 @@ function vector3FromArray(values: ArrayLike<number>, offset: number): [number, n
148
148
  return [values[offset], values[offset + 1], values[offset + 2]];
149
149
  }
150
150
 
151
- function quaternionFromArray(values: ArrayLike<number>, offset: number): [number, number, number, number] {
151
+ function quaternionFromMujocoQuat(values: ArrayLike<number>, offset: number): [number, number, number, number] {
152
152
  return [
153
- values[offset],
154
153
  values[offset + 1],
155
154
  values[offset + 2],
156
155
  values[offset + 3],
156
+ values[offset],
157
157
  ];
158
158
  }
159
159
 
@@ -993,7 +993,7 @@ export function MujocoSimProvider({
993
993
  ? vector3FromArray(model.cam_pos, posOffset)
994
994
  : null,
995
995
  quaternion: model.cam_quat
996
- ? quaternionFromArray(model.cam_quat, quatOffset)
996
+ ? quaternionFromMujocoQuat(model.cam_quat, quatOffset)
997
997
  : null,
998
998
  });
999
999
  }
@@ -1024,7 +1024,7 @@ export function MujocoSimProvider({
1024
1024
  const quaternion = data.cam_xmat
1025
1025
  ? quaternionFromXmat(data.cam_xmat, cameraId * 9)
1026
1026
  : model.cam_quat
1027
- ? quaternionFromArray(model.cam_quat, cameraId * 4)
1027
+ ? quaternionFromMujocoQuat(model.cam_quat, cameraId * 4)
1028
1028
  : undefined;
1029
1029
 
1030
1030
  if (!position || !quaternion) {
@@ -1442,7 +1442,7 @@ export function MujocoSimProvider({
1442
1442
  const cameraFrames: Record<string, CameraFrameCaptureResult> = {};
1443
1443
  for (const { key, captureOptions, mountedSource, session } of captureSessions) {
1444
1444
  const resolvedCaptureOptions = resolveCameraCaptureOptions(captureOptions);
1445
- const cameraFrame = session.captureDataUrl({
1445
+ const cameraFrame = await session.captureDataUrlAsync({
1446
1446
  ...resolvedCaptureOptions,
1447
1447
  source: mountedSource ?? resolvedCaptureOptions.source,
1448
1448
  });
@@ -94,7 +94,14 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
94
94
  return genericIkRef.current.solve(
95
95
  model, data, siteIdRef.current, controlGroup.qposAdr,
96
96
  position, quaternion, currentQ,
97
- { damping: config.damping, maxIterations: config.maxIterations },
97
+ {
98
+ damping: config.damping,
99
+ epsilon: config.epsilon,
100
+ maxIterations: config.maxIterations,
101
+ posWeight: config.posWeight,
102
+ rotWeight: config.rotWeight,
103
+ tolerance: config.tolerance,
104
+ },
98
105
  );
99
106
  },
100
107
  [config, mjModelRef, mjDataRef],
@@ -0,0 +1,170 @@
1
+ /**
2
+ * @license
3
+ * SPDX-License-Identifier: Apache-2.0
4
+ */
5
+
6
+ import { useFrame } from '@react-three/fiber';
7
+ import { useEffect, useMemo, useRef } from 'react';
8
+ import * as THREE from 'three';
9
+ import type { KeyboardIkTargetAction, KeyboardIkTargetBinding, KeyboardIkTargetConfig } from '../types';
10
+
11
+ const DEFAULT_TRANSLATE_SPEED = 0.25;
12
+ const DEFAULT_ROTATE_SPEED = 1.0;
13
+
14
+ const _translation = new THREE.Vector3();
15
+ const _axis = new THREE.Vector3();
16
+ const _quat = new THREE.Quaternion();
17
+
18
+ function actionSign(action: KeyboardIkTargetAction): 1 | -1 {
19
+ return action.endsWith('+') ? 1 : -1;
20
+ }
21
+
22
+ function actionBase(action: KeyboardIkTargetAction) {
23
+ return action.slice(0, -1);
24
+ }
25
+
26
+ function applyRotation(
27
+ target: THREE.Object3D,
28
+ action: KeyboardIkTargetAction,
29
+ amount: number,
30
+ frame: 'world' | 'target',
31
+ ) {
32
+ const base = actionBase(action);
33
+ if (base === 'pitch') {
34
+ _axis.set(1, 0, 0);
35
+ } else if (base === 'yaw') {
36
+ _axis.set(0, 1, 0);
37
+ } else if (base === 'roll') {
38
+ _axis.set(0, 0, 1);
39
+ } else {
40
+ return;
41
+ }
42
+
43
+ _quat.setFromAxisAngle(_axis, amount);
44
+ if (frame === 'target') {
45
+ target.quaternion.multiply(_quat);
46
+ } else {
47
+ target.quaternion.premultiply(_quat);
48
+ }
49
+ }
50
+
51
+ function addTranslation(action: KeyboardIkTargetAction, amount: number) {
52
+ switch (action) {
53
+ case 'x+':
54
+ _translation.x += amount;
55
+ break;
56
+ case 'x-':
57
+ _translation.x -= amount;
58
+ break;
59
+ case 'y+':
60
+ _translation.y += amount;
61
+ break;
62
+ case 'y-':
63
+ _translation.y -= amount;
64
+ break;
65
+ case 'z+':
66
+ _translation.z += amount;
67
+ break;
68
+ case 'z-':
69
+ _translation.z -= amount;
70
+ break;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Moves an existing IK target from keyboard input.
76
+ *
77
+ * This hook is intentionally robot-agnostic: it only edits the target owned by
78
+ * `useIkController`, then lets the normal IK solver write robot controls.
79
+ */
80
+ export function useKeyboardIkTarget(config: KeyboardIkTargetConfig | null) {
81
+ const pressedRef = useRef(new Set<string>());
82
+ const wasActiveRef = useRef(false);
83
+ const configRef = useRef(config);
84
+ configRef.current = config;
85
+
86
+ const boundCodes = useMemo(() => {
87
+ return new Set((config?.bindings ?? []).map((binding) => binding.code));
88
+ }, [config?.bindings]);
89
+
90
+ useEffect(() => {
91
+ const onKeyDown = (event: KeyboardEvent) => {
92
+ const current = configRef.current;
93
+ if (!current || current.enabled === false || !boundCodes.has(event.code)) return;
94
+ if (current.preventDefault !== false) event.preventDefault();
95
+ pressedRef.current.add(event.code);
96
+ };
97
+ const onKeyUp = (event: KeyboardEvent) => {
98
+ const current = configRef.current;
99
+ if (boundCodes.has(event.code) && current?.preventDefault !== false) {
100
+ event.preventDefault();
101
+ }
102
+ pressedRef.current.delete(event.code);
103
+ };
104
+ const onBlur = () => {
105
+ pressedRef.current.clear();
106
+ wasActiveRef.current = false;
107
+ };
108
+
109
+ window.addEventListener('keydown', onKeyDown);
110
+ window.addEventListener('keyup', onKeyUp);
111
+ window.addEventListener('blur', onBlur);
112
+
113
+ return () => {
114
+ window.removeEventListener('keydown', onKeyDown);
115
+ window.removeEventListener('keyup', onKeyUp);
116
+ window.removeEventListener('blur', onBlur);
117
+ };
118
+ }, [boundCodes]);
119
+
120
+ useFrame((_state, delta) => {
121
+ const current = configRef.current;
122
+ const controller = current?.controller;
123
+ if (!current || current.enabled === false || !controller) {
124
+ wasActiveRef.current = false;
125
+ return;
126
+ }
127
+
128
+ const activeBindings: KeyboardIkTargetBinding[] = [];
129
+ for (const binding of current.bindings) {
130
+ if (pressedRef.current.has(binding.code)) activeBindings.push(binding);
131
+ }
132
+
133
+ if (activeBindings.length === 0) {
134
+ wasActiveRef.current = false;
135
+ return;
136
+ }
137
+
138
+ if (!wasActiveRef.current) {
139
+ if (current.syncOnStart !== false) controller.syncTargetToSite();
140
+ if (current.autoEnableIk !== false && !controller.ikEnabledRef.current) {
141
+ controller.setIkEnabled(true);
142
+ }
143
+ }
144
+ wasActiveRef.current = true;
145
+
146
+ const target = controller.ikTargetRef.current;
147
+ if (!target) return;
148
+
149
+ const frame = current.frame ?? 'world';
150
+ _translation.set(0, 0, 0);
151
+
152
+ for (const binding of activeBindings) {
153
+ const translateSpeed = binding.translateSpeed ?? current.translateSpeed ?? DEFAULT_TRANSLATE_SPEED;
154
+ const rotateSpeed = binding.rotateSpeed ?? current.rotateSpeed ?? DEFAULT_ROTATE_SPEED;
155
+ const amount = actionSign(binding.action) * delta;
156
+ const base = actionBase(binding.action);
157
+
158
+ if (base === 'x' || base === 'y' || base === 'z') {
159
+ addTranslation(binding.action, translateSpeed * delta);
160
+ } else {
161
+ applyRotation(target, binding.action, rotateSpeed * amount, frame);
162
+ }
163
+ }
164
+
165
+ if (_translation.lengthSq() > 0) {
166
+ if (frame === 'target') _translation.applyQuaternion(target.quaternion);
167
+ target.position.add(_translation);
168
+ }
169
+ });
170
+ }
package/src/index.ts CHANGED
@@ -91,6 +91,7 @@ export { useBodyState } from './hooks/useBodyState';
91
91
  export { useCtrl } from './hooks/useCtrl';
92
92
  export { useContacts, useContactEvents } from './hooks/useContacts';
93
93
  export { useKeyboardTeleop } from './hooks/useKeyboardTeleop';
94
+ export { useKeyboardIkTarget } from './hooks/useKeyboardIkTarget';
94
95
  export { usePolicy } from './hooks/usePolicy';
95
96
  export { useObservation } from './hooks/useObservation';
96
97
  export { useTrajectoryPlayer } from './hooks/useTrajectoryPlayer';
@@ -113,6 +114,7 @@ export type {
113
114
  MountedCameraSequenceRecordResult,
114
115
  } from './hooks/useMountedCameraSequenceRecorder';
115
116
  export {
117
+ CAPTURE_EXCLUDE_KEY,
116
118
  captureCameraFrame,
117
119
  captureCameraFrameBlob,
118
120
  createCameraFrameCaptureSession,
@@ -210,6 +212,9 @@ export type {
210
212
  // Keyboard teleop
211
213
  KeyBinding,
212
214
  KeyboardTeleopConfig,
215
+ KeyboardIkTargetAction,
216
+ KeyboardIkTargetBinding,
217
+ KeyboardIkTargetConfig,
213
218
  // Policy
214
219
  PolicyConfig,
215
220
  PolicyVector,