@gnsx/three 0.184.8 → 0.184.10

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.
@@ -18,6 +18,26 @@ import {
18
18
  WebGLCubeRenderTarget,
19
19
  WebGLRenderTarget
20
20
  } from 'three';
21
+ // WITH_GENESYS
22
+ import { CubeRenderTarget, Storage3DTexture } from 'three/webgpu';
23
+ import {
24
+ Fn,
25
+ If,
26
+ Loop,
27
+ PI,
28
+ convert,
29
+ cubeTexture,
30
+ float,
31
+ int,
32
+ select,
33
+ storageTexture,
34
+ textureStore,
35
+ uniform,
36
+ uvec3,
37
+ vec3,
38
+ vec4
39
+ } from 'three/tsl';
40
+ // !WITH_GENESYS
21
41
 
22
42
  // Shared fullscreen-quad scene / camera
23
43
  let _scene = null;
@@ -38,6 +58,21 @@ let _cachedCubemapSize = 0;
38
58
  let _cachedNear = 0;
39
59
  let _cachedFar = 0;
40
60
 
61
+ // WITH_GENESYS
62
+ // Cached WebGPU bake resources. The WebGPU path projects cubemaps on the GPU
63
+ // via TSL compute and writes directly into a Storage3DTexture atlas.
64
+ let _webgpuCubeRenderTarget = null;
65
+ let _webgpuCubeCamera = null;
66
+ let _webgpuCachedCubemapSize = 0;
67
+ let _webgpuCachedNear = 0;
68
+ let _webgpuCachedFar = 0;
69
+ let _webgpuComputeNode = null;
70
+ let _webgpuComputeCubemapSize = 0;
71
+ let _webgpuComputeAtlas = null;
72
+ let _webgpuComputeEnvMap = null;
73
+ let _webgpuProbeUniforms = null;
74
+ // !WITH_GENESYS
75
+
41
76
  // Cached batch render target
42
77
  let _batchTarget = null;
43
78
  let _batchTargetProbes = 0;
@@ -161,6 +196,33 @@ class LightProbeGrid extends Object3D {
161
196
  */
162
197
  this._renderTarget = null;
163
198
 
199
+ /**
200
+ * Array of cube textures captured during baking, only populated
201
+ * when `retainCubemaps` option is true in {@link LightProbeGrid#bake}.
202
+ *
203
+ * @type {?CubeTexture[]}
204
+ * @default null
205
+ */
206
+ this.cubeTextures = null;
207
+
208
+ /**
209
+ * Render targets backing retained cube textures.
210
+ *
211
+ * @private
212
+ * @type {?Array<WebGLCubeRenderTarget|CubeRenderTarget>}
213
+ * @default null
214
+ */
215
+ this._cubeRenderTargets = null;
216
+
217
+ /**
218
+ * Whether retained cube render targets are WebGPU render targets.
219
+ *
220
+ * @private
221
+ * @type {?boolean}
222
+ * @default null
223
+ */
224
+ this._cubeRenderTargetsWebGPU = null;
225
+
164
226
  this.updateBoundingBox();
165
227
 
166
228
  }
