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.
@@ -13,6 +13,29 @@ import type {
13
13
  CameraFrameCaptureSource,
14
14
  CameraFrameCaptureVector3,
15
15
  } from '../types';
16
+ import {
17
+ pixelsToPolicyImageTensor,
18
+ type PolicyImageTensorOptions,
19
+ type PolicyImageTensorResult,
20
+ } from '../policyImageTensors';
21
+
22
+ /** Options for capturing a camera frame straight into a policy image tensor. */
23
+ export type CameraFrameCaptureTensorOptions = CameraFrameCaptureOptions &
24
+ Pick<PolicyImageTensorOptions, 'channels' | 'layout' | 'range'>;
25
+
26
+ export interface CameraFramePixelsResult {
27
+ /** Raw RGBA pixels, bottom-left origin (reused buffer — consume before the next capture). */
28
+ pixels: Uint8Array;
29
+ camera: THREE.Camera;
30
+ width: number;
31
+ height: number;
32
+ source: CameraFrameCaptureSource;
33
+ }
34
+
35
+ export interface CameraFrameTensorResult extends PolicyImageTensorResult {
36
+ camera: THREE.Camera;
37
+ source: CameraFrameCaptureSource;
38
+ }
16
39
 
17
40
  export interface CameraFrameCaptureSession {
18
41
  readonly width: number;
@@ -36,6 +59,14 @@ export interface CameraFrameCaptureSession {
36
59
  options?: CameraFrameCaptureOptions
37
60
  ): Promise<CameraFrameCaptureResult>;
38
61
  captureBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
62
+ /**
63
+ * Render and read raw RGBA pixels without any canvas/PNG round-trip. The
64
+ * returned buffer is reused between calls — copy or convert it before the
65
+ * next capture.
66
+ */
67
+ capturePixels(options?: CameraFrameCaptureOptions): CameraFramePixelsResult;
68
+ /** Render straight into a normalized policy image tensor (no canvas/PNG encode). */
69
+ captureTensor(options?: CameraFrameCaptureTensorOptions): CameraFrameTensorResult;
39
70
  dispose(): void;
40
71
  }
41
72
 
@@ -250,7 +281,7 @@ function applyProjectionMatrix(
250
281
  camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
251
282
  }
252
283
 
