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.
- package/README.md +21 -0
- package/dist/{chunk-CYDGWNKQ.js → chunk-6AZEFI6A.js} +169 -22
- package/dist/chunk-6AZEFI6A.js.map +1 -0
- package/dist/index.d.ts +4 -3
- package/dist/index.js +188 -38
- 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-BOhNDICK.d.ts} +99 -1
- package/package.json +1 -1
- 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/rendering/GeomBuilder.ts +18 -2
- package/src/rendering/cameraFrameCapture.ts +229 -19
- package/src/spark.tsx +4 -12
- package/src/types.ts +104 -0
- package/dist/chunk-CYDGWNKQ.js.map +0 -1
|
@@ -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
|
);
|
|
@@ -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(
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
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
|
-
|
|
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
|
-
|
|
675
|
+
sessionRenderer.setClearColor(previousState.clearColor, captureOptions.backgroundAlpha);
|
|
476
676
|
}
|
|
477
|
-
|
|
478
|
-
|
|
677
|
+
sessionRenderer.clear();
|
|
678
|
+
sessionRenderer.render(scene, camera);
|
|
479
679
|
readRenderTargetToCanvas(
|
|
480
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
726
|
+
sessionRenderer.xr.enabled = false;
|
|
521
727
|
if (captureOptions.background !== undefined) {
|
|
522
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|