rayzee 8.8.2 → 9.1.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.
Files changed (104) hide show
  1. package/README.md +210 -20
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-Dc6jRYke.js → NeuralSuperRes-CbNvQjCw.js} +5 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10795 -5324
  6. package/dist/rayzee.umd.js +279 -268
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +60 -76
  10. package/src/EngineEvents.js +7 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/Passes/OIDNTemporalHistory.js +25 -7
  16. package/src/PathTracerApp.js +313 -25
  17. package/src/Processor/ArchiveCache.js +120 -0
  18. package/src/Processor/ArchiveReader.js +179 -32
  19. package/src/Processor/AssetLoader.js +523 -76
  20. package/src/Processor/AtmosphereModel.js +465 -0
  21. package/src/Processor/BVHBuilder.js +4 -1
  22. package/src/Processor/BVHLeafFold.js +100 -0
  23. package/src/Processor/BVHRefitter.js +98 -214
  24. package/src/Processor/ChunkedRecords.js +189 -4
  25. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  26. package/src/Processor/GeometryExtractor.js +238 -40
  27. package/src/Processor/InstanceTable.js +55 -37
  28. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  29. package/src/Processor/PBRT/PBRTParser.js +132 -22
  30. package/src/Processor/PBRT/PBRTSceneBuilder.js +210 -55
  31. package/src/Processor/PBRT/buffers.js +27 -0
  32. package/src/Processor/PBRT/index.js +55 -15
  33. package/src/Processor/PackedRayBuffer.js +10 -13
  34. package/src/Processor/ParallelBVHBuilder.js +36 -15
  35. package/src/Processor/PhysicalSky.js +308 -0
  36. package/src/Processor/QueueManager.js +1 -0
  37. package/src/Processor/SceneProcessor.js +1132 -181
  38. package/src/Processor/SplitBLAS.js +249 -0
  39. package/src/Processor/SunPosition.js +41 -0
  40. package/src/Processor/TextureCreator.js +21 -0
  41. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  42. package/src/Processor/Workers/BVHWorker.js +17 -13
  43. package/src/Processor/ZipReader.js +207 -0
  44. package/src/RenderSettings.js +49 -8
  45. package/src/SceneState/SceneState.js +367 -0
  46. package/src/SceneState/portable.js +52 -0
  47. package/src/Stages/MotionVector.js +8 -42
  48. package/src/Stages/NRD.js +30 -21
  49. package/src/Stages/NormalDepth.js +12 -23
  50. package/src/Stages/PathTracer.js +114 -6
  51. package/src/Stages/PathTracerStage.js +251 -104
  52. package/src/Storage/BLASCache.js +190 -0
  53. package/src/Storage/CDFCache.js +77 -0
  54. package/src/Storage/DownloadCache.js +266 -0
  55. package/src/Storage/GeometrySpill.js +217 -0
  56. package/src/Storage/SceneGraphCodec.js +503 -0
  57. package/src/Storage/SpillStore.js +112 -0
  58. package/src/Storage/StorageManager.js +934 -0
  59. package/src/Storage/StorageOps.js +137 -0
  60. package/src/Storage/StorageWorker.js +37 -0
  61. package/src/Storage/events.js +29 -0
  62. package/src/Storage/identity.js +77 -0
  63. package/src/Storage/index.js +9 -0
  64. package/src/Storage/inlineTransport.js +24 -0
  65. package/src/Storage/locks.js +99 -0
  66. package/src/Storage/openStorage.js +87 -0
  67. package/src/Storage/sceneCachePolicy.js +12 -0
  68. package/src/Storage/shared.js +9 -0
  69. package/src/Storage/transport.js +62 -0
  70. package/src/TSL/Atmosphere.js +684 -0
  71. package/src/TSL/BVHTraversal.js +288 -239
  72. package/src/TSL/CameraRay.js +99 -14
  73. package/src/TSL/Common.js +11 -2
  74. package/src/TSL/DebugKernel.js +2 -2
  75. package/src/TSL/Displacement.js +6 -6
  76. package/src/TSL/EmissiveSampling.js +5 -5
  77. package/src/TSL/EnvironmentCDF.js +176 -0
  78. package/src/TSL/GenerateKernel.js +5 -5
  79. package/src/TSL/HitFacet.js +4 -4
  80. package/src/TSL/LightsDirect.js +8 -8
  81. package/src/TSL/LightsSampling.js +55 -37
  82. package/src/TSL/Random.js +1 -0
  83. package/src/TSL/ShadeKernel.js +75 -19
  84. package/src/TSL/Sun.js +40 -0
  85. package/src/TSL/TextureSampling.js +6 -7
  86. package/src/index.js +22 -0
  87. package/src/managers/CameraManager.js +412 -56
  88. package/src/managers/DenoisingManager.js +4 -3
  89. package/src/managers/EnvironmentManager.js +263 -33
  90. package/src/managers/GoboManager.js +19 -8
  91. package/src/managers/IESManager.js +3 -3
  92. package/src/managers/InteractionManager.js +30 -2
  93. package/src/managers/LightManager.js +98 -5
  94. package/src/managers/MaterialDataManager.js +22 -2
  95. package/src/managers/TransformManager.js +26 -0
  96. package/src/managers/UniformManager.js +11 -7
  97. package/src/managers/VideoRenderManager.js +38 -17
  98. package/src/managers/ViewCamera.js +77 -0
  99. package/src/managers/WalkControls.js +262 -0
  100. package/src/managers/helpers/OutlineHelper.js +22 -2
  101. package/src/managers/timeline/CameraTrack.js +235 -0
  102. package/src/managers/timeline/TimelineManager.js +146 -0
  103. package/src/version.js +3 -0
  104. package/src/Processor/ProceduralSky.js +0 -291
