mujoco-react 9.3.0 → 9.5.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 +155 -15
- package/dist/{chunk-T3GVZJ4F.js → chunk-6MOK6ZWB.js} +501 -33
- package/dist/chunk-6MOK6ZWB.js.map +1 -0
- package/dist/index.d.ts +129 -7
- package/dist/index.js +846 -437
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +27 -3
- package/dist/spark.js +156 -3
- package/dist/spark.js.map +1 -1
- package/dist/{types-oxbxOkAx.d.ts → types-BDB9QT6Z.d.ts} +42 -3
- package/dist/vite.js +8 -4
- package/dist/vite.js.map +1 -1
- package/package.json +1 -1
- package/src/components/ContactMarkers.tsx +8 -1
- package/src/components/Debug.tsx +154 -3
- package/src/components/DragInteraction.tsx +2 -0
- package/src/components/IkGizmo.tsx +5 -1
- package/src/components/SplatCollisionProxyPreview.tsx +350 -0
- package/src/components/VisualScenario.tsx +140 -41
- package/src/core/MujocoSimProvider.tsx +6 -6
- package/src/hooks/useMountedCameraSequenceRecorder.ts +54 -6
- package/src/index.ts +32 -0
- package/src/rendering/cameraFrameCapture.ts +259 -28
- package/src/rendering/cameraFrameSource.ts +382 -2
- package/src/spark.tsx +241 -1
- package/src/types.ts +45 -2
- package/src/vite.ts +9 -4
- package/dist/chunk-T3GVZJ4F.js.map +0 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { ThreeElements } from '@react-three/fiber';
|
|
7
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
8
|
+
import type { SplatCollisionProxyConfig } from '../types';
|
|
9
|
+
|
|
10
|
+
export type SplatCollisionProxyPreviewVector3 = [number, number, number];
|
|
11
|
+
|
|
12
|
+
export interface SplatCollisionProxyGeomPreview {
|
|
13
|
+
id: string;
|
|
14
|
+
type: 'box' | 'plane' | 'sphere' | 'capsule' | 'mesh';
|
|
15
|
+
position: SplatCollisionProxyPreviewVector3;
|
|
16
|
+
size: number[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SplatCollisionProxyPreviewProps
|
|
20
|
+
extends Omit<ThreeElements['group'], 'ref'> {
|
|
21
|
+
collisionProxy?: SplatCollisionProxyConfig | null;
|
|
22
|
+
xmlText?: string;
|
|
23
|
+
fetchXml?: (xmlPath: string) => Promise<string>;
|
|
24
|
+
color?: string;
|
|
25
|
+
opacity?: number;
|
|
26
|
+
planeColor?: string;
|
|
27
|
+
planeOpacity?: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type SplatCollisionProxyPreviewStatus =
|
|
31
|
+
| 'idle'
|
|
32
|
+
| 'loading'
|
|
33
|
+
| 'ready'
|
|
34
|
+
| 'error';
|
|
35
|
+
|
|
36
|
+
export interface UseSplatCollisionProxyGeomsOptions {
|
|
37
|
+
collisionProxy?: SplatCollisionProxyConfig | null;
|
|
38
|
+
xmlText?: string;
|
|
39
|
+
fetchXml?: (xmlPath: string) => Promise<string>;
|
|
40
|
+
enabled?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface SplatCollisionProxyGeomsState {
|
|
44
|
+
geoms: SplatCollisionProxyGeomPreview[];
|
|
45
|
+
status: SplatCollisionProxyPreviewStatus;
|
|
46
|
+
error: Error | null;
|
|
47
|
+
xmlPath?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function SplatCollisionProxyPreview({
|
|
51
|
+
collisionProxy,
|
|
52
|
+
xmlText,
|
|
53
|
+
fetchXml = fetchSplatCollisionProxyXml,
|
|
54
|
+
color = '#60a5fa',
|
|
55
|
+
opacity = 0.12,
|
|
56
|
+
planeColor = '#94a3b8',
|
|
57
|
+
planeOpacity = 0.08,
|
|
58
|
+
children,
|
|
59
|
+
...groupProps
|
|
60
|
+
}: SplatCollisionProxyPreviewProps) {
|
|
61
|
+
const { geoms } = useSplatCollisionProxyGeoms({
|
|
62
|
+
collisionProxy,
|
|
63
|
+
xmlText,
|
|
64
|
+
fetchXml,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (geoms.length === 0 && !children) return null;
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<group
|
|
71
|
+
{...groupProps}
|
|
72
|
+
userData={{
|
|
73
|
+
kind: 'splat-collision-proxy-preview',
|
|
74
|
+
...groupProps.userData,
|
|
75
|
+
}}
|
|
76
|
+
>
|
|
77
|
+
{geoms.map((geom) => (
|
|
78
|
+
<SplatCollisionProxyGeom
|
|
79
|
+
key={geom.id}
|
|
80
|
+
geom={geom}
|
|
81
|
+
color={color}
|
|
82
|
+
opacity={opacity}
|
|
83
|
+
planeColor={planeColor}
|
|
84
|
+
planeOpacity={planeOpacity}
|
|
85
|
+
/>
|
|
86
|
+
))}
|
|
87
|
+
{children}
|
|
88
|
+
</group>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function useSplatCollisionProxyGeoms({
|
|
93
|
+
collisionProxy,
|
|
94
|
+
xmlText,
|
|
95
|
+
fetchXml = fetchSplatCollisionProxyXml,
|
|
96
|
+
enabled = true,
|
|
97
|
+
}: UseSplatCollisionProxyGeomsOptions): SplatCollisionProxyGeomsState {
|
|
98
|
+
const [loadedXmlText, setLoadedXmlText] = useState<string | null>(null);
|
|
99
|
+
const [status, setStatus] =
|
|
100
|
+
useState<SplatCollisionProxyPreviewStatus>('idle');
|
|
101
|
+
const [error, setError] = useState<Error | null>(null);
|
|
102
|
+
const xmlPath = collisionProxy?.xmlPath;
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
let cancelled = false;
|
|
106
|
+
|
|
107
|
+
if (!enabled) {
|
|
108
|
+
setLoadedXmlText(null);
|
|
109
|
+
setStatus('idle');
|
|
110
|
+
setError(null);
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (xmlText) {
|
|
115
|
+
setLoadedXmlText(xmlText);
|
|
116
|
+
setStatus('ready');
|
|
117
|
+
setError(null);
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!xmlPath || !canFetchSplatCollisionProxyXml(xmlPath)) {
|
|
122
|
+
setLoadedXmlText(null);
|
|
123
|
+
setStatus('idle');
|
|
124
|
+
setError(null);
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
const fetchPath = xmlPath;
|
|
128
|
+
|
|
129
|
+
async function loadProxyXml() {
|
|
130
|
+
setStatus('loading');
|
|
131
|
+
setError(null);
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const nextXmlText = await fetchXml(fetchPath);
|
|
135
|
+
if (!cancelled) {
|
|
136
|
+
setLoadedXmlText(nextXmlText);
|
|
137
|
+
setStatus('ready');
|
|
138
|
+
}
|
|
139
|
+
} catch (nextError) {
|
|
140
|
+
if (!cancelled) {
|
|
141
|
+
setLoadedXmlText(null);
|
|
142
|
+
setStatus('error');
|
|
143
|
+
setError(
|
|
144
|
+
nextError instanceof Error
|
|
145
|
+
? nextError
|
|
146
|
+
: new Error('Unable to load collision proxy XML.')
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
void loadProxyXml();
|
|
153
|
+
|
|
154
|
+
return () => {
|
|
155
|
+
cancelled = true;
|
|
156
|
+
};
|
|
157
|
+
}, [enabled, fetchXml, xmlPath, xmlText]);
|
|
158
|
+
|
|
159
|
+
const geoms = useMemo(
|
|
160
|
+
() => (loadedXmlText ? parseSplatCollisionProxyGeoms(loadedXmlText) : []),
|
|
161
|
+
[loadedXmlText]
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
return useMemo(
|
|
165
|
+
() => ({
|
|
166
|
+
geoms,
|
|
167
|
+
status,
|
|
168
|
+
error,
|
|
169
|
+
xmlPath,
|
|
170
|
+
}),
|
|
171
|
+
[error, geoms, status, xmlPath]
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export async function fetchSplatCollisionProxyXml(xmlPath: string) {
|
|
176
|
+
const response = await fetch(xmlPath);
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`Unable to load collision proxy XML (${response.status}).`);
|
|
179
|
+
}
|
|
180
|
+
return response.text();
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export function canFetchSplatCollisionProxyXml(xmlPath: string) {
|
|
184
|
+
return (
|
|
185
|
+
xmlPath.startsWith('/') ||
|
|
186
|
+
xmlPath.startsWith('http://') ||
|
|
187
|
+
xmlPath.startsWith('https://')
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function parseSplatCollisionProxyGeoms(
|
|
192
|
+
xmlText: string
|
|
193
|
+
): SplatCollisionProxyGeomPreview[] {
|
|
194
|
+
const parser = typeof DOMParser === 'undefined' ? null : new DOMParser();
|
|
195
|
+
if (!parser) return [];
|
|
196
|
+
|
|
197
|
+
const document = parser.parseFromString(xmlText, 'application/xml');
|
|
198
|
+
if (document.querySelector('parsererror')) return [];
|
|
199
|
+
|
|
200
|
+
const bodyPositions = new Map<Element, SplatCollisionProxyPreviewVector3>();
|
|
201
|
+
|
|
202
|
+
for (const body of Array.from(document.querySelectorAll('body'))) {
|
|
203
|
+
const parentBody = body.parentElement?.closest('body');
|
|
204
|
+
const parentPosition: SplatCollisionProxyPreviewVector3 = parentBody
|
|
205
|
+
? bodyPositions.get(parentBody) ?? [0, 0, 0]
|
|
206
|
+
: [0, 0, 0];
|
|
207
|
+
bodyPositions.set(
|
|
208
|
+
body,
|
|
209
|
+
addProxyVectors(parentPosition, parseProxyVector(body.getAttribute('pos')))
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return Array.from(document.querySelectorAll('geom'))
|
|
214
|
+
.map((geom, index): SplatCollisionProxyGeomPreview | null => {
|
|
215
|
+
const type = getCollisionProxyGeomType(geom);
|
|
216
|
+
if (!type) return null;
|
|
217
|
+
const parentBody = geom.closest('body');
|
|
218
|
+
const bodyPosition: SplatCollisionProxyPreviewVector3 = parentBody
|
|
219
|
+
? bodyPositions.get(parentBody) ?? [0, 0, 0]
|
|
220
|
+
: [0, 0, 0];
|
|
221
|
+
const position = addProxyVectors(
|
|
222
|
+
bodyPosition,
|
|
223
|
+
parseProxyVector(geom.getAttribute('pos'))
|
|
224
|
+
);
|
|
225
|
+
const size = parseNumberList(geom.getAttribute('size'));
|
|
226
|
+
return {
|
|
227
|
+
id: geom.getAttribute('name') ?? `${type}-${index}`,
|
|
228
|
+
type,
|
|
229
|
+
position,
|
|
230
|
+
size,
|
|
231
|
+
};
|
|
232
|
+
})
|
|
233
|
+
.filter((geom): geom is SplatCollisionProxyGeomPreview => Boolean(geom));
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function SplatCollisionProxyGeom({
|
|
237
|
+
geom,
|
|
238
|
+
color,
|
|
239
|
+
opacity,
|
|
240
|
+
planeColor,
|
|
241
|
+
planeOpacity,
|
|
242
|
+
}: {
|
|
243
|
+
geom: SplatCollisionProxyGeomPreview;
|
|
244
|
+
color: string;
|
|
245
|
+
opacity: number;
|
|
246
|
+
planeColor: string;
|
|
247
|
+
planeOpacity: number;
|
|
248
|
+
}) {
|
|
249
|
+
if (geom.type === 'sphere') {
|
|
250
|
+
return (
|
|
251
|
+
<mesh position={geom.position}>
|
|
252
|
+
<sphereGeometry args={[geom.size[0] ?? 0.1, 16, 8]} />
|
|
253
|
+
<SplatCollisionProxyMaterial color={color} opacity={opacity} />
|
|
254
|
+
</mesh>
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (geom.type === 'plane') {
|
|
259
|
+
const width = geom.size[0] && geom.size[0] > 0 ? geom.size[0] * 2 : 4;
|
|
260
|
+
const height = geom.size[1] && geom.size[1] > 0 ? geom.size[1] * 2 : 4;
|
|
261
|
+
return (
|
|
262
|
+
<mesh position={geom.position}>
|
|
263
|
+
<boxGeometry args={[width, height, 0.02]} />
|
|
264
|
+
<SplatCollisionProxyMaterial color={planeColor} opacity={planeOpacity} />
|
|
265
|
+
</mesh>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const size = getCollisionProxyBoxSize(geom);
|
|
270
|
+
return (
|
|
271
|
+
<mesh position={geom.position}>
|
|
272
|
+
<boxGeometry args={size} />
|
|
273
|
+
<SplatCollisionProxyMaterial color={color} opacity={opacity} />
|
|
274
|
+
</mesh>
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function SplatCollisionProxyMaterial({
|
|
279
|
+
color,
|
|
280
|
+
opacity,
|
|
281
|
+
}: {
|
|
282
|
+
color: string;
|
|
283
|
+
opacity: number;
|
|
284
|
+
}) {
|
|
285
|
+
return (
|
|
286
|
+
<meshBasicMaterial
|
|
287
|
+
color={color}
|
|
288
|
+
transparent
|
|
289
|
+
opacity={opacity}
|
|
290
|
+
wireframe
|
|
291
|
+
/>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function getCollisionProxyGeomType(
|
|
296
|
+
geom: Element
|
|
297
|
+
): SplatCollisionProxyGeomPreview['type'] | null {
|
|
298
|
+
const type = geom.getAttribute('type') ?? 'sphere';
|
|
299
|
+
if (
|
|
300
|
+
type === 'box' ||
|
|
301
|
+
type === 'plane' ||
|
|
302
|
+
type === 'sphere' ||
|
|
303
|
+
type === 'capsule' ||
|
|
304
|
+
type === 'mesh'
|
|
305
|
+
) {
|
|
306
|
+
return type;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function getCollisionProxyBoxSize(
|
|
312
|
+
geom: SplatCollisionProxyGeomPreview
|
|
313
|
+
): SplatCollisionProxyPreviewVector3 {
|
|
314
|
+
if (geom.type === 'capsule') {
|
|
315
|
+
const radius = geom.size[0] ?? 0.05;
|
|
316
|
+
const halfLength = geom.size[1] ?? radius;
|
|
317
|
+
return [radius * 2, radius * 2, Math.max(radius * 2, halfLength * 2)];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (geom.type === 'mesh') return [0.2, 0.2, 0.2];
|
|
321
|
+
|
|
322
|
+
return [
|
|
323
|
+
(geom.size[0] ?? 0.1) * 2,
|
|
324
|
+
(geom.size[1] ?? geom.size[0] ?? 0.1) * 2,
|
|
325
|
+
(geom.size[2] ?? geom.size[0] ?? 0.1) * 2,
|
|
326
|
+
];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function parseProxyVector(
|
|
330
|
+
value: string | null
|
|
331
|
+
): SplatCollisionProxyPreviewVector3 {
|
|
332
|
+
const values = parseNumberList(value);
|
|
333
|
+
return [values[0] ?? 0, values[1] ?? 0, values[2] ?? 0];
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function parseNumberList(value: string | null) {
|
|
337
|
+
if (!value) return [];
|
|
338
|
+
return value
|
|
339
|
+
.trim()
|
|
340
|
+
.split(/\s+/)
|
|
341
|
+
.map((part) => Number(part))
|
|
342
|
+
.filter((part) => Number.isFinite(part));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function addProxyVectors(
|
|
346
|
+
a: SplatCollisionProxyPreviewVector3,
|
|
347
|
+
b: SplatCollisionProxyPreviewVector3
|
|
348
|
+
): SplatCollisionProxyPreviewVector3 {
|
|
349
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
350
|
+
}
|
|
@@ -26,6 +26,8 @@ import type {
|
|
|
26
26
|
ScenarioLightingProps,
|
|
27
27
|
SplatEnvironmentProps,
|
|
28
28
|
VisualScenarioConfig,
|
|
29
|
+
VisualScenarioExecutionContext,
|
|
30
|
+
VisualScenarioExecutionContextInput,
|
|
29
31
|
VisualScenarioEffectsProps,
|
|
30
32
|
} from '../types';
|
|
31
33
|
|
|
@@ -114,6 +116,84 @@ export function getScenarioCameraPosition(
|
|
|
114
116
|
];
|
|
115
117
|
}
|
|
116
118
|
|
|
119
|
+
export function useVisualScenarioExecutionContext({
|
|
120
|
+
scenario,
|
|
121
|
+
environment,
|
|
122
|
+
renderer,
|
|
123
|
+
variantId,
|
|
124
|
+
enabled,
|
|
125
|
+
}: VisualScenarioExecutionContextInput): VisualScenarioExecutionContext {
|
|
126
|
+
return useMemo(
|
|
127
|
+
() =>
|
|
128
|
+
createVisualScenarioExecutionContext({
|
|
129
|
+
scenario,
|
|
130
|
+
environment,
|
|
131
|
+
renderer,
|
|
132
|
+
variantId,
|
|
133
|
+
enabled,
|
|
134
|
+
}),
|
|
135
|
+
[enabled, environment, renderer, scenario, variantId]
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function createVisualScenarioExecutionContext({
|
|
140
|
+
scenario,
|
|
141
|
+
environment,
|
|
142
|
+
renderer,
|
|
143
|
+
variantId,
|
|
144
|
+
enabled = true,
|
|
145
|
+
}: VisualScenarioExecutionContextInput): VisualScenarioExecutionContext {
|
|
146
|
+
const pairedEnvironment =
|
|
147
|
+
environment ??
|
|
148
|
+
(scenario ? createPairedSplatEnvironment(scenario, { renderer }) : undefined);
|
|
149
|
+
const splat = scenario?.splat;
|
|
150
|
+
const collisionProxy =
|
|
151
|
+
pairedEnvironment?.collisionProxy ?? splat?.collisionProxy ?? undefined;
|
|
152
|
+
const readiness = getSplatEnvironmentReadiness({
|
|
153
|
+
environment: pairedEnvironment,
|
|
154
|
+
scenario,
|
|
155
|
+
renderer,
|
|
156
|
+
enabled,
|
|
157
|
+
});
|
|
158
|
+
const format =
|
|
159
|
+
pairedEnvironment?.splat.format ?? splat?.format ?? readiness.format ?? 'spz';
|
|
160
|
+
|
|
161
|
+
return {
|
|
162
|
+
scenarioId: scenario?.id ?? pairedEnvironment?.id ?? 'visual-scenario',
|
|
163
|
+
scenarioLabel:
|
|
164
|
+
scenario?.label ?? pairedEnvironment?.label ?? 'Visual scenario',
|
|
165
|
+
variantId,
|
|
166
|
+
seed: scenario?.seed ?? 0,
|
|
167
|
+
lighting: scenario?.lighting ?? 'studio',
|
|
168
|
+
environment: scenario?.environment,
|
|
169
|
+
camera: {
|
|
170
|
+
jitter: scenario?.camera?.jitter ?? 0,
|
|
171
|
+
exposure: scenario?.camera?.exposure ?? 1,
|
|
172
|
+
noise: scenario?.camera?.noise ?? 0,
|
|
173
|
+
blur: scenario?.camera?.blur ?? 0,
|
|
174
|
+
},
|
|
175
|
+
materials: {
|
|
176
|
+
randomizeObjectColors: Boolean(
|
|
177
|
+
scenario?.materials?.randomizeObjectColors
|
|
178
|
+
),
|
|
179
|
+
randomizeTableMaterial: Boolean(
|
|
180
|
+
scenario?.materials?.randomizeTableMaterial
|
|
181
|
+
),
|
|
182
|
+
roughness: scenario?.materials?.roughness,
|
|
183
|
+
metalness: scenario?.materials?.metalness,
|
|
184
|
+
},
|
|
185
|
+
splatEnabled: Boolean(splat?.enabled || pairedEnvironment),
|
|
186
|
+
splatSrc: pairedEnvironment?.splat.src ?? splat?.src,
|
|
187
|
+
splatFormat: format,
|
|
188
|
+
splatRenderer: renderer ?? pairedEnvironment?.splat.renderer,
|
|
189
|
+
collisionProxyXmlPath: collisionProxy?.xmlPath,
|
|
190
|
+
collisionProxyStatus: collisionProxy?.status,
|
|
191
|
+
collisionProxyPrimitives: collisionProxy?.primitives ?? [],
|
|
192
|
+
readiness,
|
|
193
|
+
transformSource: 'visualScenario.camera',
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
117
197
|
export function VisualScenarioEffects(props: VisualScenarioEffectsProps) {
|
|
118
198
|
useVisualScenarioEffects(props);
|
|
119
199
|
return null;
|
|
@@ -341,43 +421,55 @@ export function useSplatSceneConfig({
|
|
|
341
421
|
enabled = true,
|
|
342
422
|
renderer,
|
|
343
423
|
}: SplatSceneConfigInput): SplatSceneConfigState {
|
|
344
|
-
|
|
345
|
-
() =>
|
|
346
|
-
enabled
|
|
347
|
-
? environment ??
|
|
348
|
-
(scenario
|
|
349
|
-
? createPairedSplatEnvironment(scenario, { renderer })
|
|
350
|
-
: undefined)
|
|
351
|
-
: undefined,
|
|
352
|
-
[enabled, environment, renderer, scenario]
|
|
353
|
-
);
|
|
354
|
-
const readiness = useMemo(
|
|
424
|
+
return useMemo(
|
|
355
425
|
() =>
|
|
356
|
-
|
|
357
|
-
|
|
426
|
+
createSplatSceneConfig({
|
|
427
|
+
sceneConfig,
|
|
358
428
|
scenario,
|
|
359
|
-
|
|
429
|
+
environment,
|
|
360
430
|
enabled,
|
|
431
|
+
renderer,
|
|
361
432
|
}),
|
|
362
|
-
[enabled, renderer,
|
|
363
|
-
);
|
|
364
|
-
const resolvedSceneConfig = useMemo(
|
|
365
|
-
() =>
|
|
366
|
-
resolvedEnvironment
|
|
367
|
-
? withSplatEnvironment(sceneConfig, resolvedEnvironment)
|
|
368
|
-
: sceneConfig,
|
|
369
|
-
[resolvedEnvironment, sceneConfig]
|
|
433
|
+
[enabled, environment, renderer, scenario, sceneConfig]
|
|
370
434
|
);
|
|
435
|
+
}
|
|
371
436
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
437
|
+
/**
|
|
438
|
+
* Resolve a visual scenario's paired splat environment without requiring React.
|
|
439
|
+
*
|
|
440
|
+
* Use this in codegen, import validators, backend handoff metadata, or app code
|
|
441
|
+
* that needs the same behavior as `useSplatSceneConfig` outside a component.
|
|
442
|
+
*/
|
|
443
|
+
export function createSplatSceneConfig({
|
|
444
|
+
sceneConfig,
|
|
445
|
+
scenario,
|
|
446
|
+
environment,
|
|
447
|
+
enabled = true,
|
|
448
|
+
renderer,
|
|
449
|
+
}: SplatSceneConfigInput): SplatSceneConfigState {
|
|
450
|
+
const resolvedEnvironment = enabled
|
|
451
|
+
? environment ??
|
|
452
|
+
(scenario
|
|
453
|
+
? createPairedSplatEnvironment(scenario, { renderer })
|
|
454
|
+
: undefined)
|
|
455
|
+
: undefined;
|
|
456
|
+
const readiness = getSplatEnvironmentReadiness({
|
|
457
|
+
environment: resolvedEnvironment,
|
|
458
|
+
scenario,
|
|
459
|
+
renderer,
|
|
460
|
+
enabled,
|
|
461
|
+
});
|
|
462
|
+
const resolvedSceneConfig = resolvedEnvironment
|
|
463
|
+
? withSplatEnvironment(sceneConfig, resolvedEnvironment, { renderer })
|
|
464
|
+
: sceneConfig;
|
|
465
|
+
|
|
466
|
+
return {
|
|
467
|
+
environment: resolvedEnvironment,
|
|
468
|
+
sceneConfig: resolvedSceneConfig,
|
|
469
|
+
enabled:
|
|
470
|
+
enabled && readiness.status !== SplatEnvironmentReadinessStatus.Disabled,
|
|
471
|
+
readiness,
|
|
472
|
+
};
|
|
381
473
|
}
|
|
382
474
|
|
|
383
475
|
export function getSplatEnvironmentReadiness({
|
|
@@ -468,9 +560,9 @@ export function getSplatEnvironmentReadiness({
|
|
|
468
560
|
}
|
|
469
561
|
|
|
470
562
|
/**
|
|
471
|
-
* Convert a generic visual scenario splat block into a
|
|
472
|
-
* environment config.
|
|
473
|
-
* collision proxy
|
|
563
|
+
* Convert a generic visual scenario splat block into a composable splat
|
|
564
|
+
* environment config. Visual-only splats are valid; readiness reports whether
|
|
565
|
+
* a paired MJCF collision proxy is required before training/physics handoff.
|
|
474
566
|
*/
|
|
475
567
|
export function createPairedSplatEnvironment(
|
|
476
568
|
scenario: Pick<VisualScenarioConfig, 'id' | 'label' | 'environment' | 'splat'>,
|
|
@@ -484,7 +576,7 @@ export function createPairedSplatEnvironment(
|
|
|
484
576
|
const splat = scenario.splat;
|
|
485
577
|
const collisionProxy = splat?.collisionProxy;
|
|
486
578
|
|
|
487
|
-
if (!splat?.enabled || !splat.src
|
|
579
|
+
if (!splat?.enabled || !splat.src) {
|
|
488
580
|
return undefined;
|
|
489
581
|
}
|
|
490
582
|
|
|
@@ -501,15 +593,22 @@ export function createPairedSplatEnvironment(
|
|
|
501
593
|
format: splat.format ?? 'spz',
|
|
502
594
|
renderer: options.renderer,
|
|
503
595
|
},
|
|
504
|
-
collisionProxy:
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
596
|
+
collisionProxy: collisionProxy?.xmlPath
|
|
597
|
+
? {
|
|
598
|
+
...collisionProxy,
|
|
599
|
+
xmlPath: collisionProxy.xmlPath,
|
|
600
|
+
}
|
|
601
|
+
: undefined,
|
|
508
602
|
};
|
|
509
603
|
}
|
|
510
604
|
|
|
511
605
|
function isPairedSplatEnvironment(input: SplatSceneInput): input is PairedSplatEnvironmentConfig {
|
|
512
|
-
return
|
|
606
|
+
return (
|
|
607
|
+
!!input &&
|
|
608
|
+
'splat' in input &&
|
|
609
|
+
!!input.splat &&
|
|
610
|
+
!('enabled' in input.splat)
|
|
611
|
+
);
|
|
513
612
|
}
|
|
514
613
|
|
|
515
614
|
function sceneRelativePath(sceneConfig: SceneConfig, path: string): string {
|
|
@@ -549,7 +648,7 @@ export function withSplatEnvironment(
|
|
|
549
648
|
: input
|
|
550
649
|
? createPairedSplatEnvironment(input, options)
|
|
551
650
|
: undefined;
|
|
552
|
-
const xmlPath = environment?.collisionProxy
|
|
651
|
+
const xmlPath = environment?.collisionProxy?.xmlPath;
|
|
553
652
|
if (!xmlPath) return sceneConfig;
|
|
554
653
|
|
|
555
654
|
return {
|
|
@@ -148,12 +148,12 @@ function vector3FromArray(values: ArrayLike<number>, offset: number): [number, n
|
|
|
148
148
|
return [values[offset], values[offset + 1], values[offset + 2]];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
function
|
|
151
|
+
function quaternionFromMujocoQuat(values: ArrayLike<number>, offset: number): [number, number, number, number] {
|
|
152
152
|
return [
|
|
153
|
-
values[offset],
|
|
154
153
|
values[offset + 1],
|
|
155
154
|
values[offset + 2],
|
|
156
155
|
values[offset + 3],
|
|
156
|
+
values[offset],
|
|
157
157
|
];
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -993,7 +993,7 @@ export function MujocoSimProvider({
|
|
|
993
993
|
? vector3FromArray(model.cam_pos, posOffset)
|
|
994
994
|
: null,
|
|
995
995
|
quaternion: model.cam_quat
|
|
996
|
-
?
|
|
996
|
+
? quaternionFromMujocoQuat(model.cam_quat, quatOffset)
|
|
997
997
|
: null,
|
|
998
998
|
});
|
|
999
999
|
}
|
|
@@ -1024,7 +1024,7 @@ export function MujocoSimProvider({
|
|
|
1024
1024
|
const quaternion = data.cam_xmat
|
|
1025
1025
|
? quaternionFromXmat(data.cam_xmat, cameraId * 9)
|
|
1026
1026
|
: model.cam_quat
|
|
1027
|
-
?
|
|
1027
|
+
? quaternionFromMujocoQuat(model.cam_quat, cameraId * 4)
|
|
1028
1028
|
: undefined;
|
|
1029
1029
|
|
|
1030
1030
|
if (!position || !quaternion) {
|
|
@@ -1442,7 +1442,7 @@ export function MujocoSimProvider({
|
|
|
1442
1442
|
const cameraFrames: Record<string, CameraFrameCaptureResult> = {};
|
|
1443
1443
|
for (const { key, captureOptions, mountedSource, session } of captureSessions) {
|
|
1444
1444
|
const resolvedCaptureOptions = resolveCameraCaptureOptions(captureOptions);
|
|
1445
|
-
const cameraFrame = session.
|
|
1445
|
+
const cameraFrame = await session.captureDataUrlAsync({
|
|
1446
1446
|
...resolvedCaptureOptions,
|
|
1447
1447
|
source: mountedSource ?? resolvedCaptureOptions.source,
|
|
1448
1448
|
});
|
|
@@ -1471,7 +1471,7 @@ export function MujocoSimProvider({
|
|
|
1471
1471
|
|
|
1472
1472
|
const frame = {
|
|
1473
1473
|
frameIndex,
|
|
1474
|
-
time:
|
|
1474
|
+
time: data.time,
|
|
1475
1475
|
cameras: cameraFrames,
|
|
1476
1476
|
};
|
|
1477
1477
|
if (retainFrames) {
|