@@ -210,134 +272,395 @@ class LightProbeGrid extends Object3D {
210
272
  * @param {number} [options.cubemapSize=8] - Resolution of each cubemap face.
211
273
  * @param {number} [options.near=0.1] - Near plane for the cube camera.
212
274
  * @param {number} [options.far=100] - Far plane for the cube camera.
275
+ * @param {boolean} [options.retainCubemaps=false] - Whether to retain the captured cube maps for visualization.
276
+ * @return {Promise<void>} Resolves when baking has completed.
213
277
  */
214
- bake( renderer, scene, options = {} ) {
278
+ async bake( renderer, scene, options = {} ) {
215
279
 
216
- const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( options );
217
-
218
- this._ensureTextures();
219
- this.updateBoundingBox();
280
+ const wasVisible = this.visible; // WITH_GENESYS
281
+ const retainCubemaps = options.retainCubemaps === true;
220
282
 
221
283
  // Prevent feedback: temporarily hide the volume during baking
222
284
  this.visible = false;
223
285
 
224
- const res = this.resolution;
225
- const totalProbes = res.x * res.y * res.z;
286
+ if ( ! retainCubemaps ) this._disposeRetainedCubemaps();
226
287
 
227
- // Batch render target for SH coefficients: 9 pixels wide, one row per probe
228
- const batchTarget = _ensureBatchTarget( totalProbes );
288
+ // WITH_GENESYS
289
+ if ( renderer.isWebGPURenderer === true ) {
229
290
 
230
- // Save renderer state
231
- const savedRenderTarget = renderer.getRenderTarget();
232
- renderer.getViewport( _savedViewport );
233
- renderer.getScissor( _savedScissor );
234
- const savedScissorTest = renderer.getScissorTest();
291
+ await this._bakeWebGPU( renderer, scene, options );
235
292
 
236
- // Clear pooled batch target so skipped probes read as zero
237
- batchTarget.scissorTest = false;
238
- batchTarget.viewport.set( 0, 0, 9, totalProbes );
239
- renderer.setRenderTarget( batchTarget );
240
- renderer.clear();
293
+ } else { // !WITH_GENESYS
241
294
 
242
- // const t0 = performance.now();
295
+ const { cubeRenderTarget, cubeCamera } = _ensureBakeResources( options );
243
296
 
244
- // Phase 1: Render cubemaps and project to SH into batch target
245
- // Note: set viewport/scissor on the render target directly to avoid pixel ratio scaling
246
- batchTarget.scissorTest = true;
297
+ this._ensureTextures();
298
+ this.updateBoundingBox();
247
299
 
248
- // Disable shadow map auto-update during bake — lights don't move between probes.
249
- // Force one shadow update on the first render so maps are initialized.
250
- const savedShadowAutoUpdate = renderer.shadowMap.autoUpdate;
251
- renderer.shadowMap.autoUpdate = false;
252
- renderer.shadowMap.needsUpdate = true;
300
+ const res = this.resolution;
301
+ const totalProbes = res.x * res.y * res.z;
253
302
 
254
- for ( let iz = 0; iz < res.z; iz ++ ) {
303
+ // Batch render target for SH coefficients: 9 pixels wide, one row per probe
304
+ const batchTarget = _ensureBatchTarget( totalProbes );
255
305
 
256
- for ( let iy = 0; iy < res.y; iy ++ ) {
306
+ // Save renderer state
307
+ const savedRenderTarget = renderer.getRenderTarget();
308
+ renderer.getViewport( _savedViewport );
309
+ renderer.getScissor( _savedScissor );
310
+ const savedScissorTest = renderer.getScissorTest();
257
311
 
258
- for ( let ix = 0; ix < res.x; ix ++ ) {
312
+ // Clear pooled batch target so skipped probes read as zero
313
+ batchTarget.scissorTest = false;
314
+ batchTarget.viewport.set( 0, 0, 9, totalProbes );
315
+ renderer.setRenderTarget( batchTarget );
316
+ renderer.clear();
259
317
 
260
- const probeIndex = ix + iy * res.x + iz * res.x * res.y;
318
+ // const t0 = performance.now();
261
319
 
262
- this.getProbePosition( ix, iy, iz, _position );
263
- cubeCamera.position.copy( _position );
264
- cubeCamera.update( renderer, scene );
320
+ // Phase 1: Render cubemaps and project to SH into batch target
321
+ // Note: set viewport/scissor on the render target directly to avoid pixel ratio scaling
322
+ batchTarget.scissorTest = true;
265
323
 
266
- // SH projection
267
- _shMaterial.uniforms.envMap.value = cubeRenderTarget.texture;
268
- _mesh.material = _shMaterial;
269
- batchTarget.viewport.set( 0, probeIndex, 9, 1 );
270
- batchTarget.scissor.set( 0, probeIndex, 9, 1 );
271
- renderer.setRenderTarget( batchTarget );
272
- renderer.render( _scene, _camera );
324
+ // Disable shadow map auto-update during bake — lights don't move between probes.
325
+ // Force one shadow update on the first render so maps are initialized.
326
+ const savedShadowAutoUpdate = renderer.shadowMap.autoUpdate;
327
+ renderer.shadowMap.autoUpdate = false;
328
+ renderer.shadowMap.needsUpdate = true;
329
+
330
+ // Initialize cube texture/render target arrays if retaining
331
+ if ( retainCubemaps ) {
332
+
333
+ if ( this.cubeTextures === null || this.cubeTextures.length !== totalProbes ) {
334
+
335
+ this.cubeTextures = new Array( totalProbes );
336
+
337
+ }
338
+
339
+ if ( this._cubeRenderTargets === null || this._cubeRenderTargets.length !== totalProbes ) {
340
+
341
+ this._disposeRetainedCubemaps();
342
+ this.cubeTextures = new Array( totalProbes );
343
+
344
+ this._cubeRenderTargets = new Array( totalProbes );
273
345
 
274
346
  }
275
347
 
276
348
  }
277
349
 
278
- }
350
+ for ( let iz = 0; iz < res.z; iz ++ ) {
279
351
 
280
- renderer.shadowMap.autoUpdate = savedShadowAutoUpdate;
352
+ for ( let iy = 0; iy < res.y; iy ++ ) {
281
353
 
282
- // Phase 2: Repack SH data from batch target into the atlas 3D texture (GPU-to-GPU).
283
- //
284
- // For each of the 7 packed sub-volumes (texture index t) we write:
285
- // - A leading padding slice (copy of data slice iz = 0)
286
- // - All nz data slices (iz = 0 … nz-1)
287
- // - A trailing padding slice (copy of data slice iz = nz-1)
288
- //
289
- // In the atlas the slices for sub-volume t occupy the range:
290
- // [ t * paddedSlices, t * paddedSlices + paddedSlices - 1 ]
291
- // where paddedSlices = nz + 2 * ATLAS_PADDING.
354
+ for ( let ix = 0; ix < res.x; ix ++ ) {
292
355
 
293
- _ensureRepackResources();
356
+ const probeIndex = ix + iy * res.x + iz * res.x * res.y;
294
357
 
295
- const paddedSlices = res.z + 2 * ATLAS_PADDING;
296
- const rt = this._renderTarget;
297
- rt.scissorTest = false;
298
- rt.viewport.set( 0, 0, res.x, res.y );
358
+ this.getProbePosition( ix, iy, iz, _position );
299
359
 
300
- for ( let t = 0; t < 7; t ++ ) {
360
+ let envMapTexture = cubeRenderTarget.texture;
301
361
 
302
- _repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture;
303
- _repackMaterials[ t ].uniforms.resolution.value.copy( res );
362
+ if ( retainCubemaps ) {
304
363
 
305
- // Write data slices
306
- for ( let iz = 0; iz < res.z; iz ++ ) {
364
+ const retainedTarget = this._ensureRetainedCubeRenderTarget( probeIndex, options );
365
+ const retainedCamera = new CubeCamera(
366
+ options.near !== undefined ? options.near : 0.1,
367
+ options.far !== undefined ? options.far : 100,
368
+ retainedTarget
369
+ );
370
+
371
+ retainedCamera.position.copy( _position );
372
+ retainedCamera.update( renderer, scene );
373
+
374
+ envMapTexture = retainedTarget.texture;
375
+ this.cubeTextures[ probeIndex ] = retainedTarget.texture;
376
+
377
+ } else {
378
+
379
+ cubeCamera.position.copy( _position );
380
+ cubeCamera.update( renderer, scene );
381
+
382
+ }
383
+
384
+ // SH projection
385
+ _shMaterial.uniforms.envMap.value = envMapTexture;
386
+ _mesh.material = _shMaterial;
387
+ batchTarget.viewport.set( 0, probeIndex, 9, 1 );
388
+ batchTarget.scissor.set( 0, probeIndex, 9, 1 );
389
+ renderer.setRenderTarget( batchTarget );
390
+ renderer.render( _scene, _camera );
391
+
392
+ }
393
+
394
+ }
395
+
396
+ }
397
+
398
+ renderer.shadowMap.autoUpdate = savedShadowAutoUpdate;
399
+
400
+ // Phase 2: Repack SH data from batch target into the atlas 3D texture (GPU-to-GPU).
401
+ //
402
+ // For each of the 7 packed sub-volumes (texture index t) we write:
403
+ // - A leading padding slice (copy of data slice iz = 0)
404
+ // - All nz data slices (iz = 0 … nz-1)
405
+ // - A trailing padding slice (copy of data slice iz = nz-1)
406
+ //
407
+ // In the atlas the slices for sub-volume t occupy the range:
408
+ // [ t * paddedSlices, t * paddedSlices + paddedSlices - 1 ]
409
+ // where paddedSlices = nz + 2 * ATLAS_PADDING.
410
+
411
+ _ensureRepackResources();
412
+
413
+ const paddedSlices = res.z + 2 * ATLAS_PADDING;
414
+ const rt = this._renderTarget;
415
+ rt.scissorTest = false;
416
+ rt.viewport.set( 0, 0, res.x, res.y );
417
+
418
+ for ( let t = 0; t < 7; t ++ ) {
419
+
420
+ _repackMaterials[ t ].uniforms.batchTexture.value = batchTarget.texture;
421
+ _repackMaterials[ t ].uniforms.resolution.value.copy( res );
422
+
423
+ // Write data slices
424
+ for ( let iz = 0; iz < res.z; iz ++ ) {
425
+
426
+ _repackMaterials[ t ].uniforms.sliceZ.value = iz;
427
+ _mesh.material = _repackMaterials[ t ];
428
+ renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + iz );
429
+ renderer.render( _scene, _camera );
430
+
431
+ }
432
+
433
+ // Leading padding: copy of data slice iz = 0
434
+ _repackMaterials[ t ].uniforms.sliceZ.value = 0;
435
+ _mesh.material = _repackMaterials[ t ];
436
+ renderer.setRenderTarget( rt, t * paddedSlices );
437
+ renderer.render( _scene, _camera );
307
438
 
308
- _repackMaterials[ t ].uniforms.sliceZ.value = iz;
439
+ // Trailing padding: copy of data slice iz = nz - 1
440
+ _repackMaterials[ t ].uniforms.sliceZ.value = res.z - 1;
309
441
  _mesh.material = _repackMaterials[ t ];
310
- renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + iz );
442
+ renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + res.z );
311
443
  renderer.render( _scene, _camera );
312
444
 
313
445
  }
314
446
 
315
- // Leading padding: copy of data slice iz = 0
316
- _repackMaterials[ t ].uniforms.sliceZ.value = 0;
317
- _mesh.material = _repackMaterials[ t ];
318
- renderer.setRenderTarget( rt, t * paddedSlices );
319
- renderer.render( _scene, _camera );
447
+ // Restore renderer state
448
+ renderer.setRenderTarget( savedRenderTarget );
449
+ renderer.setViewport( _savedViewport );
450
+ renderer.setScissor( _savedScissor );
451
+ renderer.setScissorTest( savedScissorTest );
320
452
 
321
- // Trailing padding: copy of data slice iz = nz - 1
322
- _repackMaterials[ t ].uniforms.sliceZ.value = res.z - 1;
323
- _mesh.material = _repackMaterials[ t ];
324
- renderer.setRenderTarget( rt, t * paddedSlices + ATLAS_PADDING + res.z );
325
- renderer.render( _scene, _camera );
453
+ // console.log( `LightProbeGrid: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` );
326
454
 
327
455
  }
328
456
 
329
- // Restore renderer state
330
- renderer.setRenderTarget( savedRenderTarget );
331
- renderer.setViewport( _savedViewport );
332
- renderer.setScissor( _savedScissor );
333
- renderer.setScissorTest( savedScissorTest );
457
+ this.visible = wasVisible; // WITH_GENESYS
458
+ // this.visible = true;
334
459
 
335
- // console.log( `LightProbeGrid: bake complete ${ ( performance.now() - t0 ).toFixed( 1 ) }ms` );
460
+ }
461
+
462
+ // WITH_GENESYS
463
+ /**
464
+ * Ensures a retained cube render target exists for a probe.
465
+ *
466
+ * @private
467
+ * @param {number} probeIndex - The probe index.
468
+ * @param {Object} options - Bake options.
469
+ * @param {boolean} [webgpu=false] - Whether to create a WebGPU cube render target.
470
+ * @return {WebGLCubeRenderTarget|CubeRenderTarget} The retained cube render target.
471
+ */
472
+ _ensureRetainedCubeRenderTarget( probeIndex, options, webgpu = false ) {
473
+
474
+ const cubemapSize = options.cubemapSize !== undefined ? options.cubemapSize : 8;
475
+
476
+ if ( this._cubeRenderTargetsWebGPU !== null && this._cubeRenderTargetsWebGPU !== webgpu ) {
477
+
478
+ this._disposeRetainedCubemaps();
479
+
480
+ }
481
+
482
+ if ( this._cubeRenderTargets === null ) {
483
+
484
+ this._cubeRenderTargets = [];
485
+
486
+ }
487
+
488
+ this._cubeRenderTargetsWebGPU = webgpu;
489
+
490
+ let target = this._cubeRenderTargets[ probeIndex ];
336
491
 
337
- this.visible = true;
492
+ if ( target === undefined || target.width !== cubemapSize || target.height !== cubemapSize ) {
493
+
494
+ if ( target !== undefined ) target.dispose();
495
+
496
+ target = webgpu === true
497
+ ? new CubeRenderTarget( cubemapSize, { type: HalfFloatType } )
498
+ : new WebGLCubeRenderTarget( cubemapSize, { type: HalfFloatType } );
499
+ this._cubeRenderTargets[ probeIndex ] = target;
500
+
501
+ }
502
+
503
+ return target;
338
504
 
339
505
  }
340
506
 
507
+ /**
508
+ * Frees retained cube map render targets.
509
+ *
510
+ * @private
511
+ */
512
+ _disposeRetainedCubemaps() {
513
+
514
+ if ( this._cubeRenderTargets !== null ) {
515
+
516
+ for ( const target of this._cubeRenderTargets ) {
517
+
518
+ if ( target !== undefined ) target.dispose();
519
+
520
+ }
521
+
522
+ this._cubeRenderTargets = null;
523
+
524
+ }
525
+
526
+ this._cubeRenderTargetsWebGPU = null;
527
+ this.cubeTextures = null;
528
+
529
+ }
530
+
531
+ /**
532
+ * Frees retained cube maps used for cube map visualization.
533
+ */
534
+ disposeRetainedCubemaps() {
535
+
536
+ this._disposeRetainedCubemaps();
537
+
538
+ }
539
+
540
+ /**
541
+ * Bakes all probes with a WebGPU renderer.
542
+ *
543
+ * @private
544
+ * @async
545
+ * @param {WebGPURenderer} renderer - The renderer.
546
+ * @param {Scene} scene - The scene to render.
547
+ * @param {Object} [options] - Bake options.
548
+ * @return {Promise<void>} Resolves when baking has completed.
549
+ */
550
+ async _bakeWebGPU( renderer, scene, options = {} ) {
551
+
552
+ const cubemapSize = options.cubemapSize !== undefined ? options.cubemapSize : 8;
553
+ const retainCubemaps = options.retainCubemaps === true;
554
+ const { cubeRenderTarget, cubeCamera } = _ensureWebGPUBakeResources( options );
555
+
556
+ this._ensureWebGPUTexture();
557
+ this.updateBoundingBox();
558
+
559
+ const computeNode = _ensureWebGPUComputeResources( cubemapSize, cubeRenderTarget.texture, this.texture );
560
+ const uniforms = _webgpuProbeUniforms;
561
+
562
+ const res = this.resolution;
563
+ const nx = res.x, ny = res.y, nz = res.z;
564
+ const totalProbes = nx * ny * nz;
565
+ const paddedSlices = nz + 2 * ATLAS_PADDING;
566
+
567
+ uniforms.paddedSlices.value = paddedSlices;
568
+
569
+ const savedShadowAutoUpdate = renderer.shadowMap.autoUpdate;
570
+ renderer.shadowMap.autoUpdate = false;
571
+ renderer.shadowMap.needsUpdate = true;
572
+
573
+ // Initialize cube textures array if retaining
574
+ if ( retainCubemaps ) {
575
+
576
+ if ( this.cubeTextures === null || this.cubeTextures.length !== totalProbes ) {
577
+
578
+ this.cubeTextures = new Array( totalProbes );
579
+
580
+ }
581
+
582
+ if ( this._cubeRenderTargets === null || this._cubeRenderTargets.length !== totalProbes ) {
583
+
584
+ this._disposeRetainedCubemaps();
585
+ this.cubeTextures = new Array( totalProbes );
586
+ this._cubeRenderTargets = new Array( totalProbes );
587
+
588
+ }
589
+
590
+ }
591
+
592
+ for ( let iz = 0; iz < nz; iz ++ ) {
593
+
594
+ for ( let iy = 0; iy < ny; iy ++ ) {
595
+
596
+ for ( let ix = 0; ix < nx; ix ++ ) {
597
+
598
+ const probeIndex = ix + iy * nx + iz * nx * ny;
599
+
600
+ this.getProbePosition( ix, iy, iz, _position );
601
+ cubeCamera.position.copy( _position );
602
+ cubeCamera.update( renderer, scene );
603
+
604
+ if ( retainCubemaps ) {
605
+
606
+ const retainedTarget = this._ensureRetainedCubeRenderTarget( probeIndex, options, true );
607
+ const retainedCamera = new CubeCamera(
608
+ options.near !== undefined ? options.near : 0.1,
609
+ options.far !== undefined ? options.far : 100,
610
+ retainedTarget
611
+ );
612
+
613
+ retainedCamera.position.copy( _position );
614
+ retainedCamera.update( renderer, scene );
615
+
616
+ this.cubeTextures[ probeIndex ] = retainedTarget.texture;
617
+
618
+ }
619
+
620
+ uniforms.probeIx.value = ix;
621
+ uniforms.probeIy.value = iy;
622
+ uniforms.probeIz.value = iz;
623
+
624
+ renderer.compute( computeNode );
625
+
626
+ }
627
+
628
+ }
629
+
630
+ }
631
+
632
+ renderer.shadowMap.autoUpdate = savedShadowAutoUpdate;
633
+
634
+ // Padding slices (GPU-to-GPU copy of nearest edge data slices).
635
+ const atlas = this.texture;
636
+
637
+ for ( let textureIndex = 0; textureIndex < 7; textureIndex ++ ) {
638
+
639
+ const baseSlice = textureIndex * paddedSlices;
640
+ const dataSlice = baseSlice + ATLAS_PADDING;
641
+ const trailingSlice = dataSlice + nz;
642
+
643
+ renderer.copyTextureToTexture(
644
+ atlas,
645
+ atlas,
646
+ new Box3( new Vector3( 0, 0, dataSlice ), new Vector3( nx, ny, dataSlice + 1 ) ),
647
+ new Vector3( 0, 0, baseSlice )
648
+ );
649
+
650
+ renderer.copyTextureToTexture(
651
+ atlas,
652
+ atlas,
653
+ new Box3( new Vector3( 0, 0, dataSlice + nz - 1 ), new Vector3( nx, ny, dataSlice + nz ) ),
654
+ new Vector3( 0, 0, trailingSlice )
655
+ );
656
+
657
+ }
658
+
659
+ await renderer.backend.device.queue.onSubmittedWorkDone();
660
+
661
+ }
662
+ // !WITH_GENESYS
663
+
341
664
  /**
342
665
  * Ensures the atlas 3D render target exists with the correct dimensions.
343
666
  *
@@ -347,6 +670,15 @@ class LightProbeGrid extends Object3D {
347
670
 
348
671
  if ( this._renderTarget !== null ) return;
349
672
 
673
+ // WITH_GENESYS
674
+ if ( this.texture !== null ) {
675
+
676
+ this.texture.dispose();
677
+ this.texture = null;
678
+
679
+ }
680
+ // !WITH_GENESYS
681
+
350
682
  const res = this.resolution;
351
683
  const nx = res.x, ny = res.y, nz = res.z;
352
684
 
@@ -367,6 +699,54 @@ class LightProbeGrid extends Object3D {
367
699
 
368
700
  }
369
701
 
702
+ // WITH_GENESYS
703
+ /**
704
+ * Ensures the atlas Storage3DTexture exists with the correct dimensions for WebGPU.
705
+ *
706
+ * @private
707
+ */
708
+ _ensureWebGPUTexture() {
709
+
710
+ if ( this._renderTarget !== null ) {
711
+
712
+ this._renderTarget.dispose();
713
+ this._renderTarget = null;
714
+ this.texture = null;
715
+
716
+ }
717
+
718
+ const res = this.resolution;
719
+ const nx = res.x, ny = res.y, nz = res.z;
720
+ const atlasDepth = 7 * ( nz + 2 * ATLAS_PADDING );
721
+
722
+ if ( this.texture !== null ) {
723
+
724
+ const image = this.texture.image;
725
+
726
+ if ( image.width === nx && image.height === ny && image.depth === atlasDepth ) {
727
+
728
+ return;
729
+
730
+ }
731
+
732
+ this.texture.dispose();
733
+ this.texture = null;
734
+
735
+ }
736
+
737
+ const texture = new Storage3DTexture( nx, ny, atlasDepth );
738
+
739
+ texture.format = RGBAFormat;
740
+ texture.type = FloatType;
741
+ texture.minFilter = LinearFilter;
742
+ texture.magFilter = LinearFilter;
743
+ texture.generateMipmaps = false;
744
+
745
+ this.texture = texture;
746
+
747
+ }
748
+ // !WITH_GENESYS
749
+
370
750
  /**
371
751
  * Frees GPU resources.
372
752
  */
@@ -378,7 +758,14 @@ class LightProbeGrid extends Object3D {
378
758
  this._renderTarget = null;
379
759
  this.texture = null;
380
760
 
381
- }
761
+ } else if ( this.texture !== null ) { // WITH_GENESYS
762
+
763
+ this.texture.dispose();
764
+ this.texture = null;
765
+
766
+ } // !WITH_GENESYS
767
+
768
+ this._disposeRetainedCubemaps();
382
769
 
383
770
  }
384
771
 
@@ -627,6 +1014,163 @@ function _ensureBakeResources( options ) {
627
1014
 
628
1015
  }
629
1016
 
1017
+ // WITH_GENESYS
1018
+ function _ensureWebGPUBakeResources( options ) {
1019
+
1020
+ const {
1021
+ cubemapSize = 8,
1022
+ near = 0.1,
1023
+ far = 100
1024
+ } = options;
1025
+
1026
+ if ( _webgpuCubeRenderTarget === null || cubemapSize !== _webgpuCachedCubemapSize || near !== _webgpuCachedNear || far !== _webgpuCachedFar ) {
1027
+
1028
+ if ( _webgpuCubeRenderTarget !== null ) _webgpuCubeRenderTarget.dispose();
1029
+
1030
+ _webgpuCubeRenderTarget = new CubeRenderTarget( cubemapSize, { type: HalfFloatType } );
1031
+ _webgpuCubeCamera = new CubeCamera( near, far, _webgpuCubeRenderTarget );
1032
+ _webgpuCachedCubemapSize = cubemapSize;
1033
+ _webgpuCachedNear = near;
1034
+ _webgpuCachedFar = far;
1035
+
1036
+ }
1037
+
1038
+ return { cubeRenderTarget: _webgpuCubeRenderTarget, cubeCamera: _webgpuCubeCamera };
1039
+
1040
+ }
1041
+
1042
+ function _ensureWebGPUComputeResources( cubemapSize, envMapTexture, atlasTexture ) {
1043
+
1044
+ if (
1045
+ _webgpuComputeNode !== null &&
1046
+ _webgpuComputeCubemapSize === cubemapSize &&
1047
+ _webgpuComputeAtlas === atlasTexture &&
1048
+ _webgpuComputeEnvMap === envMapTexture
1049
+ ) {
1050
+
1051
+ return _webgpuComputeNode;
1052
+
1053
+ }
1054
+
1055
+ _webgpuComputeCubemapSize = cubemapSize;
1056
+ _webgpuComputeAtlas = atlasTexture;
1057
+ _webgpuComputeEnvMap = envMapTexture;
1058
+
1059
+ const probeIx = uniform( 'uint' );
1060
+ const probeIy = uniform( 'uint' );
1061
+ const probeIz = uniform( 'uint' );
1062
+ const paddedSlices = uniform( 'uint' );
1063
+
1064
+ _webgpuProbeUniforms = { probeIx, probeIy, probeIz, paddedSlices };
1065
+
1066
+ const atlas = storageTexture( atlasTexture );
1067
+
1068
+ const projectProbe = Fn( () => {
1069
+
1070
+ const accum0 = vec3( 0 ).toVar();
1071
+ const accum1 = vec3( 0 ).toVar();
1072
+ const accum2 = vec3( 0 ).toVar();
1073
+ const accum3 = vec3( 0 ).toVar();
1074
+ const accum4 = vec3( 0 ).toVar();
1075
+ const accum5 = vec3( 0 ).toVar();
1076
+ const accum6 = vec3( 0 ).toVar();
1077
+ const accum7 = vec3( 0 ).toVar();
1078
+ const accum8 = vec3( 0 ).toVar();
1079
+ const totalWeight = float( 0 ).toVar();
1080
+ const pixelSize = float( 2 ).div( float( cubemapSize ) );
1081
+
1082
+ Loop(
1083
+ { end: 6, name: 'face', type: 'int' },
1084
+ { end: cubemapSize, name: 'iy', type: 'int' },
1085
+ { end: cubemapSize, name: 'ix', type: 'int' },
1086
+ ( { face, iy, ix } ) => {
1087
+
1088
+ const col = convert( ix, 'float' ).add( 0.5 ).mul( pixelSize ).sub( 1.0 );
1089
+ const row = float( 1 ).sub( convert( iy, 'float' ).add( 0.5 ).mul( pixelSize ) );
1090
+
1091
+ const dirX = select( face.equal( int( 0 ) ), float( 1 ), select( face.equal( int( 1 ) ), float( - 1 ), select( face.equal( int( 5 ) ), col.negate(), col ) ) );
1092
+ const dirY = select( face.equal( int( 2 ) ), float( 1 ), select( face.equal( int( 3 ) ), float( - 1 ), row ) );
1093
+ const dirZ = select( face.equal( int( 0 ) ), col.negate(), select( face.equal( int( 1 ) ), col, select( face.equal( int( 2 ) ), row.negate(), select( face.equal( int( 3 ) ), row, select( face.equal( int( 4 ) ), float( 1 ), float( - 1 ) ) ) ) ) );
1094
+ const coord = vec3( dirX, dirY, dirZ );
1095
+
1096
+ const lengthSq = coord.dot( coord );
1097
+ const weight = float( 4 ).div( lengthSq.sqrt().mul( lengthSq ) );
1098
+ totalWeight.addAssign( weight );
1099
+
1100
+ const dir = coord.normalize();
1101
+ // Pass explicit direction — omitting UV would default to reflectVector (needs camera).
1102
+ const cw = cubeTexture( envMapTexture, coord ).rgb.mul( weight );
1103
+
1104
+ accum0.addAssign( cw.mul( 0.282095 ) );
1105
+ accum1.addAssign( cw.mul( 0.488603 ).mul( dir.y ) );
1106
+ accum2.addAssign( cw.mul( 0.488603 ).mul( dir.z ) );
1107
+ accum3.addAssign( cw.mul( 0.488603 ).mul( dir.x ) );
1108
+ accum4.addAssign( cw.mul( 1.092548 ).mul( dir.x ).mul( dir.y ) );
1109
+ accum5.addAssign( cw.mul( 1.092548 ).mul( dir.y ).mul( dir.z ) );
1110
+ accum6.addAssign( cw.mul( 0.315392 ).mul( dir.z.mul( dir.z ).mul( 3 ).sub( 1 ) ) );
1111
+ accum7.addAssign( cw.mul( 1.092548 ).mul( dir.x ).mul( dir.z ) );
1112
+ accum8.addAssign( cw.mul( 0.546274 ).mul( dir.x.mul( dir.x ).sub( dir.y.mul( dir.y ) ) ) );
1113
+
1114
+ }
1115
+
1116
+ );
1117
+
1118
+ const norm = float( 4 ).mul( PI ).div( totalWeight );
1119
+ const c0 = accum0.mul( norm );
1120
+ const c1 = accum1.mul( norm );
1121
+ const c2 = accum2.mul( norm );
1122
+ const c3 = accum3.mul( norm );
1123
+ const c4 = accum4.mul( norm );
1124
+ const c5 = accum5.mul( norm );
1125
+ const c6 = accum6.mul( norm );
1126
+ const c7 = accum7.mul( norm );
1127
+ const c8 = accum8.mul( norm );
1128
+
1129
+ Loop( { end: 7, name: 't', type: 'int' }, ( { t } ) => {
1130
+
1131
+ const atlasZ = paddedSlices.mul( t ).add( 1 ).add( probeIz );
1132
+
1133
+ If( t.equal( int( 0 ) ), () => {
1134
+
1135
+ textureStore( atlas, uvec3( probeIx, probeIy, atlasZ ), vec4( c0.x, c0.y, c0.z, c1.x ) ).toWriteOnly();
1136
+
1137
+ } ).ElseIf( t.equal( int( 1 ) ), () => {
1138
+
1139
+ textureStore( atlas, uvec3( probeIx, probeIy, atlasZ ), vec4( c1.y, c1.z, c2.x, c2.y ) ).toWriteOnly();
1140
+
1141
+ } ).ElseIf( t.equal( int( 2 ) ), () => {
1142
+
1143
+ textureStore( atlas, uvec3( probeIx, probeIy, atlasZ ), vec4( c2.z, c3.x, c3.y, c3.z ) ).toWriteOnly();
1144
+
1145
+ } ).ElseIf( t.equal( int( 3 ) ), () => {
1146
+
1147
+ textureStore( atlas, uvec3( probeIx, probeIy, atlasZ ), vec4( c4.x, c4.y, c4.z, c5.x ) ).toWriteOnly();
1148
+
1149
+ } ).ElseIf( t.equal( int( 4 ) ), () => {
1150
+
1151
+ textureStore( atlas, uvec3( probeIx, probeIy, atlasZ ), vec4( c5.y, c5.z, c6.x, c6.y ) ).toWriteOnly();
1152
+
1153
+ } ).ElseIf( t.equal( int( 5 ) ), () => {
1154
+
1155
+ textureStore( atlas, uvec3( probeIx, probeIy, atlasZ ), vec4( c6.z, c7.x, c7.y, c7.z ) ).toWriteOnly();
1156
+
1157
+ } ).Else( () => {
1158
+
1159
+ textureStore( atlas, uvec3( probeIx, probeIy, atlasZ ), vec4( c8.x, c8.y, c8.z, float( 0 ) ) ).toWriteOnly();
1160
+
1161
+ } );
1162
+
1163
+ } );
1164
+
1165
+ } );
1166
+
1167
+ _webgpuComputeNode = projectProbe().compute( 1 );
1168
+
1169
+ return _webgpuComputeNode;
1170
+
1171
+ }
1172
+ // !WITH_GENESYS
1173
+
630
1174
  function _ensureBatchTarget( totalProbes ) {
631
1175
 
632
1176
  if ( _batchTarget === null || _batchTargetProbes !== totalProbes ) {