mujoco-react 10.2.1 → 10.3.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.
@@ -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
- useEffect(() => { configRef.current = config; }, [config]);
466
- useEffect(() => { mujocoRef.current = mujoco; }, [mujoco]);
467
-
468
- // Sync declarative props to refs
469
- useEffect(() => { pausedRef.current = paused ?? false; }, [paused]);
470
- useEffect(() => { speedRef.current = speed ?? 1; }, [speed]);
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
  );
@@ -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
 
@@ -80,6 +80,15 @@ type RendererState = {
80
80
  clearColor: THREE.Color;
81
81
  clearAlpha: number;
82
82
  autoClear: boolean;
83
+ shadowMapEnabled: boolean;
84
+ toneMapping: THREE.WebGLRenderer['toneMapping'];
85
+ outputColorSpace: THREE.WebGLRenderer['outputColorSpace'];
86
+ };
87
+
88
+ type SceneVisualState = {
89
+ background: THREE.Scene['background'];
90
+ environment: THREE.Scene['environment'];
91
+ fog: THREE.Scene['fog'];
83
92
  };
84
93
 
85
94
  type VisibilityState = {
@@ -89,6 +98,107 @@ type VisibilityState = {
89
98
 
90
99
  type CameraFrameCapturePreRender = () => void;
91
100
 
101
+ const isolatedRendererCache = new WeakMap<
102
+ THREE.WebGLRenderer,
103
+ Map<string, THREE.WebGLRenderer>
104
+ >();
105
+
106
+ function shouldUseRenderIsolation(
107
+ options: CameraFrameCaptureOptions
108
+ ): boolean {
109
+ return options.renderIsolation === true || (
110
+ typeof options.renderIsolation === 'object' &&
111
+ options.renderIsolation.enabled !== false
112
+ );
113
+ }
114
+
115
+ function getRenderIsolationOptions(
116
+ options: CameraFrameCaptureOptions
117
+ ) {
118
+ return typeof options.renderIsolation === 'object'
119
+ ? options.renderIsolation
120
+ : {};
121
+ }
122
+
123
+ function getRenderIsolationCacheKey(
124
+ width: number,
125
+ height: number,
126
+ options: CameraFrameCaptureOptions
127
+ ) {
128
+ const isolation = getRenderIsolationOptions(options);
129
+ return JSON.stringify({
130
+ width,
131
+ height,
132
+ antialias: isolation.antialias ?? false,
133
+ alpha: isolation.alpha ?? false,
134
+ preserveDrawingBuffer: isolation.preserveDrawingBuffer ?? false,
135
+ powerPreference: isolation.powerPreference ?? null,
136
+ });
137
+ }
138
+
139
+ function createIsolatedRenderer(
140
+ sourceRenderer: THREE.WebGLRenderer,
141
+ width: number,
142
+ height: number,
143
+ options: CameraFrameCaptureOptions
144
+ ): { renderer: THREE.WebGLRenderer; cached: boolean } | null {
145
+ if (!shouldUseRenderIsolation(options)) return null;
146
+
147
+ const isolation = getRenderIsolationOptions(options);
148
+ if (isolation.cache !== false) {
149
+ const cacheKey = getRenderIsolationCacheKey(width, height, options);
150
+ let rendererCache = isolatedRendererCache.get(sourceRenderer);
151
+ if (!rendererCache) {
152
+ rendererCache = new Map();
153
+ isolatedRendererCache.set(sourceRenderer, rendererCache);
154
+ }
155
+ const cachedRenderer = rendererCache.get(cacheKey);
156
+ if (cachedRenderer) {
157
+ cachedRenderer.outputColorSpace = sourceRenderer.outputColorSpace;
158
+ cachedRenderer.toneMapping = sourceRenderer.toneMapping;
159
+ cachedRenderer.shadowMap.enabled = false;
160
+ return { renderer: cachedRenderer, cached: true };
161
+ }
162
+ const createdRenderer = createUncachedIsolatedRenderer(
163
+ sourceRenderer,
164
+ width,
165
+ height,
166
+ options
167
+ );
168
+ rendererCache.set(cacheKey, createdRenderer);
169
+ return { renderer: createdRenderer, cached: true };
170
+ }
171
+
172
+ return {
173
+ renderer: createUncachedIsolatedRenderer(sourceRenderer, width, height, options),
174
+ cached: false,
175
+ };
176
+ }
177
+
178
+ function createUncachedIsolatedRenderer(
179
+ sourceRenderer: THREE.WebGLRenderer,
180
+ width: number,
181
+ height: number,
182
+ options: CameraFrameCaptureOptions
183
+ ) {
184
+ const isolation = getRenderIsolationOptions(options);
185
+ const canvas = document.createElement('canvas');
186
+ const renderer = new THREE.WebGLRenderer({
187
+ canvas,
188
+ antialias: isolation.antialias ?? false,
189
+ alpha: isolation.alpha ?? false,
190
+ preserveDrawingBuffer: isolation.preserveDrawingBuffer ?? false,
191
+ powerPreference: isolation.powerPreference,
192
+ });
193
+
194
+ renderer.setPixelRatio(1);
195
+ renderer.setSize(width, height, false);
196
+ renderer.outputColorSpace = sourceRenderer.outputColorSpace;
197
+ renderer.toneMapping = sourceRenderer.toneMapping;
198
+ renderer.shadowMap.enabled = false;
199
+ return renderer;
200
+ }
201
+
92
202
  function toVector3(
93
203
  value: CameraFrameCaptureVector3 | undefined,
94
204
  fallback: THREE.Vector3
@@ -127,6 +237,19 @@ function applyCameraPose(
127
237
  camera.updateMatrixWorld();
128
238
  }
129
239
 
240
+ function applyProjectionMatrix(
241
+ camera: THREE.Camera,
242
+ projectionMatrix: CameraFrameCaptureOptions['projectionMatrix'] | undefined
243
+ ) {
244
+ if (!projectionMatrix) return;
245
+ if (projectionMatrix instanceof THREE.Matrix4) {
246
+ camera.projectionMatrix.copy(projectionMatrix);
247
+ } else {
248
+ camera.projectionMatrix.fromArray(projectionMatrix);
249
+ }
250
+ camera.projectionMatrixInverse.copy(camera.projectionMatrix).invert();
251
+ }
252
+
130
253
  function createCaptureCamera(
131
254
  options: CameraFrameCaptureOptions,
132
255
  fallbackCamera: THREE.Camera,
@@ -146,6 +269,7 @@ function createCaptureCamera(
146
269
  camera.far = options.far ?? camera.far;
147
270
  camera.updateProjectionMatrix();
148
271
  }
272
+ applyProjectionMatrix(camera, options.projectionMatrix);
149
273
 
150
274
  applyCameraPose(camera, options, fallbackCamera);
151
275
  return camera;
@@ -184,6 +308,7 @@ function prepareCaptureCamera(
184
308
  camera.far = options.far ?? camera.far;
185
309
  camera.updateProjectionMatrix();
186
310
  }
311
+ applyProjectionMatrix(camera, options.projectionMatrix);
187
312
 
188
313
  applyCameraPose(camera, options, fallbackCamera);
189
314
  }
@@ -362,6 +487,9 @@ function saveRendererState(renderer: THREE.WebGLRenderer): RendererState {
362
487
  clearColor,
363
488
  clearAlpha: renderer.getClearAlpha(),
364
489
  autoClear: renderer.autoClear,
490
+ shadowMapEnabled: renderer.shadowMap.enabled,
491
+ toneMapping: renderer.toneMapping,
492
+ outputColorSpace: renderer.outputColorSpace,
365
493
  };
366
494
  }
367
495
 
@@ -376,6 +504,64 @@ function restoreRendererState(
376
504
  renderer.setScissorTest(state.scissorTest);
377
505
  renderer.setClearColor(state.clearColor, state.clearAlpha);
378
506
  renderer.autoClear = state.autoClear;
507
+ renderer.shadowMap.enabled = state.shadowMapEnabled;
508
+ renderer.toneMapping = state.toneMapping;
509
+ renderer.outputColorSpace = state.outputColorSpace;
510
+ }
511
+
512
+ function saveSceneVisualState(scene: THREE.Scene): SceneVisualState {
513
+ return {
514
+ background: scene.background,
515
+ environment: scene.environment,
516
+ fog: scene.fog,
517
+ };
518
+ }
519
+
520
+ function restoreSceneVisualState(scene: THREE.Scene, state: SceneVisualState) {
521
+ scene.background = state.background;
522
+ scene.environment = state.environment;
523
+ scene.fog = state.fog;
524
+ }
525
+
526
+ function hasOwn<T extends object>(object: T, key: keyof T) {
527
+ return Object.prototype.hasOwnProperty.call(object, key);
528
+ }
529
+
530
+ function applyCaptureVisualOverrides(
531
+ renderer: THREE.WebGLRenderer,
532
+ scene: THREE.Scene,
533
+ options: CameraFrameCaptureOptions
534
+ ): SceneVisualState | null {
535
+ const overrides = options.visualOverrides;
536
+ if (!overrides) return null;
537
+
538
+ const previousSceneState = saveSceneVisualState(scene);
539
+ if (hasOwn(overrides, 'sceneBackground')) {
540
+ const background = overrides.sceneBackground;
541
+ scene.background = background === false ? null : (
542
+ typeof background === 'string' || typeof background === 'number'
543
+ ? new THREE.Color(background)
544
+ : background ?? null
545
+ );
546
+ }
547
+ if (hasOwn(overrides, 'sceneEnvironment')) {
548
+ const environment = overrides.sceneEnvironment;
549
+ scene.environment = environment === false ? null : environment ?? null;
550
+ }
551
+ if (hasOwn(overrides, 'sceneFog')) {
552
+ const fog = overrides.sceneFog;
553
+ scene.fog = fog === false ? null : fog ?? null;
554
+ }
555
+ if (overrides.shadows !== undefined) {
556
+ renderer.shadowMap.enabled = overrides.shadows;
557
+ }
558
+ if (overrides.toneMapping !== undefined) {
559
+ renderer.toneMapping = overrides.toneMapping;
560
+ }
561
+ if (overrides.outputColorSpace !== undefined) {
562
+ renderer.outputColorSpace = overrides.outputColorSpace;
563
+ }
564
+ return previousSceneState;
379
565
  }
380
566
 
381
567
  function getCaptureRenderer(
@@ -410,6 +596,8 @@ export function createCameraFrameCaptureSession(
410
596
  options: CameraFrameCaptureOptions = {}
411
597
  ): CameraFrameCaptureSession {
412
598
  const { width, height } = getCaptureDimensions(renderer, options);
599
+ const isolatedRenderer = createIsolatedRenderer(renderer, width, height, options);
600
+ const sessionRenderer = isolatedRenderer?.renderer ?? renderer;
413
601
  const camera = createCaptureCamera(options, fallbackCamera, width, height);
414
602
  const target = new THREE.WebGLRenderTarget(width, height, {
415
603
  format: THREE.RGBAFormat,
@@ -430,6 +618,13 @@ export function createCameraFrameCaptureSession(
430
618
 
431
619
  function resolveCaptureOptions(nextOptions: CameraFrameCaptureOptions = {}) {
432
620
  const captureOptions = { ...options, ...nextOptions };
621
+ if (
622
+ shouldUseRenderIsolation(captureOptions) !== shouldUseRenderIsolation(options)
623
+ ) {
624
+ throw new Error(
625
+ 'Camera frame capture sessions require stable renderIsolation settings.'
626
+ );
627
+ }
433
628
  const nextDimensions = getCaptureDimensions(renderer, captureOptions);
434
629
  if (
435
630
  nextDimensions.width !== width ||
@@ -452,7 +647,12 @@ export function createCameraFrameCaptureSession(
452
647
  }
453
648
 
454
649
  function renderPreparedCapture(captureOptions: CameraFrameCaptureOptions) {
455
- const previousState = saveRendererState(renderer);
650
+ const previousState = saveRendererState(sessionRenderer);
651
+ const previousSceneState = applyCaptureVisualOverrides(
652
+ sessionRenderer,
653
+ scene,
654
+ captureOptions
655
+ );
456
656
  const hidden = [
457
657
  ...hideExcludedCaptureObjects(scene),
458
658
  ...hideCaptureGeomGroups(scene, captureOptions),
@@ -461,23 +661,23 @@ export function createCameraFrameCaptureSession(
461
661
  runCapturePreRenderHooks(scene);
462
662
  scene.updateMatrixWorld(true);
463
663
  try {
464
- renderer.xr.enabled = false;
465
- renderer.setRenderTarget(target);
466
- renderer.setViewport(0, 0, width, height);
467
- renderer.setScissor(0, 0, width, height);
468
- renderer.setScissorTest(false);
664
+ sessionRenderer.xr.enabled = false;
665
+ sessionRenderer.setRenderTarget(target);
666
+ sessionRenderer.setViewport(0, 0, width, height);
667
+ sessionRenderer.setScissor(0, 0, width, height);
668
+ sessionRenderer.setScissorTest(false);
469
669
  if (captureOptions.background !== undefined) {
470
- renderer.setClearColor(
670
+ sessionRenderer.setClearColor(
471
671
  new THREE.Color(captureOptions.background),
472
672
  captureOptions.backgroundAlpha ?? previousState.clearAlpha
473
673
  );
474
674
  } else if (captureOptions.backgroundAlpha !== undefined) {
475
- renderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
675
+ sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
476
676
  }
477
- renderer.clear();
478
- renderer.render(scene, camera);
677
+ sessionRenderer.clear();
678
+ sessionRenderer.render(scene, camera);
479
679
  readRenderTargetToCanvas(
480
- renderer,
680
+ sessionRenderer,
481
681
  target,
482
682
  canvas,
483
683
  drawContext,
@@ -485,7 +685,7 @@ export function createCameraFrameCaptureSession(
485
685
  imageData,
486
686
  width,
487
687
  height,
488
- renderer.outputColorSpace,
688
+ sessionRenderer.outputColorSpace,
489
689
  captureOptions.flipX ?? false
490
690
  );
491
691
  return {
@@ -497,7 +697,8 @@ export function createCameraFrameCaptureSession(
497
697
  };
498
698
  } finally {
499
699
  restoreObjectVisibility(hidden);
500
- restoreRendererState(renderer, previousState);
700
+ if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
701
+ restoreRendererState(sessionRenderer, previousState);
501
702
  }
502
703
  }
503
704
 
@@ -511,23 +712,28 @@ export function createCameraFrameCaptureSession(
511
712
  scene.updateMatrixWorld(true);
512
713
  const captureRenderer = getCaptureRenderer(scene);
513
714
  if (captureRenderer) {
514
- const previousState = saveRendererState(renderer);
715
+ const previousState = saveRendererState(sessionRenderer);
716
+ const previousSceneState = applyCaptureVisualOverrides(
717
+ sessionRenderer,
718
+ scene,
719
+ captureOptions
720
+ );
515
721
  const hidden = [
516
722
  ...hideExcludedCaptureObjects(scene),
517
723
  ...hideCaptureGeomGroups(scene, captureOptions),
518
724
  ];
519
725
  try {
520
- renderer.xr.enabled = false;
726
+ sessionRenderer.xr.enabled = false;
521
727
  if (captureOptions.background !== undefined) {
522
- renderer.setClearColor(
728
+ sessionRenderer.setClearColor(
523
729
  new THREE.Color(captureOptions.background),
524
730
  captureOptions.backgroundAlpha ?? previousState.clearAlpha
525
731
  );
526
732
  } else if (captureOptions.backgroundAlpha !== undefined) {
527
- renderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
733
+ sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
528
734
  }
529
735
  const captureResult = await captureRenderer({
530
- renderer,
736
+ renderer: sessionRenderer,
531
737
  scene,
532
738
  camera,
533
739
  target,
@@ -561,7 +767,8 @@ export function createCameraFrameCaptureSession(
561
767
  }
562
768
  } finally {
563
769
  restoreObjectVisibility(hidden);
564
- restoreRendererState(renderer, previousState);
770
+ if (previousSceneState) restoreSceneVisualState(scene, previousSceneState);
771
+ restoreRendererState(sessionRenderer, previousState);
565
772
  }
566
773
  }
567
774
  return renderPreparedCapture(captureOptions);
@@ -613,6 +820,9 @@ export function createCameraFrameCaptureSession(
613
820
  },
614
821
  dispose() {
615
822
  target.dispose();
823
+ if (isolatedRenderer && !isolatedRenderer.cached) {
824
+ isolatedRenderer.renderer.dispose();
825
+ }
616
826
  },
617
827
  };
618
828
  }