mujoco-react 10.2.0 → 10.3.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
@@ -748,6 +748,7 @@ Thin wrapper around R3F `<Canvas>`. Accepts all R3F Canvas props plus:
748
748
  | `substeps` | `number` | mj_step calls per frame |
749
749
  | `paused` | `boolean` | Declarative pause |
750
750
  | `speed` | `number` | Simulation speed multiplier |
751
+ | `renderOptions` | `MujocoRenderOptions` | Optional render-time geometry settings such as `meshNormalSmoothing` |
751
752
 
752
753
  ### `<MujocoPhysics>`
753
754
 
@@ -1112,6 +1113,26 @@ needs an offscreen camera render at a stable resolution without moving the
1112
1113
  user's interactive viewport. Pass `cameraName`, `siteName`, or `bodyName` to
1113
1114
  record true MuJoCo-mounted camera frames; the returned image includes
1114
1115
  `source.kind` so dataset pipelines can reject fallback or synthetic fixed poses.
1116
+ For named MuJoCo cameras, set `mujocoCameraCompatibility` when you want the
1117
+ Three.js offscreen camera to inherit the MJCF camera's `resolution`, `fovy`,
1118
+ near/far clipping, and calibrated intrinsics when the WASM model exposes
1119
+ `cam_intrinsic` plus `cam_sensorsize`:
1120
+
1121
+ ```tsx
1122
+ const frame = await apiRef.current?.captureCameraFrame({
1123
+ cameraName: "front_camera",
1124
+ width: 640,
1125
+ type: "image/jpeg",
1126
+ mujocoCameraCompatibility: true,
1127
+ });
1128
+ ```
1129
+
1130
+ This is still rendered by Three.js. It is useful for browser policy payloads and
1131
+ dataset debugging until you have native MuJoCo framebuffer bindings available.
1132
+ For canonical policy/training captures, use `visualOverrides` to temporarily
1133
+ override scene background, environment, fog, shadow maps, tone mapping, or color
1134
+ space, and `renderIsolation` to render with an independent offscreen
1135
+ `WebGLRenderer` instead of inheriting viewer renderer state.
1115
1136
 
1116
1137
  Use `recordCameraSequence()` / `useCameraSequenceRecorder()` to step policy
1117
1138
  rollouts and capture synchronized per-camera frames from one or more MuJoCo
