mujoco-react 10.4.0 → 10.6.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 +73 -136
- package/dist/{chunk-FBXXXPLQ.js → chunk-EN55TTGH.js} +157 -16
- package/dist/chunk-EN55TTGH.js.map +1 -0
- package/dist/index.d.ts +179 -48
- package/dist/index.js +487 -20
- package/dist/index.js.map +1 -1
- package/dist/onnx.d.ts +65 -0
- package/dist/onnx.js +58 -0
- package/dist/onnx.js.map +1 -0
- package/dist/spark.d.ts +1 -1
- package/dist/spark.js +1 -1
- package/dist/{types-CdFZCYmy.d.ts → types-Dvtm4I0o.d.ts} +155 -4
- package/package.json +14 -3
- package/src/components/CameraView.tsx +245 -0
- package/src/core/GenericIK.ts +22 -3
- package/src/core/MujocoSimProvider.tsx +37 -1
- package/src/core/SceneLoader.ts +3 -2
- package/src/hooks/useCameraStream.ts +115 -0
- package/src/hooks/useControlGroup.ts +0 -0
- package/src/hooks/useIkController.ts +36 -5
- package/src/hooks/usePolicyCameraTensors.ts +215 -0
- package/src/index.ts +44 -0
- package/src/onnx.ts +126 -0
- package/src/policyImageTensors.ts +150 -0
- package/src/rendering/cameraFrameCapture.ts +112 -15
- package/src/types.ts +50 -3
- package/dist/chunk-FBXXXPLQ.js.map +0 -1
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* Live on-screen camera viewports for MuJoCo scenes. Each view renders the
|
|
6
|
+
* shared scene from a named MuJoCo camera/site/body into a `gl.scissor` region
|
|
7
|
+
* tracking a DOM element — no GPU readback, no PNG encoding.
|
|
8
|
+
*
|
|
9
|
+
* While at least one viewport is mounted the canvas switches from R3F's
|
|
10
|
+
* automatic render to a managed render loop (main scene full-frame, then each
|
|
11
|
+
* viewport). This is incompatible with `EffectComposer`/postprocessing or other
|
|
12
|
+
* custom render loops; use the offscreen capture APIs in those setups instead.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import {
|
|
16
|
+
createContext,
|
|
17
|
+
type CSSProperties,
|
|
18
|
+
type MutableRefObject,
|
|
19
|
+
type ReactNode,
|
|
20
|
+
type RefObject,
|
|
21
|
+
useCallback,
|
|
22
|
+
useContext,
|
|
23
|
+
useEffect,
|
|
24
|
+
useMemo,
|
|
25
|
+
useRef,
|
|
26
|
+
useState,
|
|
27
|
+
} from 'react';
|
|
28
|
+
import { useFrame, useThree } from '@react-three/fiber';
|
|
29
|
+
import type * as THREE from 'three';
|
|
30
|
+
import { useMujoco } from '../core/MujocoSimProvider';
|
|
31
|
+
import {
|
|
32
|
+
createCaptureCamera,
|
|
33
|
+
prepareCaptureCamera,
|
|
34
|
+
} from '../rendering/cameraFrameCapture';
|
|
35
|
+
import type {
|
|
36
|
+
Bodies,
|
|
37
|
+
Cameras,
|
|
38
|
+
CameraFrameCaptureOptions,
|
|
39
|
+
Sites,
|
|
40
|
+
} from '../types';
|
|
41
|
+
|
|
42
|
+
/** Camera selection + pose options for a live viewport. */
|
|
43
|
+
export type CameraViewportOptions = Pick<
|
|
44
|
+
CameraFrameCaptureOptions,
|
|
45
|
+
| 'camera'
|
|
46
|
+
| 'cameraName'
|
|
47
|
+
| 'siteName'
|
|
48
|
+
| 'bodyName'
|
|
49
|
+
| 'position'
|
|
50
|
+
| 'quaternion'
|
|
51
|
+
| 'lookAt'
|
|
52
|
+
| 'up'
|
|
53
|
+
| 'positionOffset'
|
|
54
|
+
| 'quaternionOffset'
|
|
55
|
+
| 'fov'
|
|
56
|
+
| 'near'
|
|
57
|
+
| 'far'
|
|
58
|
+
| 'projectionMatrix'
|
|
59
|
+
| 'mujocoCameraCompatibility'
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
interface CameraViewportDescriptor {
|
|
63
|
+
getElement: () => HTMLElement | null;
|
|
64
|
+
getOptions: () => CameraViewportOptions;
|
|
65
|
+
camera: THREE.Camera | null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface CameraViewportRegistry {
|
|
69
|
+
register: (descriptor: CameraViewportDescriptor) => () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const CameraViewportRegistryContext = createContext<CameraViewportRegistry | null>(null);
|
|
73
|
+
|
|
74
|
+
let nextViewportId = 0;
|
|
75
|
+
|
|
76
|
+
const VIEWPORT_RENDER_PRIORITY = 1;
|
|
77
|
+
|
|
78
|
+
function CameraViewportRenderer({
|
|
79
|
+
viewportsRef,
|
|
80
|
+
}: {
|
|
81
|
+
viewportsRef: MutableRefObject<Map<number, CameraViewportDescriptor>>;
|
|
82
|
+
}) {
|
|
83
|
+
const gl = useThree((state) => state.gl);
|
|
84
|
+
const scene = useThree((state) => state.scene);
|
|
85
|
+
const mainCamera = useThree((state) => state.camera);
|
|
86
|
+
const mujoco = useMujoco();
|
|
87
|
+
|
|
88
|
+
useFrame(() => {
|
|
89
|
+
const drawWidth = gl.domElement.width;
|
|
90
|
+
const drawHeight = gl.domElement.height;
|
|
91
|
+
|
|
92
|
+
// We own the render now: draw the main scene full-frame first.
|
|
93
|
+
gl.setScissorTest(false);
|
|
94
|
+
gl.setViewport(0, 0, drawWidth, drawHeight);
|
|
95
|
+
gl.render(scene, mainCamera);
|
|
96
|
+
|
|
97
|
+
const api = mujoco.api;
|
|
98
|
+
if (!api || viewportsRef.current.size === 0) return;
|
|
99
|
+
|
|
100
|
+
const dpr = gl.getPixelRatio();
|
|
101
|
+
const canvasRect = gl.domElement.getBoundingClientRect();
|
|
102
|
+
|
|
103
|
+
for (const descriptor of viewportsRef.current.values()) {
|
|
104
|
+
const element = descriptor.getElement();
|
|
105
|
+
if (!element) continue;
|
|
106
|
+
|
|
107
|
+
const rect = element.getBoundingClientRect();
|
|
108
|
+
const isOffscreen =
|
|
109
|
+
rect.bottom < canvasRect.top ||
|
|
110
|
+
rect.top > canvasRect.bottom ||
|
|
111
|
+
rect.right < canvasRect.left ||
|
|
112
|
+
rect.left > canvasRect.right;
|
|
113
|
+
if (isOffscreen) continue;
|
|
114
|
+
|
|
115
|
+
const width = Math.floor(rect.width * dpr);
|
|
116
|
+
const height = Math.floor(rect.height * dpr);
|
|
117
|
+
if (width <= 0 || height <= 0) continue;
|
|
118
|
+
|
|
119
|
+
const left = Math.floor((rect.left - canvasRect.left) * dpr);
|
|
120
|
+
const bottom = Math.floor((canvasRect.bottom - rect.bottom) * dpr);
|
|
121
|
+
|
|
122
|
+
let resolved: CameraFrameCaptureOptions;
|
|
123
|
+
try {
|
|
124
|
+
resolved = api.resolveCameraCaptureOptions(descriptor.getOptions());
|
|
125
|
+
} catch {
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!descriptor.camera) {
|
|
130
|
+
descriptor.camera = createCaptureCamera(resolved, mainCamera, width, height);
|
|
131
|
+
} else {
|
|
132
|
+
prepareCaptureCamera(descriptor.camera, resolved, mainCamera, width, height);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
gl.setViewport(left, bottom, width, height);
|
|
136
|
+
gl.setScissor(left, bottom, width, height);
|
|
137
|
+
gl.setScissorTest(true);
|
|
138
|
+
gl.render(scene, descriptor.camera);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
gl.setScissorTest(false);
|
|
142
|
+
gl.setViewport(0, 0, drawWidth, drawHeight);
|
|
143
|
+
}, VIEWPORT_RENDER_PRIORITY);
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Provides the live-viewport registry and mounts the managed render loop only
|
|
150
|
+
* while at least one viewport is active. Mounted internally by the MuJoCo
|
|
151
|
+
* provider; you do not need to add it yourself.
|
|
152
|
+
*/
|
|
153
|
+
export function CameraViewportProvider({ children }: { children?: ReactNode }) {
|
|
154
|
+
const viewportsRef = useRef<Map<number, CameraViewportDescriptor>>(new Map());
|
|
155
|
+
const [count, setCount] = useState(0);
|
|
156
|
+
|
|
157
|
+
const register = useCallback((descriptor: CameraViewportDescriptor) => {
|
|
158
|
+
const id = nextViewportId++;
|
|
159
|
+
viewportsRef.current.set(id, descriptor);
|
|
160
|
+
setCount((value) => value + 1);
|
|
161
|
+
return () => {
|
|
162
|
+
viewportsRef.current.delete(id);
|
|
163
|
+
setCount((value) => value - 1);
|
|
164
|
+
};
|
|
165
|
+
}, []);
|
|
166
|
+
|
|
167
|
+
const value = useMemo<CameraViewportRegistry>(() => ({ register }), [register]);
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<CameraViewportRegistryContext.Provider value={value}>
|
|
171
|
+
{children}
|
|
172
|
+
{count > 0 && <CameraViewportRenderer viewportsRef={viewportsRef} />}
|
|
173
|
+
</CameraViewportRegistryContext.Provider>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Render the live MuJoCo scene from a named camera into the region covered by
|
|
179
|
+
* `elementRef`'s DOM element. Call this inside `<MujocoCanvas>` with a ref to a
|
|
180
|
+
* DOM element you position anywhere (it does not need to be in the R3F tree).
|
|
181
|
+
*/
|
|
182
|
+
export function useCameraViewport<T extends HTMLElement = HTMLElement>(
|
|
183
|
+
elementRef: RefObject<T | null>,
|
|
184
|
+
options: CameraViewportOptions
|
|
185
|
+
) {
|
|
186
|
+
const registry = useContext(CameraViewportRegistryContext);
|
|
187
|
+
if (!registry) {
|
|
188
|
+
throw new Error('useCameraViewport must be used inside <MujocoCanvas>.');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const optionsRef = useRef(options);
|
|
192
|
+
optionsRef.current = options;
|
|
193
|
+
|
|
194
|
+
useEffect(() => {
|
|
195
|
+
const descriptor: CameraViewportDescriptor = {
|
|
196
|
+
getElement: () => elementRef.current,
|
|
197
|
+
getOptions: () => optionsRef.current,
|
|
198
|
+
camera: null,
|
|
199
|
+
};
|
|
200
|
+
return registry.register(descriptor);
|
|
201
|
+
}, [registry, elementRef]);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export interface CameraViewProps extends CameraViewportOptions {
|
|
205
|
+
className?: string;
|
|
206
|
+
style?: CSSProperties;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Drop-in live camera pane. Renders an absolutely-positioned overlay `<div>`
|
|
211
|
+
* over the canvas showing the selected MuJoCo camera. Position it with
|
|
212
|
+
* `style`/`className` (the canvas's parent should be positioned).
|
|
213
|
+
*/
|
|
214
|
+
export function CameraView({ className, style, ...options }: CameraViewProps) {
|
|
215
|
+
const gl = useThree((state) => state.gl);
|
|
216
|
+
const elementRef = useRef<HTMLDivElement | null>(null);
|
|
217
|
+
if (!elementRef.current && typeof document !== 'undefined') {
|
|
218
|
+
elementRef.current = document.createElement('div');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
useEffect(() => {
|
|
222
|
+
const element = elementRef.current;
|
|
223
|
+
const parent = gl.domElement.parentElement;
|
|
224
|
+
if (!element || !parent) return;
|
|
225
|
+
element.style.position = 'absolute';
|
|
226
|
+
element.style.overflow = 'hidden';
|
|
227
|
+
parent.appendChild(element);
|
|
228
|
+
return () => {
|
|
229
|
+
parent.removeChild(element);
|
|
230
|
+
};
|
|
231
|
+
}, [gl]);
|
|
232
|
+
|
|
233
|
+
useEffect(() => {
|
|
234
|
+
const element = elementRef.current;
|
|
235
|
+
if (!element) return;
|
|
236
|
+
element.className = className ?? '';
|
|
237
|
+
if (style) Object.assign(element.style, style);
|
|
238
|
+
}, [className, style]);
|
|
239
|
+
|
|
240
|
+
useCameraViewport(elementRef, options);
|
|
241
|
+
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export type { Bodies, Cameras, Sites };
|
package/src/core/GenericIK.ts
CHANGED
|
@@ -13,9 +13,13 @@ export interface GenericIKOptions {
|
|
|
13
13
|
epsilon: number;
|
|
14
14
|
posWeight: number;
|
|
15
15
|
rotWeight: number;
|
|
16
|
+
jointLimits?: ReadonlyArray<readonly [number, number] | null | undefined>;
|
|
16
17
|
}
|
|
17
18
|
|
|
18
|
-
|
|
19
|
+
type ResolvedGenericIKOptions = Required<Omit<GenericIKOptions, 'jointLimits'>> &
|
|
20
|
+
Pick<GenericIKOptions, 'jointLimits'>;
|
|
21
|
+
|
|
22
|
+
const DEFAULTS: Required<Omit<GenericIKOptions, 'jointLimits'>> = {
|
|
19
23
|
maxIterations: 50,
|
|
20
24
|
damping: 0.01,
|
|
21
25
|
tolerance: 1e-3,
|
|
@@ -24,7 +28,7 @@ const DEFAULTS: GenericIKOptions = {
|
|
|
24
28
|
rotWeight: 0.3,
|
|
25
29
|
};
|
|
26
30
|
|
|
27
|
-
function resolveOptions(opts?: Partial<GenericIKOptions>):
|
|
31
|
+
function resolveOptions(opts?: Partial<GenericIKOptions>): ResolvedGenericIKOptions {
|
|
28
32
|
return {
|
|
29
33
|
maxIterations: opts?.maxIterations ?? DEFAULTS.maxIterations,
|
|
30
34
|
damping: opts?.damping ?? DEFAULTS.damping,
|
|
@@ -32,6 +36,7 @@ function resolveOptions(opts?: Partial<GenericIKOptions>): GenericIKOptions {
|
|
|
32
36
|
epsilon: opts?.epsilon ?? DEFAULTS.epsilon,
|
|
33
37
|
posWeight: opts?.posWeight ?? DEFAULTS.posWeight,
|
|
34
38
|
rotWeight: opts?.rotWeight ?? DEFAULTS.rotWeight,
|
|
39
|
+
jointLimits: opts?.jointLimits,
|
|
35
40
|
};
|
|
36
41
|
}
|
|
37
42
|
|
|
@@ -96,6 +101,14 @@ export class GenericIK {
|
|
|
96
101
|
|
|
97
102
|
let bestQ: number[] | null = null;
|
|
98
103
|
let bestErr = Infinity;
|
|
104
|
+
// Stop early once the error stops improving. The solver always returns the
|
|
105
|
+
// best configuration it has seen, so when it stalls or diverges (an
|
|
106
|
+
// unreachable target, e.g. a near-singular wrist orientation) further
|
|
107
|
+
// iterations can't beat bestQ — they only burn (1 + n) mj_forward calls
|
|
108
|
+
// each and feed jitter into ctrl. This is what bounds the cost of dragging
|
|
109
|
+
// the target into an unreachable region.
|
|
110
|
+
const patience = 4;
|
|
111
|
+
let noImprove = 0;
|
|
99
112
|
|
|
100
113
|
if (n === 0) return null;
|
|
101
114
|
|
|
@@ -133,14 +146,20 @@ export class GenericIK {
|
|
|
133
146
|
);
|
|
134
147
|
|
|
135
148
|
// Track best solution
|
|
136
|
-
if (errNorm < bestErr) {
|
|
149
|
+
if (errNorm < bestErr - 1e-9) {
|
|
137
150
|
bestErr = errNorm;
|
|
138
151
|
bestQ = Array.from(q);
|
|
152
|
+
noImprove = 0;
|
|
153
|
+
} else {
|
|
154
|
+
noImprove++;
|
|
139
155
|
}
|
|
140
156
|
|
|
141
157
|
// Converged
|
|
142
158
|
if (errNorm < o.tolerance) break;
|
|
143
159
|
|
|
160
|
+
// Stalled or diverging — bestQ can no longer improve, so stop.
|
|
161
|
+
if (noImprove >= patience) break;
|
|
162
|
+
|
|
144
163
|
// Compute Jacobian via finite differences
|
|
145
164
|
for (let j = 0; j < n; j++) {
|
|
146
165
|
const adr = qposAdr[j];
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
import * as THREE from 'three';
|
|
17
17
|
import { MujocoData, MujocoModel, MujocoModule, getContact, withContacts } from '../types';
|
|
18
18
|
import { SceneRenderer } from '../components/SceneRenderer';
|
|
19
|
+
import { CameraViewportProvider } from '../components/CameraView';
|
|
19
20
|
import {
|
|
20
21
|
ActuatedJointInfo,
|
|
21
22
|
ActuatorInfo,
|
|
@@ -59,6 +60,7 @@ import {
|
|
|
59
60
|
captureCameraFrame,
|
|
60
61
|
captureCameraFrameBlob,
|
|
61
62
|
createCameraFrameCaptureSession,
|
|
63
|
+
type CameraFrameCaptureTensorOptions,
|
|
62
64
|
} from '../rendering/cameraFrameCapture';
|
|
63
65
|
import {
|
|
64
66
|
getCameraFrameCaptureSourceTarget,
|
|
@@ -1558,6 +1560,35 @@ export function MujocoSimProvider({
|
|
|
1558
1560
|
[camera, gl, resolveCameraCaptureOptions, scene]
|
|
1559
1561
|
);
|
|
1560
1562
|
|
|
1563
|
+
const createCameraFrameCaptureSessionApi = useCallback(
|
|
1564
|
+
(options: CameraFrameCaptureOptions = {}) =>
|
|
1565
|
+
createCameraFrameCaptureSession(
|
|
1566
|
+
gl,
|
|
1567
|
+
scene,
|
|
1568
|
+
camera,
|
|
1569
|
+
resolveCameraCaptureOptions(options)
|
|
1570
|
+
),
|
|
1571
|
+
[camera, gl, resolveCameraCaptureOptions, scene]
|
|
1572
|
+
);
|
|
1573
|
+
|
|
1574
|
+
const captureCameraFrameTensorApi = useCallback(
|
|
1575
|
+
(options: CameraFrameCaptureTensorOptions = {}) => {
|
|
1576
|
+
const resolved: CameraFrameCaptureTensorOptions = {
|
|
1577
|
+
...resolveCameraCaptureOptions(options),
|
|
1578
|
+
channels: options.channels,
|
|
1579
|
+
layout: options.layout,
|
|
1580
|
+
range: options.range,
|
|
1581
|
+
};
|
|
1582
|
+
const session = createCameraFrameCaptureSession(gl, scene, camera, resolved);
|
|
1583
|
+
try {
|
|
1584
|
+
return session.captureTensor(resolved);
|
|
1585
|
+
} finally {
|
|
1586
|
+
session.dispose();
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
[camera, gl, resolveCameraCaptureOptions, scene]
|
|
1590
|
+
);
|
|
1591
|
+
|
|
1561
1592
|
const recordCameraSequenceApi = useCallback(
|
|
1562
1593
|
async (
|
|
1563
1594
|
options: CameraFrameSequenceOptions
|
|
@@ -1882,6 +1913,9 @@ export function MujocoSimProvider({
|
|
|
1882
1913
|
captureFrameBlob: captureFrameBlobApi,
|
|
1883
1914
|
captureCameraFrame: captureCameraFrameApi,
|
|
1884
1915
|
captureCameraFrameBlob: captureCameraFrameBlobApi,
|
|
1916
|
+
captureCameraFrameTensor: captureCameraFrameTensorApi,
|
|
1917
|
+
createCameraFrameCaptureSession: createCameraFrameCaptureSessionApi,
|
|
1918
|
+
resolveCameraCaptureOptions,
|
|
1885
1919
|
recordCameraSequence: recordCameraSequenceApi,
|
|
1886
1920
|
project2DTo3D,
|
|
1887
1921
|
projectImagePointTo3D,
|
|
@@ -1903,6 +1937,8 @@ export function MujocoSimProvider({
|
|
|
1903
1937
|
loadFromFilesApi, addBodyApi, removeBodyApi, recompileApi,
|
|
1904
1938
|
getCanvas, getCanvasSnapshot, captureFrameApi, captureFrameBlobApi,
|
|
1905
1939
|
captureCameraFrameApi, captureCameraFrameBlobApi,
|
|
1940
|
+
captureCameraFrameTensorApi, createCameraFrameCaptureSessionApi,
|
|
1941
|
+
resolveCameraCaptureOptions,
|
|
1906
1942
|
recordCameraSequenceApi,
|
|
1907
1943
|
project2DTo3D,
|
|
1908
1944
|
projectImagePointTo3D,
|
|
@@ -1940,7 +1976,7 @@ export function MujocoSimProvider({
|
|
|
1940
1976
|
return (
|
|
1941
1977
|
<MujocoSimContext.Provider value={contextValue}>
|
|
1942
1978
|
<SceneRenderer renderOptions={renderOptions} />
|
|
1943
|
-
{children}
|
|
1979
|
+
<CameraViewportProvider>{children}</CameraViewportProvider>
|
|
1944
1980
|
</MujocoSimContext.Provider>
|
|
1945
1981
|
);
|
|
1946
1982
|
}
|
package/src/core/SceneLoader.ts
CHANGED
|
@@ -496,9 +496,10 @@ function sceneObjectToXml(obj: SceneObject): string {
|
|
|
496
496
|
const solref = obj.solref ? ` solref="${obj.solref}"` : '';
|
|
497
497
|
const solimp = obj.solimp ? ` solimp="${obj.solimp}"` : '';
|
|
498
498
|
const condim = obj.condim ? ` condim="${obj.condim}"` : '';
|
|
499
|
+
const contype = obj.contype ?? 1;
|
|
500
|
+
const conaffinity = obj.conaffinity ?? 1;
|
|
499
501
|
const group = obj.group !== undefined ? ` group="${obj.group}"` : '';
|
|
500
|
-
|
|
501
|
-
return `<body name="${obj.name}" pos="${pos}">${joint}<geom name="${geomName}" type="${obj.type}" size="${size}" rgba="${rgba}" contype="1" conaffinity="1"${mass}${friction}${solref}${solimp}${condim}${group}/></body>`;
|
|
502
|
+
return `<body name="${obj.name}" pos="${pos}">${joint}<geom name="${geomName}" type="${obj.type}" size="${size}" rgba="${rgba}" contype="${contype}" conaffinity="${conaffinity}"${mass}${friction}${solref}${solimp}${condim}${group}/></body>`;
|
|
502
503
|
}
|
|
503
504
|
|
|
504
505
|
/** Create virtual directory structure for a file path. */
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*
|
|
5
|
+
* Stream a live MuJoCo camera into a DOM `<canvas>`. Each frame the scene is
|
|
6
|
+
* rendered offscreen from the selected camera and blitted into the canvas, so
|
|
7
|
+
* it composites normally in the DOM (works inside opaque panels) and does NOT
|
|
8
|
+
* take over the render loop. Prefer this over `useCameraViewport` for camera
|
|
9
|
+
* tiles embedded in HTML UI; use `useCameraViewport` for transparent overlays
|
|
10
|
+
* on a full-bleed canvas.
|
|
11
|
+
*
|
|
12
|
+
* Uses the async capture path so Gaussian-splat environments render through
|
|
13
|
+
* their dedicated capture renderer — streaming a splat scene at full rate does
|
|
14
|
+
* not disturb the main view's splat sort.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useRef } from 'react';
|
|
18
|
+
import type { RefObject } from 'react';
|
|
19
|
+
import { useFrame } from '@react-three/fiber';
|
|
20
|
+
import { useMujoco } from '../core/MujocoSimProvider';
|
|
21
|
+
import type { CameraFrameCaptureSession } from '../rendering/cameraFrameCapture';
|
|
22
|
+
import type { CameraFrameCaptureOptions } from '../types';
|
|
23
|
+
|
|
24
|
+
export interface CameraStreamOptions extends CameraFrameCaptureOptions {
|
|
25
|
+
/**
|
|
26
|
+
* Optional cap on updates per second. Omit to stream as fast as captures
|
|
27
|
+
* complete (one capture is in flight at a time regardless).
|
|
28
|
+
*/
|
|
29
|
+
fps?: number;
|
|
30
|
+
/** Pause updates without unmounting. */
|
|
31
|
+
paused?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function streamSignature(options: CameraStreamOptions): string {
|
|
35
|
+
return JSON.stringify({
|
|
36
|
+
cameraName: options.cameraName,
|
|
37
|
+
siteName: options.siteName,
|
|
38
|
+
bodyName: options.bodyName,
|
|
39
|
+
width: options.width,
|
|
40
|
+
height: options.height,
|
|
41
|
+
renderIsolation: options.renderIsolation ?? false,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Render the live scene from a MuJoCo camera/site/body into `canvasRef`'s
|
|
47
|
+
* `<canvas>` every frame (throttled to `fps`). Call inside `<MujocoCanvas>`;
|
|
48
|
+
* the canvas itself can live anywhere in the DOM.
|
|
49
|
+
*/
|
|
50
|
+
export function useCameraStream(
|
|
51
|
+
canvasRef: RefObject<HTMLCanvasElement | null>,
|
|
52
|
+
options: CameraStreamOptions
|
|
53
|
+
) {
|
|
54
|
+
const mujoco = useMujoco();
|
|
55
|
+
const sessionRef = useRef<CameraFrameCaptureSession | null>(null);
|
|
56
|
+
const signatureRef = useRef<string>('');
|
|
57
|
+
const optionsRef = useRef(options);
|
|
58
|
+
optionsRef.current = options;
|
|
59
|
+
const elapsedRef = useRef(0);
|
|
60
|
+
const inFlightRef = useRef(false);
|
|
61
|
+
const mountedRef = useRef(true);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
mountedRef.current = true;
|
|
65
|
+
return () => {
|
|
66
|
+
mountedRef.current = false;
|
|
67
|
+
sessionRef.current?.dispose();
|
|
68
|
+
sessionRef.current = null;
|
|
69
|
+
signatureRef.current = '';
|
|
70
|
+
};
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
useFrame((_state, delta) => {
|
|
74
|
+
const api = mujoco.api;
|
|
75
|
+
if (!api || !canvasRef.current) return;
|
|
76
|
+
|
|
77
|
+
const opts = optionsRef.current;
|
|
78
|
+
if (opts.paused) return;
|
|
79
|
+
|
|
80
|
+
if (opts.fps && opts.fps > 0) {
|
|
81
|
+
elapsedRef.current += delta;
|
|
82
|
+
if (elapsedRef.current < 1 / opts.fps) return;
|
|
83
|
+
}
|
|
84
|
+
// One capture in flight at a time — naturally rate-limits to capture speed.
|
|
85
|
+
if (inFlightRef.current) return;
|
|
86
|
+
elapsedRef.current = 0;
|
|
87
|
+
|
|
88
|
+
const signature = streamSignature(opts);
|
|
89
|
+
if (!sessionRef.current || signatureRef.current !== signature) {
|
|
90
|
+
sessionRef.current?.dispose();
|
|
91
|
+
sessionRef.current = api.createCameraFrameCaptureSession(opts);
|
|
92
|
+
signatureRef.current = signature;
|
|
93
|
+
}
|
|
94
|
+
const session = sessionRef.current;
|
|
95
|
+
|
|
96
|
+
inFlightRef.current = true;
|
|
97
|
+
session
|
|
98
|
+
.captureAsync(api.resolveCameraCaptureOptions(opts))
|
|
99
|
+
.then((frame) => {
|
|
100
|
+
const canvas = canvasRef.current;
|
|
101
|
+
if (!mountedRef.current || !canvas) return;
|
|
102
|
+
const ctx = canvas.getContext('2d');
|
|
103
|
+
if (!ctx) return;
|
|
104
|
+
if (canvas.width !== frame.width || canvas.height !== frame.height) {
|
|
105
|
+
canvas.width = frame.width;
|
|
106
|
+
canvas.height = frame.height;
|
|
107
|
+
}
|
|
108
|
+
ctx.drawImage(frame.canvas, 0, 0);
|
|
109
|
+
})
|
|
110
|
+
.catch(() => {})
|
|
111
|
+
.finally(() => {
|
|
112
|
+
inFlightRef.current = false;
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
}
|
|
Binary file
|
|
@@ -10,11 +10,31 @@ import { createControllerHook } from '../core/createController';
|
|
|
10
10
|
import { useMujocoContext, useBeforePhysicsStep } from '../core/MujocoSimProvider';
|
|
11
11
|
import { GenericIK } from '../core/GenericIK';
|
|
12
12
|
import { createContiguousControlGroup, findSiteByName, resolveControlGroup } from '../core/SceneLoader';
|
|
13
|
-
import type {
|
|
13
|
+
import type {
|
|
14
|
+
ControlGroupInfo,
|
|
15
|
+
IkConfig,
|
|
16
|
+
IkContextValue,
|
|
17
|
+
IKSolveFn,
|
|
18
|
+
IkSolveInput,
|
|
19
|
+
IkTargetPosition,
|
|
20
|
+
MujocoData,
|
|
21
|
+
} from '../types';
|
|
14
22
|
|
|
15
23
|
// Preallocated temp for syncGizmoToSite
|
|
16
24
|
const _syncMat4 = new THREE.Matrix4();
|
|
17
25
|
|
|
26
|
+
function toIkVector3(value: IkSolveInput['position']): THREE.Vector3 {
|
|
27
|
+
return 'x' in value
|
|
28
|
+
? new THREE.Vector3(value.x, value.y, value.z)
|
|
29
|
+
: new THREE.Vector3(value[0], value[1], value[2]);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function toIkQuaternion(value: IkSolveInput['quaternion']): THREE.Quaternion {
|
|
33
|
+
return 'x' in value
|
|
34
|
+
? new THREE.Quaternion(value.x, value.y, value.z, value.w)
|
|
35
|
+
: new THREE.Quaternion(value[0], value[1], value[2], value[3]);
|
|
36
|
+
}
|
|
37
|
+
|
|
18
38
|
function syncGizmoToSite(data: MujocoData, siteId: number, target: THREE.Group) {
|
|
19
39
|
if (siteId === -1) return;
|
|
20
40
|
const sitePos = data.site_xpos.subarray(siteId * 3, siteId * 3 + 3);
|
|
@@ -86,17 +106,28 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
86
106
|
const ikSolveFn = useCallback(
|
|
87
107
|
({ position, quaternion, currentQ, context }: IkSolveInput): number[] | null => {
|
|
88
108
|
if (!config) return null;
|
|
89
|
-
|
|
109
|
+
const targetPosition = toIkVector3(position);
|
|
110
|
+
const targetQuaternion = toIkQuaternion(quaternion);
|
|
111
|
+
const normalizedInput: IkSolveInput = {
|
|
112
|
+
position: targetPosition,
|
|
113
|
+
quaternion: targetQuaternion,
|
|
114
|
+
currentQ,
|
|
115
|
+
context,
|
|
116
|
+
};
|
|
117
|
+
if (config.ikSolveFn) return config.ikSolveFn(normalizedInput);
|
|
90
118
|
const model = mjModelRef.current;
|
|
91
119
|
const data = mjDataRef.current;
|
|
92
120
|
const controlGroup = controlGroupRef.current;
|
|
93
121
|
if (!model || !data || !controlGroup || siteIdRef.current === -1) return null;
|
|
94
122
|
return genericIkRef.current.solve(
|
|
95
123
|
model, data, siteIdRef.current, controlGroup.qposAdr,
|
|
96
|
-
|
|
124
|
+
targetPosition, targetQuaternion, currentQ,
|
|
97
125
|
{
|
|
98
126
|
damping: config.damping,
|
|
99
127
|
epsilon: config.epsilon,
|
|
128
|
+
jointLimits: config.jointLimits ?? controlGroup.joints.map((joint) => (
|
|
129
|
+
joint.limited ? joint.range : joint.ctrlRange
|
|
130
|
+
)),
|
|
100
131
|
maxIterations: config.maxIterations,
|
|
101
132
|
posWeight: config.posWeight,
|
|
102
133
|
rotWeight: config.rotWeight,
|
|
@@ -215,12 +246,12 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
215
246
|
);
|
|
216
247
|
|
|
217
248
|
const moveTarget = useCallback(
|
|
218
|
-
(pos:
|
|
249
|
+
(pos: IkTargetPosition, duration = 0) => {
|
|
219
250
|
if (!ikEnabledRef.current) setIkEnabled(true);
|
|
220
251
|
const target = ikTargetRef.current;
|
|
221
252
|
if (!target) return;
|
|
222
253
|
|
|
223
|
-
const targetPos = pos
|
|
254
|
+
const targetPos = toIkVector3(pos);
|
|
224
255
|
const targetRot = new THREE.Quaternion().setFromEuler(
|
|
225
256
|
new THREE.Euler(Math.PI, 0, 0),
|
|
226
257
|
);
|