mujoco-react 10.2.1 → 10.4.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.
@@ -80,6 +80,15 @@ type RendererState = {
80
80
  clearColor: THREE.Color;
81
81
  clearAlpha: number;
82
82
  autoClear: boolean;
83
+ shadowMapEnabled: boolean;
84
+ toneMapping: THREE.WebGLRenderer['toneMapping'];
85
+ outputColorSpace: THREE.WebGLRenderer['outputColorSpace'];
86
+ };
87
+
88
+ type SceneVisualState = {
89
+ background: THREE.Scene['background'];
90
+ environment: THREE.Scene['environment'];
91
+ fog: THREE.Scene['fog'];
83
92
  };
84
93
 
85
94
  type VisibilityState = {
@@ -89,6 +98,107 @@ type VisibilityState = {
89
98
 
90
99
  type CameraFrameCapturePreRender = () => void;
91
100
 
101
+ const isolatedRendererCache = new WeakMap<
102
+ THREE.WebGLRenderer,
103
+ Map<string, THREE.WebGLRenderer>
104
+ >();
105
+
106
+ function shouldUseRenderIsolation(
107
+ options: CameraFrameCaptureOptions
108
+ ): boolean {
109
+ return options.renderIsolation === true || (
110
+ typeof options.renderIsolation === 'object' &&
111
+ options.renderIsolation.enabled !== false
112
+ );
113
+ }
114
+
115
+ function getRenderIsolationOptions(
116
+ options: CameraFrameCaptureOptions
117
+ ) {
118
+ return typeof options.renderIsolation === 'object'
119
+ ? options.renderIsolation
120
+ : {};
121
+ }
122
+
123
+ function getRenderIsolationCacheKey(
124
+ width: number,
125
+ height: number,
126
+ options: CameraFrameCaptureOptions
127
+ ) {
128
+ const isolation = getRenderIsolationOptions(options);
129
+ return JSON.stringify({
130
+ width,
131
+ height,
132
+ antialias: isolation.antialias ?? false,
133
+ alpha: isolation.alpha ?? false,
134
+ preserveDrawingBuffer: isolation.preserveDrawingBuffer ?? false,
135
+ powerPreference: isolation.powerPreference ?? null,
136
+ });
137
+ }
138
+
139
+ function createIsolatedRenderer(
140
+ sourceRenderer: THREE.WebGLRenderer,
141
+ width: number,
142
+ height: number,
143
+ options: CameraFrameCaptureOptions
144
+ ): { renderer: THREE.WebGLRenderer; cached: boolean } | null {
145
+ if (!shouldUseRenderIsolation(options)) return null;
146
+
147
+ const isolation = getRenderIsolationOptions(options);
148
+ if (isolation.cache !== false) {
149
+ const cacheKey = getRenderIsolationCacheKey(width, height, options);
150
+ let rendererCache = isolatedRendererCache.get(sourceRenderer);
151
+ if (!rendererCache) {
152
+ rendererCache = new Map();
153
+ isolatedRendererCache.set(sourceRenderer, rendererCache);
154
+ }
155
+ const cachedRenderer = rendererCache.get(cacheKey);
156
+ if (cachedRenderer) {
157
+ cachedRenderer.outputColorSpace = sourceRenderer.outputColorSpace;
158
+ cachedRenderer.toneMapping = sourceRenderer.toneMapping;
159
+ cachedRenderer.shadowMap.enabled = false;
160
+ return { renderer: cachedRenderer, cached: true };
161
+ }
162
+ const createdRenderer = createUncachedIsolatedRenderer(
163
+ sourceRenderer,
164
+ width,
165
+ height,
166
+ options
167
+ );
168
+ rendererCache.set(cacheKey, createdRenderer);
169
+ return { renderer: createdRenderer, cached: true };
170
+ }
171
+
172
+ return {
173
+ renderer: createUncachedIsolatedRenderer(sourceRenderer, width, height, options),
174
+ cached: false,
175
+ };
176
+ }
177
+
178
+ function createUncachedIsolatedRenderer(
179
+ sourceRenderer: THREE.WebGLRenderer,
180
+ width: number,
181
+ height: number,
182
+ options: CameraFrameCaptureOptions
183
+ ) {
184
+ const isolation = getRenderIsolationOptions(options);
185
+ const canvas = document.createElement('canvas');
186
+ const renderer = new THREE.WebGLRenderer({
187
+ canvas,
188
+ antialias: isolation.antialias ?? false,
189
+ alpha: isolation.alpha ?? false,
190
+ preserveDrawingBuffer: isolation.preserveDrawingBuffer ?? false,
191
+ powerPreference: isolation.powerPreference,
192
+ });
193
+
194
+ renderer.setPixelRatio(1);
195
+ renderer.setSize(width, height, false);
196
+ renderer.outputColorSpace = sourceRenderer.outputColorSpace;
197
+ renderer.toneMapping = sourceRenderer.toneMapping;
198
+ renderer.shadowMap.enabled = false;
199
+ return renderer;
200
+ }
201
+
92
202
  function toVector3(
93
203
  value: CameraFrameCaptureVector3 | undefined,
94
204
  fallback: THREE.Vector3
@@ -127,6 +237,19 @@ function applyCameraPose(
127
237
  camera.updateMatrixWorld();
128
238
  }
129
239
 
240
+ function applyProjectionMatrix(
241
+ camera: THREE.Camera,
242
+ projectionMatrix: CameraFrameCaptureOptions['projectionMatrix'] | undefined
243
+ ) {
244
+ if (!projectionMatrix) return;
245
+ if (projectionMatrix instanceof THREE.Matrix4) {
246
+ camera.projectionMatrix.copy(projectionMatrix);
247
+ } else {
248
+ camera.projectionMatrix.fromArray(projectionMatrix);
249
+ }
250
+ camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
251
+ }
252
+
130
253
  function createCaptureCamera(
131
254
  options: CameraFrameCaptureOptions,
132
255
  fallbackCamera: THREE.Camera,
@@ -146,6 +269,7 @@ function createCaptureCamera(
146
269
  camera.far = options.far ?? camera.far;
147
270
  camera.updateProjectionMatrix();
148
271
  }
272
+ applyProjectionMatrix(camera, options.projectionMatrix);
149
273
 
150
274
  applyCameraPose(camera, options, fallbackCamera);
151
275
  return camera;
@@ -184,6 +308,7 @@ function prepareCaptureCamera(
184
308
  camera.far = options.far ?? camera.far;
185
309
  camera.updateProjectionMatrix();
186
310
  }
311
+ applyProjectionMatrix(camera, options.projectionMatrix);
187
312
 
188
313
  applyCameraPose(camera, options, fallbackCamera);
189
314
  }
@@ -362,6 +487,9 @@ function saveRendererState(renderer: THREE.WebGLRenderer): RendererState {
362
487
  clearColor,
363
488
  clearAlpha: renderer.getClearAlpha(),
364
489
  autoClear: renderer.autoClear,
490
+ shadowMapEnabled: renderer.shadowMap.enabled,
491
+ toneMapping: renderer.toneMapping,
492
+ outputColorSpace: renderer.outputColorSpace,
365
493
  };
366
494
  }
367
495
 
@@ -376,6 +504,64 @@ function restoreRendererState(
376
504
  renderer.setScissorTest(state.scissorTest);
377
505
  renderer.setClearColor(state.clearColor, state.clearAlpha);
378
506
  renderer.autoClear = state.autoClear;
507
+ renderer.shadowMap.enabled = state.shadowMapEnabled;
508
+ renderer.toneMapping = state.toneMapping;
509
+ renderer.outputColorSpace = state.outputColorSpace;
510
+ }
511
+
512
+ function saveSceneVisualState(scene: THREE.Scene): SceneVisualState {
513
+ return {
514
+ background: scene.background,
515
+ environment: scene.environment,
516
+ fog: scene.fog,
517
+ };
518
+ }
519
+
520
+ function restoreSceneVisualState(scene: THREE.Scene, state: SceneVisualState) {
521
+ scene.background = state.background;
522
+ scene.environment = state.environment;
523
+ scene.fog = state.fog;
524
+ }
525
+
526
+ function hasOwn<T extends object>(object: T, key: keyof T) {
527
+ return Object.prototype.hasOwnProperty.call(object, key);
528
+ }
529
+
530
+ function applyCaptureVisualOverrides(
531
+ renderer: THREE.WebGLRenderer,
532
+ scene: THREE.Scene,
533
+ options: CameraFrameCaptureOptions
534
+ ): SceneVisualState | null {
535
+ const overrides = options.visualOverrides;
536
+ if (!overrides) return null;
537
+
538
+ const previousSceneState = saveSceneVisualState(scene);
539
+ if (hasOwn(overrides, 'sceneBackground')) {
540
+ const background = overrides.sceneBackground;
541
+ scene.background = background === false ? null : (
542
+ typeof background === 'string' || typeof background === 'number'
543
+ ? new THREE.Color(background)
544
+ : background ?? null
545
+ );
546
+ }
547
+ if (hasOwn(overrides, 'sceneEnvironment')) {
548
+ const environment = overrides.sceneEnvironment;
549
+ scene.environment = environment === false ? null : environment ?? null;
550
+ }
551
+ if (hasOwn(overrides, 'sceneFog')) {
552
+ const fog = overrides.sceneFog;
553
+ scene.fog = fog === false ? null : fog ?? null;
554
+ }
555
+ if (overrides.shadows !== undefined) {
556
+ renderer.shadowMap.enabled = overrides.shadows;
557
+ }
558
+ if (overrides.toneMapping !== undefined) {
559
+ renderer.toneMapping = overrides.toneMapping;
560
+ }
561
+ if (overrides.outputColorSpace !== undefined) {
562
+ renderer.outputColorSpace = overrides.outputColorSpace;
563
+ }
564
+ return previousSceneState;
379
565
  }
380
566
 
381
567
  function getCaptureRenderer(
@@ -410,6 +596,8 @@ export function createCameraFrameCaptureSession(
410
596
  options: CameraFrameCaptureOptions = {}
411
597
  ): CameraFrameCaptureSession {
412
598
  const { width, height } = getCaptureDimensions(renderer, options);
599
+ const isolatedRenderer = createIsolatedRenderer(renderer, width, height, options);
600
+ const sessionRenderer = isolatedRenderer?.renderer ?? renderer;
413
601
  const camera = createCaptureCamera(options, fallbackCamera, width, height);
414
602
  const target = new THREE.WebGLRenderTarget(width, height, {
415
603
  format: THREE.RGBAFormat,
@@ -430,6 +618,13 @@ export function createCameraFrameCaptureSession(
430
618
 
431
619
  function resolveCaptureOptions(nextOptions: CameraFrameCaptureOptions = {}) {
432
620
  const captureOptions = { ...options, ...nextOptions };
621
+ if (
622
+ shouldUseRenderIsolation(captureOptions) !== shouldUseRenderIsolation(options)
623
+ ) {
624
+ throw new Error(
625
+ 'Camera frame capture sessions require stable renderIsolation settings.'
626
+ );
627
+ }
433
628
  const nextDimensions = getCaptureDimensions(renderer, captureOptions);
434
629
  if (
435
630
  nextDimensions.width !== width ||
@@ -452,7 +647,12 @@ export function createCameraFrameCaptureSession(
452
647
  }
453
648
 
454
649
  function renderPreparedCapture(captureOptions: CameraFrameCaptureOptions) {
455
- const previousState = saveRendererState(renderer);
650
+ const previousState = saveRendererState(sessionRenderer);
651
+ const previousSceneState = applyCaptureVisualOverrides(
652
+ sessionRenderer,
653
+ scene,
654
+ captureOptions
655
+ );
456
656
  const hidden = [
457
657
  ...hideExcludedCaptureObjects(scene),
458
658
  ...hideCaptureGeomGroups(scene, captureOptions),
@@ -461,23 +661,23 @@ export function createCameraFrameCaptureSession(
461
661
  runCapturePreRenderHooks(scene);
462
662
  scene.updateMatrixWorld(true);
463
663
  try {
464
- renderer.xr.enabled = false;
465
- renderer.setRenderTarget(target);
466
- renderer.setViewport(0, 0, width, height);
467
- renderer.setScissor(0, 0, width, height);
468
- renderer.setScissorTest(false);
664
+ sessionRenderer.xr.enabled = false;
665
+ sessionRenderer.setRenderTarget(target);
666
+ sessionRenderer.setViewport(0, 0, width, height);
667
+ sessionRenderer.setScissor(0, 0, width, height);
668
+ sessionRenderer.setScissorTest(false);
469
669
  if (captureOptions.background !== undefined) {
470
- renderer.setClearColor(
670
+ sessionRenderer.setClearColor(
471
671
  new THREE.Color(captureOptions.background),
472
672
  captureOptions.backgroundAlpha ?? previousState.clearAlpha
473
673
  );
474
674
  } else if (captureOptions.backgroundAlpha !== undefined) {
475
- renderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
675
+ sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
476
676
  }
477
- renderer.clear();
478
- renderer.render(scene, camera);
677
+ sessionRenderer.clear();
678
+ sessionRenderer.render(scene, camera);
479
679
  readRenderTargetToCanvas(
480
- renderer,
680
+ sessionRenderer,
481
681
  target,
482
682
  canvas,
483
683
  drawContext,
@@ -485,7 +685,7 @@ export function createCameraFrameCaptureSession(
485
685
  imageData,
486
686
  width,
487
687
  height,
488
- renderer.outputColorSpace,
688
+ sessionRenderer.outputColorSpace,
489
689
  captureOptions.flipX ?? false
490
690
  );
491
691
  return {
@@ -497,7 +697,8 @@ export function createCameraFrameCaptureSession(
497
697
  };
498
698
  } finally {
499
699
  restoreObjectVisibility(hidden);
500
- restoreRendererState(renderer, previousState);
700
+ if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
701
+ restoreRendererState(sessionRenderer, previousState);
501
702
  }
502
703
  }
503
704
 
@@ -511,23 +712,28 @@ export function createCameraFrameCaptureSession(
511
712
  scene.updateMatrixWorld(true);
512
713
  const captureRenderer = getCaptureRenderer(scene);
513
714
  if (captureRenderer) {
514
- const previousState = saveRendererState(renderer);
715
+ const previousState = saveRendererState(sessionRenderer);
716
+ const previousSceneState = applyCaptureVisualOverrides(
717
+ sessionRenderer,
718
+ scene,
719
+ captureOptions
720
+ );
515
721
  const hidden = [
516
722
  ...hideExcludedCaptureObjects(scene),
517
723
  ...hideCaptureGeomGroups(scene, captureOptions),
518
724
  ];
519
725
  try {
520
- renderer.xr.enabled = false;
726
+ sessionRenderer.xr.enabled = false;
521
727
  if (captureOptions.background !== undefined) {
522
- renderer.setClearColor(
728
+ sessionRenderer.setClearColor(
523
729
  new THREE.Color(captureOptions.background),
524
730
  captureOptions.backgroundAlpha ?? previousState.clearAlpha
525
731
  );
526
732
  } else if (captureOptions.backgroundAlpha !== undefined) {
527
- renderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
733
+ sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
528
734
  }
529
735
  const captureResult = await captureRenderer({
530
- renderer,
736
+ renderer: sessionRenderer,
531
737
  scene,
532
738
  camera,
533
739
  target,
@@ -561,7 +767,8 @@ export function createCameraFrameCaptureSession(
561
767
  }
562
768
  } finally {
563
769
  restoreObjectVisibility(hidden);
564
- restoreRendererState(renderer, previousState);
770
+ if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
771
+ restoreRendererState(sessionRenderer, previousState);
565
772
  }
566
773
  }
567
774
  return renderPreparedCapture(captureOptions);
@@ -613,6 +820,9 @@ export function createCameraFrameCaptureSession(
613
820
  },
614
821
  dispose() {
615
822
  target.dispose();
823
+ if (isolatedRenderer && !isolatedRenderer.cached) {
824
+ isolatedRenderer.renderer.dispose();
825
+ }
616
826
  },
617
827
  };
618
828
  }
package/src/spark.tsx CHANGED
@@ -381,6 +381,10 @@ export function SparkSplatEnvironment({
381
381
  const onErrorRef = useRef(onError);
382
382
  const [status, setStatus] = useState<SparkSplatStatus>('idle');
383
383
  const { gl, invalidate } = useThree();
384
+ onStatusChangeRef.current = onStatusChange;
385
+ onLoadRef.current = onLoad;
386
+ onErrorRef.current = onError;
387
+
384
388
  const metadata = useSplatEnvironment({
385
389
  environment,
386
390
  scenario,
@@ -413,18 +417,6 @@ export function SparkSplatEnvironment({
413
417
  ]
414
418
  );
415
419
 
416
- useEffect(() => {
417
- onStatusChangeRef.current = onStatusChange;
418
- }, [onStatusChange]);
419
-
420
- useEffect(() => {
421
- onLoadRef.current = onLoad;
422
- }, [onLoad]);
423
-
424
- useEffect(() => {
425
- onErrorRef.current = onError;
426
- }, [onError]);
427
-
428
420
  useEffect(() => {
429
421
  let disposed = false;
430
422
  ensureSparkDisposeRejectionHandler();
package/src/types.ts CHANGED
@@ -328,6 +328,9 @@ export interface MujocoModel {
328
328
  cam_pos?: Float64Array;
329
329
  cam_quat?: Float64Array;
330
330
  cam_fovy?: Float64Array;
331
+ cam_intrinsic?: Float64Array;
332
+ cam_resolution?: Int32Array;
333
+ cam_sensorsize?: Float64Array;
331
334
 
332
335
  // Tendon
333
336
  ten_wrapadr: Int32Array;
@@ -723,6 +726,9 @@ export interface CameraInfo {
723
726
  name: string;
724
727
  bodyId: number;
725
728
  fov: number | null;
729
+ resolution: [number, number] | null;
730
+ sensorSize: [number, number] | null;
731
+ intrinsic: [number, number, number, number] | null;
726
732
  position: [number, number, number] | null;
727
733
  quaternion: [number, number, number, number] | null;
728
734
  }
@@ -1021,11 +1027,28 @@ export interface ObservationHandle {
1021
1027
 
1022
1028
  // ---- Debug Component (spec 6.1) ----
1023
1029
 
1030
+ export interface DebugVirtualCamera {
1031
+ name?: string;
1032
+ position?: CameraFrameCaptureVector3;
1033
+ lookAt?: CameraFrameCaptureVector3;
1034
+ up?: CameraFrameCaptureVector3;
1035
+ quaternion?: THREE.Quaternion | readonly [number, number, number, number];
1036
+ fov?: number;
1037
+ width?: number;
1038
+ height?: number;
1039
+ frustumDepth?: number;
1040
+ markerScale?: number;
1041
+ color?: THREE.ColorRepresentation;
1042
+ aimColor?: THREE.ColorRepresentation;
1043
+ }
1044
+
1024
1045
  export interface DebugProps {
1025
1046
  showGeoms?: boolean;
1026
1047
  showSites?: boolean;
1027
1048
  showJoints?: boolean;
1028
1049
  showCameras?: boolean;
1050
+ /** Additional explicit virtual camera poses to visualize alongside MuJoCo XML cameras. */
1051
+ virtualCameras?: readonly DebugVirtualCamera[];
1029
1052
  showContacts?: boolean;
1030
1053
  showCOM?: boolean;
1031
1054
  showInertia?: boolean;
@@ -1505,6 +1528,51 @@ export type CameraFrameCaptureQuaternion =
1505
1528
  | THREE.Quaternion
1506
1529
  | readonly [number, number, number, number];
1507
1530
 
1531
+ export interface CameraFrameVisualOverrides {
1532
+ /**
1533
+ * Override `scene.background` for this capture only.
1534
+ * Use `null` or `false` to render without the viewer scene background.
1535
+ */
1536
+ sceneBackground?: THREE.Scene['background'] | THREE.ColorRepresentation | null | false;
1537
+ /**
1538
+ * Override `scene.environment` for this capture only.
1539
+ * Use `null` or `false` to remove viewer environment lighting/maps.
1540
+ */
1541
+ sceneEnvironment?: THREE.Scene['environment'] | null | false;
1542
+ /**
1543
+ * Override `scene.fog` for this capture only.
1544
+ * Use `null` or `false` to remove viewer fog.
1545
+ */
1546
+ sceneFog?: THREE.Scene['fog'] | null | false;
1547
+ /** Override `renderer.shadowMap.enabled` while capturing. */
1548
+ shadows?: boolean;
1549
+ /** Override renderer tone mapping while capturing. */
1550
+ toneMapping?: THREE.WebGLRenderer['toneMapping'];
1551
+ /** Override renderer output color space while capturing. */
1552
+ outputColorSpace?: THREE.WebGLRenderer['outputColorSpace'];
1553
+ }
1554
+
1555
+ export interface CameraFrameRenderIsolationOptions {
1556
+ /**
1557
+ * Use an independent offscreen WebGLRenderer for this capture.
1558
+ *
1559
+ * This prevents viewer renderer settings such as antialiasing, shadow-map
1560
+ * configuration, tone mapping, and environment setup from leaking into
1561
+ * policy/training images. Leave unset for the historical shared-renderer path.
1562
+ */
1563
+ enabled?: boolean;
1564
+ /** Offscreen renderer antialiasing. Defaults to false for deterministic policy captures. */
1565
+ antialias?: boolean;
1566
+ /** Offscreen renderer alpha buffer. Defaults to false, matching Three.js WebGLRenderer. */
1567
+ alpha?: boolean;
1568
+ /** Offscreen renderer preserveDrawingBuffer flag. Defaults to false. */
1569
+ preserveDrawingBuffer?: boolean;
1570
+ /** Offscreen renderer power preference. Defaults to the browser's renderer default. */
1571
+ powerPreference?: WebGLPowerPreference;
1572
+ /** Reuse an offscreen renderer for matching capture dimensions/options. Defaults to true. */
1573
+ cache?: boolean;
1574
+ }
1575
+
1508
1576
  export interface CameraFrameCaptureOptions {
1509
1577
  /** Existing Three camera to clone before applying pose overrides. */
1510
1578
  camera?: THREE.Camera;
@@ -1529,8 +1597,36 @@ export interface CameraFrameCaptureOptions {
1529
1597
  fov?: number;
1530
1598
  near?: number;
1531
1599
  far?: number;
1600
+ /**
1601
+ * Explicit projection matrix for offscreen capture. This is useful when a
1602
+ * MuJoCo camera has calibrated intrinsics that cannot be represented by a
1603
+ * symmetric Three.js PerspectiveCamera fov alone.
1604
+ */
1605
+ projectionMatrix?: THREE.Matrix4 | readonly number[];
1532
1606
  /** Provenance for the camera pose used by the capture. Usually set by the MuJoCo provider. */
1533
1607
  source?: CameraFrameCaptureSource;
1608
+ /**
1609
+ * When resolving a named MuJoCo camera, derive Three capture settings from
1610
+ * MuJoCo camera metadata where available: cam_resolution, cam_fovy,
1611
+ * cam_intrinsic/cam_sensorsize, and visual map near/far clipping.
1612
+ */
1613
+ mujocoCameraCompatibility?: boolean | {
1614
+ useResolution?: boolean;
1615
+ useIntrinsics?: boolean;
1616
+ useClipping?: boolean;
1617
+ /**
1618
+ * When a MuJoCo camera has `resolution` metadata and the caller supplies
1619
+ * only width or only height, derive the missing dimension from the MuJoCo
1620
+ * camera aspect ratio.
1621
+ */
1622
+ preserveAspect?: boolean;
1623
+ /**
1624
+ * Prefer the MuJoCo camera's configured resolution over width/height
1625
+ * provided by the caller. Leave false when a policy wants fixed-size
1626
+ * payloads while still preserving the camera aspect ratio.
1627
+ */
1628
+ preferResolution?: boolean;
1629
+ };
1534
1630
  /** Hide rendered Three objects whose MuJoCo geom group is in this list. */
1535
1631
  hiddenGeomGroups?: readonly number[];
1536
1632
  /** When provided, only rendered Three objects whose MuJoCo geom group is in this list are visible. */
@@ -1541,6 +1637,15 @@ export interface CameraFrameCaptureOptions {
1541
1637
  background?: THREE.ColorRepresentation;
1542
1638
  /** Optional clear alpha for this capture only. Defaults to the renderer's current clear alpha. */
1543
1639
  backgroundAlpha?: number;
1640
+ /** Temporary scene/renderer visual overrides applied only for this offscreen capture. */
1641
+ visualOverrides?: CameraFrameVisualOverrides;
1642
+ /**
1643
+ * Render this capture with a separate offscreen WebGLRenderer.
1644
+ *
1645
+ * This is useful for policy or training captures that should remain canonical
1646
+ * while the interactive viewer uses richer visual effects.
1647
+ */
1648
+ renderIsolation?: boolean | CameraFrameRenderIsolationOptions;
1544
1649
  /** Mirror the captured image horizontally after rendering. Useful when matching policy datasets with mirrored camera frames. */
1545
1650
  flipX?: boolean;
1546
1651
  }
@@ -1658,6 +1763,21 @@ export interface CameraFrameSequenceRecorderAPI {
1658
1763
  reset: () => void;
1659
1764
  }
1660
1765
 
1766
+ export type MujocoMeshNormalSmoothing =
1767
+ | boolean
1768
+ | {
1769
+ /** Vertex merge tolerance used before recomputing mesh normals. Defaults to `1e-4`. */
1770
+ tolerance?: number;
1771
+ };
1772
+
1773
+ export interface MujocoRenderOptions {
1774
+ /**
1775
+ * Smooth mesh normals by welding duplicate vertices before recomputing normals.
1776
+ * Useful for faceted STL visuals; keep off for exact policy-render parity.
1777
+ */
1778
+ meshNormalSmoothing?: MujocoMeshNormalSmoothing;
1779
+ }
1780
+
1661
1781
  // ---- Canvas Props ----
1662
1782
 
1663
1783
  export type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
@@ -1675,6 +1795,7 @@ export type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
1675
1795
  paused?: boolean;
1676
1796
  speed?: number;
1677
1797
  interpolate?: boolean;
1798
+ renderOptions?: MujocoRenderOptions;
1678
1799
  };
1679
1800
 
1680
1801
  // ---- Hook Return Types ----