@@ -88,6 +88,72 @@ var SplatEnvironmentReadinessStatus = {
88
88
  var CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY = "mujocoReactCameraFrameCaptureRender";
89
89
  var CAMERA_FRAME_CAPTURE_PRE_RENDER_USER_DATA_KEY = "mujocoReactCameraFrameCapturePreRender";
90
90
  var CAPTURE_EXCLUDE_KEY = "mujoco.capture.exclude";
91
+ var isolatedRendererCache = /* @__PURE__ */ new WeakMap();
92
+ function shouldUseRenderIsolation(options) {
93
+ return options.renderIsolation === true || typeof options.renderIsolation === "object" && options.renderIsolation.enabled !== false;
94
+ }
95
+ function getRenderIsolationOptions(options) {
96
+ return typeof options.renderIsolation === "object" ? options.renderIsolation : {};
97
+ }
98
+ function getRenderIsolationCacheKey(width, height, options) {
99
+ const isolation = getRenderIsolationOptions(options);
100
+ return JSON.stringify({
101
+ width,
102
+ height,
103
+ antialias: isolation.antialias ?? false,
104
+ alpha: isolation.alpha ?? false,
105
+ preserveDrawingBuffer: isolation.preserveDrawingBuffer ?? false,
106
+ powerPreference: isolation.powerPreference ?? null
107
+ });
108
+ }
109
+ function createIsolatedRenderer(sourceRenderer, width, height, options) {
110
+ if (!shouldUseRenderIsolation(options)) return null;
111
+ const isolation = getRenderIsolationOptions(options);
112
+ if (isolation.cache !== false) {
113
+ const cacheKey = getRenderIsolationCacheKey(width, height, options);
114
+ let rendererCache = isolatedRendererCache.get(sourceRenderer);
115
+ if (!rendererCache) {
116
+ rendererCache = /* @__PURE__ */ new Map();
117
+ isolatedRendererCache.set(sourceRenderer, rendererCache);
118
+ }
119
+ const cachedRenderer = rendererCache.get(cacheKey);
120
+ if (cachedRenderer) {
121
+ cachedRenderer.outputColorSpace = sourceRenderer.outputColorSpace;
122
+ cachedRenderer.toneMapping = sourceRenderer.toneMapping;
123
+ cachedRenderer.shadowMap.enabled = false;
124
+ return { renderer: cachedRenderer, cached: true };
125
+ }
126
+ const createdRenderer = createUncachedIsolatedRenderer(
127
+ sourceRenderer,
128
+ width,
129
+ height,
130
+ options
131
+ );
132
+ rendererCache.set(cacheKey, createdRenderer);
133
+ return { renderer: createdRenderer, cached: true };
134
+ }
135
+ return {
136
+ renderer: createUncachedIsolatedRenderer(sourceRenderer, width, height, options),
137
+ cached: false
138
+ };
139
+ }
140
+ function createUncachedIsolatedRenderer(sourceRenderer, width, height, options) {
141
+ const isolation = getRenderIsolationOptions(options);
142
+ const canvas = document.createElement("canvas");
143
+ const renderer = new THREE.WebGLRenderer({
144
+ canvas,
145
+ antialias: isolation.antialias ?? false,
146
+ alpha: isolation.alpha ?? false,
147
+ preserveDrawingBuffer: isolation.preserveDrawingBuffer ?? false,
148
+ powerPreference: isolation.powerPreference
149
+ });
150
+ renderer.setPixelRatio(1);
151
+ renderer.setSize(width, height, false);
152
+ renderer.outputColorSpace = sourceRenderer.outputColorSpace;
153
+ renderer.toneMapping = sourceRenderer.toneMapping;
154
+ renderer.shadowMap.enabled = false;
155
+ return renderer;
156
+ }
91
157
  function toVector3(value, fallback) {
92
158
  if (!value) return fallback.clone();
93
159
  return value instanceof THREE.Vector3 ? value.clone() : new THREE.Vector3(value[0], value[1], value[2]);
@@ -113,6 +179,15 @@ function applyCameraPose(camera, options, fallbackCamera) {
113
179
  }
114
180
  camera.updateMatrixWorld();
115
181
  }
182
+ function applyProjectionMatrix(camera, projectionMatrix) {
183
+ if (!projectionMatrix) return;
184
+ if (projectionMatrix instanceof THREE.Matrix4) {
185
+ camera.projectionMatrix.copy(projectionMatrix);
186
+ } else {
187
+ camera.projectionMatrix.fromArray(projectionMatrix);
188
+ }
189
+ camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
190
+ }
116
191
  function createCaptureCamera(options, fallbackCamera, width, height) {
117
192
  const camera = options.camera ? options.camera.clone() : fallbackCamera instanceof THREE.PerspectiveCamera ? fallbackCamera.clone() : new THREE.PerspectiveCamera(45, width / height, 0.01, 100);
118
193
  if (camera instanceof THREE.PerspectiveCamera) {
@@ -122,6 +197,7 @@ function createCaptureCamera(options, fallbackCamera, width, height) {
122
197
  camera.far = options.far ?? camera.far;
123
198
  camera.updateProjectionMatrix();
124
199
  }
200
+ applyProjectionMatrix(camera, options.projectionMatrix);
125
201
  applyCameraPose(camera, options, fallbackCamera);
126
202
  return camera;
127
203
  }
@@ -147,9 +223,10 @@ function prepareCaptureCamera(camera, options, fallbackCamera, width, height) {
147
223
  camera.far = options.far ?? camera.far;
148
224
  camera.updateProjectionMatrix();
149
225
  }
226
+ applyProjectionMatrix(camera, options.projectionMatrix);
150
227
  applyCameraPose(camera, options, fallbackCamera);
151
228
  }
152
- function readRenderTargetToCanvas(renderer, target, canvas, context, pixels, imageData, width, height, outputColorSpace) {
229
+ function readRenderTargetToCanvas(renderer, target, canvas, context, pixels, imageData, width, height, outputColorSpace, flipX = false) {
153
230
  renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
154
231
  const rowBytes = width * 4;
155
232
  const encodeSrgb = outputColorSpace === THREE.SRGBColorSpace;
@@ -157,16 +234,18 @@ function readRenderTargetToCanvas(renderer, target, canvas, context, pixels, ima
157
234
  const sourceStart = (height - y - 1) * rowBytes;
158
235
  const targetStart = y * rowBytes;
159
236
  const row = pixels.subarray(sourceStart, sourceStart + rowBytes);
160
- if (!encodeSrgb) {
237
+ if (!encodeSrgb && !flipX) {
161
238
  imageData.data.set(row, targetStart);
162
239
  continue;
163
240
  }
164
- for (let x = 0; x < rowBytes; x += 4) {
165
- const pixelOffset = targetStart + x;
166
- imageData.data[pixelOffset] = linearByteToSrgbByte(row[x]);
167
- imageData.data[pixelOffset + 1] = linearByteToSrgbByte(row[x + 1]);
168
- imageData.data[pixelOffset + 2] = linearByteToSrgbByte(row[x + 2]);
169
- imageData.data[pixelOffset + 3] = row[x + 3];
241
+ for (let x = 0; x < width; x += 1) {
242
+ const sourceX = flipX ? width - x - 1 : x;
243
+ const sourceOffset = sourceX * 4;
244
+ const targetOffset = targetStart + x * 4;
245
+ imageData.data[targetOffset] = encodeSrgb ? linearByteToSrgbByte(row[sourceOffset]) : row[sourceOffset];
246
+ imageData.data[targetOffset + 1] = encodeSrgb ? linearByteToSrgbByte(row[sourceOffset + 1]) : row[sourceOffset + 1];
247
+ imageData.data[targetOffset + 2] = encodeSrgb ? linearByteToSrgbByte(row[sourceOffset + 2]) : row[sourceOffset + 2];
248
+ imageData.data[targetOffset + 3] = row[sourceOffset + 3];
170
249
  }
171
250
  }
172
251
  context.putImageData(imageData, 0, 0);
@@ -177,16 +256,28 @@ function linearByteToSrgbByte(value) {
177
256
  const encoded = normalized <= 31308e-7 ? normalized * 12.92 : 1.055 * Math.pow(normalized, 1 / 2.4) - 0.055;
178
257
  return Math.min(255, Math.max(0, Math.round(encoded * 255)));
179
258
  }
180
- function readPixelsToCanvas(pixels, context, imageData, width, height, flipY = true) {
259
+ function readPixelsToCanvas(pixels, context, imageData, width, height, flipY = true, flipX = false) {
181
260
  const rowBytes = width * 4;
182
261
  for (let y = 0; y < height; y += 1) {
183
262
  const sourceY = flipY ? height - y - 1 : y;
184
263
  const sourceStart = sourceY * rowBytes;
185
264
  const targetStart = y * rowBytes;
186
- imageData.data.set(
187
- pixels.subarray(sourceStart, sourceStart + rowBytes),
188
- targetStart
189
- );
265
+ if (!flipX) {
266
+ imageData.data.set(
267
+ pixels.subarray(sourceStart, sourceStart + rowBytes),
268
+ targetStart
269
+ );
270
+ continue;
271
+ }
272
+ for (let x = 0; x < width; x += 1) {
273
+ const sourceX = width - x - 1;
274
+ const sourceOffset = sourceStart + sourceX * 4;
275
+ const targetOffset = targetStart + x * 4;
276
+ imageData.data[targetOffset] = pixels[sourceOffset];
277
+ imageData.data[targetOffset + 1] = pixels[sourceOffset + 1];
278
+ imageData.data[targetOffset + 2] = pixels[sourceOffset + 2];
279
+ imageData.data[targetOffset + 3] = pixels[sourceOffset + 3];
280
+ }
190
281
  }
191
282
  context.putImageData(imageData, 0, 0);
192
283
  }
@@ -255,7 +346,10 @@ function saveRendererState(renderer) {
255
346
  scissorTest: renderer.getScissorTest(),
256
347
  clearColor,
257
348
  clearAlpha: renderer.getClearAlpha(),
258
- autoClear: renderer.autoClear
349
+ autoClear: renderer.autoClear,
350
+ shadowMapEnabled: renderer.shadowMap.enabled,
351
+ toneMapping: renderer.toneMapping,
352
+ outputColorSpace: renderer.outputColorSpace
259
353
  };
260
354
  }
261
355
  function restoreRendererState(renderer, state) {
@@ -266,6 +360,51 @@ function restoreRendererState(renderer, state) {
266
360
  renderer.setScissorTest(state.scissorTest);
267
361
  renderer.setClearColor(state.clearColor, state.clearAlpha);
268
362
  renderer.autoClear = state.autoClear;
363
+ renderer.shadowMap.enabled = state.shadowMapEnabled;
364
+ renderer.toneMapping = state.toneMapping;
365
+ renderer.outputColorSpace = state.outputColorSpace;
366
+ }
367
+ function saveSceneVisualState(scene) {
368
+ return {
369
+ background: scene.background,
370
+ environment: scene.environment,
371
+ fog: scene.fog
372
+ };
373
+ }
374
+ function restoreSceneVisualState(scene, state) {
375
+ scene.background = state.background;
376
+ scene.environment = state.environment;
377
+ scene.fog = state.fog;
378
+ }
379
+ function hasOwn(object, key) {
380
+ return Object.prototype.hasOwnProperty.call(object, key);
381
+ }
382
+ function applyCaptureVisualOverrides(renderer, scene, options) {
383
+ const overrides = options.visualOverrides;
384
+ if (!overrides) return null;
385
+ const previousSceneState = saveSceneVisualState(scene);
386
+ if (hasOwn(overrides, "sceneBackground")) {
387
+ const background = overrides.sceneBackground;
388
+ scene.background = background === false ? null : typeof background === "string" || typeof background === "number" ? new THREE.Color(background) : background ?? null;
389
+ }
390
+ if (hasOwn(overrides, "sceneEnvironment")) {
391
+ const environment = overrides.sceneEnvironment;
392
+ scene.environment = environment === false ? null : environment ?? null;
393
+ }
394
+ if (hasOwn(overrides, "sceneFog")) {
395
+ const fog = overrides.sceneFog;
396
+ scene.fog = fog === false ? null : fog ?? null;
397
+ }
398
+ if (overrides.shadows !== void 0) {
399
+ renderer.shadowMap.enabled = overrides.shadows;
400
+ }
401
+ if (overrides.toneMapping !== void 0) {
402
+ renderer.toneMapping = overrides.toneMapping;
403
+ }
404
+ if (overrides.outputColorSpace !== void 0) {
405
+ renderer.outputColorSpace = overrides.outputColorSpace;
406
+ }
407
+ return previousSceneState;
269
408
  }
270
409
  function getCaptureRenderer(scene) {
271
410
  const renderers = [];
@@ -286,6 +425,8 @@ function runCapturePreRenderHooks(scene) {
286
425
  }
287
426
  function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, options = {}) {
288
427
  const { width, height } = getCaptureDimensions(renderer, options);
428
+ const isolatedRenderer = createIsolatedRenderer(renderer, width, height, options);
429
+ const sessionRenderer = isolatedRenderer?.renderer ?? renderer;
289
430
  const camera = createCaptureCamera(options, fallbackCamera, width, height);
290
431
  const target = new THREE.WebGLRenderTarget(width, height, {
291
432
  format: THREE.RGBAFormat,
@@ -304,6 +445,11 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
304
445
  const imageData = drawContext.createImageData(width, height);
305
446
  function resolveCaptureOptions(nextOptions = {}) {
306
447
  const captureOptions = { ...options, ...nextOptions };
448
+ if (shouldUseRenderIsolation(captureOptions) !== shouldUseRenderIsolation(options)) {
449
+ throw new Error(
450
+ "Camera frame capture sessions require stable renderIsolation settings."
451
+ );
452
+ }
307
453
  const nextDimensions = getCaptureDimensions(renderer, captureOptions);
308
454
  if (nextDimensions.width !== width || nextDimensions.height !== height) {
309
455
  throw new Error(
@@ -320,7 +466,12 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
320
466
  return captureOptions;
321
467
  }
322
468
  function renderPreparedCapture(captureOptions) {
323
- const previousState = saveRendererState(renderer);
469
+ const previousState = saveRendererState(sessionRenderer);
470
+ const previousSceneState = applyCaptureVisualOverrides(
471
+ sessionRenderer,
472
+ scene,
473
+ captureOptions
474
+ );
324
475
  const hidden = [
325
476
  ...hideExcludedCaptureObjects(scene),
326
477
  ...hideCaptureGeomGroups(scene, captureOptions)
@@ -328,23 +479,23 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
328
479
  runCapturePreRenderHooks(scene);
329
480
  scene.updateMatrixWorld(true);
330
481
  try {
331
- renderer.xr.enabled = false;
332
- renderer.setRenderTarget(target);
333
- renderer.setViewport(0, 0, width, height);
334
- renderer.setScissor(0, 0, width, height);
335
- renderer.setScissorTest(false);
482
+ sessionRenderer.xr.enabled = false;
483
+ sessionRenderer.setRenderTarget(target);
484
+ sessionRenderer.setViewport(0, 0, width, height);
485
+ sessionRenderer.setScissor(0, 0, width, height);
486
+ sessionRenderer.setScissorTest(false);
336
487
  if (captureOptions.background !== void 0) {
337
- renderer.setClearColor(
488
+ sessionRenderer.setClearColor(
338
489
  new THREE.Color(captureOptions.background),
339
490
  captureOptions.backgroundAlpha ?? previousState.clearAlpha
340
491
  );
341
492
  } else if (captureOptions.backgroundAlpha !== void 0) {
342
- renderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
493
+ sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
343
494
  }
344
- renderer.clear();
345
- renderer.render(scene, camera);
495
+ sessionRenderer.clear();
496
+ sessionRenderer.render(scene, camera);
346
497
  readRenderTargetToCanvas(
347
- renderer,
498
+ sessionRenderer,
348
499
  target,
349
500
  canvas,
350
501
  drawContext,
@@ -352,7 +503,8 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
352
503
  imageData,
353
504
  width,
354
505
  height,
355
- renderer.outputColorSpace
506
+ sessionRenderer.outputColorSpace,
507
+ captureOptions.flipX ?? false
356
508
  );
357
509
  return {
358
510
  canvas,
@@ -363,7 +515,8 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
363
515
  };
364
516
  } finally {
365
517
  restoreObjectVisibility(hidden);
366
- restoreRendererState(renderer, previousState);
518
+ if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
519
+ restoreRendererState(sessionRenderer, previousState);
367
520
  }
368
521
  }
369
522
  function capture(nextOptions = {}) {
@@ -375,23 +528,28 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
375
528
  scene.updateMatrixWorld(true);
376
529
  const captureRenderer = getCaptureRenderer(scene);
377
530
  if (captureRenderer) {
378
- const previousState = saveRendererState(renderer);
531
+ const previousState = saveRendererState(sessionRenderer);
532
+ const previousSceneState = applyCaptureVisualOverrides(
533
+ sessionRenderer,
534
+ scene,
535
+ captureOptions
536
+ );
379
537
  const hidden = [
380
538
  ...hideExcludedCaptureObjects(scene),
381
539
  ...hideCaptureGeomGroups(scene, captureOptions)
382
540
  ];
383
541
  try {
384
- renderer.xr.enabled = false;
542
+ sessionRenderer.xr.enabled = false;
385
543
  if (captureOptions.background !== void 0) {
386
- renderer.setClearColor(
544
+ sessionRenderer.setClearColor(
387
545
  new THREE.Color(captureOptions.background),
388
546
  captureOptions.backgroundAlpha ?? previousState.clearAlpha
389
547
  );
390
548
  } else if (captureOptions.backgroundAlpha !== void 0) {
391
- renderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
549
+ sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
392
550
  }
393
551
  const captureResult = await captureRenderer({
394
- renderer,
552
+ renderer: sessionRenderer,
395
553
  scene,
396
554
  camera,
397
555
  target,
@@ -412,7 +570,8 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
412
570
  imageData,
413
571
  width,
414
572
  height,
415
- captureResult.flipY ?? true
573
+ captureResult.flipY ?? true,
574
+ captureResult.flipX ?? captureOptions.flipX ?? false
416
575
  );
417
576
  return {
418
577
  canvas,
@@ -424,7 +583,8 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
424
583
  }
425
584
  } finally {
426
585
  restoreObjectVisibility(hidden);
427
- restoreRendererState(renderer, previousState);
586
+ if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
587
+ restoreRendererState(sessionRenderer, previousState);
428
588
  }
429
589
  }
430
590
  return renderPreparedCapture(captureOptions);
@@ -475,6 +635,9 @@ function createCameraFrameCaptureSession(renderer, scene, fallbackCamera, option
475
635
  },
476
636
  dispose() {
477
637
  target.dispose();
638
+ if (isolatedRenderer && !isolatedRenderer.cached) {
639
+ isolatedRenderer.renderer.dispose();
640
+ }
478
641
  }
479
642
  };
480
643
  }
@@ -1129,5 +1292,5 @@ function clamp01(value) {
1129
1292
  */
1130
1293
 
1131
1294
  export { CAMERA_FRAME_CAPTURE_PRE_RENDER_USER_DATA_KEY, CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY, CAPTURE_EXCLUDE_KEY, ModelActuators, ModelBodies, ModelCameras, ModelGeoms, ModelJoints, ModelKeyframes, ModelResources, ModelSensors, ModelSites, ScenarioLighting, SplatEnvironment, SplatEnvironmentReadinessStatus, VisualScenarioEffects, captureCameraFrame, captureCameraFrameBlob, createCameraFrameCaptureSession, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, createSplatSceneConfig, createVisualScenarioExecutionContext, getContact, getScenarioBackground, getScenarioCameraPosition, getSplatEnvironmentReadiness, registerModelResources, renderCameraFrameToCanvas, useSplatEnvironment, useSplatSceneConfig, useVisualScenarioEffects, useVisualScenarioExecutionContext, withContacts, withSplatEnvironment };
1132
- //# sourceMappingURL=chunk-3BMNRSS2.js.map
1133
- //# sourceMappingURL=chunk-3BMNRSS2.js.map
1295
+ //# sourceMappingURL=chunk-6AZEFI6A.js.map
1296
+ //# sourceMappingURL=chunk-6AZEFI6A.js.map