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.
- package/README.md +200 -180
- package/dist/{chunk-XD3AY7XZ.js → chunk-SP72GTOD.js} +192 -92
- package/dist/chunk-SP72GTOD.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +25 -3
- package/dist/index.js.map +1 -1
- package/dist/onnx.d.ts +1 -1
- package/dist/spark.d.ts +1 -1
- package/dist/spark.js +25 -16
- package/dist/spark.js.map +1 -1
- package/dist/{types-CSi3fIVs.d.ts → types-CF-BZXtW.d.ts} +32 -1
- package/package.json +2 -2
- package/src/core/MujocoSimProvider.tsx +9 -1
- package/src/hooks/useCameraFrameCapture.ts +17 -0
- package/src/hooks/useCameraStream.ts +1 -1
- package/src/index.ts +2 -0
- package/src/rendering/cameraFrameCapture.ts +141 -68
- package/src/rendering/metricDepth.ts +56 -0
- package/src/spark.tsx +24 -15
- package/src/types.ts +9 -0
- package/dist/chunk-XD3AY7XZ.js.map +0 -1
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
806
|
-
const
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
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<
|
|
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
|
-
|
|
438
|
-
|
|
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
|
-
|
|
483
|
+
safelyDisposeSparkResource(captureSpark?.renderer);
|
|
480
484
|
captureSpark = {
|
|
481
485
|
renderer: new sparkModule.SparkRenderer({
|
|
482
486
|
renderer: gl,
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
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
|
|
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
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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
|
|