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
|
@@ -20,10 +20,13 @@ import {
|
|
|
20
20
|
ActuatedJointInfo,
|
|
21
21
|
ActuatorInfo,
|
|
22
22
|
BodyInfo,
|
|
23
|
+
CameraFrameCaptureOptions,
|
|
23
24
|
CameraFrameCaptureResult,
|
|
25
|
+
CameraFrameCaptureSource,
|
|
24
26
|
CameraFrameSequenceFrame,
|
|
25
27
|
CameraFrameSequenceOptions,
|
|
26
28
|
CameraFrameSequenceResult,
|
|
29
|
+
CameraInfo,
|
|
27
30
|
ControlGroupInfo,
|
|
28
31
|
ControlGroupSelector,
|
|
29
32
|
ContactInfo,
|
|
@@ -52,15 +55,22 @@ import {
|
|
|
52
55
|
import {
|
|
53
56
|
captureCameraFrame,
|
|
54
57
|
captureCameraFrameBlob,
|
|
58
|
+
createCameraFrameCaptureSession,
|
|
55
59
|
} from '../rendering/cameraFrameCapture';
|
|
60
|
+
import {
|
|
61
|
+
getCameraFrameCaptureSourceTarget,
|
|
62
|
+
isMountedCameraFrameCaptureSource,
|
|
63
|
+
} from '../rendering/cameraFrameSource';
|
|
56
64
|
import {
|
|
57
65
|
loadScene,
|
|
58
66
|
createSceneConfigFromFiles,
|
|
59
67
|
findKeyframeByName,
|
|
60
68
|
findBodyByName,
|
|
69
|
+
findSiteByName,
|
|
61
70
|
findGeomByName,
|
|
62
71
|
findSensorByName,
|
|
63
72
|
findActuatorByName,
|
|
73
|
+
findCameraByName,
|
|
64
74
|
getActuatedScalarQposAdr,
|
|
65
75
|
getActuatedJoints as getActuatedJointsFromModel,
|
|
66
76
|
getControlMap as getControlMapFromModel,
|
|
@@ -124,6 +134,98 @@ function waitForNextAnimationFrame() {
|
|
|
124
134
|
});
|
|
125
135
|
}
|
|
126
136
|
|
|
137
|
+
function throwIfCameraSequenceAborted(signal: AbortSignal | undefined) {
|
|
138
|
+
if (!signal?.aborted) return;
|
|
139
|
+
|
|
140
|
+
if (typeof signal.reason === 'object' && signal.reason instanceof Error) {
|
|
141
|
+
throw signal.reason;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
throw new DOMException('Camera sequence recording was aborted.', 'AbortError');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function vector3FromArray(values: ArrayLike<number>, offset: number): [number, number, number] {
|
|
148
|
+
return [values[offset], values[offset + 1], values[offset + 2]];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function quaternionFromArray(values: ArrayLike<number>, offset: number): [number, number, number, number] {
|
|
152
|
+
return [
|
|
153
|
+
values[offset],
|
|
154
|
+
values[offset + 1],
|
|
155
|
+
values[offset + 2],
|
|
156
|
+
values[offset + 3],
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function quaternionFromXmat(values: ArrayLike<number>, offset: number): [number, number, number, number] {
|
|
161
|
+
const matrix = new THREE.Matrix4();
|
|
162
|
+
matrix.set(
|
|
163
|
+
values[offset],
|
|
164
|
+
values[offset + 1],
|
|
165
|
+
values[offset + 2],
|
|
166
|
+
0,
|
|
167
|
+
values[offset + 3],
|
|
168
|
+
values[offset + 4],
|
|
169
|
+
values[offset + 5],
|
|
170
|
+
0,
|
|
171
|
+
values[offset + 6],
|
|
172
|
+
values[offset + 7],
|
|
173
|
+
values[offset + 8],
|
|
174
|
+
0,
|
|
175
|
+
0,
|
|
176
|
+
0,
|
|
177
|
+
0,
|
|
178
|
+
1
|
|
179
|
+
);
|
|
180
|
+
const quaternion = new THREE.Quaternion().setFromRotationMatrix(matrix);
|
|
181
|
+
return [quaternion.x, quaternion.y, quaternion.z, quaternion.w];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function omitResolvedCameraSelectors(
|
|
185
|
+
options: CameraFrameCaptureOptions
|
|
186
|
+
): CameraFrameCaptureOptions {
|
|
187
|
+
const { cameraName, siteName, bodyName, ...rest } = options;
|
|
188
|
+
return rest;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function countMountedCameraSelectors(options: CameraFrameCaptureOptions) {
|
|
192
|
+
return Number(Boolean(options.cameraName)) +
|
|
193
|
+
Number(Boolean(options.siteName)) +
|
|
194
|
+
Number(Boolean(options.bodyName));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function assertMatchingMountedCameraSource(
|
|
198
|
+
key: string,
|
|
199
|
+
requested: CameraFrameCaptureOptions,
|
|
200
|
+
source: CameraFrameCaptureSource
|
|
201
|
+
) {
|
|
202
|
+
const selectorCount = countMountedCameraSelectors(requested);
|
|
203
|
+
if (selectorCount !== 1) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Camera sequence stream "${key}" must provide exactly one mounted MuJoCo cameraName, siteName, or bodyName selector.`
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (!isMountedCameraFrameCaptureSource(source)) {
|
|
210
|
+
throw new Error(
|
|
211
|
+
`Camera sequence stream "${key}" resolved to ${source.kind}; use a MuJoCo-mounted camera, site, or body selector for sequence recording.`
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (
|
|
216
|
+
(requested.cameraName &&
|
|
217
|
+
(source.kind !== 'mujoco-camera' || source.cameraName !== requested.cameraName)) ||
|
|
218
|
+
(requested.siteName &&
|
|
219
|
+
(source.kind !== 'mujoco-site' || source.siteName !== requested.siteName)) ||
|
|
220
|
+
(requested.bodyName &&
|
|
221
|
+
(source.kind !== 'mujoco-body' || source.bodyName !== requested.bodyName))
|
|
222
|
+
) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`Camera sequence stream "${key}" resolved to ${source.kind}:${getCameraFrameCaptureSourceTarget(source)} instead of the requested mounted selector.`
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
127
229
|
// ---- Internal context types ----
|
|
128
230
|
|
|
129
231
|
export interface MujocoSimContextValue {
|
|
@@ -308,6 +410,7 @@ export function MujocoSimProvider({
|
|
|
308
410
|
const bodyReloadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
309
411
|
|
|
310
412
|
useEffect(() => { configRef.current = config; }, [config]);
|
|
413
|
+
useEffect(() => { mujocoRef.current = mujoco; }, [mujoco]);
|
|
311
414
|
|
|
312
415
|
// Sync declarative props to refs
|
|
313
416
|
useEffect(() => { pausedRef.current = paused ?? false; }, [paused]);
|
|
@@ -359,6 +462,7 @@ export function MujocoSimProvider({
|
|
|
359
462
|
return;
|
|
360
463
|
}
|
|
361
464
|
|
|
465
|
+
mujocoRef.current = mujoco;
|
|
362
466
|
mjModelRef.current = result.mjModel;
|
|
363
467
|
mjDataRef.current = result.mjData;
|
|
364
468
|
physicsAccumulatorRef.current = 0;
|
|
@@ -438,7 +542,7 @@ export function MujocoSimProvider({
|
|
|
438
542
|
// Step physics with substeps
|
|
439
543
|
if (stepsToRunRef.current > 0) {
|
|
440
544
|
for (let s = 0; s < stepsToRunRef.current; s++) {
|
|
441
|
-
|
|
545
|
+
mujocoRef.current.mj_step(model, data);
|
|
442
546
|
}
|
|
443
547
|
stepsToRunRef.current = 0;
|
|
444
548
|
} else {
|
|
@@ -447,7 +551,7 @@ export function MujocoSimProvider({
|
|
|
447
551
|
const frameTime = clampedDelta * speedRef.current;
|
|
448
552
|
while (data.time - startSimTime < frameTime) {
|
|
449
553
|
for (let s = 0; s < numSubsteps; s++) {
|
|
450
|
-
|
|
554
|
+
mujocoRef.current.mj_step(model, data);
|
|
451
555
|
}
|
|
452
556
|
}
|
|
453
557
|
}
|
|
@@ -455,7 +559,7 @@ export function MujocoSimProvider({
|
|
|
455
559
|
ensureInterpolationBuffers(model);
|
|
456
560
|
copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
|
|
457
561
|
for (let s = 0; s < stepsToRunRef.current; s++) {
|
|
458
|
-
|
|
562
|
+
mujocoRef.current.mj_step(model, data);
|
|
459
563
|
}
|
|
460
564
|
copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
|
|
461
565
|
interpolationStateRef.current.alpha = 1;
|
|
@@ -471,7 +575,7 @@ export function MujocoSimProvider({
|
|
|
471
575
|
while (physicsAccumulatorRef.current >= stepDt) {
|
|
472
576
|
copyBodyPose(data, interpolationStateRef.current.previousXpos, interpolationStateRef.current.previousXquat);
|
|
473
577
|
for (let s = 0; s < numSubsteps; s++) {
|
|
474
|
-
|
|
578
|
+
mujocoRef.current.mj_step(model, data);
|
|
475
579
|
}
|
|
476
580
|
copyBodyPose(data, interpolationStateRef.current.currentXpos, interpolationStateRef.current.currentXquat);
|
|
477
581
|
physicsAccumulatorRef.current -= stepDt;
|
|
@@ -522,7 +626,7 @@ export function MujocoSimProvider({
|
|
|
522
626
|
const data = mjDataRef.current;
|
|
523
627
|
if (!model || !data) return;
|
|
524
628
|
|
|
525
|
-
|
|
629
|
+
mujocoRef.current.mj_resetData(model, data);
|
|
526
630
|
|
|
527
631
|
const homeJoints = configRef.current.homeJoints;
|
|
528
632
|
if (homeJoints) {
|
|
@@ -537,7 +641,7 @@ export function MujocoSimProvider({
|
|
|
537
641
|
}
|
|
538
642
|
|
|
539
643
|
configRef.current.onReset?.({ model, data });
|
|
540
|
-
|
|
644
|
+
mujocoRef.current.mj_forward(model, data);
|
|
541
645
|
|
|
542
646
|
// Notify composable plugins (e.g. IkController)
|
|
543
647
|
for (const cb of resetCallbacks.current) {
|
|
@@ -574,7 +678,7 @@ export function MujocoSimProvider({
|
|
|
574
678
|
for (const cb of beforeStepCallbacks.current) {
|
|
575
679
|
cb({ model, data });
|
|
576
680
|
}
|
|
577
|
-
|
|
681
|
+
mujocoRef.current.mj_step(model, data);
|
|
578
682
|
for (const cb of afterStepCallbacks.current) {
|
|
579
683
|
cb({ model, data });
|
|
580
684
|
}
|
|
@@ -617,7 +721,7 @@ export function MujocoSimProvider({
|
|
|
617
721
|
data.ctrl.set(snapshot.ctrl);
|
|
618
722
|
if (snapshot.act.length > 0) data.act.set(snapshot.act);
|
|
619
723
|
data.qfrc_applied.set(snapshot.qfrc_applied);
|
|
620
|
-
|
|
724
|
+
mujocoRef.current.mj_forward(model, data);
|
|
621
725
|
}, [mujoco]);
|
|
622
726
|
|
|
623
727
|
const setQpos = useCallback((values: Float64Array | number[]) => {
|
|
@@ -626,7 +730,7 @@ export function MujocoSimProvider({
|
|
|
626
730
|
if (!model || !data) return;
|
|
627
731
|
const arr = values instanceof Float64Array ? values : new Float64Array(values);
|
|
628
732
|
data.qpos.set(arr.subarray(0, Math.min(arr.length, model.nq)));
|
|
629
|
-
|
|
733
|
+
mujocoRef.current.mj_forward(model, data);
|
|
630
734
|
}, [mujoco]);
|
|
631
735
|
|
|
632
736
|
const setQvel = useCallback((values: Float64Array | number[]) => {
|
|
@@ -869,6 +973,113 @@ export function MujocoSimProvider({
|
|
|
869
973
|
return result;
|
|
870
974
|
}, []);
|
|
871
975
|
|
|
976
|
+
const getCameras = useCallback((): CameraInfo[] => {
|
|
977
|
+
const model = mjModelRef.current;
|
|
978
|
+
if (!model) return [];
|
|
979
|
+
const ncam = model.ncam ?? 0;
|
|
980
|
+
const nameAddresses = model.name_camadr;
|
|
981
|
+
if (!ncam || !nameAddresses) return [];
|
|
982
|
+
|
|
983
|
+
const result: CameraInfo[] = [];
|
|
984
|
+
for (let i = 0; i < ncam; i += 1) {
|
|
985
|
+
const posOffset = i * 3;
|
|
986
|
+
const quatOffset = i * 4;
|
|
987
|
+
result.push({
|
|
988
|
+
id: i,
|
|
989
|
+
name: getName(model, nameAddresses[i]),
|
|
990
|
+
bodyId: model.cam_bodyid?.[i] ?? -1,
|
|
991
|
+
fov: model.cam_fovy?.[i] ?? null,
|
|
992
|
+
position: model.cam_pos
|
|
993
|
+
? vector3FromArray(model.cam_pos, posOffset)
|
|
994
|
+
: null,
|
|
995
|
+
quaternion: model.cam_quat
|
|
996
|
+
? quaternionFromArray(model.cam_quat, quatOffset)
|
|
997
|
+
: null,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
return result;
|
|
1001
|
+
}, []);
|
|
1002
|
+
|
|
1003
|
+
const resolveCameraCaptureOptions = useCallback(
|
|
1004
|
+
(options: CameraFrameCaptureOptions = {}): CameraFrameCaptureOptions => {
|
|
1005
|
+
const model = mjModelRef.current;
|
|
1006
|
+
const data = mjDataRef.current;
|
|
1007
|
+
if (!model || !data) {
|
|
1008
|
+
return options;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
const baseOptions = omitResolvedCameraSelectors(options);
|
|
1012
|
+
|
|
1013
|
+
if (options.cameraName) {
|
|
1014
|
+
const cameraId = findCameraByName(model, options.cameraName);
|
|
1015
|
+
if (cameraId < 0) {
|
|
1016
|
+
throw new Error(`MuJoCo camera "${options.cameraName}" was not found.`);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
const position = data.cam_xpos
|
|
1020
|
+
? vector3FromArray(data.cam_xpos, cameraId * 3)
|
|
1021
|
+
: model.cam_pos
|
|
1022
|
+
? vector3FromArray(model.cam_pos, cameraId * 3)
|
|
1023
|
+
: undefined;
|
|
1024
|
+
const quaternion = data.cam_xmat
|
|
1025
|
+
? quaternionFromXmat(data.cam_xmat, cameraId * 9)
|
|
1026
|
+
: model.cam_quat
|
|
1027
|
+
? quaternionFromArray(model.cam_quat, cameraId * 4)
|
|
1028
|
+
: undefined;
|
|
1029
|
+
|
|
1030
|
+
if (!position || !quaternion) {
|
|
1031
|
+
throw new Error(
|
|
1032
|
+
`MuJoCo camera "${options.cameraName}" does not expose a capture pose.`
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
return {
|
|
1037
|
+
...baseOptions,
|
|
1038
|
+
position,
|
|
1039
|
+
quaternion,
|
|
1040
|
+
fov: options.fov ?? model.cam_fovy?.[cameraId],
|
|
1041
|
+
source: { kind: 'mujoco-camera', cameraName: options.cameraName },
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
if (options.siteName) {
|
|
1046
|
+
const siteId = findSiteByName(model, options.siteName);
|
|
1047
|
+
if (siteId < 0) {
|
|
1048
|
+
throw new Error(`MuJoCo site "${options.siteName}" was not found.`);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
return {
|
|
1052
|
+
...baseOptions,
|
|
1053
|
+
position: vector3FromArray(data.site_xpos, siteId * 3),
|
|
1054
|
+
quaternion: quaternionFromXmat(data.site_xmat, siteId * 9),
|
|
1055
|
+
source: { kind: 'mujoco-site', siteName: options.siteName },
|
|
1056
|
+
};
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
if (options.bodyName) {
|
|
1060
|
+
const bodyId = findBodyByName(model, options.bodyName);
|
|
1061
|
+
if (bodyId < 0) {
|
|
1062
|
+
throw new Error(`MuJoCo body "${options.bodyName}" was not found.`);
|
|
1063
|
+
}
|
|
1064
|
+
if (!data.xmat) {
|
|
1065
|
+
throw new Error(
|
|
1066
|
+
`MuJoCo body "${options.bodyName}" does not expose world orientation data.`
|
|
1067
|
+
);
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
return {
|
|
1071
|
+
...baseOptions,
|
|
1072
|
+
position: vector3FromArray(data.xpos, bodyId * 3),
|
|
1073
|
+
quaternion: quaternionFromXmat(data.xmat, bodyId * 9),
|
|
1074
|
+
source: { kind: 'mujoco-body', bodyName: options.bodyName },
|
|
1075
|
+
};
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
return options;
|
|
1079
|
+
},
|
|
1080
|
+
[]
|
|
1081
|
+
);
|
|
1082
|
+
|
|
872
1083
|
const getModelOption = useCallback((): ModelOptions => {
|
|
873
1084
|
const model = mjModelRef.current;
|
|
874
1085
|
if (!model?.opt) return { timestep: 0.002, gravity: [0, 0, -9.81], integrator: 0 };
|
|
@@ -951,7 +1162,7 @@ export function MujocoSimProvider({
|
|
|
951
1162
|
for (let i = 0; i < model.nv; i++) data.qvel[i] = model.key_qvel[qvelOffset + i];
|
|
952
1163
|
}
|
|
953
1164
|
|
|
954
|
-
|
|
1165
|
+
mujocoRef.current.mj_forward(model, data);
|
|
955
1166
|
|
|
956
1167
|
// Notify composable plugins
|
|
957
1168
|
for (const cb of resetCallbacks.current) {
|
|
@@ -1081,16 +1292,26 @@ export function MujocoSimProvider({
|
|
|
1081
1292
|
|
|
1082
1293
|
const captureCameraFrameApi = useCallback(
|
|
1083
1294
|
(options = {}) => {
|
|
1084
|
-
return captureCameraFrame(
|
|
1295
|
+
return captureCameraFrame(
|
|
1296
|
+
gl,
|
|
1297
|
+
scene,
|
|
1298
|
+
camera,
|
|
1299
|
+
resolveCameraCaptureOptions(options)
|
|
1300
|
+
);
|
|
1085
1301
|
},
|
|
1086
|
-
[camera, gl, scene]
|
|
1302
|
+
[camera, gl, resolveCameraCaptureOptions, scene]
|
|
1087
1303
|
);
|
|
1088
1304
|
|
|
1089
1305
|
const captureCameraFrameBlobApi = useCallback(
|
|
1090
1306
|
(options = {}) => {
|
|
1091
|
-
return captureCameraFrameBlob(
|
|
1307
|
+
return captureCameraFrameBlob(
|
|
1308
|
+
gl,
|
|
1309
|
+
scene,
|
|
1310
|
+
camera,
|
|
1311
|
+
resolveCameraCaptureOptions(options)
|
|
1312
|
+
);
|
|
1092
1313
|
},
|
|
1093
|
-
[camera, gl, scene]
|
|
1314
|
+
[camera, gl, resolveCameraCaptureOptions, scene]
|
|
1094
1315
|
);
|
|
1095
1316
|
|
|
1096
1317
|
const recordCameraSequenceApi = useCallback(
|
|
@@ -1098,60 +1319,182 @@ export function MujocoSimProvider({
|
|
|
1098
1319
|
options: CameraFrameSequenceOptions
|
|
1099
1320
|
): Promise<CameraFrameSequenceResult> => {
|
|
1100
1321
|
const frameCount = Math.max(0, Math.floor(options.frames));
|
|
1101
|
-
const stepsPerFrame = Math.max(
|
|
1322
|
+
const stepsPerFrame = Math.max(0, Math.floor(options.stepsPerFrame ?? 1));
|
|
1102
1323
|
const cameras = options.cameras;
|
|
1103
1324
|
const frames: CameraFrameSequenceFrame[] = [];
|
|
1325
|
+
const cameraSummaries: CameraFrameSequenceResult['cameraSummaries'] = {};
|
|
1104
1326
|
const wasPaused = pausedRef.current;
|
|
1327
|
+
const retainFrames = options.retainFrames ?? true;
|
|
1328
|
+
const requireMountedSources = options.requireMountedSources ?? true;
|
|
1329
|
+
let recordedFrameCount = 0;
|
|
1330
|
+
|
|
1331
|
+
async function stepCameraSequence(frameIndex: number, steps: number) {
|
|
1332
|
+
const model = mjModelRef.current;
|
|
1333
|
+
const data = mjDataRef.current;
|
|
1334
|
+
if (!model || !data) {
|
|
1335
|
+
throw new Error('MuJoCo scene is not ready for camera sequence stepping.');
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
for (let stepIndex = 0; stepIndex < steps; stepIndex += 1) {
|
|
1339
|
+
for (let i = 0; i < model.nv; i += 1) {
|
|
1340
|
+
data.qfrc_applied[i] = 0;
|
|
1341
|
+
}
|
|
1342
|
+
await options.onBeforeStep?.({
|
|
1343
|
+
frameIndex,
|
|
1344
|
+
stepIndex,
|
|
1345
|
+
time: data.time,
|
|
1346
|
+
model,
|
|
1347
|
+
data,
|
|
1348
|
+
});
|
|
1349
|
+
for (const cb of beforeStepCallbacks.current) {
|
|
1350
|
+
cb({ model, data });
|
|
1351
|
+
}
|
|
1352
|
+
mujocoRef.current.mj_step(model, data);
|
|
1353
|
+
for (const cb of afterStepCallbacks.current) {
|
|
1354
|
+
cb({ model, data });
|
|
1355
|
+
}
|
|
1356
|
+
onStepRef.current?.({ time: data.time, model, data });
|
|
1357
|
+
await options.onAfterStep?.({
|
|
1358
|
+
frameIndex,
|
|
1359
|
+
stepIndex,
|
|
1360
|
+
time: data.time,
|
|
1361
|
+
model,
|
|
1362
|
+
data,
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
physicsAccumulatorRef.current = 0;
|
|
1367
|
+
interpolationStateRef.current.valid = false;
|
|
1368
|
+
}
|
|
1105
1369
|
|
|
1106
1370
|
if (frameCount === 0 || cameras.length === 0) {
|
|
1107
1371
|
return {
|
|
1108
1372
|
frames,
|
|
1109
1373
|
cameraKeys: cameras.map((sequenceCamera) => sequenceCamera.key),
|
|
1374
|
+
cameraSummaries,
|
|
1110
1375
|
frameCount: 0,
|
|
1111
1376
|
};
|
|
1112
1377
|
}
|
|
1113
1378
|
|
|
1379
|
+
throwIfCameraSequenceAborted(options.signal);
|
|
1380
|
+
|
|
1381
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
1382
|
+
if (mjModelRef.current && mjDataRef.current) break;
|
|
1383
|
+
await waitForNextAnimationFrame();
|
|
1384
|
+
throwIfCameraSequenceAborted(options.signal);
|
|
1385
|
+
}
|
|
1386
|
+
if (!mjModelRef.current || !mjDataRef.current) {
|
|
1387
|
+
throw new Error('MuJoCo scene is not ready for camera sequence recording.');
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
const captureSessions = cameras.map((sequenceCamera) => {
|
|
1391
|
+
const { key, ...captureOptions } = sequenceCamera;
|
|
1392
|
+
const initialCaptureOptions = resolveCameraCaptureOptions(captureOptions);
|
|
1393
|
+
const mountedSource = initialCaptureOptions.source;
|
|
1394
|
+
if (requireMountedSources) {
|
|
1395
|
+
assertMatchingMountedCameraSource(
|
|
1396
|
+
key,
|
|
1397
|
+
captureOptions,
|
|
1398
|
+
mountedSource ?? { kind: 'fallback-camera' }
|
|
1399
|
+
);
|
|
1400
|
+
}
|
|
1401
|
+
return {
|
|
1402
|
+
key,
|
|
1403
|
+
captureOptions,
|
|
1404
|
+
mountedSource,
|
|
1405
|
+
session: createCameraFrameCaptureSession(
|
|
1406
|
+
gl,
|
|
1407
|
+
scene,
|
|
1408
|
+
camera,
|
|
1409
|
+
initialCaptureOptions
|
|
1410
|
+
),
|
|
1411
|
+
};
|
|
1412
|
+
});
|
|
1413
|
+
|
|
1114
1414
|
try {
|
|
1115
1415
|
pausedRef.current = true;
|
|
1116
1416
|
stepsToRunRef.current = 0;
|
|
1117
1417
|
if (options.reset) reset();
|
|
1118
1418
|
|
|
1119
1419
|
for (let frameIndex = 0; frameIndex < frameCount; frameIndex += 1) {
|
|
1120
|
-
|
|
1121
|
-
|
|
1420
|
+
throwIfCameraSequenceAborted(options.signal);
|
|
1421
|
+
if (
|
|
1422
|
+
stepsPerFrame > 0 &&
|
|
1423
|
+
(frameIndex > 0 || options.captureInitialFrame === false)
|
|
1424
|
+
) {
|
|
1425
|
+
await stepCameraSequence(frameIndex, stepsPerFrame);
|
|
1122
1426
|
}
|
|
1123
1427
|
await waitForNextAnimationFrame();
|
|
1428
|
+
throwIfCameraSequenceAborted(options.signal);
|
|
1429
|
+
|
|
1430
|
+
const model = mjModelRef.current;
|
|
1431
|
+
const data = mjDataRef.current;
|
|
1432
|
+
if (!model || !data) {
|
|
1433
|
+
throw new Error('MuJoCo scene is not ready for camera sequence sampling.');
|
|
1434
|
+
}
|
|
1435
|
+
await options.onSample?.({
|
|
1436
|
+
frameIndex,
|
|
1437
|
+
time: data.time,
|
|
1438
|
+
model,
|
|
1439
|
+
data,
|
|
1440
|
+
});
|
|
1124
1441
|
|
|
1125
1442
|
const cameraFrames: Record<string, CameraFrameCaptureResult> = {};
|
|
1126
|
-
for (const
|
|
1127
|
-
const
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1443
|
+
for (const { key, captureOptions, mountedSource, session } of captureSessions) {
|
|
1444
|
+
const resolvedCaptureOptions = resolveCameraCaptureOptions(captureOptions);
|
|
1445
|
+
const cameraFrame = session.captureDataUrl({
|
|
1446
|
+
...resolvedCaptureOptions,
|
|
1447
|
+
source: mountedSource ?? resolvedCaptureOptions.source,
|
|
1448
|
+
});
|
|
1449
|
+
if (requireMountedSources) {
|
|
1450
|
+
assertMatchingMountedCameraSource(
|
|
1451
|
+
key,
|
|
1452
|
+
captureOptions,
|
|
1453
|
+
cameraFrame.source
|
|
1454
|
+
);
|
|
1455
|
+
}
|
|
1456
|
+
cameraSummaries[key] = {
|
|
1457
|
+
key,
|
|
1458
|
+
width: cameraFrame.width,
|
|
1459
|
+
height: cameraFrame.height,
|
|
1460
|
+
source: cameraFrame.source,
|
|
1461
|
+
frameCount: (cameraSummaries[key]?.frameCount ?? 0) + 1,
|
|
1462
|
+
firstFrameIndex:
|
|
1463
|
+
cameraSummaries[key]?.firstFrameIndex ?? frameIndex,
|
|
1464
|
+
lastFrameIndex: frameIndex,
|
|
1465
|
+
firstTimestamp:
|
|
1466
|
+
cameraSummaries[key]?.firstTimestamp ?? data.time,
|
|
1467
|
+
lastTimestamp: data.time,
|
|
1468
|
+
};
|
|
1469
|
+
cameraFrames[key] = cameraFrame;
|
|
1134
1470
|
}
|
|
1135
1471
|
|
|
1136
1472
|
const frame = {
|
|
1137
1473
|
frameIndex,
|
|
1138
|
-
time:
|
|
1474
|
+
time: data.time,
|
|
1139
1475
|
cameras: cameraFrames,
|
|
1140
1476
|
};
|
|
1141
|
-
|
|
1477
|
+
if (retainFrames) {
|
|
1478
|
+
frames.push(frame);
|
|
1479
|
+
}
|
|
1480
|
+
recordedFrameCount += 1;
|
|
1142
1481
|
await options.onFrame?.(frame);
|
|
1143
1482
|
}
|
|
1144
1483
|
} finally {
|
|
1484
|
+
for (const { session } of captureSessions) {
|
|
1485
|
+
session.dispose();
|
|
1486
|
+
}
|
|
1145
1487
|
pausedRef.current = wasPaused;
|
|
1146
1488
|
}
|
|
1147
1489
|
|
|
1148
1490
|
return {
|
|
1149
1491
|
frames,
|
|
1150
1492
|
cameraKeys: cameras.map((sequenceCamera) => sequenceCamera.key),
|
|
1151
|
-
|
|
1493
|
+
cameraSummaries,
|
|
1494
|
+
frameCount: recordedFrameCount,
|
|
1152
1495
|
};
|
|
1153
1496
|
},
|
|
1154
|
-
[camera, getTime, gl, reset,
|
|
1497
|
+
[camera, getTime, gl, mujoco, reset, resolveCameraCaptureOptions, scene]
|
|
1155
1498
|
);
|
|
1156
1499
|
|
|
1157
1500
|
const project2DTo3D = useCallback(
|
|
@@ -1252,6 +1595,7 @@ export function MujocoSimProvider({
|
|
|
1252
1595
|
getSites,
|
|
1253
1596
|
getActuators: getActuatorsApi,
|
|
1254
1597
|
getSensors,
|
|
1598
|
+
getCameras,
|
|
1255
1599
|
getModelOption,
|
|
1256
1600
|
setGravity,
|
|
1257
1601
|
setTimestep: setTimestepApi,
|
|
@@ -1284,7 +1628,7 @@ export function MujocoSimProvider({
|
|
|
1284
1628
|
getControlMapApi, getActuatedJointsApi, resolveControlGroupApi,
|
|
1285
1629
|
applyForce, applyTorqueApi, setExternalForce, applyGeneralizedForce,
|
|
1286
1630
|
getSensorData, getContacts, getBodies, getJoints, getGeoms, getSites,
|
|
1287
|
-
getActuatorsApi, getSensors, getModelOption, setGravity, setTimestepApi,
|
|
1631
|
+
getActuatorsApi, getSensors, getCameras, getModelOption, setGravity, setTimestepApi,
|
|
1288
1632
|
raycast, getKeyframeNames, getKeyframeCount, loadSceneApi,
|
|
1289
1633
|
loadFromFilesApi, addBodyApi, removeBodyApi, recompileApi,
|
|
1290
1634
|
getCanvas, getCanvasSnapshot, captureFrameApi, captureFrameBlobApi,
|
package/src/core/SceneLoader.ts
CHANGED
|
@@ -110,6 +110,19 @@ export function findSensorByName(mjModel: MujocoModel, name: string): number {
|
|
|
110
110
|
return -1;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Find a camera by name in the MuJoCo model. Returns -1 if not found.
|
|
115
|
+
*/
|
|
116
|
+
export function findCameraByName(mjModel: MujocoModel, name: string): number {
|
|
117
|
+
const ncam = mjModel.ncam ?? 0;
|
|
118
|
+
const addresses = mjModel.name_camadr;
|
|
119
|
+
if (!addresses) return -1;
|
|
120
|
+
for (let i = 0; i < ncam; i++) {
|
|
121
|
+
if (getName(mjModel, addresses[i]) === name) return i;
|
|
122
|
+
}
|
|
123
|
+
return -1;
|
|
124
|
+
}
|
|
125
|
+
|
|
113
126
|
/**
|
|
114
127
|
* Find a tendon by name in the MuJoCo model. Returns -1 if not found.
|
|
115
128
|
*/
|