mujoco-react 8.10.0 → 9.0.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 +81 -44
- package/dist/chunk-33CV6HSV.js +400 -0
- package/dist/chunk-33CV6HSV.js.map +1 -0
- package/dist/index.d.ts +92 -24
- package/dist/index.js +338 -54
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +24 -3
- package/dist/spark.js +91 -6
- package/dist/spark.js.map +1 -1
- package/dist/{types-FFW7ykBu.d.ts → types-izZlUweI.d.ts} +109 -16
- package/package.json +1 -1
- package/src/components/Body.tsx +3 -1
- package/src/components/DragInteraction.tsx +1 -1
- package/src/components/IkGizmo.tsx +2 -2
- package/src/components/SceneRenderer.tsx +1 -1
- package/src/components/TrajectoryPlayer.tsx +4 -1
- package/src/components/VisualScenario.tsx +343 -6
- package/src/core/MujocoCanvas.tsx +8 -1
- package/src/core/MujocoPhysics.tsx +10 -4
- package/src/core/MujocoSimProvider.tsx +15 -12
- package/src/core/SceneLoader.ts +182 -3
- package/src/core/createController.tsx +2 -2
- package/src/hooks/useBodyState.ts +1 -1
- package/src/hooks/useContacts.ts +1 -1
- package/src/hooks/useCtrlNoise.ts +1 -1
- package/src/hooks/useFrameCapture.ts +206 -0
- package/src/hooks/useGamepad.ts +1 -1
- package/src/hooks/useGravityCompensation.ts +1 -1
- package/src/hooks/useIkController.ts +22 -13
- package/src/hooks/useJointState.ts +1 -1
- package/src/hooks/useKeyboardTeleop.ts +1 -1
- package/src/hooks/usePolicy.ts +13 -9
- package/src/hooks/useSensor.ts +1 -1
- package/src/hooks/useTrajectoryPlayer.ts +4 -4
- package/src/hooks/useTrajectoryRecorder.ts +1 -1
- package/src/index.ts +35 -0
- package/src/spark.tsx +138 -4
- package/src/types.ts +128 -21
- package/dist/chunk-KGFRKPLS.js +0 -186
- package/dist/chunk-KGFRKPLS.js.map +0 -1
package/README.md
CHANGED
|
@@ -145,20 +145,42 @@ Use it as a child of `<MujocoCanvas>`:
|
|
|
145
145
|
|
|
146
146
|
Gaussian splats are visual context; MuJoCo XML remains the source of physics, contacts, and task fixtures. Pair each splat asset with collision proxy metadata so scene variants, rollouts, and datasets preserve both sides of the environment.
|
|
147
147
|
|
|
148
|
-
Use the
|
|
148
|
+
Use `VisualScenarioEffects` when the same MuJoCo task should render under
|
|
149
|
+
different camera exposure, fog/background, and deterministic material variants:
|
|
149
150
|
|
|
150
151
|
```tsx
|
|
151
|
-
import {
|
|
152
|
-
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
152
|
+
import { ScenarioLighting, VisualScenarioEffects } from "mujoco-react";
|
|
153
|
+
|
|
154
|
+
<MujocoCanvas config={sceneConfig}>
|
|
155
|
+
<VisualScenarioEffects
|
|
156
|
+
scenario={scenario}
|
|
157
|
+
materialFilter={({ object }) => object.name.startsWith("prop_")}
|
|
158
|
+
/>
|
|
159
|
+
<ScenarioLighting preset={scenario.lighting} />
|
|
160
|
+
</MujocoCanvas>;
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Use the renderer-agnostic boundary from the main package. If your app stores
|
|
164
|
+
visual scenarios as data, pass the scenario directly; the component resolves the
|
|
165
|
+
splat asset and paired MJCF collision proxy metadata for you.
|
|
166
|
+
|
|
167
|
+
```tsx
|
|
168
|
+
import { SplatEnvironment, withSplatEnvironment } from "mujoco-react";
|
|
169
|
+
|
|
170
|
+
<SplatEnvironment scenario={scenario} renderer="custom" />;
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
For MuJoCo + 3DGS composition, derive the collision environment from the same
|
|
174
|
+
splat metadata and pass the resulting config to `<MujocoCanvas>`:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
const sceneConfig = withSplatEnvironment(
|
|
178
|
+
{
|
|
179
|
+
src: "/models/xlerobot/",
|
|
180
|
+
sceneFile: "xlerobot.xml",
|
|
181
|
+
},
|
|
182
|
+
kitchenScenario
|
|
183
|
+
);
|
|
162
184
|
```
|
|
163
185
|
|
|
164
186
|
For first-class Spark rendering, install Spark and import the optional adapter:
|
|
@@ -168,30 +190,33 @@ npm install @sparkjsdev/spark
|
|
|
168
190
|
```
|
|
169
191
|
|
|
170
192
|
```tsx
|
|
171
|
-
import {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
193
|
+
import {
|
|
194
|
+
SparkSplatEnvironment,
|
|
195
|
+
useSparkSplatLifecycle,
|
|
196
|
+
} from "mujoco-react/spark";
|
|
197
|
+
|
|
198
|
+
function Scene() {
|
|
199
|
+
const splat = useSparkSplatLifecycle();
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<MujocoCanvas config={sceneConfig} gl={{ preserveDrawingBuffer: true }}>
|
|
203
|
+
<SparkSplatEnvironment scenario={scenario} hideGroundMeshes {...splat.props} />
|
|
204
|
+
<StatusBadge status={splat.status} error={splat.error} />
|
|
205
|
+
</MujocoCanvas>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
186
208
|
```
|
|
187
209
|
|
|
210
|
+
`SparkSplatEnvironment` currently renders `.spz` assets. Use the renderer-agnostic
|
|
211
|
+
`SplatEnvironment` for `.ply`/`.splat` metadata or when wiring a different renderer.
|
|
212
|
+
|
|
188
213
|
## Write Controllers
|
|
189
214
|
|
|
190
215
|
```tsx
|
|
191
216
|
import { useBeforePhysicsStep } from "mujoco-react";
|
|
192
217
|
|
|
193
218
|
function MyController() {
|
|
194
|
-
useBeforePhysicsStep((
|
|
219
|
+
useBeforePhysicsStep(({ data }) => {
|
|
195
220
|
data.ctrl[0] = Math.sin(data.time);
|
|
196
221
|
});
|
|
197
222
|
|
|
@@ -259,7 +284,7 @@ import type { ControlGroupInfo } from "mujoco-react";
|
|
|
259
284
|
function HoldTcpPose() {
|
|
260
285
|
const armRef = useRef<ControlGroupInfo | null>(null);
|
|
261
286
|
|
|
262
|
-
useBeforePhysicsStep((model, data) => {
|
|
287
|
+
useBeforePhysicsStep(({ model, data }) => {
|
|
263
288
|
armRef.current ??= resolveControlGroup(model, { siteName: RobotSites.franka.tcp });
|
|
264
289
|
if (!armRef.current) return;
|
|
265
290
|
|
|
@@ -280,7 +305,7 @@ Build policy-ready observation vectors from common MuJoCo state without hard-cod
|
|
|
280
305
|
import { buildObservation, useBeforePhysicsStep } from "mujoco-react";
|
|
281
306
|
|
|
282
307
|
function PolicyDriver() {
|
|
283
|
-
useBeforePhysicsStep((model, data) => {
|
|
308
|
+
useBeforePhysicsStep(({ model, data }) => {
|
|
284
309
|
const obs = buildObservation(model, data, {
|
|
285
310
|
qpos: true,
|
|
286
311
|
qvel: true,
|
|
@@ -336,7 +361,7 @@ function useWebSocketControls(url: string) {
|
|
|
336
361
|
}, [url]);
|
|
337
362
|
|
|
338
363
|
// Apply incoming actuator controls each physics step.
|
|
339
|
-
useBeforePhysicsStep((model, data) => {
|
|
364
|
+
useBeforePhysicsStep(({ model, data }) => {
|
|
340
365
|
const ctrl = latestCtrlRef.current;
|
|
341
366
|
if (!ctrl) return;
|
|
342
367
|
for (let i = 0; i < Math.min(ctrl.length, model.nu); i++) {
|
|
@@ -345,7 +370,7 @@ function useWebSocketControls(url: string) {
|
|
|
345
370
|
});
|
|
346
371
|
|
|
347
372
|
// Send simulation feedback back after physics.
|
|
348
|
-
useAfterPhysicsStep((
|
|
373
|
+
useAfterPhysicsStep(({ data }) => {
|
|
349
374
|
const ws = wsRef.current;
|
|
350
375
|
if (!ws || ws.readyState !== WebSocket.OPEN) return;
|
|
351
376
|
|
|
@@ -408,8 +433,8 @@ The built-in `useIkController()` uses Damped Least-Squares. Pass `ikSolveFn` to
|
|
|
408
433
|
import { RobotSites } from "mujoco-react";
|
|
409
434
|
import type { IKSolveFn } from "mujoco-react";
|
|
410
435
|
|
|
411
|
-
const myIK: IKSolveFn = (
|
|
412
|
-
return myAnalyticalSolver(
|
|
436
|
+
const myIK: IKSolveFn = ({ position, currentQ }) => {
|
|
437
|
+
return myAnalyticalSolver(position, currentQ); // return joint angles or null
|
|
413
438
|
};
|
|
414
439
|
|
|
415
440
|
const ik = useIkController({ siteName: RobotSites.franka.tcp, ikSolveFn: myIK });
|
|
@@ -497,13 +522,24 @@ interface SceneConfig {
|
|
|
497
522
|
src: string; // Base URL for model files
|
|
498
523
|
sceneFile: string; // Entry XML/URDF file, e.g. "scene.xml"
|
|
499
524
|
files?: File[]; // Local files for browser upload workflows
|
|
525
|
+
environmentFiles?: string[]; // Static MJCF environment XMLs merged before compile
|
|
500
526
|
sceneObjects?: SceneObject[]; // Objects injected into scene XML at load time
|
|
501
527
|
homeJoints?: number[]; // Initial joint positions
|
|
502
528
|
xmlPatches?: XmlPatch[]; // Patches applied to XML files during loading
|
|
503
|
-
onReset?: (model, data) => void; // Called during reset after mj_resetData
|
|
529
|
+
onReset?: ({ model, data }) => void; // Called during reset after mj_resetData
|
|
504
530
|
}
|
|
505
531
|
```
|
|
506
532
|
|
|
533
|
+
Use `environmentFiles` to compose reusable physics/collision layers with a robot model. For Gaussian splat scenes, keep the `.spz` as a parallel visual layer and point `environmentFiles` at the paired MJCF proxy scene:
|
|
534
|
+
|
|
535
|
+
```tsx
|
|
536
|
+
const kitchenRobot: SceneConfig = {
|
|
537
|
+
src: "/models/xlerobot/",
|
|
538
|
+
sceneFile: "xlerobot.xml",
|
|
539
|
+
environmentFiles: ["splats/tabletop/scene.xml"],
|
|
540
|
+
};
|
|
541
|
+
```
|
|
542
|
+
|
|
507
543
|
### Local Files and URDF
|
|
508
544
|
|
|
509
545
|
Load browser-selected MJCF or URDF files directly. Folder uploads preserve `webkitRelativePath`, and flat uploads fall back to matching referenced mesh/texture assets by basename:
|
|
@@ -600,10 +636,10 @@ Thin wrapper around R3F `<Canvas>`. Accepts all R3F Canvas props plus:
|
|
|
600
636
|
| Prop | Type | Description |
|
|
601
637
|
|------|------|-------------|
|
|
602
638
|
| `config` | `SceneConfig` | **Required.** Scene/robot configuration |
|
|
603
|
-
| `onReady` | `(api
|
|
639
|
+
| `onReady` | `({ api }) => void` | Fires when model is loaded |
|
|
604
640
|
| `onError` | `(error: Error) => void` | Fires on scene load failure |
|
|
605
|
-
| `onStep` | `(time
|
|
606
|
-
| `onSelection` | `(bodyId
|
|
641
|
+
| `onStep` | `({ time, model, data }) => void` | Called each physics step |
|
|
642
|
+
| `onSelection` | `({ bodyId, name }) => void` | Called on double-click |
|
|
607
643
|
| `gravity` | `[number, number, number]` | Override model gravity |
|
|
608
644
|
| `timestep` | `number` | Override model.opt.timestep |
|
|
609
645
|
| `substeps` | `number` | mj_step calls per frame |
|
|
@@ -628,10 +664,10 @@ Physics provider for use inside your own R3F `<Canvas>`. Same physics props as `
|
|
|
628
664
|
| Prop | Type | Description |
|
|
629
665
|
|------|------|-------------|
|
|
630
666
|
| `config` | `SceneConfig` | **Required.** Scene/robot configuration |
|
|
631
|
-
| `onReady` | `(api
|
|
667
|
+
| `onReady` | `({ api }) => void` | Fires when model is loaded |
|
|
632
668
|
| `onError` | `(error: Error) => void` | Fires on scene load failure |
|
|
633
|
-
| `onStep` | `(time
|
|
634
|
-
| `onSelection` | `(bodyId
|
|
669
|
+
| `onStep` | `({ time, model, data }) => void` | Called each physics step |
|
|
670
|
+
| `onSelection` | `({ bodyId, name }) => void` | Called on double-click |
|
|
635
671
|
| `gravity` | `[number, number, number]` | Override model gravity |
|
|
636
672
|
| `timestep` | `number` | Override model.opt.timestep |
|
|
637
673
|
| `substeps` | `number` | mj_step calls per frame |
|
|
@@ -679,7 +715,7 @@ drei PivotControls gizmo that tracks a MuJoCo site and drives IK on drag. Requir
|
|
|
679
715
|
| `controller` | `IkContextValue` | **required** | Controller from `useIkController()` |
|
|
680
716
|
| `siteName` | `string?` | controller's site | MuJoCo site to track |
|
|
681
717
|
| `scale` | `number?` | `0.18` | Gizmo handle scale |
|
|
682
|
-
| `onDrag` | `(
|
|
718
|
+
| `onDrag` | `({ position, quaternion }) => void` | -- | Custom drag handler (disables auto-IK) |
|
|
683
719
|
|
|
684
720
|
### `<DragInteraction />`
|
|
685
721
|
|
|
@@ -790,7 +826,7 @@ if (mujoco) {
|
|
|
790
826
|
Run logic **before** `mj_step` each frame. Write to `data.ctrl`, apply forces, drive automation.
|
|
791
827
|
|
|
792
828
|
```tsx
|
|
793
|
-
useBeforePhysicsStep((
|
|
829
|
+
useBeforePhysicsStep(({ data }) => {
|
|
794
830
|
data.ctrl[0] = Math.sin(data.time);
|
|
795
831
|
});
|
|
796
832
|
```
|
|
@@ -905,7 +941,8 @@ const obs = useObservation({ qpos: true, qvel: true, projectedGravity: "torso" }
|
|
|
905
941
|
const policy = usePolicy({
|
|
906
942
|
frequency: 50,
|
|
907
943
|
onObservation: () => obs.readValues(),
|
|
908
|
-
|
|
944
|
+
infer: ({ observation }) => policySession.run(observation),
|
|
945
|
+
onAction: ({ action, data }) => applyAction(action, data),
|
|
909
946
|
});
|
|
910
947
|
```
|
|
911
948
|
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { useThree } from '@react-three/fiber';
|
|
2
|
+
import { useEffect, useMemo } from 'react';
|
|
3
|
+
import * as THREE from 'three';
|
|
4
|
+
import { jsxs, Fragment, jsx } from 'react/jsx-runtime';
|
|
5
|
+
|
|
6
|
+
// src/components/VisualScenario.tsx
|
|
7
|
+
var DEFAULT_BACKGROUND = "#181a1f";
|
|
8
|
+
function ScenarioLighting({
|
|
9
|
+
preset = "studio",
|
|
10
|
+
castShadow = true,
|
|
11
|
+
intensity = 1
|
|
12
|
+
}) {
|
|
13
|
+
if (preset === "warehouse") {
|
|
14
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
15
|
+
/* @__PURE__ */ jsx("ambientLight", { intensity: 0.18 * intensity }),
|
|
16
|
+
/* @__PURE__ */ jsx(
|
|
17
|
+
"directionalLight",
|
|
18
|
+
{
|
|
19
|
+
position: [3.5, -2, 5],
|
|
20
|
+
intensity: 2.2 * intensity,
|
|
21
|
+
castShadow
|
|
22
|
+
}
|
|
23
|
+
),
|
|
24
|
+
/* @__PURE__ */ jsx("directionalLight", { position: [-2, 1.5, 2.5], intensity: 0.25 * intensity })
|
|
25
|
+
] });
|
|
26
|
+
}
|
|
27
|
+
if (preset === "low-light") {
|
|
28
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
29
|
+
/* @__PURE__ */ jsx("ambientLight", { intensity: 0.08 * intensity }),
|
|
30
|
+
/* @__PURE__ */ jsx(
|
|
31
|
+
"directionalLight",
|
|
32
|
+
{
|
|
33
|
+
position: [2, -2, 3],
|
|
34
|
+
intensity: 0.75 * intensity,
|
|
35
|
+
castShadow
|
|
36
|
+
}
|
|
37
|
+
),
|
|
38
|
+
/* @__PURE__ */ jsx("pointLight", { position: [-0.5, -0.8, 1.3], intensity: 0.6 * intensity })
|
|
39
|
+
] });
|
|
40
|
+
}
|
|
41
|
+
if (preset === "splat") {
|
|
42
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
43
|
+
/* @__PURE__ */ jsx("ambientLight", { intensity: 0.42 * intensity }),
|
|
44
|
+
/* @__PURE__ */ jsx(
|
|
45
|
+
"directionalLight",
|
|
46
|
+
{
|
|
47
|
+
position: [1.8, -2.4, 3.5],
|
|
48
|
+
intensity: 1.2 * intensity,
|
|
49
|
+
castShadow
|
|
50
|
+
}
|
|
51
|
+
),
|
|
52
|
+
/* @__PURE__ */ jsx("pointLight", { position: [0.4, 0.2, 1.4], intensity: 0.35 * intensity })
|
|
53
|
+
] });
|
|
54
|
+
}
|
|
55
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
56
|
+
/* @__PURE__ */ jsx("ambientLight", { intensity: 0.35 * intensity }),
|
|
57
|
+
/* @__PURE__ */ jsx(
|
|
58
|
+
"directionalLight",
|
|
59
|
+
{
|
|
60
|
+
position: [2.5, -3, 4],
|
|
61
|
+
intensity: 1.6 * intensity,
|
|
62
|
+
castShadow
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
] });
|
|
66
|
+
}
|
|
67
|
+
function getScenarioBackground(preset, fallback = DEFAULT_BACKGROUND) {
|
|
68
|
+
if (preset === "warehouse") return "#20242b";
|
|
69
|
+
if (preset === "low-light") return "#0f1115";
|
|
70
|
+
if (preset === "splat") return "#1b1f24";
|
|
71
|
+
return fallback;
|
|
72
|
+
}
|
|
73
|
+
function getScenarioCameraPosition(basePosition, scenario) {
|
|
74
|
+
const [x, y, z] = basePosition;
|
|
75
|
+
const jitter = scenario?.camera?.jitter ?? 0;
|
|
76
|
+
return [
|
|
77
|
+
Number((x + jitter * 0.6).toFixed(3)),
|
|
78
|
+
Number((y - jitter * 0.4).toFixed(3)),
|
|
79
|
+
Number((z + jitter * 0.25).toFixed(3))
|
|
80
|
+
];
|
|
81
|
+
}
|
|
82
|
+
function VisualScenarioEffects(props) {
|
|
83
|
+
useVisualScenarioEffects(props);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
function useVisualScenarioEffects({
|
|
87
|
+
scenario,
|
|
88
|
+
enabled = true,
|
|
89
|
+
applyBackground = true,
|
|
90
|
+
applyFog = true,
|
|
91
|
+
applyRenderer = true,
|
|
92
|
+
applyMaterials = true,
|
|
93
|
+
background,
|
|
94
|
+
fogNear,
|
|
95
|
+
fogFar,
|
|
96
|
+
materialFilter
|
|
97
|
+
}) {
|
|
98
|
+
const { gl, scene, invalidate } = useThree();
|
|
99
|
+
useEffect(() => {
|
|
100
|
+
if (!enabled || !scenario) {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
const previousExposure = gl.toneMappingExposure;
|
|
104
|
+
const previousBackground = scene.background;
|
|
105
|
+
const previousFog = scene.fog;
|
|
106
|
+
const materialSnapshots = /* @__PURE__ */ new Map();
|
|
107
|
+
if (applyRenderer) {
|
|
108
|
+
gl.toneMappingExposure = scenario.camera?.exposure ?? 1;
|
|
109
|
+
}
|
|
110
|
+
if (applyBackground) {
|
|
111
|
+
scene.background = new THREE.Color(
|
|
112
|
+
background ?? getScenarioBackground(scenario.lighting)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (applyFog) {
|
|
116
|
+
scene.fog = createScenarioFog(scenario, background, fogNear, fogFar);
|
|
117
|
+
}
|
|
118
|
+
if (applyMaterials && scenario.materials) {
|
|
119
|
+
applyScenarioMaterials(scene, scenario, materialSnapshots, materialFilter);
|
|
120
|
+
}
|
|
121
|
+
invalidate();
|
|
122
|
+
return () => {
|
|
123
|
+
gl.toneMappingExposure = previousExposure;
|
|
124
|
+
scene.background = previousBackground;
|
|
125
|
+
scene.fog = previousFog;
|
|
126
|
+
for (const [material, snapshot] of materialSnapshots) {
|
|
127
|
+
const mutable = getMutableScenarioMaterial(material);
|
|
128
|
+
if (!mutable) continue;
|
|
129
|
+
if (snapshot.color) mutable.color.copy(snapshot.color);
|
|
130
|
+
if (typeof snapshot.roughness === "number") {
|
|
131
|
+
mutable.roughness = snapshot.roughness;
|
|
132
|
+
}
|
|
133
|
+
if (typeof snapshot.metalness === "number") {
|
|
134
|
+
mutable.metalness = snapshot.metalness;
|
|
135
|
+
}
|
|
136
|
+
mutable.needsUpdate = true;
|
|
137
|
+
}
|
|
138
|
+
invalidate();
|
|
139
|
+
};
|
|
140
|
+
}, [
|
|
141
|
+
applyBackground,
|
|
142
|
+
applyFog,
|
|
143
|
+
applyMaterials,
|
|
144
|
+
applyRenderer,
|
|
145
|
+
background,
|
|
146
|
+
enabled,
|
|
147
|
+
fogFar,
|
|
148
|
+
fogNear,
|
|
149
|
+
gl,
|
|
150
|
+
invalidate,
|
|
151
|
+
materialFilter,
|
|
152
|
+
scenario,
|
|
153
|
+
scene
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
function SplatEnvironment({
|
|
157
|
+
environment,
|
|
158
|
+
scenario,
|
|
159
|
+
renderer,
|
|
160
|
+
src,
|
|
161
|
+
format,
|
|
162
|
+
collisionProxy,
|
|
163
|
+
collisionProxyMetadata,
|
|
164
|
+
children,
|
|
165
|
+
showPlaceholder = true,
|
|
166
|
+
...groupProps
|
|
167
|
+
}) {
|
|
168
|
+
const metadata = useSplatEnvironment({
|
|
169
|
+
environment,
|
|
170
|
+
scenario,
|
|
171
|
+
renderer,
|
|
172
|
+
src,
|
|
173
|
+
format,
|
|
174
|
+
collisionProxy: collisionProxyMetadata
|
|
175
|
+
});
|
|
176
|
+
const existingUserData = typeof groupProps.userData === "object" && groupProps.userData !== null ? groupProps.userData : {};
|
|
177
|
+
return /* @__PURE__ */ jsxs(
|
|
178
|
+
"group",
|
|
179
|
+
{
|
|
180
|
+
...groupProps,
|
|
181
|
+
userData: {
|
|
182
|
+
...existingUserData,
|
|
183
|
+
...metadata.userData
|
|
184
|
+
},
|
|
185
|
+
children: [
|
|
186
|
+
children,
|
|
187
|
+
children || !showPlaceholder ? null : /* @__PURE__ */ jsx(SplatPlaceholder, {}),
|
|
188
|
+
collisionProxy
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
);
|
|
192
|
+
}
|
|
193
|
+
function useSplatEnvironment({
|
|
194
|
+
environment,
|
|
195
|
+
scenario,
|
|
196
|
+
renderer,
|
|
197
|
+
src,
|
|
198
|
+
format,
|
|
199
|
+
collisionProxy
|
|
200
|
+
}) {
|
|
201
|
+
const scenarioEnvironment = useMemo(
|
|
202
|
+
() => environment ?? (scenario ? createPairedSplatEnvironment(scenario, { renderer }) : void 0),
|
|
203
|
+
[environment, renderer, scenario]
|
|
204
|
+
);
|
|
205
|
+
const resolvedSrc = src ?? scenarioEnvironment?.splat.src ?? scenario?.splat?.src;
|
|
206
|
+
const resolvedFormat = format ?? scenarioEnvironment?.splat.format ?? scenario?.splat?.format ?? "spz";
|
|
207
|
+
const resolvedCollisionProxy = collisionProxy ?? scenarioEnvironment?.collisionProxy ?? scenario?.splat?.collisionProxy ?? void 0;
|
|
208
|
+
return useMemo(
|
|
209
|
+
() => ({
|
|
210
|
+
src: resolvedSrc,
|
|
211
|
+
format: resolvedFormat,
|
|
212
|
+
collisionProxy: resolvedCollisionProxy,
|
|
213
|
+
userData: createSplatEnvironmentUserData({
|
|
214
|
+
environment: scenarioEnvironment,
|
|
215
|
+
src: resolvedSrc,
|
|
216
|
+
format: resolvedFormat,
|
|
217
|
+
collisionProxy: resolvedCollisionProxy
|
|
218
|
+
})
|
|
219
|
+
}),
|
|
220
|
+
[scenarioEnvironment, resolvedSrc, resolvedFormat, resolvedCollisionProxy]
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
function createPairedSplatEnvironment(scenario, options = {}) {
|
|
224
|
+
const splat = scenario.splat;
|
|
225
|
+
const collisionProxy = splat?.collisionProxy;
|
|
226
|
+
if (!splat?.enabled || !splat.src || !collisionProxy?.xmlPath) {
|
|
227
|
+
return void 0;
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
id: options.id ?? scenario.id ?? "splat-environment",
|
|
231
|
+
label: options.label ?? scenario.label ?? "Gaussian splat environment",
|
|
232
|
+
description: options.description ?? (scenario.environment ? `Visual ${scenario.environment} splat paired with MJCF collision proxy.` : void 0),
|
|
233
|
+
splat: {
|
|
234
|
+
src: splat.src,
|
|
235
|
+
format: splat.format ?? "spz",
|
|
236
|
+
renderer: options.renderer
|
|
237
|
+
},
|
|
238
|
+
collisionProxy: {
|
|
239
|
+
...collisionProxy,
|
|
240
|
+
xmlPath: collisionProxy.xmlPath
|
|
241
|
+
}
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
function isPairedSplatEnvironment(input) {
|
|
245
|
+
return !!input && "collisionProxy" in input && "splat" in input;
|
|
246
|
+
}
|
|
247
|
+
function sceneRelativePath(sceneConfig, path) {
|
|
248
|
+
const src = sceneConfig.src;
|
|
249
|
+
if (!src) return path;
|
|
250
|
+
const base = src.endsWith("/") ? src : src + "/";
|
|
251
|
+
if (path.startsWith(base)) return path.slice(base.length);
|
|
252
|
+
return path;
|
|
253
|
+
}
|
|
254
|
+
function uniquePaths(paths) {
|
|
255
|
+
const seen = /* @__PURE__ */ new Set();
|
|
256
|
+
const result = [];
|
|
257
|
+
for (const path of paths) {
|
|
258
|
+
if (seen.has(path)) continue;
|
|
259
|
+
seen.add(path);
|
|
260
|
+
result.push(path);
|
|
261
|
+
}
|
|
262
|
+
return result;
|
|
263
|
+
}
|
|
264
|
+
function withSplatEnvironment(sceneConfig, input, options = {}) {
|
|
265
|
+
const environment = isPairedSplatEnvironment(input) ? input : input ? createPairedSplatEnvironment(input, options) : void 0;
|
|
266
|
+
const xmlPath = environment?.collisionProxy.xmlPath;
|
|
267
|
+
if (!xmlPath) return sceneConfig;
|
|
268
|
+
return {
|
|
269
|
+
...sceneConfig,
|
|
270
|
+
environmentFiles: uniquePaths([
|
|
271
|
+
...sceneConfig.environmentFiles ?? [],
|
|
272
|
+
sceneRelativePath(sceneConfig, xmlPath)
|
|
273
|
+
])
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function createSplatEnvironmentUserData({
|
|
277
|
+
environment,
|
|
278
|
+
src,
|
|
279
|
+
format = "spz",
|
|
280
|
+
collisionProxy
|
|
281
|
+
}) {
|
|
282
|
+
return {
|
|
283
|
+
role: "splat-environment",
|
|
284
|
+
environmentId: environment?.id,
|
|
285
|
+
environmentLabel: environment?.label,
|
|
286
|
+
splatSrc: src,
|
|
287
|
+
splatFormat: format,
|
|
288
|
+
splatRenderer: environment?.splat.renderer,
|
|
289
|
+
collisionProxyStatus: collisionProxy?.status ?? "missing",
|
|
290
|
+
collisionProxyXmlPath: collisionProxy?.xmlPath,
|
|
291
|
+
collisionProxyPrimitives: collisionProxy?.primitives ?? []
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
function createSparkSplatViewerUrl({
|
|
295
|
+
viewerUrl,
|
|
296
|
+
splatSrc
|
|
297
|
+
}) {
|
|
298
|
+
const url = new URL(viewerUrl, "http://mujoco-react.local");
|
|
299
|
+
url.searchParams.set("splat", splatSrc);
|
|
300
|
+
return viewerUrl.startsWith("http") ? url.toString() : `${url.pathname}${url.search}`;
|
|
301
|
+
}
|
|
302
|
+
function SplatPlaceholder() {
|
|
303
|
+
return /* @__PURE__ */ jsx("group", { children: /* @__PURE__ */ jsxs("mesh", { position: [0, 0, 1.2], children: [
|
|
304
|
+
/* @__PURE__ */ jsx("boxGeometry", { args: [2.4, 2.4, 2.4] }),
|
|
305
|
+
/* @__PURE__ */ jsx(
|
|
306
|
+
"meshBasicMaterial",
|
|
307
|
+
{
|
|
308
|
+
color: "#8b8b8b",
|
|
309
|
+
transparent: true,
|
|
310
|
+
opacity: 0.06,
|
|
311
|
+
wireframe: true,
|
|
312
|
+
side: THREE.DoubleSide
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
] }) });
|
|
316
|
+
}
|
|
317
|
+
function createScenarioFog(scenario, background, fogNear, fogFar) {
|
|
318
|
+
if (scenario.lighting === "low-light") {
|
|
319
|
+
return new THREE.Fog(
|
|
320
|
+
background ?? getScenarioBackground(scenario.lighting),
|
|
321
|
+
fogNear ?? 2.5,
|
|
322
|
+
fogFar ?? 9
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
if (scenario.lighting === "warehouse") {
|
|
326
|
+
return new THREE.Fog(
|
|
327
|
+
background ?? getScenarioBackground(scenario.lighting),
|
|
328
|
+
fogNear ?? 5,
|
|
329
|
+
fogFar ?? 16
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
function applyScenarioMaterials(scene, scenario, snapshots, materialFilter) {
|
|
335
|
+
const materials = scenario.materials;
|
|
336
|
+
if (!materials) return;
|
|
337
|
+
scene.traverse((object) => {
|
|
338
|
+
if (!(object instanceof THREE.Mesh)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
for (const material of normalizeMaterials(object.material)) {
|
|
342
|
+
const mutable = getMutableScenarioMaterial(material);
|
|
343
|
+
if (!mutable) continue;
|
|
344
|
+
if (materialFilter && !materialFilter({ object, material })) continue;
|
|
345
|
+
if (!snapshots.has(material)) {
|
|
346
|
+
snapshots.set(material, {
|
|
347
|
+
color: mutable.color.clone(),
|
|
348
|
+
roughness: mutable.roughness,
|
|
349
|
+
metalness: mutable.metalness
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
applyScenarioMaterial(mutable, object, scenario, materials);
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
function applyScenarioMaterial(material, object, scenario, materials) {
|
|
357
|
+
const seed = scenario.seed ?? 0;
|
|
358
|
+
const objectKey = `${scenario.id ?? "scenario"}:${object.name}:${material.name}:${seed}`;
|
|
359
|
+
const variation = hashToUnitInterval(objectKey);
|
|
360
|
+
if (materials.randomizeObjectColors) {
|
|
361
|
+
material.color.setHSL(variation, 0.38, 0.42);
|
|
362
|
+
}
|
|
363
|
+
if (materials.randomizeTableMaterial) {
|
|
364
|
+
material.roughness = clamp01(
|
|
365
|
+
materials.roughness ?? 0.35 + variation * 0.45
|
|
366
|
+
);
|
|
367
|
+
material.metalness = clamp01(
|
|
368
|
+
materials.metalness ?? variation * 0.12
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
material.needsUpdate = true;
|
|
372
|
+
}
|
|
373
|
+
function normalizeMaterials(material) {
|
|
374
|
+
return Array.isArray(material) ? material : [material];
|
|
375
|
+
}
|
|
376
|
+
function getMutableScenarioMaterial(material) {
|
|
377
|
+
if (material instanceof THREE.MeshStandardMaterial || material instanceof THREE.MeshPhysicalMaterial) {
|
|
378
|
+
return material;
|
|
379
|
+
}
|
|
380
|
+
return null;
|
|
381
|
+
}
|
|
382
|
+
function hashToUnitInterval(value) {
|
|
383
|
+
let hash = 2166136261;
|
|
384
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
385
|
+
hash ^= value.charCodeAt(index);
|
|
386
|
+
hash = Math.imul(hash, 16777619);
|
|
387
|
+
}
|
|
388
|
+
return (hash >>> 0) / 4294967295;
|
|
389
|
+
}
|
|
390
|
+
function clamp01(value) {
|
|
391
|
+
return Math.max(0, Math.min(1, value));
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* @license
|
|
395
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
396
|
+
*/
|
|
397
|
+
|
|
398
|
+
export { ScenarioLighting, SplatEnvironment, VisualScenarioEffects, createPairedSplatEnvironment, createSparkSplatViewerUrl, createSplatEnvironmentUserData, getScenarioBackground, getScenarioCameraPosition, useSplatEnvironment, useVisualScenarioEffects, withSplatEnvironment };
|
|
399
|
+
//# sourceMappingURL=chunk-33CV6HSV.js.map
|
|
400
|
+
//# sourceMappingURL=chunk-33CV6HSV.js.map
|