253
- function createCaptureCamera(
284
+ export function createCaptureCamera(
254
285
  options: CameraFrameCaptureOptions,
255
286
  fallbackCamera: THREE.Camera,
256
287
  width: number,
@@ -290,7 +321,7 @@ function getCaptureDimensions(
290
321
  return { width, height };
291
322
  }
292
323
 
293
- function prepareCaptureCamera(
324
+ export function prepareCaptureCamera(
294
325
  camera: THREE.Camera,
295
326
  options: CameraFrameCaptureOptions,
296
327
  fallbackCamera: THREE.Camera,
@@ -646,7 +677,10 @@ export function createCameraFrameCaptureSession(
646
677
  return captureOptions;
647
678
  }
648
679
 
649
- function renderPreparedCapture(captureOptions: CameraFrameCaptureOptions) {
680
+ function renderCaptureToTarget(
681
+ captureOptions: CameraFrameCaptureOptions,
682
+ readback: () => void
683
+ ) {
650
684
  const previousState = saveRendererState(sessionRenderer);
651
685
  const previousSceneState = applyCaptureVisualOverrides(
652
686
  sessionRenderer,
@@ -676,6 +710,16 @@ export function createCameraFrameCaptureSession(
676
710
  }
677
711
  sessionRenderer.clear();
678
712
  sessionRenderer.render(scene, camera);
713
+ readback();
714
+ } finally {
715
+ restoreObjectVisibility(hidden);
716
+ if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
717
+ restoreRendererState(sessionRenderer, previousState);
718
+ }
719
+ }
720
+
721
+ function renderPreparedCapture(captureOptions: CameraFrameCaptureOptions) {
722
+ renderCaptureToTarget(captureOptions, () => {
679
723
  readRenderTargetToCanvas(
680
724
  sessionRenderer,
681
725
  target,
@@ -688,24 +732,50 @@ export function createCameraFrameCaptureSession(
688
732
  sessionRenderer.outputColorSpace,
689
733
  captureOptions.flipX ?? false
690
734
  );
691
- return {
692
- canvas,
693
- camera,
694
- width,
695
- height,
696
- source: getCameraFrameCaptureSource(captureOptions),
697
- };
698
- } finally {
699
- restoreObjectVisibility(hidden);
700
- if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
701
- restoreRendererState(sessionRenderer, previousState);
702
- }
735
+ });
736
+ return {
737
+ canvas,
738
+ camera,
739
+ width,
740
+ height,
741
+ source: getCameraFrameCaptureSource(captureOptions),
742
+ };
703
743
  }
704
744
 
705
745
  function capture(nextOptions: CameraFrameCaptureOptions = {}) {
706
746
  return renderPreparedCapture(resolveCaptureOptions(nextOptions));
707
747
  }
708
748
 
749
+ function capturePixels(nextOptions: CameraFrameCaptureOptions = {}): CameraFramePixelsResult {
750
+ const captureOptions = resolveCaptureOptions(nextOptions);
751
+ renderCaptureToTarget(captureOptions, () => {
752
+ sessionRenderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
753
+ });
754
+ return {
755
+ pixels,
756
+ camera,
757
+ width,
758
+ height,
759
+ source: getCameraFrameCaptureSource(captureOptions),
760
+ };
761
+ }
762
+
763
+ function captureTensor(
764
+ nextOptions: CameraFrameCaptureTensorOptions = {}
765
+ ): CameraFrameTensorResult {
766
+ const result = capturePixels(nextOptions);
767
+ const tensor = pixelsToPolicyImageTensor(pixels, {
768
+ width,
769
+ height,
770
+ channels: nextOptions.channels,
771
+ layout: nextOptions.layout,
772
+ range: nextOptions.range,
773
+ sourceOrigin: 'bottom-left',
774
+ flipX: nextOptions.flipX,
775
+ });
776
+ return { ...tensor, camera, source: result.source };
777
+ }
778
+
709
779
  async function captureAsync(nextOptions: CameraFrameCaptureOptions = {}) {
710
780
  const captureOptions = resolveCaptureOptions(nextOptions);
711
781
  runCapturePreRenderHooks(scene);
@@ -779,6 +849,8 @@ export function createCameraFrameCaptureSession(
779
849
  height,
780
850
  capture,
781
851
  captureAsync,
852
+ capturePixels,
853
+ captureTensor,
782
854
  captureDataUrl(nextOptions = {}) {
783
855
  const type = nextOptions.type ?? options.type ?? 'image/png';
784
856
  const result = capture(nextOptions);
@@ -889,3 +961,28 @@ export async function captureCameraFrameBlob(
889
961
  session.dispose();
890
962
  }
891
963
  }
964
+
965
+ /**
966
+ * One-shot camera frame capture straight into a policy image tensor, skipping
967
+ * the canvas/PNG round-trip. For repeated captures (live inference, recording),
968
+ * create a session once with {@link createCameraFrameCaptureSession} and call
969
+ * `session.captureTensor()` so the render target and buffers are reused.
970
+ */
971
+ export function captureCameraFrameTensor(
972
+ renderer: THREE.WebGLRenderer,
973
+ scene: THREE.Scene,
974
+ fallbackCamera: THREE.Camera,
975
+ options: CameraFrameCaptureTensorOptions = {}
976
+ ): CameraFrameTensorResult {
977
+ const session = createCameraFrameCaptureSession(
978
+ renderer,
979
+ scene,
980
+ fallbackCamera,
981
+ options
982
+ );
983
+ try {
984
+ return session.captureTensor(options);
985
+ } finally {
986
+ session.dispose();
987
+ }
988
+ }
package/src/types.ts CHANGED
@@ -7,6 +7,11 @@ import type React from 'react';
7
7
  import type { ReactNode } from 'react';
8
8
  import type { CanvasProps, ThreeElements } from '@react-three/fiber';
9
9
  import * as THREE from 'three';
10
+ import type {
11
+ CameraFrameCaptureSession,
12
+ CameraFrameCaptureTensorOptions,
13
+ CameraFrameTensorResult,
14
+ } from './rendering/cameraFrameCapture';
10
15
 
11
16
  // ---- Register (type-safe named resources) ----
12
17
 
@@ -459,6 +464,10 @@ export interface SceneObject {
459
464
  solref?: string;
460
465
  solimp?: string;
461
466
  condim?: number;
467
+ /** MuJoCo geom contact type bitmask. Defaults to 1 for generated objects. */
468
+ contype?: number;
469
+ /** MuJoCo geom contact affinity bitmask. Defaults to 1 for generated objects. */
470
+ conaffinity?: number;
462
471
  /** MuJoCo geom group. Group 3 is conventionally used for collision-only helper geoms. */
463
472
  group?: number;
464
473
  }
@@ -527,6 +536,12 @@ export interface IkConfig {
527
536
  * starting at index 0. Prefer inferred IK or `joints`/`actuators`.
528
537
  */
529
538
  numJoints?: number;
539
+ /**
540
+ * Optional solve-space joint limits in the same order as the resolved joints.
541
+ * Use this when MJCF limits are intentionally broad or when a setup/calibration
542
+ * tool should stay within a narrower envelope.
543
+ */
544
+ jointLimits?: ReadonlyArray<readonly [number, number] | null | undefined>;
530
545
  /** Custom IK solver. When omitted, uses built-in Damped Least-Squares solver. */
531
546
  ikSolveFn?: IKSolveFn;
532
547
  /** DLS damping. Default: 0.01. */
@@ -549,7 +564,7 @@ export interface IkContextValue {
549
564
  ikTargetRef: React.RefObject<THREE.Group>;
550
565
  siteIdRef: React.RefObject<number>;
551
566
  setIkEnabled: (enabled: boolean) => void;
552
- moveTarget: (pos: THREE.Vector3, duration?: number) => void;
567
+ moveTarget: (pos: IkTargetPosition, duration?: number) => void;
553
568
  syncTargetToSite: () => void;
554
569
  solveIK: (input: IkSolveInput) => number[] | null;
555
570
  getGizmoStats: () => { pos: THREE.Vector3; rot: THREE.Euler } | null;
@@ -577,9 +592,28 @@ export type IKSolveFn = (
577
592
  input: IkSolveInput
578
593
  ) => number[] | null;
579
594
 
595
+ export type IkTargetPosition =
596
+ | THREE.Vector3
597
+ | readonly [number, number, number]
598
+ | {
599
+ readonly x: number;
600
+ readonly y: number;
601
+ readonly z: number;
602
+ };
603
+
604
+ export type IkTargetQuaternion =
605
+ | THREE.Quaternion
606
+ | readonly [number, number, number, number]
607
+ | {
608
+ readonly x: number;
609
+ readonly y: number;
610
+ readonly z: number;
611
+ readonly w: number;
612
+ };
613
+
580
614
  export interface IkSolveInput {
581
- position: THREE.Vector3;
582
- quaternion: THREE.Quaternion;
615
+ position: IkTargetPosition;
616
+ quaternion: IkTargetQuaternion;
583
617
  currentQ: number[];
584
618
  context?: IKSolveContext;
585
619
  }
@@ -1458,6 +1492,19 @@ export interface MujocoSimAPI {
1458
1492
  captureFrameBlob(options?: MujocoFrameCaptureOptions): Promise<FrameCaptureBlobResult>;
1459
1493
  captureCameraFrame(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureResult>;
1460
1494
  captureCameraFrameBlob(options?: CameraFrameCaptureOptions): Promise<CameraFrameCaptureBlobResult>;
1495
+ /** Capture a camera frame straight into a policy image tensor (no canvas/PNG encode). */
1496
+ captureCameraFrameTensor(options?: CameraFrameCaptureTensorOptions): CameraFrameTensorResult;
1497
+ /**
1498
+ * Create a reusable offscreen capture session bound to this scene. Reuse it
1499
+ * for live inference/recording so the render target and buffers persist
1500
+ * across frames; call `session.captureTensor()` / `capturePixels()` each step.
1501
+ */
1502
+ createCameraFrameCaptureSession(options?: CameraFrameCaptureOptions): CameraFrameCaptureSession;
1503
+ /**
1504
+ * Resolve a named MuJoCo camera/site/body into concrete capture options with
1505
+ * the current world pose. Useful for re-aiming a persistent session each step.
1506
+ */
1507
+ resolveCameraCaptureOptions(options?: CameraFrameCaptureOptions): CameraFrameCaptureOptions;
1461
1508
  recordCameraSequence(options: CameraFrameSequenceOptions): Promise<CameraFrameSequenceResult>;
1462
1509
  project2DTo3D(
1463
1510
  x: number,