mujoco-react 9.2.0 → 9.4.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 +225 -15
- package/dist/{chunk-33CV6HSV.js → chunk-VDSEPZYQ.js} +303 -14
- package/dist/chunk-VDSEPZYQ.js.map +1 -0
- package/dist/index.d.ts +274 -7
- package/dist/index.js +1172 -131
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +24 -2
- package/dist/spark.js +89 -3
- package/dist/spark.js.map +1 -1
- package/dist/{types-S8ggQY2n.d.ts → types-BuJ4boaq.d.ts} +160 -5
- package/dist/vite.d.ts +1 -1
- package/dist/vite.js +14 -7
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/src/components/SplatCollisionProxyPreview.tsx +350 -0
- package/src/components/VisualScenario.tsx +287 -11
- package/src/core/MujocoSimProvider.tsx +374 -30
- package/src/core/SceneLoader.ts +13 -0
- package/src/hooks/useMountedCameraSequenceRecorder.ts +155 -0
- package/src/index.ts +80 -0
- package/src/rendering/cameraFrameCapture.ts +195 -26
- package/src/rendering/cameraFrameSource.ts +747 -0
- package/src/spark.tsx +144 -0
- package/src/types.ts +166 -4
- package/src/vite.ts +14 -6
- package/dist/chunk-33CV6HSV.js.map +0 -1
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* React state wrapper for named MuJoCo camera/site/body sequence recording.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { useCallback, useState } from 'react';
|
|
9
|
+
import { useMujoco } from '../core/MujocoSimProvider';
|
|
10
|
+
import {
|
|
11
|
+
createMountedCameraFrameSequenceReadiness,
|
|
12
|
+
createMountedCameraFrameSequencePlanFromApi,
|
|
13
|
+
recordMountedCameraFrameSequence,
|
|
14
|
+
type MountedCameraFrameSequencePlan,
|
|
15
|
+
type MountedCameraFrameSequencePlanOptions,
|
|
16
|
+
type MountedCameraFrameSequenceReadiness,
|
|
17
|
+
type MountedCameraFrameSequenceRecordOptions,
|
|
18
|
+
type MountedCameraFrameSequenceRecordResult,
|
|
19
|
+
} from '../rendering/cameraFrameSource';
|
|
20
|
+
import type {
|
|
21
|
+
CameraFrameSequenceRecorderAPI,
|
|
22
|
+
FrameCaptureStatus,
|
|
23
|
+
} from '../types';
|
|
24
|
+
|
|
25
|
+
export type MountedCameraSequencePlanOptions =
|
|
26
|
+
MountedCameraFrameSequencePlanOptions;
|
|
27
|
+
export type MountedCameraSequenceRecordOptions =
|
|
28
|
+
MountedCameraFrameSequenceRecordOptions;
|
|
29
|
+
export type MountedCameraSequenceRecordResult =
|
|
30
|
+
MountedCameraFrameSequenceRecordResult;
|
|
31
|
+
export type MountedCameraSequenceReadiness =
|
|
32
|
+
MountedCameraFrameSequenceReadiness;
|
|
33
|
+
|
|
34
|
+
export interface MountedCameraSequenceRecorderAPI
|
|
35
|
+
extends Omit<CameraFrameSequenceRecorderAPI, 'record'> {
|
|
36
|
+
plan: MountedCameraFrameSequencePlan | null;
|
|
37
|
+
readiness: MountedCameraSequenceReadiness | null;
|
|
38
|
+
result: MountedCameraSequenceRecordResult | null;
|
|
39
|
+
createPlan: (
|
|
40
|
+
cameraKeys: readonly string[],
|
|
41
|
+
options?: MountedCameraSequencePlanOptions
|
|
42
|
+
) => MountedCameraFrameSequencePlan;
|
|
43
|
+
checkReadiness: (
|
|
44
|
+
cameraKeys: readonly string[],
|
|
45
|
+
options?: MountedCameraSequencePlanOptions
|
|
46
|
+
) => MountedCameraSequenceReadiness;
|
|
47
|
+
record: (
|
|
48
|
+
options: MountedCameraSequenceRecordOptions
|
|
49
|
+
) => Promise<MountedCameraSequenceRecordResult>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function useMountedCameraSequenceRecorder(
|
|
53
|
+
defaultOptions: MountedCameraSequencePlanOptions = {}
|
|
54
|
+
): MountedCameraSequenceRecorderAPI {
|
|
55
|
+
const mujoco = useMujoco();
|
|
56
|
+
const [status, setStatus] = useState<FrameCaptureStatus>('idle');
|
|
57
|
+
const [error, setError] = useState<Error | null>(null);
|
|
58
|
+
const [plan, setPlan] = useState<MountedCameraFrameSequencePlan | null>(null);
|
|
59
|
+
const [readiness, setReadiness] =
|
|
60
|
+
useState<MountedCameraSequenceReadiness | null>(null);
|
|
61
|
+
const [result, setResult] = useState<MountedCameraSequenceRecordResult | null>(
|
|
62
|
+
null
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
const reset = useCallback(() => {
|
|
66
|
+
setStatus('idle');
|
|
67
|
+
setError(null);
|
|
68
|
+
setPlan(null);
|
|
69
|
+
setReadiness(null);
|
|
70
|
+
setResult(null);
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
const createPlan = useCallback(
|
|
74
|
+
(
|
|
75
|
+
cameraKeys: readonly string[],
|
|
76
|
+
options: MountedCameraSequencePlanOptions = {}
|
|
77
|
+
) => {
|
|
78
|
+
if (!mujoco.api) {
|
|
79
|
+
throw new Error('MuJoCo scene is not ready for mounted camera sequence planning.');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const nextPlan = createMountedCameraFrameSequencePlanFromApi(
|
|
83
|
+
mujoco.api,
|
|
84
|
+
cameraKeys,
|
|
85
|
+
{
|
|
86
|
+
...defaultOptions,
|
|
87
|
+
...options,
|
|
88
|
+
}
|
|
89
|
+
);
|
|
90
|
+
setPlan(nextPlan);
|
|
91
|
+
setReadiness(null);
|
|
92
|
+
return nextPlan;
|
|
93
|
+
},
|
|
94
|
+
[defaultOptions, mujoco.api]
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const checkReadiness = useCallback(
|
|
98
|
+
(
|
|
99
|
+
cameraKeys: readonly string[],
|
|
100
|
+
options: MountedCameraSequencePlanOptions = {}
|
|
101
|
+
) => {
|
|
102
|
+
const nextPlan = createPlan(cameraKeys, options);
|
|
103
|
+
const nextReadiness = createMountedCameraFrameSequenceReadiness(nextPlan);
|
|
104
|
+
setReadiness(nextReadiness);
|
|
105
|
+
return nextReadiness;
|
|
106
|
+
},
|
|
107
|
+
[createPlan]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const record = useCallback(
|
|
111
|
+
async (options: MountedCameraSequenceRecordOptions) => {
|
|
112
|
+
if (!mujoco.api) {
|
|
113
|
+
throw new Error('MuJoCo scene is not ready for mounted camera sequence recording.');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
setStatus('capturing');
|
|
117
|
+
setError(null);
|
|
118
|
+
setResult(null);
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
const nextResult = await recordMountedCameraFrameSequence(mujoco.api, {
|
|
122
|
+
...defaultOptions,
|
|
123
|
+
...options,
|
|
124
|
+
});
|
|
125
|
+
setPlan(nextResult.plan);
|
|
126
|
+
setReadiness(nextResult.readiness);
|
|
127
|
+
setResult(nextResult);
|
|
128
|
+
setStatus('captured');
|
|
129
|
+
return nextResult;
|
|
130
|
+
} catch (nextError) {
|
|
131
|
+
const error =
|
|
132
|
+
nextError instanceof Error
|
|
133
|
+
? nextError
|
|
134
|
+
: new Error('Unable to record the requested mounted camera sequence.');
|
|
135
|
+
setError(error);
|
|
136
|
+
setStatus('error');
|
|
137
|
+
throw error;
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
[defaultOptions, mujoco.api]
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
status,
|
|
145
|
+
error,
|
|
146
|
+
plan,
|
|
147
|
+
readiness,
|
|
148
|
+
result,
|
|
149
|
+
isRecording: status === 'capturing',
|
|
150
|
+
createPlan,
|
|
151
|
+
checkReadiness,
|
|
152
|
+
record,
|
|
153
|
+
reset,
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -48,12 +48,32 @@ export {
|
|
|
48
48
|
createPairedSplatEnvironment,
|
|
49
49
|
createSparkSplatViewerUrl,
|
|
50
50
|
createSplatEnvironmentUserData,
|
|
51
|
+
createSplatSceneConfig,
|
|
52
|
+
createVisualScenarioExecutionContext,
|
|
53
|
+
getSplatEnvironmentReadiness,
|
|
51
54
|
getScenarioBackground,
|
|
52
55
|
getScenarioCameraPosition,
|
|
53
56
|
useSplatEnvironment,
|
|
57
|
+
useSplatSceneConfig,
|
|
58
|
+
useVisualScenarioExecutionContext,
|
|
54
59
|
useVisualScenarioEffects,
|
|
55
60
|
withSplatEnvironment,
|
|
56
61
|
} from './components/VisualScenario';
|
|
62
|
+
export {
|
|
63
|
+
canFetchSplatCollisionProxyXml,
|
|
64
|
+
fetchSplatCollisionProxyXml,
|
|
65
|
+
parseSplatCollisionProxyGeoms,
|
|
66
|
+
SplatCollisionProxyPreview,
|
|
67
|
+
useSplatCollisionProxyGeoms,
|
|
68
|
+
} from './components/SplatCollisionProxyPreview';
|
|
69
|
+
export type {
|
|
70
|
+
SplatCollisionProxyGeomPreview,
|
|
71
|
+
SplatCollisionProxyGeomsState,
|
|
72
|
+
SplatCollisionProxyPreviewProps,
|
|
73
|
+
SplatCollisionProxyPreviewStatus,
|
|
74
|
+
SplatCollisionProxyPreviewVector3,
|
|
75
|
+
UseSplatCollisionProxyGeomsOptions,
|
|
76
|
+
} from './components/SplatCollisionProxyPreview';
|
|
57
77
|
export { Debug } from './components/Debug';
|
|
58
78
|
export { TendonRenderer } from './components/TendonRenderer';
|
|
59
79
|
export { FlexRenderer } from './components/FlexRenderer';
|
|
@@ -84,11 +104,56 @@ export {
|
|
|
84
104
|
} from './hooks/useFrameCapture';
|
|
85
105
|
export { useCameraFrameCapture } from './hooks/useCameraFrameCapture';
|
|
86
106
|
export { useCameraSequenceRecorder } from './hooks/useCameraSequenceRecorder';
|
|
107
|
+
export { useMountedCameraSequenceRecorder } from './hooks/useMountedCameraSequenceRecorder';
|
|
108
|
+
export type {
|
|
109
|
+
MountedCameraSequencePlanOptions,
|
|
110
|
+
MountedCameraSequenceRecorderAPI,
|
|
111
|
+
MountedCameraSequenceReadiness,
|
|
112
|
+
MountedCameraSequenceRecordOptions,
|
|
113
|
+
MountedCameraSequenceRecordResult,
|
|
114
|
+
} from './hooks/useMountedCameraSequenceRecorder';
|
|
87
115
|
export {
|
|
88
116
|
captureCameraFrame,
|
|
89
117
|
captureCameraFrameBlob,
|
|
118
|
+
createCameraFrameCaptureSession,
|
|
90
119
|
renderCameraFrameToCanvas,
|
|
91
120
|
} from './rendering/cameraFrameCapture';
|
|
121
|
+
export {
|
|
122
|
+
createMountedCameraFrameSequenceManifest,
|
|
123
|
+
createMountedCameraFrameSequenceReadiness,
|
|
124
|
+
createMountedCameraFrameSourceSuggestions,
|
|
125
|
+
MountedCameraFrameSequenceManifestStatus,
|
|
126
|
+
MountedCameraFrameSequenceReadinessStatus,
|
|
127
|
+
MountedCameraFrameSourceSuggestionMatch,
|
|
128
|
+
createMountedCameraFrameSequencePlanFromApi,
|
|
129
|
+
createMountedCameraFrameSequencePlan,
|
|
130
|
+
getCameraFrameCaptureSourceTarget,
|
|
131
|
+
getMountedCameraFrameCaptureSource,
|
|
132
|
+
isMountedCameraFrameCaptureSource,
|
|
133
|
+
recordMountedCameraFrameSequence,
|
|
134
|
+
resolveMountedCameraFrameSource,
|
|
135
|
+
} from './rendering/cameraFrameSource';
|
|
136
|
+
export type {
|
|
137
|
+
CameraFrameMountSelector,
|
|
138
|
+
CreateMountedCameraFrameSequenceManifestOptions,
|
|
139
|
+
CreateMountedCameraFrameSequencePlanOptions,
|
|
140
|
+
MountedCameraFrameCaptureSource,
|
|
141
|
+
MountedCameraFrameSequenceManifest,
|
|
142
|
+
MountedCameraFrameSequencePlanOptions,
|
|
143
|
+
MountedCameraFrameSequenceRecorderTarget,
|
|
144
|
+
MountedCameraFrameSequenceCameraOptions,
|
|
145
|
+
MountedCameraFrameSequenceDefaults,
|
|
146
|
+
MountedCameraFrameSequencePlan,
|
|
147
|
+
MountedCameraFrameSequenceReadiness,
|
|
148
|
+
MountedCameraFrameSequenceRecordOptions,
|
|
149
|
+
MountedCameraFrameSequenceRecordResult,
|
|
150
|
+
MountedCameraFrameSequenceSourceReadiness,
|
|
151
|
+
MountedCameraFrameSequenceStreamSummary,
|
|
152
|
+
MountedCameraFrameSourceSuggestion,
|
|
153
|
+
NamedCameraFrameResource,
|
|
154
|
+
ResolveMountedCameraFrameSourceOptions,
|
|
155
|
+
ResolvedMountedCameraFrameSource,
|
|
156
|
+
} from './rendering/cameraFrameSource';
|
|
92
157
|
export { useCtrlNoise } from './hooks/useCtrlNoise';
|
|
93
158
|
export { useBodyMeshes } from './hooks/useBodyMeshes';
|
|
94
159
|
export { useSelectionHighlight } from './hooks/useSelectionHighlight';
|
|
@@ -130,6 +195,7 @@ export type {
|
|
|
130
195
|
SiteInfo,
|
|
131
196
|
ActuatorInfo,
|
|
132
197
|
SensorInfo,
|
|
198
|
+
CameraInfo,
|
|
133
199
|
// Contacts
|
|
134
200
|
ContactInfo,
|
|
135
201
|
// Raycast
|
|
@@ -172,8 +238,15 @@ export type {
|
|
|
172
238
|
SplatScenarioConfig,
|
|
173
239
|
SplatCollisionProxyConfig,
|
|
174
240
|
PairedSplatEnvironmentConfig,
|
|
241
|
+
SplatEnvironmentReadiness,
|
|
175
242
|
SplatEnvironmentMetadataInput,
|
|
176
243
|
SplatEnvironmentMetadata,
|
|
244
|
+
VisualScenarioExecutionContext,
|
|
245
|
+
VisualScenarioExecutionContextInput,
|
|
246
|
+
ResolvedScenarioCameraConfig,
|
|
247
|
+
ResolvedScenarioMaterialConfig,
|
|
248
|
+
SplatSceneConfigInput,
|
|
249
|
+
SplatSceneConfigState,
|
|
177
250
|
SplatSceneInput,
|
|
178
251
|
VisualScenarioConfig,
|
|
179
252
|
ScenarioLightingProps,
|
|
@@ -193,12 +266,16 @@ export type {
|
|
|
193
266
|
CameraFrameCaptureOptions,
|
|
194
267
|
CameraFrameCaptureQuaternion,
|
|
195
268
|
CameraFrameCaptureResult,
|
|
269
|
+
CameraFrameCaptureSource,
|
|
196
270
|
CameraFrameCaptureVector3,
|
|
197
271
|
CameraFrameSequenceCamera,
|
|
272
|
+
CameraFrameSequenceCameraSummary,
|
|
198
273
|
CameraFrameSequenceFrame,
|
|
199
274
|
CameraFrameSequenceOptions,
|
|
200
275
|
CameraFrameSequenceRecorderAPI,
|
|
201
276
|
CameraFrameSequenceResult,
|
|
277
|
+
CameraFrameSequenceSampleInput,
|
|
278
|
+
CameraFrameSequenceStepInput,
|
|
202
279
|
MujocoCanvasProps,
|
|
203
280
|
MujocoContextValue,
|
|
204
281
|
// Hook return types
|
|
@@ -227,6 +304,7 @@ export type {
|
|
|
227
304
|
Sites,
|
|
228
305
|
Geoms,
|
|
229
306
|
Keyframes,
|
|
307
|
+
Cameras,
|
|
230
308
|
} from './types';
|
|
231
309
|
|
|
232
310
|
export {
|
|
@@ -239,6 +317,8 @@ export {
|
|
|
239
317
|
RobotSites,
|
|
240
318
|
RobotGeoms,
|
|
241
319
|
RobotKeyframes,
|
|
320
|
+
RobotCameras,
|
|
321
|
+
SplatEnvironmentReadinessStatus,
|
|
242
322
|
} from './types';
|
|
243
323
|
|
|
244
324
|
// Re-export MuJoCo types for convenience
|
|
@@ -10,9 +10,25 @@ import type {
|
|
|
10
10
|
CameraFrameCaptureBlobResult,
|
|
11
11
|
CameraFrameCaptureOptions,
|
|
12
12
|
CameraFrameCaptureResult,
|
|
13
|
+
CameraFrameCaptureSource,
|
|
13
14
|
CameraFrameCaptureVector3,
|
|
14
15
|
} from '../types';
|
|
15
16
|
|
|
17
|
+
export interface CameraFrameCaptureSession {
|
|
18
|
+
readonly width: number;
|
|
19
|
+
readonly height: number;
|
|
20
|
+
capture(options?: CameraFrameCaptureOptions): {
|
|
21
|
+
canvas: HTMLCanvasElement;
|
|
22
|
+
camera: THREE.Camera;
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
source: CameraFrameCaptureSource;
|
|
26
|
+
};
|
|
27
|
+
captureDataUrl(options?: CameraFrameCaptureOptions): CameraFrameCaptureResult;
|
|
28
|
+
captureBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
}
|
|
31
|
+
|
|
16
32
|
function toVector3(
|
|
17
33
|
value: CameraFrameCaptureVector3 | undefined,
|
|
18
34
|
fallback: THREE.Vector3
|
|
@@ -75,24 +91,55 @@ function createCaptureCamera(
|
|
|
75
91
|
return camera;
|
|
76
92
|
}
|
|
77
93
|
|
|
94
|
+
function getCaptureDimensions(
|
|
95
|
+
renderer: THREE.WebGLRenderer,
|
|
96
|
+
options: CameraFrameCaptureOptions
|
|
97
|
+
) {
|
|
98
|
+
const width = Math.max(
|
|
99
|
+
1,
|
|
100
|
+
Math.floor(options.width ?? renderer.domElement.width)
|
|
101
|
+
);
|
|
102
|
+
const height = Math.max(
|
|
103
|
+
1,
|
|
104
|
+
Math.floor(options.height ?? renderer.domElement.height)
|
|
105
|
+
);
|
|
106
|
+
return { width, height };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function prepareCaptureCamera(
|
|
110
|
+
camera: THREE.Camera,
|
|
111
|
+
options: CameraFrameCaptureOptions,
|
|
112
|
+
fallbackCamera: THREE.Camera,
|
|
113
|
+
width: number,
|
|
114
|
+
height: number
|
|
115
|
+
) {
|
|
116
|
+
if (options.camera) {
|
|
117
|
+
camera.copy(options.camera);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (camera instanceof THREE.PerspectiveCamera) {
|
|
121
|
+
camera.aspect = width / height;
|
|
122
|
+
camera.fov = options.fov ?? camera.fov;
|
|
123
|
+
camera.near = options.near ?? camera.near;
|
|
124
|
+
camera.far = options.far ?? camera.far;
|
|
125
|
+
camera.updateProjectionMatrix();
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
applyCameraPose(camera, options, fallbackCamera);
|
|
129
|
+
}
|
|
130
|
+
|
|
78
131
|
function readRenderTargetToCanvas(
|
|
79
132
|
renderer: THREE.WebGLRenderer,
|
|
80
133
|
target: THREE.WebGLRenderTarget,
|
|
134
|
+
canvas: HTMLCanvasElement,
|
|
135
|
+
context: CanvasRenderingContext2D,
|
|
136
|
+
pixels: Uint8Array,
|
|
137
|
+
imageData: ImageData,
|
|
81
138
|
width: number,
|
|
82
139
|
height: number
|
|
83
140
|
) {
|
|
84
|
-
const pixels = new Uint8Array(width * height * 4);
|
|
85
141
|
renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
|
|
86
142
|
|
|
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
143
|
const rowBytes = width * 4;
|
|
97
144
|
for (let y = 0; y < height; y += 1) {
|
|
98
145
|
const sourceStart = (height - y - 1) * rowBytes;
|
|
@@ -106,34 +153,156 @@ function readRenderTargetToCanvas(
|
|
|
106
153
|
return canvas;
|
|
107
154
|
}
|
|
108
155
|
|
|
109
|
-
|
|
156
|
+
function getCameraFrameCaptureSource(
|
|
157
|
+
options: CameraFrameCaptureOptions
|
|
158
|
+
): CameraFrameCaptureSource {
|
|
159
|
+
if (options.source) return options.source;
|
|
160
|
+
if (options.cameraName) {
|
|
161
|
+
return { kind: 'mujoco-camera', cameraName: options.cameraName };
|
|
162
|
+
}
|
|
163
|
+
if (options.siteName) {
|
|
164
|
+
return { kind: 'mujoco-site', siteName: options.siteName };
|
|
165
|
+
}
|
|
166
|
+
if (options.bodyName) {
|
|
167
|
+
return { kind: 'mujoco-body', bodyName: options.bodyName };
|
|
168
|
+
}
|
|
169
|
+
if (options.camera) return { kind: 'custom-camera' };
|
|
170
|
+
if (options.position || options.lookAt || options.quaternion) {
|
|
171
|
+
return { kind: 'explicit-pose' };
|
|
172
|
+
}
|
|
173
|
+
return { kind: 'fallback-camera' };
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function createCameraFrameCaptureSession(
|
|
110
177
|
renderer: THREE.WebGLRenderer,
|
|
111
178
|
scene: THREE.Scene,
|
|
112
179
|
fallbackCamera: THREE.Camera,
|
|
113
180
|
options: CameraFrameCaptureOptions = {}
|
|
114
|
-
) {
|
|
115
|
-
const width =
|
|
116
|
-
const height = Math.max(1, Math.floor(options.height ?? renderer.domElement.height));
|
|
181
|
+
): CameraFrameCaptureSession {
|
|
182
|
+
const { width, height } = getCaptureDimensions(renderer, options);
|
|
117
183
|
const camera = createCaptureCamera(options, fallbackCamera, width, height);
|
|
118
184
|
const target = new THREE.WebGLRenderTarget(width, height, {
|
|
119
185
|
format: THREE.RGBAFormat,
|
|
120
186
|
type: THREE.UnsignedByteType,
|
|
121
187
|
});
|
|
122
|
-
const
|
|
123
|
-
|
|
188
|
+
const canvas = document.createElement('canvas');
|
|
189
|
+
canvas.width = width;
|
|
190
|
+
canvas.height = height;
|
|
191
|
+
const context = canvas.getContext('2d');
|
|
192
|
+
if (!context) {
|
|
193
|
+
target.dispose();
|
|
194
|
+
throw new Error('Unable to create a 2D canvas for camera frame capture.');
|
|
195
|
+
}
|
|
196
|
+
const drawContext = context;
|
|
197
|
+
|
|
198
|
+
const pixels = new Uint8Array(width * height * 4);
|
|
199
|
+
const imageData = drawContext.createImageData(width, height);
|
|
200
|
+
|
|
201
|
+
function capture(nextOptions: CameraFrameCaptureOptions = {}) {
|
|
202
|
+
const captureOptions = { ...options, ...nextOptions };
|
|
203
|
+
const nextDimensions = getCaptureDimensions(renderer, captureOptions);
|
|
204
|
+
if (
|
|
205
|
+
nextDimensions.width !== width ||
|
|
206
|
+
nextDimensions.height !== height
|
|
207
|
+
) {
|
|
208
|
+
throw new Error(
|
|
209
|
+
'Camera frame capture sessions require stable width and height.'
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
prepareCaptureCamera(
|
|
214
|
+
camera,
|
|
215
|
+
captureOptions,
|
|
216
|
+
fallbackCamera,
|
|
217
|
+
width,
|
|
218
|
+
height
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const previousTarget = renderer.getRenderTarget();
|
|
222
|
+
const previousXrEnabled = renderer.xr.enabled;
|
|
124
223
|
|
|
125
|
-
|
|
224
|
+
scene.updateMatrixWorld(true);
|
|
225
|
+
try {
|
|
226
|
+
renderer.xr.enabled = false;
|
|
227
|
+
renderer.setRenderTarget(target);
|
|
228
|
+
renderer.clear();
|
|
229
|
+
renderer.render(scene, camera);
|
|
230
|
+
readRenderTargetToCanvas(
|
|
231
|
+
renderer,
|
|
232
|
+
target,
|
|
233
|
+
canvas,
|
|
234
|
+
drawContext,
|
|
235
|
+
pixels,
|
|
236
|
+
imageData,
|
|
237
|
+
width,
|
|
238
|
+
height
|
|
239
|
+
);
|
|
240
|
+
return {
|
|
241
|
+
canvas,
|
|
242
|
+
camera,
|
|
243
|
+
width,
|
|
244
|
+
height,
|
|
245
|
+
source: getCameraFrameCaptureSource(captureOptions),
|
|
246
|
+
};
|
|
247
|
+
} finally {
|
|
248
|
+
renderer.setRenderTarget(previousTarget);
|
|
249
|
+
renderer.xr.enabled = previousXrEnabled;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
width,
|
|
255
|
+
height,
|
|
256
|
+
capture,
|
|
257
|
+
captureDataUrl(nextOptions = {}) {
|
|
258
|
+
const type = nextOptions.type ?? options.type ?? 'image/png';
|
|
259
|
+
const result = capture(nextOptions);
|
|
260
|
+
return {
|
|
261
|
+
...result,
|
|
262
|
+
dataUrl: result.canvas.toDataURL(
|
|
263
|
+
type,
|
|
264
|
+
nextOptions.quality ?? options.quality
|
|
265
|
+
),
|
|
266
|
+
type,
|
|
267
|
+
};
|
|
268
|
+
},
|
|
269
|
+
async captureBlob(nextOptions = {}) {
|
|
270
|
+
const type = nextOptions.type ?? options.type ?? 'image/png';
|
|
271
|
+
const result = capture(nextOptions);
|
|
272
|
+
const blob = await new Promise<Blob>((resolve, reject) => {
|
|
273
|
+
result.canvas.toBlob(
|
|
274
|
+
(nextBlob) => {
|
|
275
|
+
if (nextBlob) resolve(nextBlob);
|
|
276
|
+
else reject(new Error('Camera frame capture did not produce a Blob.'));
|
|
277
|
+
},
|
|
278
|
+
type,
|
|
279
|
+
nextOptions.quality ?? options.quality
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
return { ...result, blob, type };
|
|
283
|
+
},
|
|
284
|
+
dispose() {
|
|
285
|
+
target.dispose();
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
export function renderCameraFrameToCanvas(
|
|
291
|
+
renderer: THREE.WebGLRenderer,
|
|
292
|
+
scene: THREE.Scene,
|
|
293
|
+
fallbackCamera: THREE.Camera,
|
|
294
|
+
options: CameraFrameCaptureOptions = {}
|
|
295
|
+
) {
|
|
296
|
+
const session = createCameraFrameCaptureSession(
|
|
297
|
+
renderer,
|
|
298
|
+
scene,
|
|
299
|
+
fallbackCamera,
|
|
300
|
+
options
|
|
301
|
+
);
|
|
126
302
|
try {
|
|
127
|
-
|
|
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 };
|
|
303
|
+
return session.capture();
|
|
133
304
|
} finally {
|
|
134
|
-
|
|
135
|
-
renderer.xr.enabled = previousXrEnabled;
|
|
136
|
-
target.dispose();
|
|
305
|
+
session.dispose();
|
|
137
306
|
}
|
|
138
307
|
}
|
|
139
308
|
|