mujoco-react 9.0.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.
@@ -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
+ }
package/src/types.ts CHANGED
@@ -1100,7 +1100,13 @@ export interface MujocoSimAPI {
1100
1100
  recompile(patches?: XmlPatch[]): Promise<void>;
1101
1101
 
1102
1102
  // Canvas
1103
+ getCanvas(): HTMLCanvasElement | null;
1103
1104
  getCanvasSnapshot(width?: number, height?: number, mimeType?: string): string;
1105
+ captureFrame(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureResult>;
1106
+ captureFrameBlob(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureBlobResult>;
1107
+ captureCameraFrame(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureResult>;
1108
+ captureCameraFrameBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
1109
+ recordCameraSequence(options: CameraFrameSequenceOptions): Promise<CameraFrameSequenceResult>;
1104
1110
  project2DTo3D(
1105
1111
  x: number,
1106
1112
  y: number,
@@ -1118,6 +1124,136 @@ export interface MujocoSimAPI {
1118
1124
  readonly mjDataRef: React.RefObject<MujocoData | null>;
1119
1125
  }
1120
1126
 
1127
+ export type FrameCaptureStatus = 'idle' | 'capturing' | 'captured' | 'error';
1128
+
1129
+ export type FrameCaptureTarget =
1130
+ | HTMLCanvasElement
1131
+ | HTMLElement
1132
+ | null
1133
+ | undefined;
1134
+
1135
+ export type FrameCaptureTargetRef =
1136
+ React.RefObject<HTMLCanvasElement | HTMLElement | null>;
1137
+
1138
+ export interface FrameCaptureOptions {
1139
+ target?: FrameCaptureTarget | FrameCaptureTargetRef;
1140
+ type?: string;
1141
+ quality?: number;
1142
+ waitForAnimationFrame?: boolean;
1143
+ }
1144
+
1145
+ export type MujocoFrameCaptureOptions = Omit<FrameCaptureOptions, 'target'>;
1146
+
1147
+ export interface FrameCaptureResult {
1148
+ canvas: HTMLCanvasElement;
1149
+ dataUrl: string;
1150
+ type: string;
1151
+ }
1152
+
1153
+ export interface FrameCaptureBlobResult {
1154
+ canvas: HTMLCanvasElement;
1155
+ blob: Blob;
1156
+ type: string;
1157
+ }
1158
+
1159
+ export interface FrameCaptureAPI {
1160
+ status: FrameCaptureStatus;
1161
+ error: Error | null;
1162
+ isCapturing: boolean;
1163
+ capture: (options?: FrameCaptureOptions) => Promise<FrameCaptureResult>;
1164
+ captureBlob: (
1165
+ options?: FrameCaptureOptions
1166
+ ) => Promise<FrameCaptureBlobResult>;
1167
+ reset: () => void;
1168
+ }
1169
+
1170
+ export type CameraFrameCaptureVector3 =
1171
+ | THREE.Vector3
1172
+ | readonly [number, number, number];
1173
+
1174
+ export type CameraFrameCaptureQuaternion =
1175
+ | THREE.Quaternion
1176
+ | readonly [number, number, number, number];
1177
+
1178
+ export interface CameraFrameCaptureOptions {
1179
+ camera?: THREE.Camera;
1180
+ position?: CameraFrameCaptureVector3;
1181
+ lookAt?: CameraFrameCaptureVector3;
1182
+ quaternion?: CameraFrameCaptureQuaternion;
1183
+ up?: CameraFrameCaptureVector3;
1184
+ width?: number;
1185
+ height?: number;
1186
+ type?: string;
1187
+ quality?: number;
1188
+ fov?: number;
1189
+ near?: number;
1190
+ far?: number;
1191
+ }
1192
+
1193
+ export interface CameraFrameCaptureResult {
1194
+ canvas: HTMLCanvasElement;
1195
+ camera: THREE.Camera;
1196
+ dataUrl: string;
1197
+ type: string;
1198
+ width: number;
1199
+ height: number;
1200
+ }
1201
+
1202
+ export interface CameraFrameCaptureBlobResult {
1203
+ canvas: HTMLCanvasElement;
1204
+ camera: THREE.Camera;
1205
+ blob: Blob;
1206
+ type: string;
1207
+ width: number;
1208
+ height: number;
1209
+ }
1210
+
1211
+ export interface CameraFrameCaptureAPI {
1212
+ status: FrameCaptureStatus;
1213
+ error: Error | null;
1214
+ isCapturing: boolean;
1215
+ capture: (
1216
+ options?: CameraFrameCaptureOptions
1217
+ ) => Promise<CameraFrameCaptureResult>;
1218
+ captureBlob: (
1219
+ options?: CameraFrameCaptureOptions
1220
+ ) => Promise<CameraFrameCaptureBlobResult>;
1221
+ reset: () => void;
1222
+ }
1223
+
1224
+ export interface CameraFrameSequenceCamera extends CameraFrameCaptureOptions {
1225
+ key: string;
1226
+ }
1227
+
1228
+ export interface CameraFrameSequenceFrame {
1229
+ frameIndex: number;
1230
+ time: number;
1231
+ cameras: Record<string, CameraFrameCaptureResult>;
1232
+ }
1233
+
1234
+ export interface CameraFrameSequenceOptions {
1235
+ cameras: readonly CameraFrameSequenceCamera[];
1236
+ frames: number;
1237
+ stepsPerFrame?: number;
1238
+ reset?: boolean;
1239
+ captureInitialFrame?: boolean;
1240
+ onFrame?: (frame: CameraFrameSequenceFrame) => void | Promise<void>;
1241
+ }
1242
+
1243
+ export interface CameraFrameSequenceResult {
1244
+ frames: CameraFrameSequenceFrame[];
1245
+ cameraKeys: string[];
1246
+ frameCount: number;
1247
+ }
1248
+
1249
+ export interface CameraFrameSequenceRecorderAPI {
1250
+ status: FrameCaptureStatus;
1251
+ error: Error | null;
1252
+ isRecording: boolean;
1253
+ record: (options: CameraFrameSequenceOptions) => Promise<CameraFrameSequenceResult>;
1254
+ reset: () => void;
1255
+ }
1256
+
1121
1257
  // ---- Canvas Props ----
1122
1258
 
1123
1259
  export type MujocoCanvasProps = Omit<CanvasProps, 'onError'> & {