@@ -11,31 +11,51 @@ import {
11
11
  RGBAFormat, RedFormat, FloatType, LinearFilter, Vector2, Vector3, Color, Matrix4, DataTexture,
12
12
  } from 'three';
13
13
  import { EquirectHDRInfo } from '../Processor/EquirectHDRInfo.js';
14
- import { ProceduralSky } from '../Processor/ProceduralSky.js';
14
+ import { PhysicalSky } from '../Processor/PhysicalSky.js';
15
15
  import { SimpleSky } from '../Processor/SimpleSky.js';
16
+ import { convertLinearTriple } from '../Color/WorkingMatrix.js';
16
17
  import { createLogger, fmt } from '../utils/Logger.js';
17
18
 
18
19
  const log = createLogger( 'env' );
19
20
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
20
21
  import { getActiveColorManagement } from '../Color/ColorManagement.js';
22
+ import { loadCDF, saveCDF } from '../Storage/CDFCache.js';
23
+
24
+ const SKY_WIDTH = 1024;
25
+ const SKY_HEIGHT = 512;
26
+ const DEG2RAD = Math.PI / 180;
27
+ const _rotation = new Matrix4();
28
+
29
+ // Bakes queued on the GPU at once. More only queue behind frames already waiting there.
30
+ const SKY_BAKES_IN_FLIGHT = 4;
21
31
 
