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/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-C5gTvR7b.js';
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 ResetCallbackInput as aA, type ResourceSelector as aB, RobotActuators as aC, RobotBodies as aD, RobotGeoms as aE, RobotJoints as aF, RobotKeyframes as aG, type RobotResource as aH, RobotResources as aI, RobotSensors as aJ, RobotSites as aK, type Robots as aL, type ScenarioCameraConfig as aM, type ScenarioMaterialConfig as aN, type SceneMarker as aO, type SceneObject as aP, type SensorResult as aQ, type SiteInfo as aR, type SplatAssetConfig as aS, type SplatScenarioConfig as aT, type StateSnapshot as aU, type TrajectoryData as aV, type TrajectoryFrameCallbackInput as aW, type VisualScenarioMaterialFilterInput as aX, type XmlPatch as aY, getContact as aZ, registerRobotResources as a_, type BodyInfo as aa, type ControlJointInfo as ab, type FrameCaptureStatus as ac, type FrameCaptureTarget as ad, type FrameCaptureTargetRef as ae, type Geoms as af, type IKSolveFn as ag, type IkGizmoDragInput as ah, type IkSolveInput as ai, type JointInfo as aj, type KeyBinding as ak, type Keyframes as al, type ModelOptions as am, type MujocoContact as an, type MujocoContactArray as ao, type MujocoFrameCaptureOptions as ap, type ObservationLayoutItem as aq, type ObservationOutput as ar, type PhysicsConfig as as, type PhysicsStepInput as at, type PolicyActionInput as au, type PolicyInferenceInput as av, type PolicyObservationInput as aw, type RayHit as ax, type Register as ay, type RegisteredRobotMap as az, type MujocoSimAPI as b, 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 };
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mujoco-react",
3
- "version": "9.1.0",
3
+ "version": "9.2.0",
4
4
  "description": "Composable React Three Fiber building blocks for MuJoCo WASM simulations",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -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
- * data.ctrl[0] = config.speed;
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
- * data.ctrl[0] = config.gain * Math.sin(data.time);
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
+ }