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.
- package/README.md +45 -10
- package/dist/index.d.ts +43 -33
- package/dist/index.js +547 -195
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +1 -1
- package/dist/{types-izZlUweI.d.ts → types-S8ggQY2n.d.ts} +103 -1
- package/package.json +1 -1
- package/src/core/MujocoSimProvider.tsx +146 -2
- package/src/core/createController.tsx +6 -2
- package/src/hooks/useCameraFrameCapture.ts +94 -0
- package/src/hooks/useCameraSequenceRecorder.ts +59 -0
- package/src/hooks/useFrameCapture.ts +8 -42
- package/src/index.ts +26 -9
- package/src/rendering/cameraFrameCapture.ts +184 -0
- package/src/types.ts +136 -0
|
@@ -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'> & {
|