mujoco-react 12.1.1 → 12.2.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.
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  import * as THREE from 'three';
9
+ import { createMetricDepthReader } from './metricDepth';
9
10
  import type {
10
11
  CameraFrameCaptureBlobResult,
11
12
  CameraFrameCaptureOptions,
@@ -32,6 +33,26 @@ export interface CameraFramePixelsResult {
32
33
  source: CameraFrameCaptureSource;
33
34
  }
34
35
 
36
+ /** An owned, aligned RGB-D observation from one render. Scene units must be meters. */
37
+ export interface CameraRGBDResult {
38
+ /** Interleaved sRGB bytes (RGB), row-major, top-left origin. */
39
+ rgb: Uint8Array;
40
+ /** Positive camera-axis distance (-view Z) in meters, not Euclidean range.
41
+ * Background / no depth-writing surface is +Infinity. Same pixel order as rgb. */
42
+ depth: Float32Array;
43
+ width: number;
44
+ height: number;
45
+ source: CameraFrameCaptureSource;
46
+ origin: 'top-left';
47
+ depthUnits: 'meters';
48
+ depthConvention: 'camera-z';
49
+ /** Horizontal mirroring applied to both arrays. Undo before unprojecting. */
50
+ flipX: boolean;
51
+ /** Owned column-major matrices. Camera coordinates: +X right, +Y up, -Z forward. */
52
+ projectionMatrix: number[];
53
+ cameraWorldMatrix: number[];
54
+ }
55
+
35
56
  export interface CameraFrameTensorResult extends PolicyImageTensorResult {
36
57
  camera: THREE.Camera;
37
58
  source: CameraFrameCaptureSource;
@@ -67,6 +88,8 @@ export interface CameraFrameCaptureSession {
67
88
  capturePixels(options?: CameraFrameCaptureOptions): CameraFramePixelsResult;
68
89
  /** Render straight into a normalized policy image tensor (no canvas/PNG encode). */
69
90
  captureTensor(options?: CameraFrameCaptureTensorOptions): CameraFrameTensorResult;
91
+ /** Synchronous RGB + metric depth from one render. Returned arrays are owned. */
92
+ captureRGBD(options?: CameraFrameCaptureOptions): CameraRGBDResult;
70
93
  dispose(): void;
71
94
  }
72
95
 
@@ -78,6 +101,12 @@ export const CAPTURE_EXCLUDE_KEY =
78
101
  'mujoco.capture.exclude';
79
102
 
80
103
  export type CameraFrameCaptureRenderInput = {
104
+ renderMode: 'snapshot' | 'preview';
105
+ /** Apply capture visibility/visual overrides only for a synchronous render.
106
+ * Async adapters must wrap each render after awaiting preparation, and await
107
+ * readback outside this scope so the interactive viewport stays untouched.
108
+ */
109
+ withCaptureState: (render: () => void) => void;
81
110
  renderer: THREE.WebGLRenderer;
82
111
  scene: THREE.Scene;
83
112
  camera: THREE.Camera;
@@ -300,6 +329,11 @@ export function createCaptureCamera(
300
329
  camera.far = options.far ?? camera.far;
301
330
  camera.updateProjectionMatrix();
302
331
  }
332
+ if (camera instanceof THREE.OrthographicCamera) {
333
+ camera.near = options.near ?? camera.near;
334
+ camera.far = options.far ?? camera.far;
335
+ camera.updateProjectionMatrix();
336
+ }
303
337
  applyProjectionMatrix(camera, options.projectionMatrix);
304
338
 
305
339
  applyCameraPose(camera, options, fallbackCamera);
@@ -339,6 +373,11 @@ export function prepareCaptureCamera(
339
373
  camera.far = options.far ?? camera.far;
340
374
  camera.updateProjectionMatrix();
341
375
  }
376
+ if (camera instanceof THREE.OrthographicCamera) {
377
+ camera.near = options.near ?? camera.near;
378
+ camera.far = options.far ?? camera.far;
379
+ camera.updateProjectionMatrix();
380
+ }
342
381
  applyProjectionMatrix(camera, options.projectionMatrix);
343
382
 
344
383
  applyCameraPose(camera, options, fallbackCamera);
@@ -647,12 +686,14 @@ export function createCameraFrameCaptureSession(
647
686
 
648
687
  const pixels = new Uint8Array(width * height * 4);
649
688
  const imageData = drawContext.createImageData(width, height);
689
+ let depthReader: ReturnType<typeof createMetricDepthReader> | undefined;
650
690
  let inFlight = false, disposed = false;
651
691
  function assertAvailable() {
652
692
  if (disposed) throw new Error('Camera capture session is disposed');
653
693
  if (inFlight) throw new Error('Camera capture session is busy; await the current capture');
654
694
  }
655
695
  function releaseResources() {
696
+ depthReader?.dispose();
656
697
  target.dispose();
657
698
  if (isolatedRenderer && !isolatedRenderer.cached) isolatedRenderer.renderer.dispose();
658
699
  }
@@ -688,29 +729,17 @@ export function createCameraFrameCaptureSession(
688
729
  return captureOptions;
689
730
  }
690
731
 
691
- function renderCaptureToTarget(
692
- captureOptions: CameraFrameCaptureOptions,
693
- readback: () => void
694
- ) {
732
+ function withCaptureState(captureOptions: CameraFrameCaptureOptions, render: () => void) {
695
733
  const previousState = saveRendererState(sessionRenderer);
696
- const previousSceneState = applyCaptureVisualOverrides(
697
- sessionRenderer,
698
- scene,
699
- captureOptions
700
- );
734
+ const previousSceneState = applyCaptureVisualOverrides(sessionRenderer, scene, captureOptions);
701
735
  const hidden = [
702
736
  ...hideExcludedCaptureObjects(scene),
703
737
  ...hideCaptureGeomGroups(scene, captureOptions),
704
738
  ];
705
-
706
- runCapturePreRenderHooks(scene);
707
- scene.updateMatrixWorld(true);
708
739
  try {
740
+ runCapturePreRenderHooks(scene);
741
+ scene.updateMatrixWorld(true);
709
742
  sessionRenderer.xr.enabled = false;
710
- sessionRenderer.setRenderTarget(target);
711
- sessionRenderer.setViewport(0, 0, width, height);
712
- sessionRenderer.setScissor(0, 0, width, height);
713
- sessionRenderer.setScissorTest(false);
714
743
  if (captureOptions.background !== undefined) {
715
744
  sessionRenderer.setClearColor(
716
745
  new THREE.Color(captureOptions.background),
@@ -719,9 +748,7 @@ export function createCameraFrameCaptureSession(
719
748
  } else if (captureOptions.backgroundAlpha !== undefined) {
720
749
  sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
721
750
  }
722
- sessionRenderer.clear();
723
- sessionRenderer.render(scene, camera);
724
- readback();
751
+ render();
725
752
  } finally {
726
753
  restoreObjectVisibility(hidden);
727
754
  if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
@@ -729,6 +756,20 @@ export function createCameraFrameCaptureSession(
729
756
  }
730
757
  }
731
758
 
759
+ function renderCaptureToTarget(
760
+ captureOptions: CameraFrameCaptureOptions,
761
+ readback: () => void
762
+ ) {
763
+ withCaptureState(captureOptions, () => {
764
+ sessionRenderer.setRenderTarget(target);
765
+ // The render target installs its pixel viewport; setters would apply DPR twice.
766
+ sessionRenderer.setScissorTest(false);
767
+ sessionRenderer.clear();
768
+ sessionRenderer.render(scene, camera);
769
+ readback();
770
+ });
771
+ }
772
+
732
773
  function renderPreparedCapture(captureOptions: CameraFrameCaptureOptions) {
733
774
  renderCaptureToTarget(captureOptions, () => {
734
775
  readRenderTargetToCanvas(
@@ -773,6 +814,51 @@ export function createCameraFrameCaptureSession(
773
814
  };
774
815
  }
775
816
 
817
+ function captureRGBD(nextOptions: CameraFrameCaptureOptions = {}): CameraRGBDResult {
818
+ assertAvailable();
819
+ const captureOptions = resolveCaptureOptions(nextOptions);
820
+ if (getCaptureRenderer(scene)) {
821
+ throw new Error('Metric RGB-D capture requires the standard Three renderer; custom capture renderers do not provide aligned depth.');
822
+ }
823
+ if (sessionRenderer.capabilities.logarithmicDepthBuffer || sessionRenderer.capabilities.reversedDepthBuffer) {
824
+ throw new Error('Metric RGB-D capture does not support logarithmic or reversed depth buffers.');
825
+ }
826
+ if (!camera.projectionMatrix.elements.every(Number.isFinite) ||
827
+ !Number.isFinite(camera.projectionMatrix.determinant()) || camera.projectionMatrix.determinant() === 0) {
828
+ throw new Error('Metric RGB-D capture requires a finite, invertible projection matrix.');
829
+ }
830
+ if (!depthReader) {
831
+ // Recreate the framebuffer if this session has already rendered RGB-only frames.
832
+ target.dispose();
833
+ target.depthTexture = new THREE.DepthTexture(width, height, THREE.UnsignedIntType);
834
+ depthReader = createMetricDepthReader(width, height, target.depthTexture);
835
+ }
836
+ const rgb = new Uint8Array(width * height * 3);
837
+ let depth!: Float32Array;
838
+ renderCaptureToTarget(captureOptions, () => {
839
+ sessionRenderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
840
+ for (let y = 0; y < height; y++) {
841
+ for (let x = 0; x < width; x++) {
842
+ const sx = captureOptions.flipX ? width - x - 1 : x;
843
+ const src = ((height - y - 1) * width + sx) * 4;
844
+ const dst = (y * width + x) * 3;
845
+ for (let c = 0; c < 3; c++) {
846
+ rgb[dst + c] = linearByteToSrgbByte(pixels[src + c]);
847
+ }
848
+ }
849
+ }
850
+ depth = depthReader!.read(sessionRenderer, camera, captureOptions.flipX ?? false);
851
+ });
852
+ return {
853
+ rgb, depth, width, height,
854
+ source: getCameraFrameCaptureSource(captureOptions),
855
+ origin: 'top-left', depthUnits: 'meters', depthConvention: 'camera-z',
856
+ flipX: captureOptions.flipX ?? false,
857
+ projectionMatrix: camera.projectionMatrix.toArray(),
858
+ cameraWorldMatrix: camera.matrixWorld.toArray(),
859
+ };
860
+ }
861
+
776
862
  function captureTensor(
777
863
  nextOptions: CameraFrameCaptureTensorOptions = {}
778
864
  ): CameraFrameTensorResult {
@@ -802,63 +888,35 @@ export function createCameraFrameCaptureSession(
802
888
  scene.updateMatrixWorld(true);
803
889
  const captureRenderer = getCaptureRenderer(scene);
804
890
  if (captureRenderer) {
805
- const previousState = saveRendererState(sessionRenderer);
806
- const previousSceneState = applyCaptureVisualOverrides(
807
- sessionRenderer,
808
- scene,
809
- captureOptions
810
- );
811
- const hidden = [
812
- ...hideExcludedCaptureObjects(scene),
813
- ...hideCaptureGeomGroups(scene, captureOptions),
814
- ];
815
- try {
816
- sessionRenderer.xr.enabled = false;
817
- if (captureOptions.background !== undefined) {
818
- sessionRenderer.setClearColor(
819
- new THREE.Color(captureOptions.background),
820
- captureOptions.backgroundAlpha ?? previousState.clearAlpha
821
- );
822
- } else if (captureOptions.backgroundAlpha !== undefined) {
823
- sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
824
- }
825
- const captureResult = await captureRenderer({
891
+ let pending: ReturnType<CameraFrameCaptureRender>;
892
+ const renderWithCaptureState = (render: () => void) => withCaptureState(captureOptions, render);
893
+ // Preserve synchronous adapters while restoring shared state before any
894
+ // asynchronous preparation/readback yields to the viewer's next frame.
895
+ renderWithCaptureState(() => {
896
+ pending = captureRenderer({
826
897
  renderer: sessionRenderer,
827
898
  scene,
828
899
  camera,
829
900
  target,
830
901
  width,
831
902
  height,
903
+ withCaptureState: renderWithCaptureState,
904
+ renderMode: captureOptions.renderMode ?? 'snapshot',
832
905
  });
833
- if (captureResult) {
834
- const captureWidth = captureResult.width ?? width;
835
- const captureHeight = captureResult.height ?? height;
836
- if (captureWidth !== width || captureHeight !== height) {
837
- throw new Error(
838
- 'Camera frame capture renderer returned unexpected dimensions.'
839
- );
840
- }
841
- readPixelsToCanvas(
842
- captureResult.pixels,
843
- drawContext,
844
- imageData,
845
- width,
846
- height,
847
- captureResult.flipY ?? true,
848
- captureResult.flipX ?? captureOptions.flipX ?? false
849
- );
850
- return {
851
- canvas,
852
- camera,
853
- width,
854
- height,
855
- source: getCameraFrameCaptureSource(captureOptions),
856
- };
906
+ });
907
+ const captureResult = await pending;
908
+ if (captureResult) {
909
+ const captureWidth = captureResult.width ?? width;
910
+ const captureHeight = captureResult.height ?? height;
911
+ if (captureWidth !== width || captureHeight !== height) {
912
+ throw new Error('Camera frame capture renderer returned unexpected dimensions.');
857
913
  }
858
- } finally {
859
- restoreObjectVisibility(hidden);
860
- if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
861
- restoreRendererState(sessionRenderer, previousState);
914
+ readPixelsToCanvas(
915
+ captureResult.pixels, drawContext, imageData, width, height,
916
+ captureResult.flipY ?? true,
917
+ captureResult.flipX ?? captureOptions.flipX ?? false
918
+ );
919
+ return { canvas, camera, width, height, source: getCameraFrameCaptureSource(captureOptions) };
862
920
  }
863
921
  }
864
922
  let readback: Promise<unknown> | undefined;
@@ -878,6 +936,7 @@ export function createCameraFrameCaptureSession(
878
936
  captureAsync,
879
937
  capturePixels,
880
938
  captureTensor,
939
+ captureRGBD,
881
940
  captureDataUrl(nextOptions = {}) {
882
941
  const type = nextOptions.type ?? options.type ?? 'image/png';
883
942
  const result = capture(nextOptions);
@@ -1012,3 +1071,17 @@ export function captureCameraFrameTensor(
1012
1071
  session.dispose();
1013
1072
  }
1014
1073
  }
1074
+ /** One-shot aligned RGB-D observation. Use a session for repeated observations. */
1075
+ export function captureCameraRGBD(
1076
+ renderer: THREE.WebGLRenderer,
1077
+ scene: THREE.Scene,
1078
+ fallbackCamera: THREE.Camera,
1079
+ options: CameraFrameCaptureOptions = {}
1080
+ ): CameraRGBDResult {
1081
+ const session = createCameraFrameCaptureSession(renderer, scene, fallbackCamera, options);
1082
+ try {
1083
+ return session.captureRGBD();
1084
+ } finally {
1085
+ session.dispose();
1086
+ }
1087
+ }
@@ -0,0 +1,56 @@
1
+ /** @license SPDX-License-Identifier: Apache-2.0 */
2
+ import * as THREE from 'three';
3
+
4
+ /** Internal GPU readback adapter. Read the real depth attachment, preserving
5
+ * alpha tests, displacement, skinning, and depthWrite from the RGB render. */
6
+ export function createMetricDepthReader(width: number, height: number, texture: THREE.DepthTexture) {
7
+ const target = new THREE.WebGLRenderTarget(width, height, { depthBuffer: false });
8
+ const material = new THREE.ShaderMaterial({
9
+ uniforms: { depthTexture: { value: texture } },
10
+ vertexShader: `varying vec2 vUv;
11
+ void main() { vUv = uv; gl_Position = vec4(position.xy, 0.0, 1.0); }`,
12
+ fragmentShader: `#include <packing>
13
+ uniform sampler2D depthTexture;
14
+ varying vec2 vUv;
15
+ void main() { gl_FragColor = packDepthToRGBA(texture2D(depthTexture, vUv).r); }`,
16
+ depthTest: false, depthWrite: false, blending: THREE.NoBlending,
17
+ toneMapped: false,
18
+ });
19
+ const geometry = new THREE.PlaneGeometry(2, 2);
20
+ const scene = new THREE.Scene();
21
+ scene.add(new THREE.Mesh(geometry, material));
22
+ const screenCamera = new THREE.Camera();
23
+ const packed = new Uint8Array(width * height * 4);
24
+ return {
25
+ read(renderer: THREE.WebGLRenderer, camera: THREE.Camera, flipX: boolean) {
26
+ renderer.setRenderTarget(target);
27
+ renderer.clear();
28
+ renderer.render(scene, screenCamera);
29
+ renderer.readRenderTargetPixels(target, 0, 0, width, height, packed);
30
+ const result = new Float32Array(width * height);
31
+ const inverse = camera.projectionMatrixInverse.elements;
32
+ for (let y = 0; y < height; y++) {
33
+ for (let x = 0; x < width; x++) {
34
+ const sx = flipX ? width - x - 1 : x;
35
+ const sy = height - y - 1;
36
+ const offset = (sy * width + sx) * 4;
37
+ const r = packed[offset], g = packed[offset + 1], b = packed[offset + 2], a = packed[offset + 3];
38
+ if (r === 255 && g === 255 && b === 255 && a === 255) {
39
+ result[y * width + x] = Infinity;
40
+ continue;
41
+ }
42
+ // Mirrors Three's unpackRGBAToDepth (packing.glsl.js).
43
+ const d = r / 256 + g / 65536 + b / 16777216 + a / (255 * 16777216);
44
+ const nx = (sx + 0.5) / width * 2 - 1;
45
+ const ny = (sy + 0.5) / height * 2 - 1;
46
+ const nz = d * 2 - 1;
47
+ const z = inverse[2] * nx + inverse[6] * ny + inverse[10] * nz + inverse[14];
48
+ const w = inverse[3] * nx + inverse[7] * ny + inverse[11] * nz + inverse[15];
49
+ result[y * width + x] = -z / w;
50
+ }
51
+ }
52
+ return result;
53
+ },
54
+ dispose() { target.dispose(); material.dispose(); geometry.dispose(); },
55
+ };
56
+ }
package/src/spark.tsx CHANGED
@@ -373,7 +373,7 @@ export function SparkSplatEnvironment({
373
373
  }: SparkSplatEnvironmentProps) {
374
374
  const groupRef = useRef<THREE.Group>(null);
375
375
  const sparkRef = useRef<SparkRendererInstance | null>(null);
376
- const captureSparkRef = useRef<SparkCaptureRenderer | null>(null);
376
+ const captureSparkRef = useRef(new Map<'snapshot' | 'preview', SparkCaptureRenderer>());
377
377
  const meshRef = useRef<SparkSplatMeshInstance | null>(null);
378
378
  const hiddenMeshesRef = useRef<THREE.Mesh[]>([]);
379
379
  const onStatusChangeRef = useRef(onStatusChange);
@@ -434,8 +434,10 @@ export function SparkSplatEnvironment({
434
434
  }
435
435
 
436
436
  function disposeCaptureSpark() {
437
- safelyDisposeSparkResource(captureSparkRef.current?.renderer);
438
- captureSparkRef.current = null;
437
+ for (const capture of captureSparkRef.current.values()) {
438
+ safelyDisposeSparkResource(capture.renderer);
439
+ }
440
+ captureSparkRef.current.clear();
439
441
  }
440
442
 
441
443
  async function loadSplat() {
@@ -465,25 +467,32 @@ export function SparkSplatEnvironment({
465
467
  ...resolvedRenderTuning,
466
468
  });
467
469
  spark.userData[CAMERA_FRAME_CAPTURE_RENDER_USER_DATA_KEY] = async ({
470
+ withCaptureState,
471
+ renderMode,
468
472
  scene,
469
473
  camera,
470
474
  width,
471
475
  height,
472
476
  }: CameraFrameCaptureRenderInput): Promise<CameraFrameCaptureRenderResult> => {
473
- let captureSpark = captureSparkRef.current;
477
+ let captureSpark = captureSparkRef.current.get(renderMode);
474
478
  if (
475
479
  !captureSpark ||
476
480
  captureSpark.width !== width ||
477
481
  captureSpark.height !== height
478
482
  ) {
479
- disposeCaptureSpark();
483
+ safelyDisposeSparkResource(captureSpark?.renderer);
480
484
  captureSpark = {
481
485
  renderer: new sparkModule.SparkRenderer({
482
486
  renderer: gl,
483
- autoUpdate: false,
484
- lodSplatScale: resolvedCaptureTuning.lodSplatScale,
485
- lodRenderScale: resolvedCaptureTuning.lodRenderScale,
486
- minSortIntervalMs: resolvedCaptureTuning.minSortIntervalMs,
487
+ // Preview uses Spark's change detection and background sorting;
488
+ // forced update() regenerates even an unchanged static scene.
489
+ autoUpdate: renderMode === 'preview',
490
+ preUpdate: false,
491
+ ...(renderMode === 'preview' ? resolvedRenderTuning : {
492
+ lodSplatScale: resolvedCaptureTuning.lodSplatScale,
493
+ lodRenderScale: resolvedCaptureTuning.lodRenderScale,
494
+ minSortIntervalMs: resolvedCaptureTuning.minSortIntervalMs,
495
+ }),
487
496
  target: {
488
497
  width,
489
498
  height,
@@ -492,7 +501,7 @@ export function SparkSplatEnvironment({
492
501
  width,
493
502
  height,
494
503
  };
495
- captureSparkRef.current = captureSpark;
504
+ captureSparkRef.current.set(renderMode, captureSpark);
496
505
  }
497
506
 
498
507
  const captureRenderer = captureSpark.renderer;
@@ -503,11 +512,11 @@ export function SparkSplatEnvironment({
503
512
  let pixels: Uint8Array | undefined;
504
513
  for (let attempt = 0; attempt < maxWarmupFrames; attempt += 1) {
505
514
  captureRenderer.renderSize.set(width, height);
506
- await captureRenderer.update({ scene, camera });
507
- pixels = await captureRenderer.renderReadTarget({
508
- scene,
509
- camera,
510
- });
515
+ if (renderMode === 'snapshot') {
516
+ await captureRenderer.update({ scene, camera });
517
+ }
518
+ withCaptureState(() => captureRenderer.renderTarget({ scene, camera }));
519
+ pixels = await captureRenderer.readTarget();
511
520
  if (
512
521
  captureRenderer.activeSplats > 0 &&
513
522
  !isMostlyBlankCapture(pixels, resolvedCaptureTuning)
package/src/types.ts CHANGED
@@ -13,6 +13,7 @@ import type {
13
13
  CameraFrameCaptureSession,
14
14
  CameraFrameCaptureTensorOptions,
15
15
  CameraFrameTensorResult,
16
+ CameraRGBDResult,
16
17
  } from './rendering/cameraFrameCapture';
17
18
 
18
19
  // ---- Register (type-safe named resources) ----
@@ -1548,6 +1549,8 @@ export interface MujocoSimAPI {
1548
1549
  captureFrameBlob(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureBlobResult>;
1549
1550
  captureCameraFrame(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureResult>;
1550
1551
  captureCameraFrameBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
1552
+ /** Aligned sRGB + camera-axis depth in meters from one render. */
1553
+ captureCameraRGBD(options?: CameraFrameCaptureOptions): CameraRGBDResult;
1551
1554
  /** Capture a camera frame straight into a policy image tensor (no canvas/PNG encode). */
1552
1555
  captureCameraFrameTensor(options?: CameraFrameCaptureTensorOptions): CameraFrameTensorResult;
1553
1556
  /**
@@ -1677,6 +1680,11 @@ export interface CameraFrameRenderIsolationOptions {
1677
1680
  }
1678
1681
 
1679
1682
  export interface CameraFrameCaptureOptions {
1683
+ /** Snapshot (default) waits for splat preparation. Preview reuses the latest
1684
+ * sorted splats while updating in the background for responsive live motion.
1685
+ * useCameraStream defaults to preview; policy/dataset captures stay snapshot.
1686
+ */
1687
+ renderMode?: 'snapshot' | 'preview';
1680
1688
  /** Existing Three camera to clone before applying pose overrides. */
1681
1689
  camera?: THREE.Camera;
1682
1690
  /** Named MuJoCo `<camera>` to render from when available in the loaded model. */
@@ -1791,6 +1799,7 @@ export interface CameraFrameCaptureAPI {
1791
1799
  captureBlob: (
1792
1800
  options?: CameraFrameCaptureOptions
1793
1801
  ) => Promise<CameraFrameCaptureBlobResult>;
1802
+ captureRGBD: (options?: CameraFrameCaptureOptions) => Promise<CameraRGBDResult>;
1794
1803
  reset: () => void;
1795
1804
  }
1796
1805