mujoco-react 8.9.1 → 8.10.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 +47 -22
- package/bin/mujoco-react.mjs +1 -1
- package/dist/chunk-KGFRKPLS.js +186 -0
- package/dist/chunk-KGFRKPLS.js.map +1 -0
- package/dist/index.d.ts +37 -735
- package/dist/index.js +41 -19
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +32 -0
- package/dist/spark.js +150 -0
- package/dist/spark.js.map +1 -0
- package/dist/types-FFW7ykBu.d.ts +817 -0
- package/dist/vite.d.ts +9 -0
- package/dist/vite.js +4 -0
- package/dist/vite.js.map +1 -1
- package/package.json +15 -2
- package/src/components/VisualScenario.tsx +229 -0
- package/src/hooks/useSceneLights.ts +49 -18
- package/src/index.ts +23 -0
- package/src/spark.tsx +202 -0
- package/src/types.ts +92 -1
- package/src/vite.ts +8 -0
package/src/spark.tsx
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useThree } from '@react-three/fiber';
|
|
7
|
+
import { useEffect, useRef, useState } from 'react';
|
|
8
|
+
import * as THREE from 'three';
|
|
9
|
+
import {
|
|
10
|
+
SplatEnvironment,
|
|
11
|
+
useSplatEnvironment,
|
|
12
|
+
} from './components/VisualScenario';
|
|
13
|
+
import type {
|
|
14
|
+
SplatEnvironmentProps,
|
|
15
|
+
} from './types';
|
|
16
|
+
|
|
17
|
+
type SparkModule = typeof import('@sparkjsdev/spark');
|
|
18
|
+
type SparkRendererInstance = InstanceType<SparkModule['SparkRenderer']>;
|
|
19
|
+
type SparkSplatMeshInstance = InstanceType<SparkModule['SplatMesh']>;
|
|
20
|
+
|
|
21
|
+
export type SparkSplatStatus = 'idle' | 'loading' | 'ready' | 'error';
|
|
22
|
+
|
|
23
|
+
export interface SparkSplatEnvironmentProps extends SplatEnvironmentProps {
|
|
24
|
+
/** Enable Spark LoD handling for large splat assets. Default: true. */
|
|
25
|
+
lod?: boolean | 'quality';
|
|
26
|
+
/**
|
|
27
|
+
* Hide meshes whose names include floor, ground, or plane while the splat is
|
|
28
|
+
* active. This mirrors the common hybrid-rendering setup where MJCF keeps
|
|
29
|
+
* collision geometry but the splat owns the visual environment.
|
|
30
|
+
*/
|
|
31
|
+
hideGroundMeshes?: boolean;
|
|
32
|
+
onStatusChange?: (status: SparkSplatStatus) => void;
|
|
33
|
+
onLoad?: (mesh: SparkSplatMeshInstance) => void;
|
|
34
|
+
onError?: (error: Error) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Optional SparkJS-backed Gaussian splat renderer for React Three Fiber scenes.
|
|
39
|
+
*
|
|
40
|
+
* Import from `mujoco-react/spark` and install `@sparkjsdev/spark` in the app
|
|
41
|
+
* that uses it. The core `mujoco-react` entrypoint does not depend on Spark.
|
|
42
|
+
*/
|
|
43
|
+
export function SparkSplatEnvironment({
|
|
44
|
+
environment,
|
|
45
|
+
src,
|
|
46
|
+
format,
|
|
47
|
+
collisionProxy,
|
|
48
|
+
collisionProxyMetadata,
|
|
49
|
+
showPlaceholder,
|
|
50
|
+
children,
|
|
51
|
+
lod = true,
|
|
52
|
+
hideGroundMeshes = false,
|
|
53
|
+
onStatusChange,
|
|
54
|
+
onLoad,
|
|
55
|
+
onError,
|
|
56
|
+
...groupProps
|
|
57
|
+
}: SparkSplatEnvironmentProps) {
|
|
58
|
+
const groupRef = useRef<THREE.Group>(null);
|
|
59
|
+
const sparkRef = useRef<SparkRendererInstance | null>(null);
|
|
60
|
+
const meshRef = useRef<SparkSplatMeshInstance | null>(null);
|
|
61
|
+
const hiddenMeshesRef = useRef<THREE.Mesh[]>([]);
|
|
62
|
+
const onStatusChangeRef = useRef(onStatusChange);
|
|
63
|
+
const onLoadRef = useRef(onLoad);
|
|
64
|
+
const onErrorRef = useRef(onError);
|
|
65
|
+
const [status, setStatus] = useState<SparkSplatStatus>('idle');
|
|
66
|
+
const { gl, invalidate } = useThree();
|
|
67
|
+
const metadata = useSplatEnvironment({
|
|
68
|
+
environment,
|
|
69
|
+
src,
|
|
70
|
+
format,
|
|
71
|
+
collisionProxy: collisionProxyMetadata,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
onStatusChangeRef.current = onStatusChange;
|
|
76
|
+
}, [onStatusChange]);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
onLoadRef.current = onLoad;
|
|
80
|
+
}, [onLoad]);
|
|
81
|
+
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
onErrorRef.current = onError;
|
|
84
|
+
}, [onError]);
|
|
85
|
+
|
|
86
|
+
useEffect(() => {
|
|
87
|
+
let disposed = false;
|
|
88
|
+
|
|
89
|
+
function setLifecycleStatus(nextStatus: SparkSplatStatus) {
|
|
90
|
+
setStatus(nextStatus);
|
|
91
|
+
onStatusChangeRef.current?.(nextStatus);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function restoreHiddenMeshes() {
|
|
95
|
+
for (const mesh of hiddenMeshesRef.current) {
|
|
96
|
+
mesh.visible = true;
|
|
97
|
+
}
|
|
98
|
+
hiddenMeshesRef.current = [];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function loadSplat() {
|
|
102
|
+
if (!metadata.src || metadata.format !== 'spz') {
|
|
103
|
+
setLifecycleStatus('idle');
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setLifecycleStatus('loading');
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const sparkModule = await import('@sparkjsdev/spark');
|
|
111
|
+
if (disposed || !groupRef.current) return;
|
|
112
|
+
|
|
113
|
+
const spark = new sparkModule.SparkRenderer({
|
|
114
|
+
renderer: gl,
|
|
115
|
+
onDirty: invalidate,
|
|
116
|
+
});
|
|
117
|
+
const mesh = new sparkModule.SplatMesh({
|
|
118
|
+
url: metadata.src,
|
|
119
|
+
lod,
|
|
120
|
+
});
|
|
121
|
+
mesh.name = 'GaussianSplatMesh';
|
|
122
|
+
|
|
123
|
+
groupRef.current.add(spark);
|
|
124
|
+
groupRef.current.add(mesh);
|
|
125
|
+
sparkRef.current = spark;
|
|
126
|
+
meshRef.current = mesh;
|
|
127
|
+
|
|
128
|
+
if (hideGroundMeshes && groupRef.current.parent) {
|
|
129
|
+
groupRef.current.parent.traverse((object) => {
|
|
130
|
+
if (
|
|
131
|
+
!(object instanceof THREE.Mesh) ||
|
|
132
|
+
object === (mesh as unknown as THREE.Object3D)
|
|
133
|
+
) {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const name = object.name.toLowerCase();
|
|
137
|
+
if (
|
|
138
|
+
name.includes('floor') ||
|
|
139
|
+
name.includes('ground') ||
|
|
140
|
+
name.includes('plane')
|
|
141
|
+
) {
|
|
142
|
+
object.visible = false;
|
|
143
|
+
hiddenMeshesRef.current.push(object);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
await mesh.initialized;
|
|
149
|
+
if (disposed) return;
|
|
150
|
+
setLifecycleStatus('ready');
|
|
151
|
+
onLoadRef.current?.(mesh);
|
|
152
|
+
invalidate();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
const normalizedError =
|
|
155
|
+
error instanceof Error ? error : new Error(String(error));
|
|
156
|
+
setLifecycleStatus('error');
|
|
157
|
+
onErrorRef.current?.(normalizedError);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
void loadSplat();
|
|
162
|
+
|
|
163
|
+
return () => {
|
|
164
|
+
disposed = true;
|
|
165
|
+
restoreHiddenMeshes();
|
|
166
|
+
|
|
167
|
+
if (meshRef.current) {
|
|
168
|
+
groupRef.current?.remove(meshRef.current);
|
|
169
|
+
meshRef.current.dispose?.();
|
|
170
|
+
meshRef.current = null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (sparkRef.current) {
|
|
174
|
+
groupRef.current?.remove(sparkRef.current);
|
|
175
|
+
sparkRef.current.dispose?.();
|
|
176
|
+
sparkRef.current = null;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
}, [
|
|
180
|
+
gl,
|
|
181
|
+
hideGroundMeshes,
|
|
182
|
+
invalidate,
|
|
183
|
+
lod,
|
|
184
|
+
metadata.format,
|
|
185
|
+
metadata.src,
|
|
186
|
+
]);
|
|
187
|
+
|
|
188
|
+
return (
|
|
189
|
+
<SplatEnvironment
|
|
190
|
+
{...groupProps}
|
|
191
|
+
environment={environment}
|
|
192
|
+
src={metadata.src}
|
|
193
|
+
format={metadata.format}
|
|
194
|
+
collisionProxyMetadata={metadata.collisionProxy}
|
|
195
|
+
collisionProxy={collisionProxy}
|
|
196
|
+
showPlaceholder={showPlaceholder ?? status !== 'ready'}
|
|
197
|
+
>
|
|
198
|
+
<group ref={groupRef} />
|
|
199
|
+
{children}
|
|
200
|
+
</SplatEnvironment>
|
|
201
|
+
);
|
|
202
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import type React from 'react';
|
|
7
7
|
import type { ReactNode } from 'react';
|
|
8
|
-
import type { CanvasProps } from '@react-three/fiber';
|
|
8
|
+
import type { CanvasProps, ThreeElements } from '@react-three/fiber';
|
|
9
9
|
import * as THREE from 'three';
|
|
10
10
|
|
|
11
11
|
// ---- Register (type-safe named resources) ----
|
|
@@ -791,6 +791,97 @@ export interface SceneLightsProps {
|
|
|
791
791
|
intensity?: number;
|
|
792
792
|
}
|
|
793
793
|
|
|
794
|
+
// ---- Visual scenarios / 3DGS composition ----
|
|
795
|
+
|
|
796
|
+
export type ScenarioLightingPreset = 'studio' | 'warehouse' | 'low-light' | 'splat';
|
|
797
|
+
export type SplatFormat = 'spz' | 'ply' | 'splat';
|
|
798
|
+
export type SplatRendererKind = 'spark' | 'custom';
|
|
799
|
+
export type SplatCollisionPrimitive = 'plane' | 'box' | 'sphere' | 'capsule' | 'mesh';
|
|
800
|
+
|
|
801
|
+
export interface ScenarioCameraConfig {
|
|
802
|
+
jitter?: number;
|
|
803
|
+
exposure?: number;
|
|
804
|
+
noise?: number;
|
|
805
|
+
blur?: number;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export interface SplatAssetConfig {
|
|
809
|
+
src: string;
|
|
810
|
+
/** Common browser-friendly splat format. Renderer-specific loaders may accept more. */
|
|
811
|
+
format?: SplatFormat;
|
|
812
|
+
/** Optional renderer hint. The library does not import renderer-specific code. */
|
|
813
|
+
renderer?: SplatRendererKind;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
export interface SplatScenarioConfig {
|
|
817
|
+
enabled: boolean;
|
|
818
|
+
/** Common browser-friendly splat format. Renderer-specific loaders may accept more. */
|
|
819
|
+
format?: SplatFormat;
|
|
820
|
+
src?: string;
|
|
821
|
+
requiresCollisionProxy?: boolean;
|
|
822
|
+
collisionProxy?: SplatCollisionProxyConfig;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
export interface SplatCollisionProxyConfig {
|
|
826
|
+
/** MJCF/XML file or artifact path that provides physics collision for the visual splat. */
|
|
827
|
+
xmlPath?: string;
|
|
828
|
+
/** Human-readable status for authoring and validation flows. */
|
|
829
|
+
status?: 'missing' | 'planned' | 'generated' | 'validated';
|
|
830
|
+
/** Primitive proxy shapes expected in the MJCF collision proxy. */
|
|
831
|
+
primitives?: SplatCollisionPrimitive[];
|
|
832
|
+
/** Optional notes that should travel with scene variants and rollout metadata. */
|
|
833
|
+
notes?: string[];
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
export interface PairedSplatEnvironmentConfig {
|
|
837
|
+
id: string;
|
|
838
|
+
label: string;
|
|
839
|
+
description?: string;
|
|
840
|
+
/** Visual-only Gaussian splat asset. */
|
|
841
|
+
splat: SplatAssetConfig;
|
|
842
|
+
/** MJCF/XML contact geometry paired with the visual splat. */
|
|
843
|
+
collisionProxy: SplatCollisionProxyConfig & { xmlPath: string };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
export interface SplatEnvironmentMetadataInput {
|
|
847
|
+
environment?: PairedSplatEnvironmentConfig;
|
|
848
|
+
src?: string;
|
|
849
|
+
format?: SplatFormat;
|
|
850
|
+
collisionProxy?: SplatCollisionProxyConfig;
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
export interface SplatEnvironmentMetadata {
|
|
854
|
+
src?: string;
|
|
855
|
+
format: SplatFormat;
|
|
856
|
+
collisionProxy?: SplatCollisionProxyConfig;
|
|
857
|
+
userData: Record<string, unknown>;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
export interface VisualScenarioConfig {
|
|
861
|
+
id?: string;
|
|
862
|
+
label?: string;
|
|
863
|
+
seed?: number;
|
|
864
|
+
lighting?: ScenarioLightingPreset;
|
|
865
|
+
environment?: string;
|
|
866
|
+
camera?: ScenarioCameraConfig;
|
|
867
|
+
splat?: SplatScenarioConfig;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
export interface ScenarioLightingProps {
|
|
871
|
+
preset?: ScenarioLightingPreset;
|
|
872
|
+
intensity?: number;
|
|
873
|
+
castShadow?: boolean;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
export interface SplatEnvironmentProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
877
|
+
environment?: PairedSplatEnvironmentConfig;
|
|
878
|
+
src?: string;
|
|
879
|
+
format?: SplatFormat;
|
|
880
|
+
collisionProxy?: ReactNode;
|
|
881
|
+
collisionProxyMetadata?: SplatCollisionProxyConfig;
|
|
882
|
+
showPlaceholder?: boolean;
|
|
883
|
+
}
|
|
884
|
+
|
|
794
885
|
export type TrajectoryInput = TrajectoryFrame[] | number[][];
|
|
795
886
|
|
|
796
887
|
export interface TrajectoryPlayerProps {
|
package/src/vite.ts
CHANGED
|
@@ -86,6 +86,14 @@ export function mujocoReact(options: MujocoReactPluginOptions) {
|
|
|
86
86
|
return {
|
|
87
87
|
name: 'mujoco-react',
|
|
88
88
|
enforce: 'pre' as const,
|
|
89
|
+
config(userConfig: { build?: { chunkSizeWarningLimit?: number } }) {
|
|
90
|
+
// three + drei + MuJoCo WASM glue are inherently large; the large-chunk
|
|
91
|
+
// warning is expected, not a failure. Raise the limit so consumers don't
|
|
92
|
+
// see it. Vite merges plugin config on top of user config, so only set a
|
|
93
|
+
// default when the consumer hasn't specified their own limit.
|
|
94
|
+
if (userConfig.build?.chunkSizeWarningLimit !== undefined) return;
|
|
95
|
+
return { build: { chunkSizeWarningLimit: 4000 } };
|
|
96
|
+
},
|
|
89
97
|
configResolved(config: ViteConfig) {
|
|
90
98
|
root = config.root;
|
|
91
99
|
generatedRegister = path.resolve(root, generatedRegister);
|