mujoco-react 9.1.0 → 9.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 +28 -10
- package/dist/index.d.ts +43 -5
- package/dist/index.js +463 -132
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +1 -1
- package/dist/{types-C5gTvR7b.d.ts → types-S8ggQY2n.d.ts} +72 -1
- package/package.json +1 -1
- package/src/core/MujocoSimProvider.tsx +119 -1
- package/src/core/createController.tsx +6 -2
- package/src/hooks/useCameraFrameCapture.ts +94 -0
- package/src/hooks/useCameraSequenceRecorder.ts +59 -0
- package/src/index.ts +18 -0
- package/src/rendering/cameraFrameCapture.ts +184 -0
- package/src/types.ts +90 -0
package/dist/spark.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as react_jsx_runtime from 'react/jsx-runtime';
|
|
2
2
|
import * as _sparkjsdev_spark from '@sparkjsdev/spark';
|
|
3
|
-
import { n as SplatEnvironmentProps } from './types-
|
|
3
|
+
import { n as SplatEnvironmentProps } from './types-S8ggQY2n.js';
|
|
4
4
|
import 'react';
|
|
5
5
|
import '@react-three/fiber';
|
|
6
6
|
import 'three';
|
|
@@ -841,6 +841,9 @@ interface MujocoSimAPI {
|
|
|
841
841
|
getCanvasSnapshot(width?: number, height?: number, mimeType?: string): string;
|
|
842
842
|
captureFrame(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureResult>;
|
|
843
843
|
captureFrameBlob(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureBlobResult>;
|
|
844
|
+
captureCameraFrame(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureResult>;
|
|
845
|
+
captureCameraFrameBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
|
|
846
|
+
recordCameraSequence(options: CameraFrameSequenceOptions): Promise<CameraFrameSequenceResult>;
|
|
844
847
|
project2DTo3D(x: number, y: number, cameraPos: THREE.Vector3, lookAt: THREE.Vector3): {
|
|
845
848
|
point: THREE.Vector3;
|
|
846
849
|
bodyId: number;
|
|
@@ -880,6 +883,74 @@ interface FrameCaptureAPI {
|
|
|
880
883
|
captureBlob: (options?: FrameCaptureOptions) => Promise<FrameCaptureBlobResult>;
|
|
881
884
|
reset: () => void;
|
|
882
885
|
}
|
|
886
|
+
type CameraFrameCaptureVector3 = THREE.Vector3 | readonly [number, number, number];
|
|
887
|
+
type CameraFrameCaptureQuaternion = THREE.Quaternion | readonly [number, number, number, number];
|
|
888
|
+
interface CameraFrameCaptureOptions {
|
|
889
|
+
camera?: THREE.Camera;
|
|
890
|
+
position?: CameraFrameCaptureVector3;
|
|
891
|
+
lookAt?: CameraFrameCaptureVector3;
|
|
892
|
+
quaternion?: CameraFrameCaptureQuaternion;
|
|
893
|
+
up?: CameraFrameCaptureVector3;
|
|
894
|
+
width?: number;
|
|
895
|
+
height?: number;
|
|
896
|
+
type?: string;
|
|
897
|
+
quality?: number;
|
|
898
|
+
fov?: number;
|
|
899
|
+
near?: number;
|
|
900
|
+
far?: number;
|
|
901
|
+
}
|
|
902
|
+
interface CameraFrameCaptureResult {
|
|
903
|
+
canvas: HTMLCanvasElement;
|
|
904
|
+
camera: THREE.Camera;
|
|
905
|
+
dataUrl: string;
|
|
906
|
+
type: string;
|
|
907
|
+
width: number;
|
|
908
|
+
height: number;
|
|
909
|
+
}
|
|
910
|
+
interface CameraFrameCaptureBlobResult {
|
|
911
|
+
canvas: HTMLCanvasElement;
|
|
912
|
+
camera: THREE.Camera;
|
|
913
|
+
blob: Blob;
|
|
914
|
+
type: string;
|
|
915
|
+
width: number;
|
|
916
|
+
height: number;
|
|
917
|
+
}
|
|
918
|
+
interface CameraFrameCaptureAPI {
|
|
919
|
+
status: FrameCaptureStatus;
|
|
920
|
+
error: Error | null;
|
|
921
|
+
isCapturing: boolean;
|
|
922
|
+
capture: (options?: CameraFrameCaptureOptions) => Promise<CameraFrameCaptureResult>;
|
|
923
|
+
captureBlob: (options?: CameraFrameCaptureOptions) => Promise<CameraFrameCaptureBlobResult>;
|
|
924
|
+
reset: () => void;
|
|
925
|
+
}
|
|
926
|
+
interface CameraFrameSequenceCamera extends CameraFrameCaptureOptions {
|
|
927
|
+
key: string;
|
|
928
|
+
}
|
|
929
|
+
interface CameraFrameSequenceFrame {
|
|
930
|
+
frameIndex: number;
|
|
931
|
+
time: number;
|
|
932
|
+
cameras: Record<string, CameraFrameCaptureResult>;
|
|
933
|
+
}
|
|
934
|
+
interface CameraFrameSequenceOptions {
|
|
935
|
+
cameras: readonly CameraFrameSequenceCamera[];
|
|
936
|
+
frames: number;
|
|
937
|
+
stepsPerFrame?: number;
|
|
938
|
+
reset?: boolean;
|
|
939
|
+
captureInitialFrame?: boolean;
|
|
940
|
+
onFrame?: (frame: CameraFrameSequenceFrame) => void | Promise<void>;
|
|
941
|
+
}
|
|
942
|
+
interface CameraFrameSequenceResult {
|
|
943
|
+
frames: CameraFrameSequenceFrame[];
|
|
944
|
+
cameraKeys: string[];
|
|
945
|
+
frameCount: number;
|
|
946
|
+
}
|
|
947
|
+
interface CameraFrameSequenceRecorderAPI {
|
|
948
|
+
status: FrameCaptureStatus;
|
|
949
|
+
error: Error | null;
|
|
950
|
+
isRecording: boolean;
|
|
951
|
+
record: (options: CameraFrameSequenceOptions) => Promise<CameraFrameSequenceResult>;
|
|
952
|
+
reset: () => void;
|
|
953
|
+
}
|
|
883
954
|
type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {
|
|
884
955
|
config: SceneConfig;
|
|
885
956
|
/** R3F content rendered while the MuJoCo WASM module is still loading. */
|
|
@@ -938,4 +1009,4 @@ interface JointStateResult {
|
|
|
938
1009
|
velocity: React__default.RefObject<number | Float64Array>;
|
|
939
1010
|
}
|
|
940
1011
|
|
|
941
|
-
export { type PolicyConfig as $, type ActuatedJointInfo as A, type BodyProps as B, type ControlGroupInfo as C, type DragInteractionProps as D, type ActuatorInfo as E, type Sites as F, type GeomInfo as G, type SitePositionResult as H, type IkConfig as I, type Sensors as J, type SensorHandle as K, type SensorInfo as L, type MujocoContextValue as M, type Joints as N, type ObservationConfig as O, type PhysicsStepCallback as P, type JointStateResult as Q, type ReadyCallbackInput as R, type SceneConfig as S, type TrajectoryPlayerProps as T, type Bodies as U, type VisualScenarioEffectsProps as V, type BodyStateResult as W, type Actuators as X, type CtrlHandle as Y, type ContactInfo as Z, type KeyboardTeleopConfig as _, type MujocoCanvasProps as a, type PolicyVector as a0, type ObservationHandle as a1, type TrajectoryInput as a2, type TrajectoryStateChangeInput as a3, type PlaybackState as a4, type TrajectoryFrame as a5, type FrameCaptureOptions as a6, type FrameCaptureResult as a7, type FrameCaptureBlobResult as a8, type FrameCaptureAPI as a9, type
|
|
1012
|
+
export { type PolicyConfig as $, type ActuatedJointInfo as A, type BodyProps as B, type ControlGroupInfo as C, type DragInteractionProps as D, type ActuatorInfo as E, type Sites as F, type GeomInfo as G, type SitePositionResult as H, type IkConfig as I, type Sensors as J, type SensorHandle as K, type SensorInfo as L, type MujocoContextValue as M, type Joints as N, type ObservationConfig as O, type PhysicsStepCallback as P, type JointStateResult as Q, type ReadyCallbackInput as R, type SceneConfig as S, type TrajectoryPlayerProps as T, type Bodies as U, type VisualScenarioEffectsProps as V, type BodyStateResult as W, type Actuators as X, type CtrlHandle as Y, type ContactInfo as Z, type KeyboardTeleopConfig as _, type MujocoCanvasProps as a, type SensorResult as a$, type PolicyVector as a0, type ObservationHandle as a1, type TrajectoryInput as a2, type TrajectoryStateChangeInput as a3, type PlaybackState as a4, type TrajectoryFrame as a5, type FrameCaptureOptions as a6, type FrameCaptureResult as a7, type FrameCaptureBlobResult as a8, type FrameCaptureAPI as a9, type MujocoFrameCaptureOptions as aA, type ObservationLayoutItem as aB, type ObservationOutput as aC, type PhysicsConfig as aD, type PhysicsStepInput as aE, type PolicyActionInput as aF, type PolicyInferenceInput as aG, type PolicyObservationInput as aH, type RayHit as aI, type Register as aJ, type RegisteredRobotMap as aK, type ResetCallbackInput as aL, type ResourceSelector as aM, RobotActuators as aN, RobotBodies as aO, RobotGeoms as aP, RobotJoints as aQ, RobotKeyframes as aR, type RobotResource as aS, RobotResources as aT, RobotSensors as aU, RobotSites as aV, type Robots as aW, type ScenarioCameraConfig as aX, type ScenarioMaterialConfig as aY, type SceneMarker as aZ, type SceneObject as a_, type CameraFrameCaptureOptions as aa, type CameraFrameCaptureAPI as ab, type CameraFrameSequenceRecorderAPI as ac, type CameraFrameCaptureResult as ad, type CameraFrameCaptureBlobResult as ae, type BodyInfo as af, type CameraFrameCaptureQuaternion as ag, type CameraFrameCaptureVector3 as ah, type CameraFrameSequenceCamera as ai, type CameraFrameSequenceFrame as aj, type CameraFrameSequenceOptions as ak, type CameraFrameSequenceResult as al, type ControlJointInfo as am, type FrameCaptureStatus as an, type FrameCaptureTarget as ao, type FrameCaptureTargetRef as ap, type Geoms as aq, type IKSolveFn as ar, type IkGizmoDragInput as as, type IkSolveInput as at, type JointInfo as au, type KeyBinding as av, type Keyframes as aw, type ModelOptions as ax, type MujocoContact as ay, type MujocoContactArray as az, type MujocoSimAPI as b, type SiteInfo as b0, type SplatAssetConfig as b1, type SplatScenarioConfig as b2, type StateSnapshot as b3, type TrajectoryData as b4, type TrajectoryFrameCallbackInput as b5, type VisualScenarioMaterialFilterInput as b6, type XmlPatch as b7, getContact as b8, registerRobotResources as b9, type StepCallbackInput as c, type SelectionCallbackInput as d, type MujocoModule as e, type MujocoModel as f, type MujocoData as g, type ControlGroupSelector as h, type ObservationResult as i, type IkContextValue as j, type IkGizmoProps as k, type SceneLightsProps as l, type ScenarioLightingProps as m, type SplatEnvironmentProps as n, type VisualScenarioConfig as o, type SplatRendererKind as p, type PairedSplatEnvironmentConfig as q, type SplatFormat as r, type SplatCollisionProxyConfig as s, type SplatCollisionPrimitive as t, type ScenarioLightingPreset as u, type SplatEnvironmentMetadataInput as v, type SplatEnvironmentMetadata as w, type SplatSceneInput as x, type DebugProps as y, type ContactListenerProps as z };
|
package/package.json
CHANGED
|
@@ -20,6 +20,10 @@ import {
|
|
|
20
20
|
ActuatedJointInfo,
|
|
21
21
|
ActuatorInfo,
|
|
22
22
|
BodyInfo,
|
|
23
|
+
CameraFrameCaptureResult,
|
|
24
|
+
CameraFrameSequenceFrame,
|
|
25
|
+
CameraFrameSequenceOptions,
|
|
26
|
+
CameraFrameSequenceResult,
|
|
23
27
|
ControlGroupInfo,
|
|
24
28
|
ControlGroupSelector,
|
|
25
29
|
ContactInfo,
|
|
@@ -45,6 +49,10 @@ import {
|
|
|
45
49
|
captureFrame as captureCanvasFrame,
|
|
46
50
|
captureFrameBlob as captureCanvasFrameBlob,
|
|
47
51
|
} from '../hooks/useFrameCapture';
|
|
52
|
+
import {
|
|
53
|
+
captureCameraFrame,
|
|
54
|
+
captureCameraFrameBlob,
|
|
55
|
+
} from '../rendering/cameraFrameCapture';
|
|
48
56
|
import {
|
|
49
57
|
loadScene,
|
|
50
58
|
createSceneConfigFromFiles,
|
|
@@ -110,6 +118,12 @@ const _rayGeomId = new Int32Array(1);
|
|
|
110
118
|
const _projRaycaster = new THREE.Raycaster();
|
|
111
119
|
const _projNdc = new THREE.Vector2();
|
|
112
120
|
|
|
121
|
+
function waitForNextAnimationFrame() {
|
|
122
|
+
return new Promise<void>((resolve) => {
|
|
123
|
+
requestAnimationFrame(() => resolve());
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
113
127
|
// ---- Internal context types ----
|
|
114
128
|
|
|
115
129
|
export interface MujocoSimContextValue {
|
|
@@ -256,7 +270,7 @@ export function MujocoSimProvider({
|
|
|
256
270
|
interpolate,
|
|
257
271
|
children,
|
|
258
272
|
}: MujocoSimProviderProps) {
|
|
259
|
-
const { gl, camera } = useThree();
|
|
273
|
+
const { gl, camera, scene } = useThree();
|
|
260
274
|
const [status, setStatus] = useState<'loading' | 'ready' | 'error'>('loading');
|
|
261
275
|
|
|
262
276
|
// --- Refs ---
|
|
@@ -548,6 +562,30 @@ export function MujocoSimProvider({
|
|
|
548
562
|
stepsToRunRef.current = n;
|
|
549
563
|
}, []);
|
|
550
564
|
|
|
565
|
+
const stepImmediately = useCallback((steps = 1) => {
|
|
566
|
+
const model = mjModelRef.current;
|
|
567
|
+
const data = mjDataRef.current;
|
|
568
|
+
if (!model || !data) return false;
|
|
569
|
+
|
|
570
|
+
for (let stepIndex = 0; stepIndex < steps; stepIndex += 1) {
|
|
571
|
+
for (let i = 0; i < model.nv; i += 1) {
|
|
572
|
+
data.qfrc_applied[i] = 0;
|
|
573
|
+
}
|
|
574
|
+
for (const cb of beforeStepCallbacks.current) {
|
|
575
|
+
cb({ model, data });
|
|
576
|
+
}
|
|
577
|
+
mujoco.mj_step(model, data);
|
|
578
|
+
for (const cb of afterStepCallbacks.current) {
|
|
579
|
+
cb({ model, data });
|
|
580
|
+
}
|
|
581
|
+
onStepRef.current?.({ time: data.time, model, data });
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
physicsAccumulatorRef.current = 0;
|
|
585
|
+
interpolationStateRef.current.valid = false;
|
|
586
|
+
return true;
|
|
587
|
+
}, [mujoco]);
|
|
588
|
+
|
|
551
589
|
const getTime = useCallback((): number => {
|
|
552
590
|
return mjDataRef.current?.time ?? 0;
|
|
553
591
|
}, []);
|
|
@@ -1041,6 +1079,81 @@ export function MujocoSimProvider({
|
|
|
1041
1079
|
[gl]
|
|
1042
1080
|
);
|
|
1043
1081
|
|
|
1082
|
+
const captureCameraFrameApi = useCallback(
|
|
1083
|
+
(options = {}) => {
|
|
1084
|
+
return captureCameraFrame(gl, scene, camera, options);
|
|
1085
|
+
},
|
|
1086
|
+
[camera, gl, scene]
|
|
1087
|
+
);
|
|
1088
|
+
|
|
1089
|
+
const captureCameraFrameBlobApi = useCallback(
|
|
1090
|
+
(options = {}) => {
|
|
1091
|
+
return captureCameraFrameBlob(gl, scene, camera, options);
|
|
1092
|
+
},
|
|
1093
|
+
[camera, gl, scene]
|
|
1094
|
+
);
|
|
1095
|
+
|
|
1096
|
+
const recordCameraSequenceApi = useCallback(
|
|
1097
|
+
async (
|
|
1098
|
+
options: CameraFrameSequenceOptions
|
|
1099
|
+
): Promise<CameraFrameSequenceResult> => {
|
|
1100
|
+
const frameCount = Math.max(0, Math.floor(options.frames));
|
|
1101
|
+
const stepsPerFrame = Math.max(1, Math.floor(options.stepsPerFrame ?? 1));
|
|
1102
|
+
const cameras = options.cameras;
|
|
1103
|
+
const frames: CameraFrameSequenceFrame[] = [];
|
|
1104
|
+
const wasPaused = pausedRef.current;
|
|
1105
|
+
|
|
1106
|
+
if (frameCount === 0 || cameras.length === 0) {
|
|
1107
|
+
return {
|
|
1108
|
+
frames,
|
|
1109
|
+
cameraKeys: cameras.map((sequenceCamera) => sequenceCamera.key),
|
|
1110
|
+
frameCount: 0,
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
try {
|
|
1115
|
+
pausedRef.current = true;
|
|
1116
|
+
stepsToRunRef.current = 0;
|
|
1117
|
+
if (options.reset) reset();
|
|
1118
|
+
|
|
1119
|
+
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
|
|
1120
|
+
if (frameIndex > 0 || options.captureInitialFrame === false) {
|
|
1121
|
+
stepImmediately(stepsPerFrame);
|
|
1122
|
+
}
|
|
1123
|
+
await waitForNextAnimationFrame();
|
|
1124
|
+
|
|
1125
|
+
const cameraFrames: Record<string, CameraFrameCaptureResult> = {};
|
|
1126
|
+
for (const sequenceCamera of cameras) {
|
|
1127
|
+
const { key, ...captureOptions } = sequenceCamera;
|
|
1128
|
+
cameraFrames[key] = await captureCameraFrame(
|
|
1129
|
+
gl,
|
|
1130
|
+
scene,
|
|
1131
|
+
camera,
|
|
1132
|
+
captureOptions
|
|
1133
|
+
);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
const frame = {
|
|
1137
|
+
frameIndex,
|
|
1138
|
+
time: getTime(),
|
|
1139
|
+
cameras: cameraFrames,
|
|
1140
|
+
};
|
|
1141
|
+
frames.push(frame);
|
|
1142
|
+
await options.onFrame?.(frame);
|
|
1143
|
+
}
|
|
1144
|
+
} finally {
|
|
1145
|
+
pausedRef.current = wasPaused;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
return {
|
|
1149
|
+
frames,
|
|
1150
|
+
cameraKeys: cameras.map((sequenceCamera) => sequenceCamera.key),
|
|
1151
|
+
frameCount: frames.length,
|
|
1152
|
+
};
|
|
1153
|
+
},
|
|
1154
|
+
[camera, getTime, gl, reset, scene, stepImmediately]
|
|
1155
|
+
);
|
|
1156
|
+
|
|
1044
1157
|
const project2DTo3D = useCallback(
|
|
1045
1158
|
(x: number, y: number, cameraPos: THREE.Vector3, lookAt: THREE.Vector3): { point: THREE.Vector3; bodyId: number; geomId: number } | null => {
|
|
1046
1159
|
const virtCam = (camera as THREE.PerspectiveCamera).clone();
|
|
@@ -1154,6 +1267,9 @@ export function MujocoSimProvider({
|
|
|
1154
1267
|
getCanvasSnapshot,
|
|
1155
1268
|
captureFrame: captureFrameApi,
|
|
1156
1269
|
captureFrameBlob: captureFrameBlobApi,
|
|
1270
|
+
captureCameraFrame: captureCameraFrameApi,
|
|
1271
|
+
captureCameraFrameBlob: captureCameraFrameBlobApi,
|
|
1272
|
+
recordCameraSequence: recordCameraSequenceApi,
|
|
1157
1273
|
project2DTo3D,
|
|
1158
1274
|
setBodyMass,
|
|
1159
1275
|
setGeomFriction,
|
|
@@ -1172,6 +1288,8 @@ export function MujocoSimProvider({
|
|
|
1172
1288
|
raycast, getKeyframeNames, getKeyframeCount, loadSceneApi,
|
|
1173
1289
|
loadFromFilesApi, addBodyApi, removeBodyApi, recompileApi,
|
|
1174
1290
|
getCanvas, getCanvasSnapshot, captureFrameApi, captureFrameBlobApi,
|
|
1291
|
+
captureCameraFrameApi, captureCameraFrameBlobApi,
|
|
1292
|
+
recordCameraSequenceApi,
|
|
1175
1293
|
project2DTo3D,
|
|
1176
1294
|
setBodyMass, setGeomFriction, setGeomSize,
|
|
1177
1295
|
]
|
|
@@ -43,8 +43,10 @@ export type ControllerComponent<TConfig> = React.FC<{
|
|
|
43
43
|
* const MyController = createController<{ speed: number }>(
|
|
44
44
|
* { name: 'my-controller', defaultConfig: { speed: 1.0 } },
|
|
45
45
|
* function MyControllerImpl({ config }) {
|
|
46
|
+
* const wheel = useCtrl(RobotActuators.mobile.leftWheel);
|
|
47
|
+
*
|
|
46
48
|
* useBeforePhysicsStep(({ data }) => {
|
|
47
|
-
*
|
|
49
|
+
* wheel.write(config.speed);
|
|
48
50
|
* });
|
|
49
51
|
* return null;
|
|
50
52
|
* },
|
|
@@ -100,9 +102,11 @@ export function createController<TConfig>(
|
|
|
100
102
|
* { name: 'useMyController', defaultConfig: { gain: 1.0 } },
|
|
101
103
|
* function useMyControllerImpl(config) {
|
|
102
104
|
* // config is MyConfig | null — hooks must be called unconditionally
|
|
105
|
+
* const joint = useCtrl(config?.actuator ?? RobotActuators.franka.actuator1);
|
|
106
|
+
*
|
|
103
107
|
* useBeforePhysicsStep(({ data }) => {
|
|
104
108
|
* if (!config) return;
|
|
105
|
-
*
|
|
109
|
+
* joint.write(config.gain * Math.sin(data.time));
|
|
106
110
|
* });
|
|
107
111
|
* if (!config) return null;
|
|
108
112
|
* return { /* value *\/ };
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* React state wrapper around MuJoCo/R3F offscreen camera-frame capture.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback, useState } from 'react';
|
|
9
|
+
import { useMujoco } from '../core/MujocoSimProvider';
|
|
10
|
+
import type {
|
|
11
|
+
CameraFrameCaptureAPI,
|
|
12
|
+
CameraFrameCaptureOptions,
|
|
13
|
+
FrameCaptureStatus,
|
|
14
|
+
} from '../types';
|
|
15
|
+
|
|
16
|
+
export function useCameraFrameCapture(
|
|
17
|
+
defaultOptions: CameraFrameCaptureOptions = {}
|
|
18
|
+
): CameraFrameCaptureAPI {
|
|
19
|
+
const mujoco = useMujoco();
|
|
20
|
+
const [status, setStatus] = useState<FrameCaptureStatus>('idle');
|
|
21
|
+
const [error, setError] = useState<Error | null>(null);
|
|
22
|
+
|
|
23
|
+
const reset = useCallback(() => {
|
|
24
|
+
setStatus('idle');
|
|
25
|
+
setError(null);
|
|
26
|
+
}, []);
|
|
27
|
+
|
|
28
|
+
const capture = useCallback(
|
|
29
|
+
async (options: CameraFrameCaptureOptions = {}) => {
|
|
30
|
+
if (!mujoco.api) {
|
|
31
|
+
throw new Error('MuJoCo scene is not ready for camera frame capture.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
setStatus('capturing');
|
|
35
|
+
setError(null);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const result = await mujoco.api.captureCameraFrame({
|
|
39
|
+
...defaultOptions,
|
|
40
|
+
...options,
|
|
41
|
+
});
|
|
42
|
+
setStatus('captured');
|
|
43
|
+
return result;
|
|
44
|
+
} catch (nextError) {
|
|
45
|
+
const error =
|
|
46
|
+
nextError instanceof Error
|
|
47
|
+
? nextError
|
|
48
|
+
: new Error('Unable to capture the requested camera frame.');
|
|
49
|
+
setError(error);
|
|
50
|
+
setStatus('error');
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
[defaultOptions, mujoco.api]
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const captureBlob = useCallback(
|
|
58
|
+
async (options: CameraFrameCaptureOptions = {}) => {
|
|
59
|
+
if (!mujoco.api) {
|
|
60
|
+
throw new Error('MuJoCo scene is not ready for camera frame capture.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
setStatus('capturing');
|
|
64
|
+
setError(null);
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
const result = await mujoco.api.captureCameraFrameBlob({
|
|
68
|
+
...defaultOptions,
|
|
69
|
+
...options,
|
|
70
|
+
});
|
|
71
|
+
setStatus('captured');
|
|
72
|
+
return result;
|
|
73
|
+
} catch (nextError) {
|
|
74
|
+
const error =
|
|
75
|
+
nextError instanceof Error
|
|
76
|
+
? nextError
|
|
77
|
+
: new Error('Unable to capture the requested camera frame.');
|
|
78
|
+
setError(error);
|
|
79
|
+
setStatus('error');
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
[defaultOptions, mujoco.api]
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
return {
|
|
87
|
+
status,
|
|
88
|
+
error,
|
|
89
|
+
isCapturing: status === 'capturing',
|
|
90
|
+
capture,
|
|
91
|
+
captureBlob,
|
|
92
|
+
reset,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* React state wrapper around fixed-camera simulation sequence recording.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback, useState } from 'react';
|
|
9
|
+
import { useMujoco } from '../core/MujocoSimProvider';
|
|
10
|
+
import type {
|
|
11
|
+
CameraFrameSequenceOptions,
|
|
12
|
+
CameraFrameSequenceRecorderAPI,
|
|
13
|
+
FrameCaptureStatus,
|
|
14
|
+
} from '../types';
|
|
15
|
+
|
|
16
|
+
export function useCameraSequenceRecorder(): CameraFrameSequenceRecorderAPI {
|
|
17
|
+
const mujoco = useMujoco();
|
|
18
|
+
const [status, setStatus] = useState<FrameCaptureStatus>('idle');
|
|
19
|
+
const [error, setError] = useState<Error | null>(null);
|
|
20
|
+
|
|
21
|
+
const reset = useCallback(() => {
|
|
22
|
+
setStatus('idle');
|
|
23
|
+
setError(null);
|
|
24
|
+
}, []);
|
|
25
|
+
|
|
26
|
+
const record = useCallback(
|
|
27
|
+
async (options: CameraFrameSequenceOptions) => {
|
|
28
|
+
if (!mujoco.api) {
|
|
29
|
+
throw new Error('MuJoCo scene is not ready for camera sequence recording.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
setStatus('capturing');
|
|
33
|
+
setError(null);
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const result = await mujoco.api.recordCameraSequence(options);
|
|
37
|
+
setStatus('captured');
|
|
38
|
+
return result;
|
|
39
|
+
} catch (nextError) {
|
|
40
|
+
const error =
|
|
41
|
+
nextError instanceof Error
|
|
42
|
+
? nextError
|
|
43
|
+
: new Error('Unable to record the requested camera sequence.');
|
|
44
|
+
setError(error);
|
|
45
|
+
setStatus('error');
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
[mujoco.api]
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
status,
|
|
54
|
+
error,
|
|
55
|
+
isRecording: status === 'capturing',
|
|
56
|
+
record,
|
|
57
|
+
reset,
|
|
58
|
+
};
|
|
59
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -82,6 +82,13 @@ export {
|
|
|
82
82
|
captureFrameBlob,
|
|
83
83
|
useFrameCapture,
|
|
84
84
|
} from './hooks/useFrameCapture';
|
|
85
|
+
export { useCameraFrameCapture } from './hooks/useCameraFrameCapture';
|
|
86
|
+
export { useCameraSequenceRecorder } from './hooks/useCameraSequenceRecorder';
|
|
87
|
+
export {
|
|
88
|
+
captureCameraFrame,
|
|
89
|
+
captureCameraFrameBlob,
|
|
90
|
+
renderCameraFrameToCanvas,
|
|
91
|
+
} from './rendering/cameraFrameCapture';
|
|
85
92
|
export { useCtrlNoise } from './hooks/useCtrlNoise';
|
|
86
93
|
export { useBodyMeshes } from './hooks/useBodyMeshes';
|
|
87
94
|
export { useSelectionHighlight } from './hooks/useSelectionHighlight';
|
|
@@ -181,6 +188,17 @@ export type {
|
|
|
181
188
|
// API
|
|
182
189
|
MujocoSimAPI,
|
|
183
190
|
MujocoFrameCaptureOptions,
|
|
191
|
+
CameraFrameCaptureAPI,
|
|
192
|
+
CameraFrameCaptureBlobResult,
|
|
193
|
+
CameraFrameCaptureOptions,
|
|
194
|
+
CameraFrameCaptureQuaternion,
|
|
195
|
+
CameraFrameCaptureResult,
|
|
196
|
+
CameraFrameCaptureVector3,
|
|
197
|
+
CameraFrameSequenceCamera,
|
|
198
|
+
CameraFrameSequenceFrame,
|
|
199
|
+
CameraFrameSequenceOptions,
|
|
200
|
+
CameraFrameSequenceRecorderAPI,
|
|
201
|
+
CameraFrameSequenceResult,
|
|
184
202
|
MujocoCanvasProps,
|
|
185
203
|
MujocoContextValue,
|
|
186
204
|
// Hook return types
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* Offscreen camera-frame capture for R3F/MuJoCo scenes.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as THREE from 'three';
|
|
9
|
+
import type {
|
|
10
|
+
CameraFrameCaptureBlobResult,
|
|
11
|
+
CameraFrameCaptureOptions,
|
|
12
|
+
CameraFrameCaptureResult,
|
|
13
|
+
CameraFrameCaptureVector3,
|
|
14
|
+
} from '../types';
|
|
15
|
+
|
|
16
|
+
function toVector3(
|
|
17
|
+
value: CameraFrameCaptureVector3 | undefined,
|
|
18
|
+
fallback: THREE.Vector3
|
|
19
|
+
): THREE.Vector3 {
|
|
20
|
+
if (!value) return fallback.clone();
|
|
21
|
+
return value instanceof THREE.Vector3
|
|
22
|
+
? value.clone()
|
|
23
|
+
: new THREE.Vector3(value[0], value[1], value[2]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function applyCameraPose(
|
|
27
|
+
camera: THREE.Camera,
|
|
28
|
+
options: CameraFrameCaptureOptions,
|
|
29
|
+
fallbackCamera: THREE.Camera
|
|
30
|
+
) {
|
|
31
|
+
camera.position.copy(toVector3(options.position, fallbackCamera.position));
|
|
32
|
+
camera.up.copy(toVector3(options.up, fallbackCamera.up));
|
|
33
|
+
|
|
34
|
+
if (options.quaternion) {
|
|
35
|
+
if (options.quaternion instanceof THREE.Quaternion) {
|
|
36
|
+
camera.quaternion.copy(options.quaternion);
|
|
37
|
+
} else {
|
|
38
|
+
camera.quaternion.set(
|
|
39
|
+
options.quaternion[0],
|
|
40
|
+
options.quaternion[1],
|
|
41
|
+
options.quaternion[2],
|
|
42
|
+
options.quaternion[3]
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
} else if (options.lookAt) {
|
|
46
|
+
camera.lookAt(toVector3(options.lookAt, new THREE.Vector3()));
|
|
47
|
+
} else {
|
|
48
|
+
camera.quaternion.copy(fallbackCamera.quaternion);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
camera.updateMatrixWorld();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createCaptureCamera(
|
|
55
|
+
options: CameraFrameCaptureOptions,
|
|
56
|
+
fallbackCamera: THREE.Camera,
|
|
57
|
+
width: number,
|
|
58
|
+
height: number
|
|
59
|
+
): THREE.Camera {
|
|
60
|
+
const camera = options.camera
|
|
61
|
+
? options.camera.clone()
|
|
62
|
+
: fallbackCamera instanceof THREE.PerspectiveCamera
|
|
63
|
+
? fallbackCamera.clone()
|
|
64
|
+
: new THREE.PerspectiveCamera(45, width / height, 0.01, 100);
|
|
65
|
+
|
|
66
|
+
if (camera instanceof THREE.PerspectiveCamera) {
|
|
67
|
+
camera.aspect = width / height;
|
|
68
|
+
camera.fov = options.fov ?? camera.fov;
|
|
69
|
+
camera.near = options.near ?? camera.near;
|
|
70
|
+
camera.far = options.far ?? camera.far;
|
|
71
|
+
camera.updateProjectionMatrix();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
applyCameraPose(camera, options, fallbackCamera);
|
|
75
|
+
return camera;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function readRenderTargetToCanvas(
|
|
79
|
+
renderer: THREE.WebGLRenderer,
|
|
80
|
+
target: THREE.WebGLRenderTarget,
|
|
81
|
+
width: number,
|
|
82
|
+
height: number
|
|
83
|
+
) {
|
|
84
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
85
|
+
renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
|
|
86
|
+
|
|
87
|
+
const canvas = document.createElement('canvas');
|
|
88
|
+
canvas.width = width;
|
|
89
|
+
canvas.height = height;
|
|
90
|
+
const context = canvas.getContext('2d');
|
|
91
|
+
if (!context) {
|
|
92
|
+
throw new Error('Unable to create a 2D canvas for camera frame capture.');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const imageData = context.createImageData(width, height);
|
|
96
|
+
const rowBytes = width * 4;
|
|
97
|
+
for (let y = 0; y < height; y += 1) {
|
|
98
|
+
const sourceStart = (height - y - 1) * rowBytes;
|
|
99
|
+
const targetStart = y * rowBytes;
|
|
100
|
+
imageData.data.set(
|
|
101
|
+
pixels.subarray(sourceStart, sourceStart + rowBytes),
|
|
102
|
+
targetStart
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
context.putImageData(imageData, 0, 0);
|
|
106
|
+
return canvas;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function renderCameraFrameToCanvas(
|
|
110
|
+
renderer: THREE.WebGLRenderer,
|
|
111
|
+
scene: THREE.Scene,
|
|
112
|
+
fallbackCamera: THREE.Camera,
|
|
113
|
+
options: CameraFrameCaptureOptions = {}
|
|
114
|
+
) {
|
|
115
|
+
const width = Math.max(1, Math.floor(options.width ?? renderer.domElement.width));
|
|
116
|
+
const height = Math.max(1, Math.floor(options.height ?? renderer.domElement.height));
|
|
117
|
+
const camera = createCaptureCamera(options, fallbackCamera, width, height);
|
|
118
|
+
const target = new THREE.WebGLRenderTarget(width, height, {
|
|
119
|
+
format: THREE.RGBAFormat,
|
|
120
|
+
type: THREE.UnsignedByteType,
|
|
121
|
+
});
|
|
122
|
+
const previousTarget = renderer.getRenderTarget();
|
|
123
|
+
const previousXrEnabled = renderer.xr.enabled;
|
|
124
|
+
|
|
125
|
+
scene.updateMatrixWorld(true);
|
|
126
|
+
try {
|
|
127
|
+
renderer.xr.enabled = false;
|
|
128
|
+
renderer.setRenderTarget(target);
|
|
129
|
+
renderer.clear();
|
|
130
|
+
renderer.render(scene, camera);
|
|
131
|
+
const canvas = readRenderTargetToCanvas(renderer, target, width, height);
|
|
132
|
+
return { canvas, camera, width, height };
|
|
133
|
+
} finally {
|
|
134
|
+
renderer.setRenderTarget(previousTarget);
|
|
135
|
+
renderer.xr.enabled = previousXrEnabled;
|
|
136
|
+
target.dispose();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export async function captureCameraFrame(
|
|
141
|
+
renderer: THREE.WebGLRenderer,
|
|
142
|
+
scene: THREE.Scene,
|
|
143
|
+
fallbackCamera: THREE.Camera,
|
|
144
|
+
options: CameraFrameCaptureOptions = {}
|
|
145
|
+
): Promise<CameraFrameCaptureResult> {
|
|
146
|
+
const type = options.type ?? 'image/png';
|
|
147
|
+
const result = renderCameraFrameToCanvas(
|
|
148
|
+
renderer,
|
|
149
|
+
scene,
|
|
150
|
+
fallbackCamera,
|
|
151
|
+
options
|
|
152
|
+
);
|
|
153
|
+
return {
|
|
154
|
+
...result,
|
|
155
|
+
dataUrl: result.canvas.toDataURL(type, options.quality),
|
|
156
|
+
type,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export async function captureCameraFrameBlob(
|
|
161
|
+
renderer: THREE.WebGLRenderer,
|
|
162
|
+
scene: THREE.Scene,
|
|
163
|
+
fallbackCamera: THREE.Camera,
|
|
164
|
+
options: CameraFrameCaptureOptions = {}
|
|
165
|
+
): Promise<CameraFrameCaptureBlobResult> {
|
|
166
|
+
const type = options.type ?? 'image/png';
|
|
167
|
+
const result = renderCameraFrameToCanvas(
|
|
168
|
+
renderer,
|
|
169
|
+
scene,
|
|
170
|
+
fallbackCamera,
|
|
171
|
+
options
|
|
172
|
+
);
|
|
173
|
+
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
174
|
+
result.canvas.toBlob(
|
|
175
|
+
(nextBlob) => {
|
|
176
|
+
if (nextBlob) resolve(nextBlob);
|
|
177
|
+
else reject(new Error('Camera frame capture did not produce a Blob.'));
|
|
178
|
+
},
|
|
179
|
+
type,
|
|
180
|
+
options.quality
|
|
181
|
+
);
|
|
182
|
+
});
|
|
183
|
+
return { ...result, blob, type };
|
|
184
|
+
}
|