22
32
  export class EnvironmentManager {
23
33
 
24
34
  /**
25
35
  * @param {Object} scene - Three.js scene
26
36
  * @param {import('./UniformManager').UniformManager} uniforms
37
+ * @param {import('three/webgpu').WebGPURenderer} [renderer] - bakes the physical sky
27
38
  */
28
- constructor( scene, uniforms ) {
39
+ constructor( scene, uniforms, renderer = null ) {
29
40
 
30
41
  this.scene = scene;
31
42
  this.uniforms = uniforms;
43
+ this.renderer = renderer;
32
44
 
33
45
  // CDF computation engine
34
46
  this.equirectHdrInfo = new EquirectHDRInfo();
35
47
 
36
48
  // Sky renderers (lazy init)
37
- this.proceduralSkyRenderer = null;
49
+ this.physicalSky = null;
38
50
  this.simpleSkyRenderer = null;
51
+ this._sun = null;
52
+ this._skyRequested = false;
53
+ this._skyScheduled = false;
54
+ this._skyInFlight = 0;
55
+ this._skyCaughtUp = null;
56
+ this._resolveSky = null;
57
+ this._skyBakes = 0;
58
+ this._skyStatsBake = 0;
39
59
 
40
60
  // Environment texture — 1×1 black placeholder for shader compilation. Filters must be
41
61
  // Linear: DataTexture defaults to Nearest, which is unfilterable, and WGSLNodeBuilder then
@@ -71,12 +91,15 @@ export class EnvironmentManager {
71
91
  // Solid Color Sky
72
92
  solidSkyColor: new Color( DEFAULT_STATE.solidSkyColor ),
73
93
 
74
- // Procedural Sky (Preetham Model)
94
+ // Physical sky. The sun direction is in the sky's own frame; environment rotation turns both.
75
95
  skySunDirection: this._calculateInitialSunDirection(),
76
- skySunIntensity: DEFAULT_STATE.skySunIntensity,
77
- skyRayleighDensity: DEFAULT_STATE.skyRayleighDensity,
96
+ skySunStrength: DEFAULT_STATE.skySunStrength,
97
+ skySunSize: DEFAULT_STATE.skySunSize,
78
98
  skyTurbidity: DEFAULT_STATE.skyTurbidity,
79
- skyMieAnisotropy: DEFAULT_STATE.skyMieAnisotropy,
99
+ skyOzone: DEFAULT_STATE.skyOzone,
100
+ skyAirDensity: DEFAULT_STATE.skyAirDensity,
101
+ skyGroundAlbedo: new Color( DEFAULT_STATE.skyGroundAlbedo ),
102
+ skyAltitude: DEFAULT_STATE.skyAltitude,
80
103
  };
81
104
 
82
105
  /**
@@ -93,7 +116,7 @@ export class EnvironmentManager {
93
116
  // ===== MODE STATE MACHINE =====
94
117
 
95
118
  /**
96
- * Switches the environment mode (hdri, gradient, color, procedural).
119
+ * Switches the environment mode (hdri, gradient, color, procedural — the physical sky).
97
120
  * Preserves the HDRI texture when switching away, restores when switching back.
98
121
  * @param {'hdri'|'gradient'|'color'|'procedural'} mode
99
122
  */
@@ -128,6 +151,8 @@ export class EnvironmentManager {
128
151
 
129
152
  }
130
153
 
154
+ if ( mode !== 'procedural' && prev === 'procedural' ) this._releaseSky();
155
+
131
156
  this.markDirty();
132
157
  this.callbacks.onAutoExposureReset?.();
133
158
  this._notifyReset();
@@ -143,6 +168,71 @@ export class EnvironmentManager {
143
168
 
144
169
  }
145
170
 
171
+ // ===== SAVED SESSIONS =====
172
+
173
+ /** Where the HDRI came from (see Storage/CDFCache.js), shown or held while a sky is. */
174
+ get hdriSource() {
175
+
176
+ const hdri = this.envParams.mode === 'hdri' ? this.environmentTexture : this._previousHDRI;
177
+ return hdri?.userData?.__rayzeeSource ?? null;
178
+
179
+ }
180
+
181
+ /** The mode, the sky parameters and the HDRI's source, as plain data. */
182
+ serialize() {
183
+
184
+ const p = this.envParams;
185
+ return {
186
+ mode: p.mode,
187
+ hdri: this.hdriSource,
188
+ gradientZenithColor: p.gradientZenithColor.toArray(),
189
+ gradientHorizonColor: p.gradientHorizonColor.toArray(),
190
+ gradientGroundColor: p.gradientGroundColor.toArray(),
191
+ solidSkyColor: p.solidSkyColor.toArray(),
192
+ skyModel: 'atmosphere',
193
+ skySunDirection: p.skySunDirection.toArray(),
194
+ skySunStrength: p.skySunStrength,
195
+ skySunSize: p.skySunSize,
196
+ skyTurbidity: p.skyTurbidity,
197
+ skyOzone: p.skyOzone,
198
+ skyAirDensity: p.skyAirDensity,
199
+ skyGroundAlbedo: p.skyGroundAlbedo.toArray(),
200
+ skyAltitude: p.skyAltitude,
201
+ };
202
+
203
+ }
204
+
205
+ /**
206
+ * Puts back {@link serialize}'s parameters and mode. The HDRI itself is the caller's to load
207
+ * first: only it can resolve the source.
208
+ */
209
+ async restore( state ) {
210
+
211
+ if ( ! state ) return;
212
+ const p = this.envParams;
213
+ // Sessions saved before the physical sky gave the same names other meanings.
214
+ const atmosphere = state.skyModel === 'atmosphere';
215
+ const colors = [ 'gradientZenithColor', 'gradientHorizonColor', 'gradientGroundColor', 'solidSkyColor', 'skySunDirection' ];
216
+ for ( const key of atmosphere ? [ ...colors, 'skyGroundAlbedo' ] : colors ) {
217
+
218
+ if ( Array.isArray( state[ key ] ) ) p[ key ].fromArray( state[ key ] );
219
+
220
+ }
221
+
222
+ if ( atmosphere ) {
223
+
224
+ for ( const key of [ 'skySunStrength', 'skySunSize', 'skyTurbidity', 'skyOzone', 'skyAirDensity', 'skyAltitude' ] ) {
225
+
226
+ if ( typeof state[ key ] === 'number' ) p[ key ] = state[ key ];
227
+
228
+ }
229
+
230
+ }
231
+
232
+ if ( state.mode && ( state.mode !== 'hdri' || p.mode !== 'hdri' ) ) await this.setMode( state.mode );
233
+
234
+ }
235
+
146
236
  // ===== Aliases (match Sub-API surface for zero-churn migration) =====
147
237
 
148
238
  /** @see envParams */
@@ -219,7 +309,7 @@ export class EnvironmentManager {
219
309
 
220
310
  }
221
311
 
222
- this.envCDFTexture?.dispose?.();
312
+ if ( ! this.envCDFTexture?._isPhysicalSky ) this.envCDFTexture?.dispose?.();
223
313
  this.envCDFTexture = new DataTexture( data, texW, H, RedFormat, FloatType );
224
314
  this.envCDFTexture.needsUpdate = true;
225
315
 
@@ -280,6 +370,7 @@ export class EnvironmentManager {
280
370
  const rotationRadians = rotationDegrees * ( Math.PI / 180 );
281
371
  this.environmentRotationMatrix.makeRotationY( rotationRadians );
282
372
  this.uniforms.get( 'environmentMatrix' ).value.copy( this.environmentRotationMatrix );
373
+ this.refreshSun();
283
374
 
284
375
  }
285
376
 
@@ -294,6 +385,7 @@ export class EnvironmentManager {
294
385
 
295
386
  if ( ! this.scene.environment ) {
296
387
 
388
+ this._cdfSignature = null;
297
389
  this._updateCDFTexture();
298
390
  this.uniforms.set( 'envTotalSum', 0.0 );
299
391
  this.uniforms.set( 'envCompensationDelta', 0.0 );
@@ -301,6 +393,9 @@ export class EnvironmentManager {
301
393
 
302
394
  }
303
395
 
396
+ // Its table is built with it, on the GPU.
397
+ if ( this.scene.environment._isPhysicalSky ) return;
398
+
304
399
  try {
305
400
 
306
401
  const startTime = performance.now();
@@ -308,6 +403,7 @@ export class EnvironmentManager {
308
403
 
309
404
  if ( ! textureForCDF.image ) {
310
405
 
406
+ this._cdfSignature = null;
311
407
  this._updateCDFTexture();
312
408
  this.uniforms.set( 'envTotalSum', 0.0 );
313
409
  this.uniforms.set( 'envCompensationDelta', 0.0 );
@@ -315,7 +411,16 @@ export class EnvironmentManager {
315
411
 
316
412
  }
317
413
 
318
- if ( useWorker ) {
414
+ // The same pixels as last time: the tables and uniforms already describe them.
415
+ const signature = `${textureForCDF.uuid}:${textureForCDF.version}`;
416
+ if ( signature === this._cdfSignature ) return;
417
+
418
+ const cached = await loadCDF( textureForCDF ).catch( () => null );
419
+ if ( cached ) {
420
+
421
+ Object.assign( this.equirectHdrInfo, cached );
422
+
423
+ } else if ( useWorker ) {
319
424
 
320
425
  await this.equirectHdrInfo.updateFromAsync( textureForCDF );
321
426
 
@@ -325,6 +430,8 @@ export class EnvironmentManager {
325
430
 
326
431
  }
327
432
 
433
+ if ( ! cached ) saveCDF( textureForCDF, this.equirectHdrInfo ).catch( () => {} );
434
+ this._cdfSignature = signature;
328
435
  this.cdfBuildTime = performance.now() - startTime;
329
436
 
330
437
  this._updateCDFTexture();
@@ -340,11 +447,12 @@ export class EnvironmentManager {
340
447
 
341
448
  log.info( fmt.list( [
342
449
  fmt.px( this.envTexSize.x, this.envTexSize.y ),
343
- `CDF ${fmt.ms( this.cdfBuildTime )}${useWorker ? '' : ' (main thread)'}`,
450
+ `CDF ${fmt.ms( this.cdfBuildTime )}${cached ? ' (stored)' : useWorker ? '' : ' (main thread)'}`,
344
451
  ] ) );
345
452
 
346
453
  } catch ( error ) {
347
454
 
455
+ this._cdfSignature = null;
348
456
  log.error( 'CDF build failed:', error );
349
457
  this.uniforms.set( 'envTotalSum', 0.0 );
350
458
  this.uniforms.set( 'envCompensationDelta', 0.0 );
@@ -391,7 +499,7 @@ export class EnvironmentManager {
391
499
  // stashed by setMode() for a later sky→hdri restore. Only when actually
392
500
  // installing a new non-null texture (the null-env branch keeps the old ref).
393
501
  const oldTex = this.environmentTexture;
394
- if ( envMap && oldTex && oldTex !== envMap && oldTex !== this._envPlaceholder && oldTex !== this._previousHDRI ) {
502
+ if ( envMap && oldTex && oldTex !== envMap && oldTex !== this._envPlaceholder && oldTex !== this._previousHDRI && ! oldTex._isPhysicalSky ) {
395
503
 
396
504
  oldTex.dispose?.();
397
505
 
@@ -406,6 +514,7 @@ export class EnvironmentManager {
406
514
 
407
515
  } else {
408
516
 
517
+ this._cdfSignature = null;
409
518
  this._updateCDFTexture();
410
519
  this.uniforms.set( 'envTotalSum', 0.0 );
411
520
  this.uniforms.set( 'envCompensationDelta', 0.0 );
@@ -430,6 +539,8 @@ export class EnvironmentManager {
430
539
 
431
540
  }
432
541
 
542
+ if ( envMap && oldTex?._isPhysicalSky && this.envParams.mode !== 'procedural' ) this._releaseSky();
543
+
433
544
  this._notifyReset();
434
545
 
435
546
  }
@@ -528,43 +639,158 @@ export class EnvironmentManager {
528
639
  }
529
640
 
530
641
  /**
531
- * Generate procedural (Preetham) sky texture and set as environment.
642
+ * Bake the physical sky and set it as the environment. Requests made in one task become one
643
+ * bake, queued straight away while the GPU keeps up and otherwise as soon as it finishes an
644
+ * earlier one. Resolves once the environment has caught up with the latest request.
532
645
  */
533
- async generateProceduralSkyTexture() {
646
+ generateProceduralSkyTexture() {
647
+
648
+ this._skyRequested = true;
649
+ this._skyCaughtUp ??= new Promise( resolve => void ( this._resolveSky = resolve ) );
650
+ this._pumpSky();
651
+ return this._skyCaughtUp;
652
+
653
+ }
534
654
 
535
- if ( ! this.proceduralSkyRenderer ) {
655
+ /** @private */
656
+ _pumpSky() {
657
+
658
+ if ( this._skyScheduled || ! this._skyRequested || this._skyInFlight >= SKY_BAKES_IN_FLIGHT ) return;
659
+ this._skyScheduled = true;
660
+ queueMicrotask( () => {
661
+
662
+ this._skyScheduled = false;
663
+ if ( this._skyRequested && this._skyInFlight < SKY_BAKES_IN_FLIGHT ) this._bakePhysicalSky();
536
664
 
537
- this.proceduralSkyRenderer = new ProceduralSky( 512, 256 );
665
+ } );
666
+
667
+ }
668
+
669
+ /** @private */
670
+ _settleSky() {
671
+
672
+ const resolve = this._resolveSky;
673
+ this._skyCaughtUp = this._resolveSky = null;
674
+ resolve?.();
675
+
676
+ }
677
+
678
+ /** @private */
679
+ async _bakePhysicalSky() {
680
+
681
+ this._skyRequested = false;
682
+ const p = this.envParams;
683
+ if ( ! this.renderer || p.mode !== 'procedural' ) {
684
+
685
+ if ( ! this.renderer ) log.warn( 'physical sky needs a renderer' );
686
+ if ( this._skyInFlight === 0 ) this._settleSky();
687
+ return;
538
688
 
539
689
  }
540
690
 
541
- const params = {
542
- sunDirection: this.envParams.skySunDirection.clone(),
543
- sunIntensity: this.envParams.skySunIntensity * 0.05,
544
- rayleighDensity: this.envParams.skyRayleighDensity * 2.0,
545
- mieDensity: this.envParams.skyTurbidity * 0.005,
546
- mieAnisotropy: this.envParams.skyMieAnisotropy,
547
- turbidity: this.envParams.skyTurbidity * 2.0,
548
- };
691
+ const sky = this.physicalSky ??= new PhysicalSky( SKY_WIDTH, SKY_HEIGHT );
692
+ const albedo = p.skyGroundAlbedo;
693
+ const bake = ++ this._skyBakes;
694
+ const latest = () => bake === this._skyBakes && ! this._skyRequested;
695
+ this._skyInFlight ++;
549
696
 
550
697
  try {
551
698
 
552
- const texture = this.proceduralSkyRenderer.render( params );
553
- texture._isGeneratedProcedural = true;
554
- await this.setEnvironmentMap( texture );
699
+ const { texture, cdfTexture, sun, stats } = sky.bake( this.renderer, {
700
+ sunDirection: p.skySunDirection.toArray(),
701
+ turbidity: p.skyTurbidity,
702
+ ozone: p.skyOzone,
703
+ airDensity: p.skyAirDensity,
704
+ groundAlbedo: [ albedo.r, albedo.g, albedo.b ],
705
+ altitude: p.skyAltitude,
706
+ sunAngularDiameter: p.skySunSize * DEG2RAD,
707
+ sunStrength: p.skySunStrength,
708
+ } );
709
+ this._sun = sun;
710
+ this._installSky( texture, cdfTexture );
711
+
712
+ // Frames until they land normalise by an older bake's.
713
+ const { totalSum, compensationDelta } = await stats;
714
+ if ( p.mode === 'procedural' && sky === this.physicalSky && bake > this._skyStatsBake ) {
715
+
716
+ this._skyStatsBake = bake;
717
+ this.uniforms.set( 'envTotalSum', totalSum );
718
+ this.uniforms.set( 'envCompensationDelta', compensationDelta );
719
+ if ( latest() ) this._notifyReset();
555
720
 
556
- this.uniforms.get( 'sunDirection' ).value.copy( this.envParams.skySunDirection );
557
- this.uniforms.set( 'sunAngularSize', 0.0087 );
558
- this.uniforms.set( 'hasSun', 1 );
721
+ }
559
722
 
560
- log.debug( `sun synced · dir ${this.envParams.skySunDirection.toArray().map( v => v.toFixed( 2 ) ).join( ',' )}` );
723
+ if ( latest() ) log.debug( `physical sky ${fmt.ms( sky.getLastRenderTime() )} · sun ${sun.radiance.map( v => v.toPrecision( 3 ) ).join( ',' )}` );
561
724
 
562
725
  } catch ( error ) {
563
726
 
564
- log.error( 'procedural sky generation failed:', error );
727
+ log.error( 'physical sky generation failed:', error );
728
+
729
+ } finally {
730
+
731
+ this._skyInFlight --;
732
+ if ( latest() ) this._settleSky();
733
+ else this._pumpSky();
734
+
735
+ }
736
+
737
+ }
738
+
739
+ /** @private */
740
+ _installSky( texture, cdfTexture ) {
741
+
742
+ const oldTex = this.environmentTexture;
743
+ if ( oldTex && oldTex !== texture && oldTex !== this._envPlaceholder && oldTex !== this._previousHDRI ) oldTex.dispose?.();
744
+ if ( this.envCDFTexture !== cdfTexture ) {
745
+
746
+ this.envCDFTexture?.dispose?.();
747
+ this.envCDFTexture = cdfTexture;
565
748
 
566
749
  }
567
750
 
751
+ // So an HDRI coming back rebuilds its own table.
752
+ this._cdfSignature = null;
753
+ texture._isGeneratedProcedural = true;
754
+ this.scene.environment = texture;
755
+ this.environmentTexture = texture;
756
+ this.envTexSize.set( texture.image.width, texture.image.height );
757
+ this.uniforms.get( 'envResolution' ).value.set( texture.image.width, texture.image.height );
758
+ const nodes = this.callbacks.getSceneTextureNodes?.();
759
+ if ( nodes?.envTex ) nodes.envTex.value = texture;
760
+ this.refreshSun();
761
+ this._notifyReset();
762
+
763
+ }
764
+
765
+ /** @private */
766
+ _releaseSky() {
767
+
768
+ if ( ! this.physicalSky ) return;
769
+ this.physicalSky.dispose( this.renderer );
770
+ this.physicalSky = null;
771
+ this._sun = null;
772
+ this.refreshSun();
773
+
774
+ }
775
+
776
+ /**
777
+ * Push the sun to the shader: into world space through the environment rotation, and into
778
+ * the working colour space. Call after either changes.
779
+ */
780
+ refreshSun() {
781
+
782
+ const sun = this.envParams.mode === 'procedural' ? this._sun : null;
783
+ const on = !! sun && sun.radiance.some( v => v > 0 );
784
+ this.uniforms.set( 'hasSun', on ? 1 : 0 );
785
+ if ( ! on ) return;
786
+
787
+ const direction = this.uniforms.get( 'sunDirection' ).value;
788
+ direction.fromArray( sun.direction ).transformDirection( _rotation.copy( this.environmentRotationMatrix ).transpose() );
789
+ const rgb = [ ...sun.radiance ];
790
+ convertLinearTriple( rgb );
791
+ this.uniforms.get( 'sunRadiance' ).value.fromArray( rgb );
792
+ this.uniforms.get( 'sunParams' ).value.set( sun.cosHalfAngle, sun.solidAngle, 1 / ( sun.sinHalfAngle * sun.sinHalfAngle ), sun.horizonSin );
793
+
568
794
  }
569
795
 
570
796
  // ===== HELPERS =====
@@ -597,8 +823,12 @@ export class EnvironmentManager {
597
823
 
598
824
  dispose() {
599
825
 
600
- this.proceduralSkyRenderer = null;
826
+ this.callbacks = {};
827
+ this._settleSky();
828
+ this.physicalSky?.dispose( this.renderer );
829
+ this.physicalSky = null;
601
830
  this.simpleSkyRenderer = null;
831
+ this._sun = null;
602
832
 
603
833
  this.envCDFTexture?.dispose?.();
604
834
  this.envCDFTexture = null;
@@ -1,4 +1,5 @@
1
1
  import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
2
+ import { cachedObjectURL } from '../Storage/DownloadCache.js';
2
3
 
3
4
  /**
4
5
  * Manages projection masks ("gobos" / "cookies") for spot lights.
@@ -256,17 +257,27 @@ export class GoboManager {
256
257
 
257
258
  }
258
259
 
259
- function loadImage( url ) {
260
+ async function loadImage( url ) {
260
261
 
261
- return new Promise( ( resolve, reject ) => {
262
+ const source = await cachedObjectURL( url );
262
263
 
263
- const img = new Image();
264
- img.crossOrigin = 'anonymous';
265
- img.onload = () => resolve( img );
266
- img.onerror = () => reject( new Error( `Failed to load gobo image: ${url}` ) );
267
- img.src = url;
264
+ try {
268
265
 
269
- } );
266
+ return await new Promise( ( resolve, reject ) => {
267
+
268
+ const img = new Image();
269
+ img.crossOrigin = 'anonymous';
270
+ img.onload = () => resolve( img );
271
+ img.onerror = () => reject( new Error( `Failed to load gobo image: ${url}` ) );
272
+ img.src = source.url;
273
+
274
+ } );
275
+
276
+ } finally {
277
+
278
+ source.release();
279
+
280
+ }
270
281
 
271
282
  }
272
283
 
@@ -1,5 +1,6 @@
1
1
  import { DataArrayTexture, LinearFilter, RGBAFormat, UnsignedByteType } from 'three';
2
2
  import { parseIES, resampleIESToGrid, deriveIESBeamAngle, deriveIESPenumbra } from '../Processor/IESParser.js';
3
+ import { sharedDownloads } from '../Storage/DownloadCache.js';
3
4
 
4
5
  /**
5
6
  * Manages IES photometric profiles for spot lights.
@@ -63,9 +64,8 @@ export class IESManager {
63
64
 
64
65
  try {
65
66
 
66
- const res = await fetch( it.url );
67
- if ( ! res.ok ) throw new Error( `HTTP ${res.status}` );
68
- const text = await res.text();
67
+ const { file, release } = await sharedDownloads().fetch( it.url );
68
+ const text = await file.text().finally( release );
69
69
  const profile = parseIES( text, it.name );
70
70
  const grid = resampleIESToGrid( profile, gridWidth, gridHeight );
71
71
  return { it, profile, grid };
@@ -3,9 +3,28 @@ import {
3
3
  Raycaster,
4
4
  SphereGeometry,
5
5
  Mesh,
6
- MeshBasicMaterial
6
+ MeshBasicMaterial,
7
+ Vector3
7
8
  } from 'three';
8
9
  import { EngineEvents } from '../EngineEvents.js';
10
+ import { CAMERA_PROJECTION_IDS } from '../EngineDefaults.js';
11
+
12
+ const _viewAxis = new Vector3();
13
+ const _toPoint = new Vector3();
14
+
15
+ /** Depth of a point along the camera's view axis — the distance the flat focal plane is measured in. */
16
+ export function viewDepth( point, camera ) {
17
+
18
+ return _toPoint.subVectors( point, camera.position ).dot( camera.getWorldDirection( _viewAxis ) );
19
+
20
+ }
21
+
22
+ /** The focus distance that puts a raycast hit in focus: a panorama focuses along each ray, not on a plane. */
23
+ export function focusDistanceOfHit( hit, camera, panorama = false ) {
24
+
25
+ return panorama ? hit.distance : viewDepth( hit.point, camera );
26
+
27
+ }
9
28
 
10
29
  /**
11
30
  * InteractionManager
@@ -207,7 +226,8 @@ export class InteractionManager extends EventDispatcher {
207
226
  if ( intersects.length > 0 ) {
208
227
 
209
228
  const intersection = intersects[ 0 ];
210
- const distance = intersection.distance;
229
+ const panorama = this.pathTracer?.cameraProjection?.value === CAMERA_PROJECTION_IDS.equirectangular;
230
+ const distance = focusDistanceOfHit( intersection, this.camera, panorama );
211
231
 
212
232
  // Show visual indicator at focus point
213
233
  this.showFocusPoint( intersection.point );
@@ -675,6 +695,14 @@ export class InteractionManager extends EventDispatcher {
675
695
 
676
696
  }
677
697
 
698
+ /** The nearest scene surface under an NDC point, skipping the floor and helpers. */
699
+ pickSurface( ndcX, ndcY ) {
700
+
701
+ this.raycaster.setFromCamera( { x: ndcX, y: ndcY }, this.camera );
702
+ return this.filterValidIntersects( this.raycaster.intersectObjects( this.scene.children, true ) )[ 0 ] ?? null;
703
+
704
+ }
705
+
678
706
  /**
679
707
  * Filter intersections to exclude helper objects and floor plane
680
708
  * @private