mujoco-react 10.3.0 → 10.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 +75 -137
- package/dist/{chunk-6AZEFI6A.js → chunk-KHZ5U36J.js} +157 -16
- package/dist/chunk-KHZ5U36J.js.map +1 -0
- package/dist/index.d.ts +180 -49
- package/dist/index.js +627 -19
- 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-BOhNDICK.d.ts → types-CViUme8D.d.ts} +157 -1
- package/package.json +14 -3
- package/src/components/CameraView.tsx +245 -0
- package/src/components/Debug.tsx +174 -3
- package/src/core/GenericIK.ts +16 -4
- 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 +3 -0
- package/src/hooks/usePolicyCameraTensors.ts +215 -0
- package/src/index.ts +45 -0
- package/src/onnx.ts +126 -0
- package/src/policyImageTensors.ts +150 -0
- package/src/rendering/cameraFrameCapture.ts +112 -15
- package/src/types.ts +45 -0
- package/dist/chunk-6AZEFI6A.js.map +0 -1
package/src/components/Debug.tsx
CHANGED
|
@@ -13,7 +13,7 @@ import { useMujocoContext } from '../core/MujocoSimProvider';
|
|
|
13
13
|
import { getName } from '../core/SceneLoader';
|
|
14
14
|
import { CAPTURE_EXCLUDE_KEY } from '../rendering/cameraFrameCapture';
|
|
15
15
|
import { getContact, withContacts } from '../types';
|
|
16
|
-
import type { DebugProps } from '../types';
|
|
16
|
+
import type { CameraFrameCaptureVector3, DebugProps, DebugVirtualCamera } from '../types';
|
|
17
17
|
|
|
18
18
|
const JOINT_COLORS: Record<number, number> = {
|
|
19
19
|
0: 0xff0000, // free - red
|
|
@@ -41,6 +41,170 @@ type CameraDebugObject = THREE.Group & {
|
|
|
41
41
|
};
|
|
42
42
|
};
|
|
43
43
|
|
|
44
|
+
function toVector3(
|
|
45
|
+
value: CameraFrameCaptureVector3 | undefined,
|
|
46
|
+
fallback: THREE.Vector3
|
|
47
|
+
) {
|
|
48
|
+
if (!value) return fallback.clone();
|
|
49
|
+
return value instanceof THREE.Vector3
|
|
50
|
+
? value.clone()
|
|
51
|
+
: new THREE.Vector3(value[0], value[1], value[2]);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function createCameraLabel(text: string, color: THREE.ColorRepresentation) {
|
|
55
|
+
const canvas = document.createElement('canvas');
|
|
56
|
+
canvas.width = 256;
|
|
57
|
+
canvas.height = 64;
|
|
58
|
+
const ctx = canvas.getContext('2d')!;
|
|
59
|
+
ctx.fillStyle = new THREE.Color(color).getStyle();
|
|
60
|
+
ctx.font = 'bold 32px monospace';
|
|
61
|
+
ctx.textAlign = 'center';
|
|
62
|
+
ctx.fillText(text, 128, 42);
|
|
63
|
+
const texture = new THREE.CanvasTexture(canvas);
|
|
64
|
+
const sprite = new THREE.Sprite(
|
|
65
|
+
new THREE.SpriteMaterial({
|
|
66
|
+
map: texture,
|
|
67
|
+
depthTest: false,
|
|
68
|
+
transparent: true,
|
|
69
|
+
})
|
|
70
|
+
);
|
|
71
|
+
sprite.position.set(0, 0.014, 0.01);
|
|
72
|
+
sprite.scale.set(0.05, 0.012, 1);
|
|
73
|
+
sprite.renderOrder = 999;
|
|
74
|
+
return sprite;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function createVirtualCameraDebugObject(
|
|
78
|
+
camera: DebugVirtualCamera,
|
|
79
|
+
index: number
|
|
80
|
+
) {
|
|
81
|
+
const color = camera.color ?? '#ff3d71';
|
|
82
|
+
const aimColor = camera.aimColor ?? '#38bdf8';
|
|
83
|
+
const markerScale = camera.markerScale ?? 1;
|
|
84
|
+
const cameraPosition = toVector3(camera.position, new THREE.Vector3());
|
|
85
|
+
const configuredUp = toVector3(camera.up, new THREE.Vector3(0, 0, 1)).normalize();
|
|
86
|
+
const cameraQuaternion = new THREE.Quaternion();
|
|
87
|
+
const forward = new THREE.Vector3();
|
|
88
|
+
|
|
89
|
+
if (camera.quaternion) {
|
|
90
|
+
if (camera.quaternion instanceof THREE.Quaternion) {
|
|
91
|
+
cameraQuaternion.copy(camera.quaternion);
|
|
92
|
+
} else {
|
|
93
|
+
cameraQuaternion.set(
|
|
94
|
+
camera.quaternion[0],
|
|
95
|
+
camera.quaternion[1],
|
|
96
|
+
camera.quaternion[2],
|
|
97
|
+
camera.quaternion[3]
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
forward.set(0, 0, -1).applyQuaternion(cameraQuaternion).normalize();
|
|
101
|
+
} else {
|
|
102
|
+
const target = toVector3(
|
|
103
|
+
camera.lookAt,
|
|
104
|
+
cameraPosition.clone().add(new THREE.Vector3(0, 0, -1))
|
|
105
|
+
);
|
|
106
|
+
forward.copy(target).sub(cameraPosition);
|
|
107
|
+
if (forward.lengthSq() < 1e-8) forward.set(0, 0, -1);
|
|
108
|
+
forward.normalize();
|
|
109
|
+
cameraQuaternion.setFromRotationMatrix(
|
|
110
|
+
new THREE.Matrix4().lookAt(cameraPosition, target, configuredUp)
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const target = camera.lookAt
|
|
115
|
+
? toVector3(camera.lookAt, cameraPosition.clone().add(forward))
|
|
116
|
+
: cameraPosition.clone().addScaledVector(forward, 0.4);
|
|
117
|
+
const distanceToTarget = Math.max(target.distanceTo(cameraPosition), 0.001);
|
|
118
|
+
const depth = camera.frustumDepth ?? Math.min(Math.max(distanceToTarget * 0.42, 0.16), 0.45);
|
|
119
|
+
const fov = camera.fov ?? 50;
|
|
120
|
+
const aspect = (camera.width ?? 640) / (camera.height ?? 480);
|
|
121
|
+
|
|
122
|
+
const right = forward.clone().cross(configuredUp);
|
|
123
|
+
if (right.lengthSq() < 1e-8) right.set(1, 0, 0);
|
|
124
|
+
right.normalize();
|
|
125
|
+
const orthogonalUp = right.clone().cross(forward).normalize();
|
|
126
|
+
const frustumHeight = 2 * Math.tan(THREE.MathUtils.degToRad(fov) / 2) * depth;
|
|
127
|
+
const frustumWidth = frustumHeight * aspect;
|
|
128
|
+
const center = cameraPosition.clone().addScaledVector(forward, depth);
|
|
129
|
+
const halfRight = right.clone().multiplyScalar(frustumWidth / 2);
|
|
130
|
+
const halfUp = orthogonalUp.clone().multiplyScalar(frustumHeight / 2);
|
|
131
|
+
|
|
132
|
+
const topLeft = center.clone().sub(halfRight).add(halfUp);
|
|
133
|
+
const topRight = center.clone().add(halfRight).add(halfUp);
|
|
134
|
+
const bottomRight = center.clone().add(halfRight).sub(halfUp);
|
|
135
|
+
const bottomLeft = center.clone().sub(halfRight).sub(halfUp);
|
|
136
|
+
const frustumPoints = [
|
|
137
|
+
cameraPosition, topLeft,
|
|
138
|
+
cameraPosition, topRight,
|
|
139
|
+
cameraPosition, bottomRight,
|
|
140
|
+
cameraPosition, bottomLeft,
|
|
141
|
+
topLeft, topRight,
|
|
142
|
+
topRight, bottomRight,
|
|
143
|
+
bottomRight, bottomLeft,
|
|
144
|
+
bottomLeft, topLeft,
|
|
145
|
+
];
|
|
146
|
+
|
|
147
|
+
const group = new THREE.Group();
|
|
148
|
+
group.name = camera.name ?? `virtual-camera-${index}`;
|
|
149
|
+
group.renderOrder = 999;
|
|
150
|
+
group.frustumCulled = false;
|
|
151
|
+
|
|
152
|
+
const frustum = new THREE.LineSegments(
|
|
153
|
+
new THREE.BufferGeometry().setFromPoints(frustumPoints),
|
|
154
|
+
new THREE.LineBasicMaterial({
|
|
155
|
+
color,
|
|
156
|
+
transparent: true,
|
|
157
|
+
opacity: 0.9,
|
|
158
|
+
depthTest: false,
|
|
159
|
+
})
|
|
160
|
+
);
|
|
161
|
+
frustum.renderOrder = 999;
|
|
162
|
+
frustum.frustumCulled = false;
|
|
163
|
+
group.add(frustum);
|
|
164
|
+
|
|
165
|
+
const aim = new THREE.LineSegments(
|
|
166
|
+
new THREE.BufferGeometry().setFromPoints([cameraPosition, target]),
|
|
167
|
+
new THREE.LineBasicMaterial({
|
|
168
|
+
color: aimColor,
|
|
169
|
+
transparent: true,
|
|
170
|
+
opacity: 0.95,
|
|
171
|
+
depthTest: false,
|
|
172
|
+
})
|
|
173
|
+
);
|
|
174
|
+
aim.renderOrder = 999;
|
|
175
|
+
aim.frustumCulled = false;
|
|
176
|
+
group.add(aim);
|
|
177
|
+
|
|
178
|
+
const markerGroup = new THREE.Group();
|
|
179
|
+
markerGroup.position.copy(cameraPosition);
|
|
180
|
+
markerGroup.quaternion.copy(cameraQuaternion);
|
|
181
|
+
markerGroup.renderOrder = 999;
|
|
182
|
+
markerGroup.frustumCulled = false;
|
|
183
|
+
markerGroup.add(new THREE.Mesh(
|
|
184
|
+
new THREE.BoxGeometry(0.045 * markerScale, 0.028 * markerScale, 0.022 * markerScale),
|
|
185
|
+
new THREE.MeshBasicMaterial({ color, depthTest: false })
|
|
186
|
+
));
|
|
187
|
+
const lens = new THREE.Mesh(
|
|
188
|
+
new THREE.BoxGeometry(0.025 * markerScale, 0.018 * markerScale, 0.014 * markerScale),
|
|
189
|
+
new THREE.MeshBasicMaterial({ color: aimColor, depthTest: false })
|
|
190
|
+
);
|
|
191
|
+
lens.position.set(0, 0, -0.021 * markerScale);
|
|
192
|
+
markerGroup.add(lens);
|
|
193
|
+
if (camera.name) markerGroup.add(createCameraLabel(camera.name, color));
|
|
194
|
+
group.add(markerGroup);
|
|
195
|
+
|
|
196
|
+
const targetMarker = new THREE.Mesh(
|
|
197
|
+
new THREE.SphereGeometry(0.018 * markerScale, 16, 10),
|
|
198
|
+
new THREE.MeshBasicMaterial({ color: aimColor, depthTest: false })
|
|
199
|
+
);
|
|
200
|
+
targetMarker.position.copy(target);
|
|
201
|
+
targetMarker.renderOrder = 999;
|
|
202
|
+
targetMarker.frustumCulled = false;
|
|
203
|
+
group.add(targetMarker);
|
|
204
|
+
|
|
205
|
+
return group;
|
|
206
|
+
}
|
|
207
|
+
|
|
44
208
|
/**
|
|
45
209
|
* Declarative debug visualization component.
|
|
46
210
|
* Renders wireframe geoms, site markers, joint axes, contact forces, COM markers, etc.
|
|
@@ -50,6 +214,7 @@ export function Debug({
|
|
|
50
214
|
showSites = false,
|
|
51
215
|
showJoints = false,
|
|
52
216
|
showCameras = false,
|
|
217
|
+
virtualCameras = [],
|
|
53
218
|
showContacts = false,
|
|
54
219
|
showCOM = false,
|
|
55
220
|
showInertia = false,
|
|
@@ -69,6 +234,7 @@ export function Debug({
|
|
|
69
234
|
const sites: THREE.Object3D[] = [];
|
|
70
235
|
const joints: THREE.Object3D[] = [];
|
|
71
236
|
const cameras: CameraDebugObject[] = [];
|
|
237
|
+
const virtualCameraObjects: THREE.Object3D[] = [];
|
|
72
238
|
const comMarkers: THREE.Object3D[] = [];
|
|
73
239
|
|
|
74
240
|
// Wireframe geoms
|
|
@@ -270,6 +436,10 @@ export function Debug({
|
|
|
270
436
|
}
|
|
271
437
|
}
|
|
272
438
|
|
|
439
|
+
for (let i = 0; i < virtualCameras.length; i += 1) {
|
|
440
|
+
virtualCameraObjects.push(createVirtualCameraDebugObject(virtualCameras[i], i));
|
|
441
|
+
}
|
|
442
|
+
|
|
273
443
|
// COM markers
|
|
274
444
|
if (showCOM) {
|
|
275
445
|
for (let i = 1; i < model.nbody; i++) {
|
|
@@ -281,8 +451,8 @@ export function Debug({
|
|
|
281
451
|
}
|
|
282
452
|
}
|
|
283
453
|
|
|
284
|
-
return { geoms, sites, joints, cameras, comMarkers };
|
|
285
|
-
}, [status, mjModelRef, showGeoms, showSites, showJoints, showCameras, showCOM]);
|
|
454
|
+
return { geoms, sites, joints, cameras, virtualCameraObjects, comMarkers };
|
|
455
|
+
}, [status, mjModelRef, showGeoms, showSites, showJoints, showCameras, virtualCameras, showCOM]);
|
|
286
456
|
|
|
287
457
|
// Add/remove debug objects from scene
|
|
288
458
|
useEffect(() => {
|
|
@@ -294,6 +464,7 @@ export function Debug({
|
|
|
294
464
|
...debugGeometry.sites,
|
|
295
465
|
...debugGeometry.joints,
|
|
296
466
|
...debugGeometry.cameras,
|
|
467
|
+
...debugGeometry.virtualCameraObjects,
|
|
297
468
|
...debugGeometry.comMarkers,
|
|
298
469
|
];
|
|
299
470
|
for (const obj of allObjects) group.add(obj);
|
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,9 +36,17 @@ 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
|
|
|
43
|
+
function clampJoint(value: number, limit: readonly [number, number] | null | undefined) {
|
|
44
|
+
if (!limit) return value;
|
|
45
|
+
const [min, max] = limit;
|
|
46
|
+
if (!Number.isFinite(min) || !Number.isFinite(max) || min >= max) return value;
|
|
47
|
+
return Math.max(min, Math.min(max, value));
|
|
48
|
+
}
|
|
49
|
+
|
|
38
50
|
/**
|
|
39
51
|
* Generic Damped Least-Squares IK solver.
|
|
40
52
|
* Uses finite-difference Jacobian via MuJoCo's mj_forward.
|
|
@@ -81,7 +93,7 @@ export class GenericIK {
|
|
|
81
93
|
|
|
82
94
|
// Working joint angles — start from current configuration
|
|
83
95
|
const q = new Float64Array(n);
|
|
84
|
-
for (let i = 0; i < n; i++) q[i] = currentQ[i];
|
|
96
|
+
for (let i = 0; i < n; i++) q[i] = clampJoint(currentQ[i], o.jointLimits?.[i]);
|
|
85
97
|
|
|
86
98
|
// Pre-allocate work arrays
|
|
87
99
|
const J = new Float64Array(6 * n); // 6×n Jacobian (row-major)
|
|
@@ -196,7 +208,7 @@ export class GenericIK {
|
|
|
196
208
|
}
|
|
197
209
|
|
|
198
210
|
// Update joints
|
|
199
|
-
for (let i = 0; i < n; i++) q[i]
|
|
211
|
+
for (let i = 0; i < n; i++) q[i] = clampJoint(q[i] + dq[i], o.jointLimits?.[i]);
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
// Restore original qpos
|
|
@@ -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
|
|
@@ -97,6 +97,9 @@ export const useIkController = createControllerHook<IkConfig, IkContextValue>(
|
|
|
97
97
|
{
|
|
98
98
|
damping: config.damping,
|
|
99
99
|
epsilon: config.epsilon,
|
|
100
|
+
jointLimits: config.jointLimits ?? controlGroup.joints.map((joint) => (
|
|
101
|
+
joint.limited ? joint.range : joint.ctrlRange
|
|
102
|
+
)),
|
|
100
103
|
maxIterations: config.maxIterations,
|
|
101
104
|
posWeight: config.posWeight,
|
|
102
105
|
rotWeight: config.rotWeight,
|