mujoco-react 10.2.1 → 10.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 +23 -1
- package/dist/{chunk-CYDGWNKQ.js → chunk-FBXXXPLQ.js} +169 -22
- package/dist/chunk-FBXXXPLQ.js.map +1 -0
- package/dist/index.d.ts +5 -4
- package/dist/index.js +345 -40
- package/dist/index.js.map +1 -1
- package/dist/spark.d.ts +1 -1
- package/dist/spark.js +4 -10
- package/dist/spark.js.map +1 -1
- package/dist/{types-B-J8fpPP.d.ts → types-CdFZCYmy.d.ts} +115 -1
- package/package.json +1 -1
- package/src/components/Debug.tsx +174 -3
- package/src/components/SceneRenderer.tsx +25 -6
- package/src/core/MujocoCanvas.tsx +8 -4
- package/src/core/MujocoPhysics.tsx +6 -4
- package/src/core/MujocoProvider.tsx +6 -4
- package/src/core/MujocoSimProvider.tsx +189 -9
- package/src/index.ts +1 -0
- package/src/rendering/GeomBuilder.ts +18 -2
- package/src/rendering/cameraFrameCapture.ts +229 -19
- package/src/spark.tsx +4 -12
- package/src/types.ts +121 -0
- package/dist/chunk-CYDGWNKQ.js.map +0 -1
|
@@ -9,7 +9,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react';
|
|
|
9
9
|
import * as THREE from 'three';
|
|
10
10
|
import { GeomBuilder } from '../rendering/GeomBuilder';
|
|
11
11
|
import { CAMERA_FRAME_CAPTURE_PRE_RENDER_USER_DATA_KEY } from '../rendering/cameraFrameCapture';
|
|
12
|
-
import { MujocoModel } from '../types';
|
|
12
|
+
import { MujocoModel, MujocoRenderOptions } from '../types';
|
|
13
13
|
import { getName } from '../core/SceneLoader';
|
|
14
14
|
import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
15
15
|
|
|
@@ -17,7 +17,18 @@ import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
|
17
17
|
* SceneRenderer — creates and syncs MuJoCo body meshes every frame.
|
|
18
18
|
* Accepts standard R3F group props (position, rotation, scale, visible, etc.).
|
|
19
19
|
*/
|
|
20
|
-
export
|
|
20
|
+
export interface SceneRendererProps extends Omit<ThreeElements['group'], 'ref'> {
|
|
21
|
+
renderOptions?: MujocoRenderOptions;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getRenderOptionsKey(renderOptions: MujocoRenderOptions | undefined) {
|
|
25
|
+
const smoothing = renderOptions?.meshNormalSmoothing;
|
|
26
|
+
if (!smoothing) return 'default';
|
|
27
|
+
if (smoothing === true) return 'meshNormalSmoothing:true';
|
|
28
|
+
return `meshNormalSmoothing:${smoothing.tolerance ?? 'default'}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function SceneRenderer({ renderOptions, ...props }: SceneRendererProps) {
|
|
21
32
|
const {
|
|
22
33
|
mjModelRef,
|
|
23
34
|
mjDataRef,
|
|
@@ -31,11 +42,13 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
|
31
42
|
const groupRef = useRef<THREE.Group>(null);
|
|
32
43
|
const bodyRefs = useRef<(THREE.Group | null)[]>([]);
|
|
33
44
|
const prevModelRef = useRef<MujocoModel | null>(null);
|
|
45
|
+
const prevRenderOptionsKeyRef = useRef<string | null>(null);
|
|
46
|
+
const renderOptionsKey = getRenderOptionsKey(renderOptions);
|
|
34
47
|
|
|
35
48
|
const geomBuilder = useMemo(() => {
|
|
36
49
|
if (status !== 'ready') return null;
|
|
37
|
-
return new GeomBuilder(mujocoRef.current);
|
|
38
|
-
}, [status, mujocoRef]);
|
|
50
|
+
return new GeomBuilder(mujocoRef.current, renderOptions);
|
|
51
|
+
}, [status, mujocoRef, renderOptionsKey]);
|
|
39
52
|
|
|
40
53
|
// Build body groups when model loads
|
|
41
54
|
useEffect(() => {
|
|
@@ -45,8 +58,14 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
|
45
58
|
if (!model || !group) return;
|
|
46
59
|
|
|
47
60
|
// Skip if model hasn't changed
|
|
48
|
-
if (
|
|
61
|
+
if (
|
|
62
|
+
prevModelRef.current === model &&
|
|
63
|
+
prevRenderOptionsKeyRef.current === renderOptionsKey
|
|
64
|
+
) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
49
67
|
prevModelRef.current = model;
|
|
68
|
+
prevRenderOptionsKeyRef.current = renderOptionsKey;
|
|
50
69
|
|
|
51
70
|
// Clear previous bodies
|
|
52
71
|
while (group.children.length > 0) {
|
|
@@ -73,7 +92,7 @@ export function SceneRenderer(props: Omit<ThreeElements['group'], 'ref'>) {
|
|
|
73
92
|
refs.push(bodyGroup);
|
|
74
93
|
}
|
|
75
94
|
bodyRefs.current = refs;
|
|
76
|
-
}, [status, geomBuilder, mjModelRef]);
|
|
95
|
+
}, [status, geomBuilder, mjModelRef, renderOptionsKey]);
|
|
77
96
|
|
|
78
97
|
const syncBodiesToData = useCallback(() => {
|
|
79
98
|
const data = mjDataRef.current;
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Canvas } from '@react-three/fiber';
|
|
7
|
-
import { forwardRef, useEffect } from 'react';
|
|
7
|
+
import { forwardRef, useEffect, useRef } from 'react';
|
|
8
8
|
import { useMujocoWasm } from './MujocoProvider';
|
|
9
9
|
import { MujocoSimProvider } from './MujocoSimProvider';
|
|
10
10
|
import { MujocoCanvasProps, MujocoSimAPI } from '../types';
|
|
@@ -31,6 +31,7 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
|
|
|
31
31
|
paused,
|
|
32
32
|
speed,
|
|
33
33
|
interpolate,
|
|
34
|
+
renderOptions,
|
|
34
35
|
loadingFallback,
|
|
35
36
|
children,
|
|
36
37
|
...canvasProps
|
|
@@ -38,12 +39,14 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
|
|
|
38
39
|
ref
|
|
39
40
|
) {
|
|
40
41
|
const { mujoco, status: wasmStatus, error: wasmError } = useMujocoWasm();
|
|
42
|
+
const onErrorRef = useRef(onError);
|
|
43
|
+
onErrorRef.current = onError;
|
|
41
44
|
|
|
42
45
|
useEffect(() => {
|
|
43
|
-
if (wasmStatus === 'error'
|
|
44
|
-
|
|
46
|
+
if (wasmStatus === 'error') {
|
|
47
|
+
onErrorRef.current?.(new Error(wasmError ?? 'WASM load failed'));
|
|
45
48
|
}
|
|
46
|
-
}, [wasmStatus, wasmError
|
|
49
|
+
}, [wasmStatus, wasmError]);
|
|
47
50
|
|
|
48
51
|
if (wasmStatus === 'loading' || !mujoco) {
|
|
49
52
|
return loadingFallback ? (
|
|
@@ -71,6 +74,7 @@ export const MujocoCanvas = forwardRef<MujocoSimAPI, MujocoCanvasProps>(
|
|
|
71
74
|
paused={paused}
|
|
72
75
|
speed={speed}
|
|
73
76
|
interpolate={interpolate}
|
|
77
|
+
renderOptions={renderOptions}
|
|
74
78
|
>
|
|
75
79
|
{children}
|
|
76
80
|
</MujocoSimProvider>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* SPDX-License-Identifier: Apache-2.0
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { forwardRef, useEffect } from 'react';
|
|
6
|
+
import { forwardRef, useEffect, useRef } from 'react';
|
|
7
7
|
import { useMujocoWasm } from './MujocoProvider';
|
|
8
8
|
import { MujocoSimProvider } from './MujocoSimProvider';
|
|
9
9
|
import type {
|
|
@@ -62,12 +62,14 @@ export interface MujocoPhysicsProps {
|
|
|
62
62
|
export const MujocoPhysics = forwardRef<MujocoSimAPI, MujocoPhysicsProps>(
|
|
63
63
|
function MujocoPhysics({ onError, children, ...props }, ref) {
|
|
64
64
|
const { mujoco, status: wasmStatus, error: wasmError } = useMujocoWasm();
|
|
65
|
+
const onErrorRef = useRef(onError);
|
|
66
|
+
onErrorRef.current = onError;
|
|
65
67
|
|
|
66
68
|
useEffect(() => {
|
|
67
|
-
if (wasmStatus === 'error'
|
|
68
|
-
|
|
69
|
+
if (wasmStatus === 'error') {
|
|
70
|
+
onErrorRef.current?.(new Error(wasmError ?? 'WASM load failed'));
|
|
69
71
|
}
|
|
70
|
-
}, [wasmStatus, wasmError
|
|
72
|
+
}, [wasmStatus, wasmError]);
|
|
71
73
|
|
|
72
74
|
if (wasmStatus === 'error' || wasmStatus === 'loading' || !mujoco) {
|
|
73
75
|
return null;
|
|
@@ -96,6 +96,8 @@ export function MujocoProvider({
|
|
|
96
96
|
const [error, setError] = useState<string | null>(null);
|
|
97
97
|
const moduleRef = useRef<MujocoModule | null>(null);
|
|
98
98
|
const isMounted = useRef(true);
|
|
99
|
+
const onErrorRef = useRef(onError);
|
|
100
|
+
onErrorRef.current = onError;
|
|
99
101
|
|
|
100
102
|
useEffect(() => {
|
|
101
103
|
isMounted.current = true;
|
|
@@ -105,7 +107,7 @@ export function MujocoProvider({
|
|
|
105
107
|
const err = new Error('MujocoProvider wasmVariant="threaded" requires a threadedLoader from @mujoco/mujoco/mt');
|
|
106
108
|
setError(err.message);
|
|
107
109
|
setStatus('error');
|
|
108
|
-
|
|
110
|
+
onErrorRef.current?.(err);
|
|
109
111
|
return;
|
|
110
112
|
}
|
|
111
113
|
let selectedWasmUrl = wasmUrl ?? defaultMujocoWasmUrl;
|
|
@@ -115,7 +117,7 @@ export function MujocoProvider({
|
|
|
115
117
|
const err = new Error('MujocoProvider wasmVariant="threaded" requires mtWasmUrl from @mujoco/mujoco/mt/mujoco.wasm?url');
|
|
116
118
|
setError(err.message);
|
|
117
119
|
setStatus('error');
|
|
118
|
-
|
|
120
|
+
onErrorRef.current?.(err);
|
|
119
121
|
return;
|
|
120
122
|
}
|
|
121
123
|
selectedWasmUrl = mtWasmUrl;
|
|
@@ -152,14 +154,14 @@ export function MujocoProvider({
|
|
|
152
154
|
const msg = err.message || 'Failed to init spatial simulation';
|
|
153
155
|
setError(msg);
|
|
154
156
|
setStatus('error');
|
|
155
|
-
|
|
157
|
+
onErrorRef.current?.(new Error(msg));
|
|
156
158
|
}
|
|
157
159
|
});
|
|
158
160
|
|
|
159
161
|
return () => {
|
|
160
162
|
isMounted.current = false;
|
|
161
163
|
};
|
|
162
|
-
}, [wasmUrl, mtWasmUrl, threadedLoader, wasmVariant, timeout
|
|
164
|
+
}, [wasmUrl, mtWasmUrl, threadedLoader, wasmVariant, timeout]);
|
|
163
165
|
|
|
164
166
|
return (
|
|
165
167
|
<MujocoContext.Provider
|
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
LoadFromFilesOptions,
|
|
38
38
|
LocalMujocoFile,
|
|
39
39
|
ModelOptions,
|
|
40
|
+
MujocoRenderOptions,
|
|
40
41
|
MujocoSimAPI,
|
|
41
42
|
PhysicsStepCallback,
|
|
42
43
|
RayHit,
|
|
@@ -241,6 +242,143 @@ function applyMountedCameraPoseOffsets(
|
|
|
241
242
|
};
|
|
242
243
|
}
|
|
243
244
|
|
|
245
|
+
function resolveMujocoCameraCompatibilityOptions(
|
|
246
|
+
options: CameraFrameCaptureOptions
|
|
247
|
+
) {
|
|
248
|
+
const compatibility = options.mujocoCameraCompatibility;
|
|
249
|
+
if (!compatibility) return null;
|
|
250
|
+
if (compatibility === true) {
|
|
251
|
+
return {
|
|
252
|
+
useResolution: true,
|
|
253
|
+
useIntrinsics: true,
|
|
254
|
+
useClipping: true,
|
|
255
|
+
preserveAspect: true,
|
|
256
|
+
preferResolution: false,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
useResolution: compatibility.useResolution ?? true,
|
|
261
|
+
useIntrinsics: compatibility.useIntrinsics ?? true,
|
|
262
|
+
useClipping: compatibility.useClipping ?? true,
|
|
263
|
+
preserveAspect: compatibility.preserveAspect ?? true,
|
|
264
|
+
preferResolution: compatibility.preferResolution ?? false,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function mujocoVisualClip(model: MujocoModel) {
|
|
269
|
+
const map = (model as unknown as {
|
|
270
|
+
vis?: { map?: { znear?: number; zfar?: number } };
|
|
271
|
+
}).vis?.map;
|
|
272
|
+
const near = typeof map?.znear === 'number' && map.znear > 0
|
|
273
|
+
? map.znear
|
|
274
|
+
: undefined;
|
|
275
|
+
const far = typeof map?.zfar === 'number' && map.zfar > 0
|
|
276
|
+
? map.zfar
|
|
277
|
+
: undefined;
|
|
278
|
+
return { near, far };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function mujocoCameraResolution(
|
|
282
|
+
model: MujocoModel,
|
|
283
|
+
cameraId: number
|
|
284
|
+
): { width?: number; height?: number } {
|
|
285
|
+
const resolution = model.cam_resolution;
|
|
286
|
+
if (!resolution) return {};
|
|
287
|
+
const width = Number(resolution[cameraId * 2]);
|
|
288
|
+
const height = Number(resolution[cameraId * 2 + 1]);
|
|
289
|
+
return {
|
|
290
|
+
width: Number.isFinite(width) && width > 0 ? width : undefined,
|
|
291
|
+
height: Number.isFinite(height) && height > 0 ? height : undefined,
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function mujocoCameraProjectionMatrix(
|
|
296
|
+
model: MujocoModel,
|
|
297
|
+
cameraId: number,
|
|
298
|
+
width: number | undefined,
|
|
299
|
+
height: number | undefined,
|
|
300
|
+
near: number | undefined,
|
|
301
|
+
far: number | undefined
|
|
302
|
+
): THREE.Matrix4 | undefined {
|
|
303
|
+
const intrinsic = model.cam_intrinsic;
|
|
304
|
+
const sensorSize = model.cam_sensorsize;
|
|
305
|
+
if (!intrinsic || !sensorSize || !width || !height) return undefined;
|
|
306
|
+
|
|
307
|
+
const intrinsicOffset = cameraId * 4;
|
|
308
|
+
const sensorOffset = cameraId * 2;
|
|
309
|
+
const focalX = Number(intrinsic[intrinsicOffset]);
|
|
310
|
+
const focalY = Number(intrinsic[intrinsicOffset + 1]);
|
|
311
|
+
const principalX = Number(intrinsic[intrinsicOffset + 2]);
|
|
312
|
+
const principalY = Number(intrinsic[intrinsicOffset + 3]);
|
|
313
|
+
const sensorWidth = Number(sensorSize[sensorOffset]);
|
|
314
|
+
const sensorHeight = Number(sensorSize[sensorOffset + 1]);
|
|
315
|
+
if (
|
|
316
|
+
!Number.isFinite(focalX) ||
|
|
317
|
+
!Number.isFinite(focalY) ||
|
|
318
|
+
!Number.isFinite(principalX) ||
|
|
319
|
+
!Number.isFinite(principalY) ||
|
|
320
|
+
!Number.isFinite(sensorWidth) ||
|
|
321
|
+
!Number.isFinite(sensorHeight) ||
|
|
322
|
+
focalX <= 0 ||
|
|
323
|
+
focalY <= 0 ||
|
|
324
|
+
sensorWidth <= 0 ||
|
|
325
|
+
sensorHeight <= 0
|
|
326
|
+
) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const fx = focalX / sensorWidth * width;
|
|
331
|
+
const fy = focalY / sensorHeight * height;
|
|
332
|
+
const cx = width * (0.5 + principalX / sensorWidth);
|
|
333
|
+
const cy = height * (0.5 + principalY / sensorHeight);
|
|
334
|
+
const znear = near ?? 0.01;
|
|
335
|
+
const zfar = far ?? 100;
|
|
336
|
+
|
|
337
|
+
return new THREE.Matrix4().set(
|
|
338
|
+
2 * fx / width, 0, 1 - 2 * cx / width, 0,
|
|
339
|
+
0, 2 * fy / height, 2 * cy / height - 1, 0,
|
|
340
|
+
0, 0, -(zfar + znear) / (zfar - znear), -2 * zfar * znear / (zfar - znear),
|
|
341
|
+
0, 0, -1, 0
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function resolveMujocoCameraCaptureDimensions(
|
|
346
|
+
requested: CameraFrameCaptureOptions,
|
|
347
|
+
cameraResolution: { width?: number; height?: number },
|
|
348
|
+
compatibility: NonNullable<ReturnType<typeof resolveMujocoCameraCompatibilityOptions>>
|
|
349
|
+
) {
|
|
350
|
+
if (!compatibility.useResolution) {
|
|
351
|
+
return {
|
|
352
|
+
width: requested.width,
|
|
353
|
+
height: requested.height,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
if (compatibility.preferResolution) {
|
|
358
|
+
return {
|
|
359
|
+
width: cameraResolution.width ?? requested.width,
|
|
360
|
+
height: cameraResolution.height ?? requested.height,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
let width = requested.width ?? cameraResolution.width;
|
|
365
|
+
let height = requested.height ?? cameraResolution.height;
|
|
366
|
+
|
|
367
|
+
if (
|
|
368
|
+
compatibility.preserveAspect &&
|
|
369
|
+
cameraResolution.width &&
|
|
370
|
+
cameraResolution.height
|
|
371
|
+
) {
|
|
372
|
+
if (requested.width !== undefined && requested.height === undefined) {
|
|
373
|
+
height = requested.width * cameraResolution.height / cameraResolution.width;
|
|
374
|
+
} else if (requested.height !== undefined && requested.width === undefined) {
|
|
375
|
+
width = requested.height * cameraResolution.width / cameraResolution.height;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return { width, height };
|
|
380
|
+
}
|
|
381
|
+
|
|
244
382
|
function countMountedCameraSelectors(options: CameraFrameCaptureOptions) {
|
|
245
383
|
return Number(Boolean(options.cameraName)) +
|
|
246
384
|
Number(Boolean(options.siteName)) +
|
|
@@ -406,6 +544,7 @@ interface MujocoSimProviderProps {
|
|
|
406
544
|
paused?: boolean;
|
|
407
545
|
speed?: number;
|
|
408
546
|
interpolate?: boolean;
|
|
547
|
+
renderOptions?: MujocoRenderOptions;
|
|
409
548
|
children: React.ReactNode;
|
|
410
549
|
}
|
|
411
550
|
|
|
@@ -423,6 +562,7 @@ export function MujocoSimProvider({
|
|
|
423
562
|
paused,
|
|
424
563
|
speed,
|
|
425
564
|
interpolate,
|
|
565
|
+
renderOptions,
|
|
426
566
|
children,
|
|
427
567
|
}: MujocoSimProviderProps) {
|
|
428
568
|
const { gl, camera, scene } = useThree();
|
|
@@ -462,14 +602,12 @@ export function MujocoSimProvider({
|
|
|
462
602
|
const hiddenBodiesRef = useRef(new Set<string>());
|
|
463
603
|
const bodyReloadTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
464
604
|
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
useEffect(() => { substepsRef.current = substeps ?? 1; }, [substeps]);
|
|
472
|
-
useEffect(() => { interpolateRef.current = interpolate ?? false; }, [interpolate]);
|
|
605
|
+
configRef.current = config;
|
|
606
|
+
mujocoRef.current = mujoco;
|
|
607
|
+
pausedRef.current = paused ?? false;
|
|
608
|
+
speedRef.current = speed ?? 1;
|
|
609
|
+
substepsRef.current = substeps ?? 1;
|
|
610
|
+
interpolateRef.current = interpolate ?? false;
|
|
473
611
|
|
|
474
612
|
// Sync gravity prop
|
|
475
613
|
useEffect(() => {
|
|
@@ -1037,11 +1175,33 @@ export function MujocoSimProvider({
|
|
|
1037
1175
|
for (let i = 0; i < ncam; i += 1) {
|
|
1038
1176
|
const posOffset = i * 3;
|
|
1039
1177
|
const quatOffset = i * 4;
|
|
1178
|
+
const intrinsicOffset = i * 4;
|
|
1179
|
+
const resolutionOffset = i * 2;
|
|
1040
1180
|
result.push({
|
|
1041
1181
|
id: i,
|
|
1042
1182
|
name: getName(model, nameAddresses[i]),
|
|
1043
1183
|
bodyId: model.cam_bodyid?.[i] ?? -1,
|
|
1044
1184
|
fov: model.cam_fovy?.[i] ?? null,
|
|
1185
|
+
resolution: model.cam_resolution
|
|
1186
|
+
? [
|
|
1187
|
+
model.cam_resolution[resolutionOffset],
|
|
1188
|
+
model.cam_resolution[resolutionOffset + 1],
|
|
1189
|
+
]
|
|
1190
|
+
: null,
|
|
1191
|
+
sensorSize: model.cam_sensorsize
|
|
1192
|
+
? [
|
|
1193
|
+
model.cam_sensorsize[resolutionOffset],
|
|
1194
|
+
model.cam_sensorsize[resolutionOffset + 1],
|
|
1195
|
+
]
|
|
1196
|
+
: null,
|
|
1197
|
+
intrinsic: model.cam_intrinsic
|
|
1198
|
+
? [
|
|
1199
|
+
model.cam_intrinsic[intrinsicOffset],
|
|
1200
|
+
model.cam_intrinsic[intrinsicOffset + 1],
|
|
1201
|
+
model.cam_intrinsic[intrinsicOffset + 2],
|
|
1202
|
+
model.cam_intrinsic[intrinsicOffset + 3],
|
|
1203
|
+
]
|
|
1204
|
+
: null,
|
|
1045
1205
|
position: model.cam_pos
|
|
1046
1206
|
? vector3FromArray(model.cam_pos, posOffset)
|
|
1047
1207
|
: null,
|
|
@@ -1087,11 +1247,31 @@ export function MujocoSimProvider({
|
|
|
1087
1247
|
}
|
|
1088
1248
|
|
|
1089
1249
|
const pose = applyMountedCameraPoseOffsets(options, position, quaternion);
|
|
1250
|
+
const compatibility = resolveMujocoCameraCompatibilityOptions(options);
|
|
1251
|
+
const cameraResolution = compatibility?.useResolution
|
|
1252
|
+
? mujocoCameraResolution(model, cameraId)
|
|
1253
|
+
: { width: undefined, height: undefined };
|
|
1254
|
+
const clip = compatibility?.useClipping
|
|
1255
|
+
? mujocoVisualClip(model)
|
|
1256
|
+
: { near: undefined, far: undefined };
|
|
1257
|
+
const { width, height } = compatibility
|
|
1258
|
+
? resolveMujocoCameraCaptureDimensions(options, cameraResolution, compatibility)
|
|
1259
|
+
: { width: options.width, height: options.height };
|
|
1260
|
+
const near = options.near ?? clip.near;
|
|
1261
|
+
const far = options.far ?? clip.far;
|
|
1262
|
+
const projectionMatrix = compatibility?.useIntrinsics
|
|
1263
|
+
? mujocoCameraProjectionMatrix(model, cameraId, width, height, near, far)
|
|
1264
|
+
: undefined;
|
|
1090
1265
|
|
|
1091
1266
|
return {
|
|
1092
1267
|
...baseOptions,
|
|
1268
|
+
width,
|
|
1269
|
+
height,
|
|
1093
1270
|
...pose,
|
|
1094
1271
|
fov: options.fov ?? model.cam_fovy?.[cameraId],
|
|
1272
|
+
near,
|
|
1273
|
+
far,
|
|
1274
|
+
projectionMatrix: options.projectionMatrix ?? projectionMatrix,
|
|
1095
1275
|
source: { kind: 'mujoco-camera', cameraName: options.cameraName },
|
|
1096
1276
|
};
|
|
1097
1277
|
}
|
|
@@ -1759,7 +1939,7 @@ export function MujocoSimProvider({
|
|
|
1759
1939
|
|
|
1760
1940
|
return (
|
|
1761
1941
|
<MujocoSimContext.Provider value={contextValue}>
|
|
1762
|
-
<SceneRenderer />
|
|
1942
|
+
<SceneRenderer renderOptions={renderOptions} />
|
|
1763
1943
|
{children}
|
|
1764
1944
|
</MujocoSimContext.Provider>
|
|
1765
1945
|
);
|
package/src/index.ts
CHANGED
|
@@ -5,9 +5,12 @@
|
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
import * as THREE from 'three';
|
|
8
|
+
import { mergeVertices } from 'three/examples/jsm/utils/BufferGeometryUtils.js';
|
|
8
9
|
import { CapsuleGeometry } from './CapsuleGeometry';
|
|
9
10
|
import { getName } from '../core/SceneLoader';
|
|
10
|
-
import { MujocoModel, MujocoModule } from '../types';
|
|
11
|
+
import { MujocoModel, MujocoModule, MujocoRenderOptions } from '../types';
|
|
12
|
+
|
|
13
|
+
const DEFAULT_MESH_NORMAL_SMOOTHING_TOLERANCE = 1e-4;
|
|
11
14
|
|
|
12
15
|
/**
|
|
13
16
|
* GeomBuilder
|
|
@@ -20,9 +23,18 @@ import { MujocoModel, MujocoModule } from '../types';
|
|
|
20
23
|
export class GeomBuilder {
|
|
21
24
|
private mujoco: MujocoModule;
|
|
22
25
|
private textureCache = new Map<number, THREE.Texture>();
|
|
26
|
+
private renderOptions?: MujocoRenderOptions;
|
|
23
27
|
|
|
24
|
-
constructor(mujoco: MujocoModule) {
|
|
28
|
+
constructor(mujoco: MujocoModule, renderOptions?: MujocoRenderOptions) {
|
|
25
29
|
this.mujoco = mujoco;
|
|
30
|
+
this.renderOptions = renderOptions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
private getMeshNormalSmoothingTolerance(): number | null {
|
|
34
|
+
const smoothing = this.renderOptions?.meshNormalSmoothing;
|
|
35
|
+
if (!smoothing) return null;
|
|
36
|
+
if (smoothing === true) return DEFAULT_MESH_NORMAL_SMOOTHING_TOLERANCE;
|
|
37
|
+
return smoothing.tolerance ?? DEFAULT_MESH_NORMAL_SMOOTHING_TOLERANCE;
|
|
26
38
|
}
|
|
27
39
|
|
|
28
40
|
private getMaterialTexture(mjModel: MujocoModel, matId: number): THREE.Texture | null {
|
|
@@ -148,6 +160,10 @@ export class GeomBuilder {
|
|
|
148
160
|
geo.setAttribute('position', new THREE.Float32BufferAttribute(mjModel.mesh_vert.subarray(vAdr * 3, (vAdr + vNum) * 3), 3));
|
|
149
161
|
// 'index' = faces (triangles connecting vertices)
|
|
150
162
|
geo.setIndex(Array.from(mjModel.mesh_face.subarray(fAdr * 3, (fAdr + fNum) * 3)));
|
|
163
|
+
const smoothingTolerance = this.getMeshNormalSmoothingTolerance();
|
|
164
|
+
if (smoothingTolerance !== null) {
|
|
165
|
+
geo = mergeVertices(geo, smoothingTolerance);
|
|
166
|
+
}
|
|
151
167
|
geo.computeVertexNormals(); // Auto-calculate smooth lighting normals
|
|
152
168
|
}
|
|
153
169
|
|