mujoco-react 9.4.0 → 9.5.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.
package/README.md CHANGED
@@ -297,6 +297,16 @@ function Scene() {
297
297
 
298
298
  `SparkSplatEnvironment` currently renders `.spz` assets. Use the renderer-agnostic
299
299
  `SplatEnvironment` for `.ply`/`.splat` metadata or when wiring a different renderer.
300
+ Tune live rendering and snapshots separately with `renderTuning` and
301
+ `captureTuning`:
302
+
303
+ ```tsx
304
+ <SparkSplatEnvironment
305
+ {...splat.props}
306
+ renderTuning={{ lodSplatScale: 0.75, minSortIntervalMs: 50 }}
307
+ captureTuning={{ lodSplatScale: 1.4, lodRenderScale: 0.45, maxWarmupFrames: 6 }}
308
+ />
309
+ ```
300
310
 
301
311
  ## Write Controllers
302
312
 
@@ -850,6 +860,7 @@ Visualization overlays:
850
860
  | `showSites` | `boolean?` | `false` | Site markers |
851
861
  | `showJoints` | `boolean?` | `false` | Joint axes |
852
862
  | `showContacts` | `boolean?` | `false` | Contact force vectors |
863
+ | `showCameras` | `boolean?` | `false` | MuJoCo camera positions, frustums, and forward rays |
853
864
  | `showCOM` | `boolean?` | `false` | Center of mass markers |
854
865
  | `showInertia` | `boolean?` | `false` | Inertia ellipsoids |
855
866
  | `showTendons` | `boolean?` | `false` | Tendon paths |
@@ -858,6 +869,8 @@ Visualization overlays:
858
869
  | `contactColor` | `string?` | `"#ff4444"` | Color for contact force arrows |
859
870
  | `comColor` | `string?` | `"#ff0000"` | Color for COM markers |
860
871
 
872
+ Camera debug overlays use the live MuJoCo `cam_xpos` / `cam_xmat` frame, so the frustum matches mounted camera captures and follows parent body motion.
873
+
861
874
  ### `<TendonRenderer />`
862
875
 
863
876
  Renders tendons as tube geometry from wrap paths.
@@ -1198,12 +1211,15 @@ function DatasetRecorder() {
1198
1211
  recent recording.
1199
1212
 
1200
1213
  Use `resolveMountedCameraFrameSource()` when dataset feature names need to map
1201
- to named MuJoCo cameras, sites, or bodies before recording. The helper first
1202
- checks exact names and aliases, then falls back to normalized/prefix/suffix
1203
- matches such as `left_wrist` -> `left_wrist_camera_optical_frame`, and
1204
- token-contained imported-model names such as `observation.images.head` ->
1205
- `robot_head_camera`. It returns both the capture selector and the
1206
- mounted-source provenance that should be stored beside the dataset:
1214
+ to named MuJoCo cameras, sites, or bodies before recording. The helper honors
1215
+ aliases first, then prefers camera matches over sites and bodies, then falls
1216
+ back to exact body/site names. This keeps streams such as `wrist` mapped to a
1217
+ real `<camera name="wrist_cam">` even when the model also has a body named
1218
+ `wrist`. It also handles normalized/prefix/suffix matches such as
1219
+ `left_wrist` -> `left_wrist_camera_optical_frame`, and token-contained
1220
+ imported-model names such as `observation.images.head` ->
1221
+ `robot_head_camera`. It returns both the capture selector and the mounted-source
1222
+ provenance that should be stored beside the dataset:
1207
1223
 
1208
1224
  ```tsx
1209
1225
  const resolved = resolveMountedCameraFrameSource("head", {
@@ -1,6 +1,6 @@
1
+ import * as THREE from 'three';
1
2
  import { useThree } from '@react-three/fiber';
2
3
  import { useMemo, useEffect } from 'react';
3
- import * as THREE from 'three';
4
4
  import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
5
5
 
6
6
  // src/types.ts
@@ -85,6 +85,393 @@ var SplatEnvironmentReadinessStatus = {
85
85
  UnsupportedFormat: "unsupported-format",
86
86
  Ready: "ready"
87
87
  };
88
+ var CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY = "mujocoReactCameraFrameCaptureRender";
89
+ var CAPTURE_EXCLUDE_KEY = "mujoco.capture.exclude";
90
+ function toVector3(value, fallback) {
91
+ if (!value) return fallback.clone();
92
+ return value instanceof THREE.Vector3 ? value.clone() : new THREE.Vector3(value[0], value[1], value[2]);
93
+ }
94
+ function applyCameraPose(camera, options, fallbackCamera) {
95
+ camera.position.copy(toVector3(options.position, fallbackCamera.position));
96
+ camera.up.copy(toVector3(options.up, fallbackCamera.up));
97
+ if (options.quaternion) {
98
+ if (options.quaternion instanceof THREE.Quaternion) {
99
+ camera.quaternion.copy(options.quaternion);
100
+ } else {
101
+ camera.quaternion.set(
102
+ options.quaternion[0],
103
+ options.quaternion[1],
104
+ options.quaternion[2],
105
+ options.quaternion[3]
106
+ );
107
+ }
108
+ } else if (options.lookAt) {
109
+ camera.lookAt(toVector3(options.lookAt, new THREE.Vector3()));
110
+ } else {
111
+ camera.quaternion.copy(fallbackCamera.quaternion);
112
+ }
113
+ camera.updateMatrixWorld();
114
+ }
115
+ function createCaptureCamera(options, fallbackCamera, width, height) {
116
+ const camera = options.camera ? options.camera.clone() : fallbackCamera instanceof THREE.PerspectiveCamera ? fallbackCamera.clone() : new THREE.PerspectiveCamera(45, width / height, 0.01, 100);
117
+ if (camera instanceof THREE.PerspectiveCamera) {
118
+ camera.aspect = width / height;
119
+ camera.fov = options.fov ?? camera.fov;
120
+ camera.near = options.near ?? camera.near;
121
+ camera.far = options.far ?? camera.far;
122
+ camera.updateProjectionMatrix();
123
+ }
124
+ applyCameraPose(camera, options, fallbackCamera);
125
+ return camera;
126
+ }
127
+ function getCaptureDimensions(renderer, options) {
128
+ const width = Math.max(
129
+ 1,
130
+ Math.floor(options.width ?? renderer.domElement.width)
131
+ );
132
+ const height = Math.max(
133
+ 1,
134
+ Math.floor(options.height ?? renderer.domElement.height)
135
+ );
136
+ return { width, height };
137
+ }
138
+ function prepareCaptureCamera(camera, options, fallbackCamera, width, height) {
139
+ if (options.camera) {
140
+ camera.copy(options.camera);
141
+ }
142
+ if (camera instanceof THREE.PerspectiveCamera) {
143
+ camera.aspect = width / height;
144
+ camera.fov = options.fov ?? camera.fov;
145
+ camera.near = options.near ?? camera.near;
146
+ camera.far = options.far ?? camera.far;
147
+ camera.updateProjectionMatrix();
148
+ }
149
+ applyCameraPose(camera, options, fallbackCamera);
150
+ }
151
+ function readRenderTargetToCanvas(renderer, target, canvas, context, pixels, imageData, width, height, outputColorSpace) {
152
+ renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
153
+ const rowBytes = width * 4;
154
+ const encodeSrgb = outputColorSpace === THREE.SRGBColorSpace;
155
+ for (let y = 0; y < height; y += 1) {
156
+ const sourceStart = (height - y - 1) * rowBytes;
157
+ const targetStart = y * rowBytes;
158
+ const row = pixels.subarray(sourceStart, sourceStart + rowBytes);
159
+ if (!encodeSrgb) {
160
+ imageData.data.set(row, targetStart);
161
+ continue;
162
+ }
163
+ for (let x = 0; x < rowBytes; x += 4) {
164
+ const pixelOffset = targetStart + x;
165
+ imageData.data[pixelOffset] = linearByteToSrgbByte(row[x]);
166
+ imageData.data[pixelOffset + 1] = linearByteToSrgbByte(row[x + 1]);
167
+ imageData.data[pixelOffset + 2] = linearByteToSrgbByte(row[x + 2]);
168
+ imageData.data[pixelOffset + 3] = row[x + 3];
169
+ }
170
+ }
171
+ context.putImageData(imageData, 0, 0);
172
+ return canvas;
173
+ }
174
+ function linearByteToSrgbByte(value) {
175
+ const normalized = value / 255;
176
+ const encoded = normalized <= 31308e-7 ? normalized * 12.92 : 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;
177
+ return Math.min(255, Math.max(0, Math.round(encoded * 255)));
178
+ }
179
+ function readPixelsToCanvas(pixels, context, imageData, width, height, flipY = true) {
180
+ const rowBytes = width * 4;
181
+ for (let y = 0; y < height; y += 1) {
182
+ const sourceY = flipY ? height - y - 1 : y;
183
+ const sourceStart = sourceY * rowBytes;
184
+ const targetStart = y * rowBytes;
185
+ imageData.data.set(
186
+ pixels.subarray(sourceStart, sourceStart + rowBytes),
187
+ targetStart
188
+ );
189
+ }
190
+ context.putImageData(imageData, 0, 0);
191
+ }
192
+ function hideExcludedCaptureObjects(scene) {
193
+ const hidden = [];
194
+ scene.traverse((object) => {
195
+ if (!object.visible) return;
196
+ if (!object.userData[CAPTURE_EXCLUDE_KEY]) return;
197
+ hidden.push({ object, visible: object.visible });
198
+ object.visible = false;
199
+ });
200
+ return hidden;
201
+ }
202
+ function restoreObjectVisibility(hidden) {
203
+ for (const { object, visible } of hidden) {
204
+ object.visible = visible;
205
+ }
206
+ }
207
+ function getCameraFrameCaptureSource(options) {
208
+ if (options.source) return options.source;
209
+ if (options.cameraName) {
210
+ return { kind: "mujoco-camera", cameraName: options.cameraName };
211
+ }
212
+ if (options.siteName) {
213
+ return { kind: "mujoco-site", siteName: options.siteName };
214
+ }
215
+ if (options.bodyName) {
216
+ return { kind: "mujoco-body", bodyName: options.bodyName };
217
+ }
218
+ if (options.camera) return { kind: "custom-camera" };
219
+ if (options.position || options.lookAt || options.quaternion) {
220
+ return { kind: "explicit-pose" };
221
+ }
222
+ return { kind: "fallback-camera" };
223
+ }
224
+ function saveRendererState(renderer) {
225
+ const viewport = new THREE.Vector4();
226
+ const scissor = new THREE.Vector4();
227
+ const clearColor = new THREE.Color();
228
+ renderer.getViewport(viewport);
229
+ renderer.getScissor(scissor);
230
+ renderer.getClearColor(clearColor);
231
+ return {
232
+ target: renderer.getRenderTarget(),
233
+ xrEnabled: renderer.xr.enabled,
234
+ viewport,
235
+ scissor,
236
+ scissorTest: renderer.getScissorTest(),
237
+ clearColor,
238
+ clearAlpha: renderer.getClearAlpha(),
239
+ autoClear: renderer.autoClear
240
+ };
241
+ }
242
+ function restoreRendererState(renderer, state) {
243
+ renderer.setRenderTarget(state.target);
244
+ renderer.xr.enabled = state.xrEnabled;
245
+ renderer.setViewport(state.viewport);
246
+ renderer.setScissor(state.scissor);
247
+ renderer.setScissorTest(state.scissorTest);
248
+ renderer.setClearColor(state.clearColor, state.clearAlpha);
249
+ renderer.autoClear = state.autoClear;
250
+ }
251
+ function getCaptureRenderer(scene) {
252
+ const renderers = [];
253
+ scene.traverse((object) => {
254
+ if (renderers.length) return;
255
+ const render = object.userData[CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY];
256
+ if (typeof render === "function") renderers.push(render);
257
+ });
258
+ return renderers[0] ?? null;
259
+ }
260
+ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, options = {}) {
261
+ const { width, height } = getCaptureDimensions(renderer, options);
262
+ const camera = createCaptureCamera(options, fallbackCamera, width, height);
263
+ const target = new THREE.WebGLRenderTarget(width, height, {
264
+ format: THREE.RGBAFormat,
265
+ type: THREE.UnsignedByteType
266
+ });
267
+ const canvas = document.createElement("canvas");
268
+ canvas.width = width;
269
+ canvas.height = height;
270
+ const context = canvas.getContext("2d");
271
+ if (!context) {
272
+ target.dispose();
273
+ throw new Error("Unable to create a 2D canvas for camera frame capture.");
274
+ }
275
+ const drawContext = context;
276
+ const pixels = new Uint8Array(width * height * 4);
277
+ const imageData = drawContext.createImageData(width, height);
278
+ function resolveCaptureOptions(nextOptions = {}) {
279
+ const captureOptions = { ...options, ...nextOptions };
280
+ const nextDimensions = getCaptureDimensions(renderer, captureOptions);
281
+ if (nextDimensions.width !== width || nextDimensions.height !== height) {
282
+ throw new Error(
283
+ "Camera frame capture sessions require stable width and height."
284
+ );
285
+ }
286
+ prepareCaptureCamera(
287
+ camera,
288
+ captureOptions,
289
+ fallbackCamera,
290
+ width,
291
+ height
292
+ );
293
+ return captureOptions;
294
+ }
295
+ function renderPreparedCapture(captureOptions) {
296
+ const previousState = saveRendererState(renderer);
297
+ const hidden = hideExcludedCaptureObjects(scene);
298
+ scene.updateMatrixWorld(true);
299
+ try {
300
+ renderer.xr.enabled = false;
301
+ renderer.setRenderTarget(target);
302
+ renderer.setViewport(0, 0, width, height);
303
+ renderer.setScissor(0, 0, width, height);
304
+ renderer.setScissorTest(false);
305
+ renderer.clear();
306
+ renderer.render(scene, camera);
307
+ readRenderTargetToCanvas(
308
+ renderer,
309
+ target,
310
+ canvas,
311
+ drawContext,
312
+ pixels,
313
+ imageData,
314
+ width,
315
+ height,
316
+ renderer.outputColorSpace
317
+ );
318
+ return {
319
+ canvas,
320
+ camera,
321
+ width,
322
+ height,
323
+ source: getCameraFrameCaptureSource(captureOptions)
324
+ };
325
+ } finally {
326
+ restoreObjectVisibility(hidden);
327
+ restoreRendererState(renderer, previousState);
328
+ }
329
+ }
330
+ function capture(nextOptions = {}) {
331
+ return renderPreparedCapture(resolveCaptureOptions(nextOptions));
332
+ }
333
+ async function captureAsync(nextOptions = {}) {
334
+ const captureOptions = resolveCaptureOptions(nextOptions);
335
+ scene.updateMatrixWorld(true);
336
+ const captureRenderer = getCaptureRenderer(scene);
337
+ if (captureRenderer) {
338
+ const previousState = saveRendererState(renderer);
339
+ const hidden = hideExcludedCaptureObjects(scene);
340
+ try {
341
+ renderer.xr.enabled = false;
342
+ const captureResult = await captureRenderer({
343
+ renderer,
344
+ scene,
345
+ camera,
346
+ target,
347
+ width,
348
+ height
349
+ });
350
+ if (captureResult) {
351
+ const captureWidth = captureResult.width ?? width;
352
+ const captureHeight = captureResult.height ?? height;
353
+ if (captureWidth !== width || captureHeight !== height) {
354
+ throw new Error(
355
+ "Camera frame capture renderer returned unexpected dimensions."
356
+ );
357
+ }
358
+ readPixelsToCanvas(
359
+ captureResult.pixels,
360
+ drawContext,
361
+ imageData,
362
+ width,
363
+ height,
364
+ captureResult.flipY ?? true
365
+ );
366
+ return {
367
+ canvas,
368
+ camera,
369
+ width,
370
+ height,
371
+ source: getCameraFrameCaptureSource(captureOptions)
372
+ };
373
+ }
374
+ } finally {
375
+ restoreObjectVisibility(hidden);
376
+ restoreRendererState(renderer, previousState);
377
+ }
378
+ }
379
+ return renderPreparedCapture(captureOptions);
380
+ }
381
+ return {
382
+ width,
383
+ height,
384
+ capture,
385
+ captureAsync,
386
+ captureDataUrl(nextOptions = {}) {
387
+ const type = nextOptions.type ?? options.type ?? "image/png";
388
+ const result = capture(nextOptions);
389
+ return {
390
+ ...result,
391
+ dataUrl: result.canvas.toDataURL(
392
+ type,
393
+ nextOptions.quality ?? options.quality
394
+ ),
395
+ type
396
+ };
397
+ },
398
+ async captureDataUrlAsync(nextOptions = {}) {
399
+ const type = nextOptions.type ?? options.type ?? "image/png";
400
+ const result = await captureAsync(nextOptions);
401
+ return {
402
+ ...result,
403
+ dataUrl: result.canvas.toDataURL(
404
+ type,
405
+ nextOptions.quality ?? options.quality
406
+ ),
407
+ type
408
+ };
409
+ },
410
+ async captureBlob(nextOptions = {}) {
411
+ const type = nextOptions.type ?? options.type ?? "image/png";
412
+ const result = await captureAsync(nextOptions);
413
+ const blob = await new Promise((resolve, reject) => {
414
+ result.canvas.toBlob(
415
+ (nextBlob) => {
416
+ if (nextBlob) resolve(nextBlob);
417
+ else reject(new Error("Camera frame capture did not produce a Blob."));
418
+ },
419
+ type,
420
+ nextOptions.quality ?? options.quality
421
+ );
422
+ });
423
+ return { ...result, blob, type };
424
+ },
425
+ dispose() {
426
+ target.dispose();
427
+ }
428
+ };
429
+ }
430
+ function renderCameraFrameToCanvas(renderer, scene, fallbackCamera, options = {}) {
431
+ const session = createCameraFrameCaptureSession(
432
+ renderer,
433
+ scene,
434
+ fallbackCamera,
435
+ options
436
+ );
437
+ try {
438
+ return session.capture();
439
+ } finally {
440
+ session.dispose();
441
+ }
442
+ }
443
+ async function captureCameraFrame(renderer, scene, fallbackCamera, options = {}) {
444
+ const type = options.type ?? "image/png";
445
+ const session = createCameraFrameCaptureSession(
446
+ renderer,
447
+ scene,
448
+ fallbackCamera,
449
+ options
450
+ );
451
+ try {
452
+ const result = await session.captureAsync();
453
+ return {
454
+ ...result,
455
+ dataUrl: result.canvas.toDataURL(type, options.quality),
456
+ type
457
+ };
458
+ } finally {
459
+ session.dispose();
460
+ }
461
+ }
462
+ async function captureCameraFrameBlob(renderer, scene, fallbackCamera, options = {}) {
463
+ const session = createCameraFrameCaptureSession(
464
+ renderer,
465
+ scene,
466
+ fallbackCamera,
467
+ options
468
+ );
469
+ try {
470
+ return await session.captureBlob();
471
+ } finally {
472
+ session.dispose();
473
+ }
474
+ }
88
475
  var DEFAULT_BACKGROUND = "#181a1f";
89
476
  function ScenarioLighting({
90
477
  preset = "studio",
@@ -683,7 +1070,13 @@ function clamp01(value) {
683
1070
  * @license
684
1071
  * SPDX-License-Identifier: Apache-2.0
685
1072
  */
1073
+ /**
1074
+ * @license
1075
+ * SPDX-License-Identifier: Apache-2.0
1076
+ *
1077
+ * Offscreen camera-frame capture for R3F/MuJoCo scenes.
1078
+ */
686
1079
 
687
- export { RobotActuators, RobotBodies, RobotCameras, RobotGeoms, RobotJoints, RobotKeyframes, RobotResources, RobotSensors, RobotSites, ScenarioLighting, SplatEnvironment, SplatEnvironmentReadinessStatus, VisualScenarioEffects, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, createSplatSceneConfig, createVisualScenarioExecutionContext, getContact, getScenarioBackground, getScenarioCameraPosition, getSplatEnvironmentReadiness, registerRobotResources, useSplatEnvironment, useSplatSceneConfig, useVisualScenarioEffects, useVisualScenarioExecutionContext, withContacts, withSplatEnvironment };
688
- //# sourceMappingURL=chunk-VDSEPZYQ.js.map
689
- //# sourceMappingURL=chunk-VDSEPZYQ.js.map
1080
+ export { CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY, CAPTURE_EXCLUDE_KEY, RobotActuators, RobotBodies, RobotCameras, RobotGeoms, RobotJoints, RobotKeyframes, RobotResources, RobotSensors, RobotSites, ScenarioLighting, SplatEnvironment, SplatEnvironmentReadinessStatus, VisualScenarioEffects, captureCameraFrame, captureCameraFrameBlob, createCameraFrameCaptureSession, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, createSplatSceneConfig, createVisualScenarioExecutionContext, getContact, getScenarioBackground, getScenarioCameraPosition, getSplatEnvironmentReadiness, registerRobotResources, renderCameraFrameToCanvas, useSplatEnvironment, useSplatSceneConfig, useVisualScenarioEffects, useVisualScenarioExecutionContext, withContacts, withSplatEnvironment };
1081
+ //# sourceMappingURL=chunk-6MOK6ZWB.js.map
1082
+ //# sourceMappingURL=chunk-6MOK6ZWB.js.map