rayzee 9.1.1 → 9.2.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 (48) hide show
  1. package/README.md +174 -15
  2. package/dist/{NeuralRetouch-BRquqfmZ.js → NeuralRetouch-DgnfldWO.js} +1 -1
  3. package/dist/{NeuralSuperRes-CbNvQjCw.js → NeuralSuperRes-Baa2w8VU.js} +72 -40
  4. package/dist/{ToneMapGPU-DpUZDMbQ.js → ToneMapGPU-CNgexVto.js} +75 -71
  5. package/dist/rayzee.es.js +4704 -4103
  6. package/dist/rayzee.umd.js +96 -96
  7. package/package.json +3 -2
  8. package/src/AssetConfig.js +10 -5
  9. package/src/EngineEvents.js +41 -1
  10. package/src/EngineIssues.js +3 -0
  11. package/src/Headless.js +23 -7
  12. package/src/HeadlessCanvas.js +104 -0
  13. package/src/Passes/OIDNDenoiser.js +29 -4
  14. package/src/Passes/OIDNTemporalHistory.js +7 -0
  15. package/src/PathTracerApp.js +432 -119
  16. package/src/Platform.js +76 -0
  17. package/src/Processor/AssetLoader.js +66 -4
  18. package/src/Processor/BVHBuilder.js +2 -1
  19. package/src/Processor/EquirectHDRInfo.js +2 -1
  20. package/src/Processor/HostMemory.js +14 -0
  21. package/src/Processor/IESParser.js +1 -2
  22. package/src/Processor/PBRT/PBRTSceneBuilder.js +26 -0
  23. package/src/Processor/ParallelBVHBuilder.js +7 -6
  24. package/src/Processor/PlatformImageLoader.js +118 -0
  25. package/src/Processor/ResampleRGBA8.js +102 -0
  26. package/src/Processor/SceneProcessor.js +8 -8
  27. package/src/Processor/TextureCreator.js +162 -13
  28. package/src/Processor/Workers/PackWorker.js +28 -0
  29. package/src/Stages/Compositor.js +25 -6
  30. package/src/Stages/PathTracer.js +143 -26
  31. package/src/Stages/PathTracerStage.js +7 -98
  32. package/src/TSL/DebugKernel.js +1 -1
  33. package/src/TSL/EmissiveSampling.js +1 -1
  34. package/src/TSL/GenerateKernel.js +3 -7
  35. package/src/TSL/Random.js +3 -120
  36. package/src/TSL/ShadeKernel.js +6 -6
  37. package/src/index.js +2 -1
  38. package/src/managers/CameraManager.js +2 -2
  39. package/src/managers/DenoisingManager.js +77 -8
  40. package/src/managers/GoboManager.js +8 -1
  41. package/src/managers/IESManager.js +13 -2
  42. package/src/managers/InteractionManager.js +1 -1
  43. package/src/managers/LightManager.js +31 -0
  44. package/src/managers/OverlayManager.js +11 -1
  45. package/src/managers/VideoRenderManager.js +8 -0
  46. package/src/node/NodeWorker.js +144 -0
  47. package/src/node/index.js +9 -0
  48. package/src/node/nodePlatform.js +86 -0
@@ -25,6 +25,7 @@ import {
25
25
  } from '../TSL/SortGlobalKernels.js';
26
26
  import { ENGINE_DEFAULTS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
27
27
  import { createLogger, fmt } from '../utils/Logger.js';
28
+ import { deviceMemoryGB } from '../Processor/HostMemory.js';
28
29
  import {
29
30
  Fn, uint, int, atomicStore, atomicLoad, atomicAdd, instanceIndex, If, Return,
30
31
  } from 'three/tsl';
@@ -113,6 +114,11 @@ export class PathTracer extends PathTracerStage {
113
114
  this._geometryPixelCount = 0;
114
115
  this._convergedReadbackPending = false;
115
116
 
117
+ // Lockstep: readbacks issued on a fixed cadence and applied a fixed number of frames later,
118
+ // so when one lands cannot change the image. { due, read, apply } while one is in flight.
119
+ this._lockstep = false;
120
+ this._lockstepRead = null;
121
+
116
122
  // Tier-2: last settled active-pixel count (maxRays − frozen), sizes next frame's bounce-0 grid.
117
123
  // 0 until a settled readback lands (and on reset/camera-move/resize) → grid stays full-size until then.
118
124
  this._lastActivePixelCount = 0;
@@ -131,6 +137,7 @@ export class PathTracer extends PathTracerStage {
131
137
  this._wfIsFirstChunk = uniform( 1, 'uint' ); // 1 → zero frame-scoped counters (CONVERGED/FROZEN)
132
138
  this._pathBudget = 0; // B, paths-in-flight; computed once from device limits in _buildWavefrontKernels
133
139
  this._pathBudgetOverride = 0; // test hook: force B (0 = derive from device)
140
+ this.hostMemoryGB = options.hostMemoryGB;
134
141
  this._chunkRows = 0; // rows per chunk = floor(B / renderWidth), clamped ≥1
135
142
  this._numChunks = 1; // ceil(renderHeight / chunkRows)
136
143
 
@@ -244,6 +251,15 @@ export class PathTracer extends PathTracerStage {
244
251
 
245
252
  }
246
253
 
254
+ const lockstepRead = this._lockstepRead;
255
+ if ( lockstepRead && this.frameCount >= lockstepRead.due ) {
256
+
257
+ if ( ! lockstepRead.apply ) return; // see readbackWait()
258
+ this._lockstepRead = null;
259
+ lockstepRead.apply();
260
+
261
+ }
262
+
247
263
  if ( this.isComplete || this.frameCount >= this.completionThreshold || this._isConvergedComplete() ) {
248
264
 
249
265
  // Denoiser switched on after the render finished — spend one more frame to fill the aux MRT.
@@ -299,7 +315,7 @@ export class PathTracer extends PathTracerStage {
299
315
 
300
316
  }
301
317
 
302
- this._updateAccumulationUniforms( frameValue, renderMode );
318
+ this._updateAccumulationUniforms( frameValue );
303
319
  const auxMixes = this._updateAuxAccumulationUniforms( frameValue );
304
320
  this.frame.value = frameValue;
305
321
  this.seedFrame.value = this._seedTick ++;
@@ -657,6 +673,54 @@ export class PathTracer extends PathTracerStage {
657
673
  this._auxSamples = 0;
658
674
  this._auxSeedPending = false;
659
675
 
676
+ if ( this._lockstep ) {
677
+
678
+ // Nothing measured by the previous render may steer this one.
679
+ this._lockstepRead = null;
680
+ this._readbackGeneration ++;
681
+ this._lastBounceCounts = null;
682
+ this._lastBounceEnergy = null;
683
+ this._lastBounceCountsBudget = - 1;
684
+ this._lastBounceCountsLoopBound = - 1;
685
+ this._curveSizingValid = false;
686
+
687
+ }
688
+
689
+ }
690
+
691
+ /**
692
+ * Lockstep readbacks: the survivor curve and the convergence counters are read every N frames
693
+ * and applied exactly N frames later, waiting when they have not landed, and every reset starts
694
+ * from nothing measured and from seed 0. The same input then renders the same image however fast
695
+ * frames are submitted. Off by default: waiting costs a stall whenever the GPU falls N frames behind.
696
+ * @param {boolean} enabled
697
+ */
698
+ setLockstepReadbacks( enabled ) {
699
+
700
+ enabled = !! enabled;
701
+ if ( enabled === this._lockstep ) return;
702
+ this._lockstep = enabled;
703
+ this._lockstepRead = null;
704
+ this._readbackGeneration ++;
705
+
706
+ }
707
+
708
+ get lockstepReadbacks() {
709
+
710
+ return this._lockstep;
711
+
712
+ }
713
+
714
+ /**
715
+ * While a lockstep readback is due and has not landed, render() traces nothing: await this
716
+ * before the next frame. Null otherwise.
717
+ * @returns {?Promise<void>}
718
+ */
719
+ readbackWait() {
720
+
721
+ const pending = this._lockstepRead;
722
+ return pending && ! pending.apply && this.frameCount >= pending.due ? pending.read : null;
723
+
660
724
  }
661
725
 
662
726
  /**
@@ -805,6 +869,7 @@ export class PathTracer extends PathTracerStage {
805
869
  _willReadCountersThisFrame() {
806
870
 
807
871
  if ( this.cameraChanged || this.cameraOptimizer?.isInInteractionMode() ) return false;
872
+ if ( this._lockstep ) return ! this._lockstepRead && this.frameCount % this._readbackEveryNFrames === 0;
808
873
  if ( this._readbackPending || this._convergedReadbackPending ) return false;
809
874
  return this._readbackFrameCounter + 1 >= this._readbackEveryNFrames;
810
875
 
@@ -819,6 +884,13 @@ export class PathTracer extends PathTracerStage {
819
884
  // Async readback of the per-bounce snapshot every N frames; never awaited, so the early-exit uses past-frame data.
820
885
  _maybeReadbackCounters() {
821
886
 
887
+ if ( this._lockstep ) {
888
+
889
+ this._issueLockstepReadback();
890
+ return;
891
+
892
+ }
893
+
822
894
  // Never sample the survivor curve mid-motion, nor while the CameraOptimizer is holding maxBounces
823
895
  // down to its interaction value (1): mid-motion counts belong to a pose we're leaving, and a curve
824
896
  // measured at the interaction budget stores _lastBounceCountsBudget=1 — when the real (high) budget
@@ -864,16 +936,7 @@ export class PathTracer extends PathTracerStage {
864
936
  // Drop counts measured at a now-stale generation (a resize or camera move happened mid-flight).
865
937
  // A surviving readback was initiated while settled (init is skipped mid-motion) and no motion
866
938
  // happened before it resolved, so its counts match the current view — safe to size from.
867
- if ( gen === this._readbackGeneration ) {
868
-
869
- const all = new Uint32Array( buf.slice( 0 ) );
870
- this._lastBounceCounts = all.subarray( 0, n );
871
- this._lastBounceEnergy = all.subarray( n, 2 * n );
872
- this._lastBounceCountsBudget = budget;
873
- this._lastBounceCountsLoopBound = measuredLoopBound;
874
- this._curveSizingValid = true;
875
-
876
- }
939
+ if ( gen === this._readbackGeneration ) this._applyBounceCounts( buf, budget, measuredLoopBound, n );
877
940
 
878
941
  this._readbackPending = false;
879
942
 
@@ -899,19 +962,7 @@ export class PathTracer extends PathTracerStage {
899
962
  const total = this._wfRenderWidth.value * this._wfRenderHeight.value;
900
963
  this.renderer.getArrayBufferAsync( cAttr ).then( ( buf ) => {
901
964
 
902
- if ( cgen === this._readbackGeneration && total > 0 ) {
903
-
904
- const c = new Uint32Array( buf );
905
- this._convergedFraction = c[ COUNTER.CONVERGED_COUNT ] / total;
906
- // No geometry at all (pure environment) leaves nothing to gate on — 1 lets the
907
- // whole-frame fraction decide alone.
908
- const geo = c[ COUNTER.GEOMETRY_COUNT ];
909
- this._geometryPixelCount = geo;
910
- this._convergedGeometryFraction = geo > 0 ? c[ COUNTER.CONVERGED_GEOMETRY_COUNT ] / geo : 1;
911
- // Tier-2: bounce-0 active-pixel count measured this settled frame → sizes next frame's grid.
912
- this._lastActivePixelCount = c[ COUNTER.ACTIVE_PIXEL_COUNT ];
913
-
914
- }
965
+ if ( cgen === this._readbackGeneration && total > 0 ) this._applyCounters( buf, total );
915
966
 
916
967
  this._convergedReadbackPending = false;
917
968
 
@@ -927,6 +978,72 @@ export class PathTracer extends PathTracerStage {
927
978
 
928
979
  }
929
980
 
981
+ _issueLockstepReadback() {
982
+
983
+ // One in flight, and none mid-motion: frames there do not count, so frameCount stands still.
984
+ if ( this._lockstepRead || this.frameCount % this._readbackEveryNFrames !== 0 ) return;
985
+ if ( this.cameraChanged || this.cameraOptimizer?.isInInteractionMode() ) return;
986
+
987
+ const bounceAttr = this._queueManager?.getBounceCountsAttribute();
988
+ const counterAttr = this._queueManager?.getCountersAttribute();
989
+ if ( ! bounceAttr || ! counterAttr ) return;
990
+
991
+ const gen = this._readbackGeneration;
992
+ const budget = this.maxBounces.value;
993
+ const loopBound = this._bounceLoopBound();
994
+ const n = this._queueManager.MAX_BOUNCE_SNAPSHOTS;
995
+ const total = this._wfRenderWidth.value * this._wfRenderHeight.value;
996
+
997
+ const entry = { due: this.frameCount + this._readbackEveryNFrames, read: null, apply: null };
998
+ entry.read = Promise.all( [
999
+ this.renderer.getArrayBufferAsync( bounceAttr ),
1000
+ this.renderer.getArrayBufferAsync( counterAttr ),
1001
+ ] ).then( ( [ bounceBuf, counterBuf ] ) => {
1002
+
1003
+ entry.apply = () => {
1004
+
1005
+ if ( gen !== this._readbackGeneration ) return;
1006
+ this._applyBounceCounts( bounceBuf, budget, loopBound, n );
1007
+ if ( total > 0 ) this._applyCounters( counterBuf, total );
1008
+
1009
+ };
1010
+
1011
+ }, ( e ) => {
1012
+
1013
+ log.warn( 'lockstep readback failed:', e );
1014
+ entry.apply = () => {};
1015
+
1016
+ } );
1017
+
1018
+ this._lockstepRead = entry;
1019
+
1020
+ }
1021
+
1022
+ _applyBounceCounts( buf, budget, loopBound, n ) {
1023
+
1024
+ const all = new Uint32Array( buf.slice( 0 ) );
1025
+ this._lastBounceCounts = all.subarray( 0, n );
1026
+ this._lastBounceEnergy = all.subarray( n, 2 * n );
1027
+ this._lastBounceCountsBudget = budget;
1028
+ this._lastBounceCountsLoopBound = loopBound;
1029
+ this._curveSizingValid = true;
1030
+
1031
+ }
1032
+
1033
+ _applyCounters( buf, total ) {
1034
+
1035
+ const c = new Uint32Array( buf );
1036
+ this._convergedFraction = c[ COUNTER.CONVERGED_COUNT ] / total;
1037
+ // No geometry at all (pure environment) leaves nothing to gate on — 1 lets the
1038
+ // whole-frame fraction decide alone.
1039
+ const geo = c[ COUNTER.GEOMETRY_COUNT ];
1040
+ this._geometryPixelCount = geo;
1041
+ this._convergedGeometryFraction = geo > 0 ? c[ COUNTER.CONVERGED_GEOMETRY_COUNT ] / geo : 1;
1042
+ // Tier-2: bounce-0 active-pixel count measured this settled frame → sizes next frame's grid.
1043
+ this._lastActivePixelCount = c[ COUNTER.ACTIVE_PIXEL_COUNT ];
1044
+
1045
+ }
1046
+
930
1047
  // Sync wavefront's texture nodes with current env/material textures; only a changed ref triggers GPU rebind.
931
1048
  _refreshWfTextureNodes() {
932
1049
 
@@ -1031,7 +1148,7 @@ export class PathTracer extends PathTracerStage {
1031
1148
  const maxBuffer = limits?.maxBufferSize || maxBinding;
1032
1149
 
1033
1150
  const bByBinding = Math.floor( ( maxBinding * 0.9 ) / RAY_BYTES );
1034
- const deviceMemBytes = ( ( typeof navigator !== 'undefined' && navigator.deviceMemory ) || 4 ) * 1024 * 1024 * 1024;
1151
+ const deviceMemBytes = deviceMemoryGB( this.hostMemoryGB ).gb * 1024 * 1024 * 1024;
1035
1152
  const poolVramBudget = Math.min( deviceMemBytes * 0.25, maxBuffer * 4 );
1036
1153
  const bByVram = Math.floor( poolVramBudget / bytesPerPath );
1037
1154
 
@@ -1596,7 +1713,7 @@ export class PathTracer extends PathTracerStage {
1596
1713
  cameraViewMatrix: this.cameraViewMatrix,
1597
1714
  fireflyThreshold: this.fireflyThreshold,
1598
1715
  shadowTerminatorOffset: this.shadowTerminatorOffset,
1599
- // RNG axis only (keys STBN via frame & 63).
1716
+ // RNG axis only.
1600
1717
  frame: this.seedFrame,
1601
1718
  accumFrame: this.frame,
1602
1719
  resolution: this.resolution,
@@ -1,11 +1,7 @@
1
1
  import { storage } from 'three/tsl';
2
2
  import { gpuOnlyStorageAttribute, uploadStorageChunkRange, uploadStorageChunks } from '../TSL/patches.js';
3
3
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
4
- import {
5
- NearestFilter, Vector2, Matrix4,
6
- TextureLoader, RepeatWrapping
7
- } from 'three';
8
- import { stbnScalarTextureNode, stbnVec2TextureNode } from '../TSL/Random.js';
4
+ import { Vector2, Matrix4 } from 'three';
9
5
 
10
6
  // Pipeline system
11
7
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
@@ -26,8 +22,6 @@ import { LightSerializer } from '../Processor/LightSerializer';
26
22
  // Constants
27
23
  import { ENGINE_DEFAULTS as DEFAULT_STATE, TRIANGLE_DATA_LAYOUT } from '../EngineDefaults.js';
28
24
  import { TRI_GEO_ROWS, TRI_SHADE_ROWS } from '../TSL/Common.js';
29
- import { getAssetConfig } from '../AssetConfig.js';
30
- import { cachedObjectURL, DOWNLOAD_POLICY } from '../Storage/DownloadCache.js';
31
25
  import { createLogger, fmt } from '../utils/Logger.js';
32
26
 
33
27
  // Triangles converted per staging pass: 256K is 12 + 8 MB of staging, kept for the stage's life.
@@ -144,9 +138,6 @@ export class PathTracerStage extends RenderStage {
144
138
  // Initialize rendering state
145
139
  this._initRenderingState();
146
140
 
147
- // Setup blue noise
148
- this.setupBlueNoise();
149
-
150
141
  // Cache frequently used objects
151
142
  this.tempVector2 = new Vector2();
152
143
  this.lastCameraMatrix = new Matrix4();
@@ -216,14 +207,8 @@ export class PathTracerStage extends RenderStage {
216
207
  // and refreshes the bound TextureNode in-place when it changes.
217
208
  this.iesProfiles = null;
218
209
 
219
- // STBN noise textures
220
- this.stbnScalarTexture = null;
221
- this.stbnVec2Texture = null;
222
-
223
210
  /**
224
- * Resolves once both STBN atlases have loaded. Await before any render whose
225
- * output must be reproducible — see PathTracerApp.setDeterministicMode().
226
- * Replaced by the real promise in setupBlueNoise(), which runs after this.
211
+ * @deprecated Always resolved: the engine no longer loads blue-noise textures.
227
212
  * @type {Promise<void>}
228
213
  */
229
214
  this.blueNoiseReady = Promise.resolve();
@@ -369,74 +354,6 @@ export class PathTracerStage extends RenderStage {
369
354
 
370
355
  }
371
356
 
372
- /**
373
- * Load STBN (Spatiotemporal Blue Noise) atlas textures.
374
- * Each atlas is 1024×1024: 8×8 grid of 128×128 tiles, 64 temporal slices.
375
- */
376
- setupBlueNoise() {
377
-
378
- const loader = new TextureLoader();
379
- loader.setCrossOrigin( 'anonymous' );
380
-
381
- const configure = ( tex ) => {
382
-
383
- tex.minFilter = NearestFilter;
384
- tex.magFilter = NearestFilter;
385
- tex.wrapS = RepeatWrapping;
386
- tex.wrapT = RepeatWrapping;
387
- tex.generateMipmaps = false;
388
- return tex;
389
-
390
- };
391
-
392
- const { stbnScalarAtlas, stbnVec2Atlas } = getAssetConfig();
393
-
394
- const load = async ( url ) => {
395
-
396
- const source = await cachedObjectURL( url, { policy: DOWNLOAD_POLICY.REVALIDATE } );
397
- try {
398
-
399
- return await loader.loadAsync( source.url );
400
-
401
- } finally {
402
-
403
- source.release();
404
-
405
- }
406
-
407
- };
408
-
409
- const scalarLoad = load( stbnScalarAtlas ).then( ( tex ) => {
410
-
411
- this.stbnScalarTexture = configure( tex );
412
- stbnScalarTextureNode.value = tex;
413
- log.debug( `STBN scalar atlas ${fmt.px( tex.image.width, tex.image.height )}` );
414
-
415
- } );
416
-
417
- const vec2Load = load( stbnVec2Atlas ).then( ( tex ) => {
418
-
419
- this.stbnVec2Texture = configure( tex );
420
- stbnVec2TextureNode.value = tex;
421
- log.debug( `STBN vec2 atlas ${fmt.px( tex.image.width, tex.image.height )}` );
422
-
423
- } );
424
-
425
- // Until the atlases land the STBN sampler reads a constant-0.5 placeholder and bakes
426
- // that degenerate "noise" permanently into the accumulation buffer. The cut-over frame
427
- // is disk/network dependent, so reproducible renders must await this. Never rejects.
428
- this.blueNoiseReady = Promise.allSettled( [ scalarLoad, vec2Load ] ).then( ( results ) => {
429
-
430
- for ( const result of results ) {
431
-
432
- if ( result.status === 'rejected' ) log.warn( 'STBN atlas failed to load', result.reason );
433
-
434
- }
435
-
436
- } );
437
-
438
- }
439
-
440
357
  /**
441
358
  * Setup event listeners for pipeline events
442
359
  */
@@ -646,8 +563,8 @@ export class PathTracerStage extends RenderStage {
646
563
  // ASVGF's history. A real mode change is still caught by the renderMode comparison.
647
564
  this.lastInteractionModeState = false;
648
565
 
649
- // Only deterministic mode rewinds the seed axis; see _pinSeedToFrame.
650
- if ( this._pinSeedToFrame ) this._seedTick = 0;
566
+ // Only deterministic mode and lockstep readbacks rewind the seed axis; see _pinSeedToFrame.
567
+ if ( this._pinSeedToFrame || this._lockstep ) this._seedTick = 0;
651
568
 
652
569
  }
653
570
 
@@ -1406,9 +1323,8 @@ export class PathTracerStage extends RenderStage {
1406
1323
  /**
1407
1324
  * Update accumulation uniforms
1408
1325
  * @param {number} frameValue
1409
- * @param {number} renderMode
1410
1326
  */
1411
- _updateAccumulationUniforms( frameValue, renderMode ) {
1327
+ _updateAccumulationUniforms( frameValue ) {
1412
1328
 
1413
1329
  const currentInteractionMode = this.cameraOptimizer?.isInInteractionMode() ?? false;
1414
1330
  this.lastInteractionModeState = currentInteractionMode;
@@ -1545,13 +1461,8 @@ export class PathTracerStage extends RenderStage {
1545
1461
 
1546
1462
  }
1547
1463
 
1548
- setBlueNoiseTexture( tex ) {
1549
-
1550
- // Legacy API — sets the scalar STBN atlas texture
1551
- this.stbnScalarTexture = tex;
1552
- if ( tex ) stbnScalarTextureNode.value = tex;
1553
-
1554
- }
1464
+ /** @deprecated No shader reads a blue-noise texture any more; kept so existing callers do not break. */
1465
+ setBlueNoiseTexture() {}
1555
1466
 
1556
1467
  /**
1557
1468
  * Rebuild the packed light buffer from cached lightBVH + emissive data.
@@ -1745,8 +1656,6 @@ export class PathTracerStage extends RenderStage {
1745
1656
  this.storageTextures?.dispose();
1746
1657
 
1747
1658
  // Dispose textures
1748
- this.stbnScalarTexture?.dispose();
1749
- this.stbnVec2Texture?.dispose();
1750
1659
  this.placeholderTexture?.dispose();
1751
1660
 
1752
1661
  // Clear data references
@@ -59,7 +59,7 @@ export function buildDebugKernel( params ) {
59
59
  If( visMode.equal( int( 9 ) ), () => {
60
60
 
61
61
  // One ray per pixel — plain per-frame jitter (totalRays = 1 → random, no stratified lattice).
62
- const jitter = getStratifiedSample( pixelCoord, int( 0 ), int( 1 ), seed, resolution, frame );
62
+ const jitter = getStratifiedSample( pixelCoord, int( 0 ), seed, resolution, frame );
63
63
  color.assign( vec4( jitter, 1.0, 1.0 ) );
64
64
 
65
65
  } ).Else( () => {
@@ -552,7 +552,7 @@ export const sampleEmissiveTriangle = Fn( ( [
552
552
 
553
553
  export const calculateEmissiveTriangleContributionDebug = Fn( ( [
554
554
  hitPoint, normal, geomNormal, viewDir, material,
555
- bounceIndex, rngState,
555
+ _bounceIndex, rngState,
556
556
  pixelCoord, resolution, frame, dimBase,
557
557
  emissiveBoost,
558
558
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
@@ -12,7 +12,7 @@
12
12
 
13
13
  import {
14
14
  Fn, float, vec2, vec3, vec4, int, uint,
15
- If, select, instanceIndex, atomicLoad,
15
+ If, instanceIndex, atomicLoad,
16
16
  localId, workgroupId,
17
17
  } from 'three/tsl';
18
18
 
@@ -44,7 +44,6 @@ export function buildGenerateKernel( params ) {
44
44
  renderWidth,
45
45
  chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
46
46
  transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
47
- transparentBackground, // alpha inits to 1 here (megakernel parity: PathTracerCore.js:554) — env-escape-without-opaque zeroes it in Shade
48
47
  auxGBufferEnabled, // live uniform: 1 = init the per-pixel G-buffer (denoiser on), 0 = skip it
49
48
  // listDriven: 1D dispatch over the active-pixel list (activeIndicesRO[tid] = LOCAL slot) instead of 2D.
50
49
  listDriven = false, activeIndicesRO = null, counters = null,
@@ -60,11 +59,8 @@ export function buildGenerateKernel( params ) {
60
59
  const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
61
60
  const seed = pcgHash( { state: baseSeed } ).toVar();
62
61
 
63
- // Sample index 1 (not 0) so the AA sub-pixel jitter draws a DIFFERENT STBN cell
64
- // than the first-bounce BSDF sample (ShadeKernel uses sampleIndex 0). Every bounce
65
- // samples at index 0, so index 1 is collision-free — this decorrelates the sub-pixel
66
- // position from the first scatter direction (they were reading the identical cell).
67
- const stratifiedJitter = getStratifiedSample( pixelCoord, int( 1 ), int( 1 ), seed, resolution, frame ).toVar();
62
+ // Sample index 1, not the bounces' 0, so the sub-pixel position is independent of the first scatter.
63
+ const stratifiedJitter = getStratifiedSample( pixelCoord, int( 1 ), seed, resolution, frame ).toVar();
68
64
 
69
65
  // Y is subtracted because uv.y grows downward while the NDC this used to build grew upward —
70
66
  // keeps the sub-pixel sample sequence bit-identical to the pre-panorama ray gen.
package/src/TSL/Random.js CHANGED
@@ -1,7 +1,6 @@
1
1
  // Three.js Transpiler r182
2
2
 
3
- import { uniform, texture, float, If, wgslFn, uint, TWO_PI, cos, sin, vec2, sqrt, fract, mod, ivec2, select, int, vec4, mix } from 'three/tsl';
4
- import { DataTexture, FloatType } from 'three';
3
+ import { uniform, float, If, wgslFn, uint, TWO_PI, cos, sin, vec2, sqrt, select, int, vec4 } from 'three/tsl';
5
4
 
6
5
  // -----------------------------------------------------------------------------
7
6
  // Uniform declarations and constants
@@ -12,31 +11,6 @@ const samplingTechnique = samplingTechniqueUniform;
12
11
 
13
12
  // 0: PCG, 1: Halton, 2: Owen-scrambled Sobol (default; anything higher falls back to it)
14
13
 
15
- // 1x1 placeholder — real texture assigned later via .value = ...
16
- const _placeholderData = new Float32Array( [ 0.5, 0.5, 0.5, 1.0 ] );
17
-
18
- const _placeholderScalar = new DataTexture( _placeholderData, 1, 1 );
19
- _placeholderScalar.type = FloatType;
20
- _placeholderScalar.needsUpdate = true;
21
-
22
- const _placeholderVec2 = new DataTexture( new Float32Array( [ 0.5, 0.5, 0.0, 1.0 ] ), 1, 1 );
23
- _placeholderVec2.type = FloatType;
24
- _placeholderVec2.needsUpdate = true;
25
-
26
- // STBN (Spatiotemporal Blue Noise) atlas textures — Heitz 2019
27
- // Each atlas: 1024×1024, 8×8 grid of 128×128 tiles, 64 temporal slices
28
- // Scalar atlas: single-channel (R) — optimal for 1D decisions (RR, lobe selection)
29
- // Vec2 atlas: two-channel (R,G) — decorrelated 2D pairs (direction sampling Xi)
30
- export const stbnScalarTextureNode = texture( _placeholderScalar );
31
- stbnScalarTextureNode.setUpdateMatrix( false );
32
-
33
- export const stbnVec2TextureNode = texture( _placeholderVec2 );
34
- stbnVec2TextureNode.setUpdateMatrix( false );
35
-
36
- // R2 quasi-random sequence constants (Roberts 2018) — optimal 2D additive offsets
37
- const R2_A1 = float( 0.7548776662466927 );
38
- const R2_A2 = float( 0.5698402909980532 );
39
-
40
14
  // -----------------------------------------------------------------------------
41
15
  // Basic random number generation
42
16
  // -----------------------------------------------------------------------------
@@ -160,53 +134,6 @@ export const RandomPointInCircle = ( rngState ) => {
160
134
  export const SAMPLER_DIMS_PER_BOUNCE = 32;
161
135
  export const SAMPLER_DIM_AUX_BASE = 4096;
162
136
 
163
- // -----------------------------------------------------------------------------
164
- // STBN atlas sampling — spatiotemporal blue noise
165
- // -----------------------------------------------------------------------------
166
- // Atlas layout: 8×8 grid of 128×128 tiles = 1024×1024 texture.
167
- // Temporal axis: frame % 64 selects tile (true STBN temporal decorrelation).
168
- // Spatial decorrelation: R2 quasi-random offset keyed on dimension + sample index.
169
-
170
- const computeSTBNAtlasCoord = ( pixelCoords, sampleIndex, dimensionIndex, frame ) => {
171
-
172
- // Temporal slice — true STBN temporal axis
173
- const slice = uint( frame ).bitAnd( uint( 63 ) ); // frame % 64
174
-
175
- // R2 quasi-random spatial offset for per-dimension/per-sample decorrelation
176
- const n = float( dimensionIndex ).add( float( sampleIndex ).mul( 7.0 ) );
177
- const offsetX = int( fract( n.mul( R2_A1 ).add( 0.5 ) ).mul( 128.0 ) );
178
- const offsetY = int( fract( n.mul( R2_A2 ).add( 0.5 ) ).mul( 128.0 ) );
179
-
180
- // Pixel within 128×128 tile (toroidal wrap via bitmask)
181
- const px = int( pixelCoords.x ).add( offsetX ).bitAnd( int( 127 ) );
182
- const py = int( pixelCoords.y ).add( offsetY ).bitAnd( int( 127 ) );
183
-
184
- // Atlas tile position from slice index
185
- const tileCol = int( slice ).bitAnd( int( 7 ) ); // slice % 8
186
- const tileRow = int( slice ).shiftRight( int( 3 ) ); // slice / 8
187
-
188
- return ivec2( tileCol.mul( int( 128 ) ).add( px ), tileRow.mul( int( 128 ) ).add( py ) );
189
-
190
- };
191
-
192
- // Sample decorrelated 2D STBN pair in [0,1]²
193
- export const sampleSTBN2D = ( pixelCoords, sampleIndex, dimensionPairIndex, frame ) => {
194
-
195
- const coord = computeSTBNAtlasCoord( pixelCoords, sampleIndex, dimensionPairIndex, frame );
196
- const raw = stbnVec2TextureNode.load( coord ).xy;
197
-
198
- // The atlas has only 64 temporal slices, so frame N and N+64 read the same slice: the
199
- // sample repeats and accumulation stops improving past 64 frames. Decorrelate across
200
- // 64-frame cycles with a Cranley-Patterson rotation (toroidal shift) by an R2 offset
201
- // keyed on the cycle index (frame >> 6). The offset is uniform per cycle, preserving
202
- // spatial and within-window temporal blue noise; cycle 0's offset is 0, so frames
203
- // 0-63 stay bit-identical. A toroidal shift of uniform samples stays uniform (unbiased).
204
- const cycle = float( uint( frame ).shiftRight( uint( 6 ) ) );
205
- const rotation = fract( vec2( R2_A1, R2_A2 ).mul( cycle ) );
206
- return fract( raw.add( rotation ) );
207
-
208
- };
209
-
210
137
  // -----------------------------------------------------------------------------
211
138
  // Low-discrepancy sequence generators
212
139
  // -----------------------------------------------------------------------------
@@ -531,52 +458,8 @@ export const getRandomSample1D = ( pixelCoord, sampleIndex, dimensionIndex, rngS
531
458
 
532
459
  };
533
460
 
534
- // Stratified sample. Both call sites pass totalRays = 1, so only the first branch is live;
535
- // the strata path below is kept for the multi-ray-per-pixel mode that was removed.
536
-
537
- export const getStratifiedSample = ( pixelCoord, rayIndex, totalRays, rngState, resolution, frame ) => {
538
-
539
- // result variable avoids early-return ReturnNode escaping into outer Fn scope
540
- const result = vec2( 0.0 ).toVar();
541
-
542
- If( totalRays.lessThanEqual( int( 1 ) ), () => {
543
-
544
- result.assign( getRandomSample( pixelCoord, rayIndex, int( 0 ), rngState, int( - 1 ), resolution, frame ) );
545
-
546
- } ).Else( () => {
547
-
548
- // Calculate strata dimensions
549
-
550
- const strataX = int( sqrt( float( totalRays ) ) );
551
- const strataY = totalRays.add( strataX ).sub( 1 ).div( strataX );
552
- const strataIdx = mod( rayIndex, strataX.mul( strataY ) );
553
- const sx = mod( strataIdx, strataX );
554
- const sy = strataIdx.div( strataX );
555
-
556
- // Base stratified position
557
-
558
- const strataPos = vec2( float( sx ), float( sy ) ).div( vec2( float( strataX ), float( strataY ) ) );
559
-
560
- const j1 = RandomValueFast( rngState ).toVar();
561
- const j2 = RandomValueFast( rngState ).toVar();
562
- const jitter = vec2( j1, j2 ).toVar();
563
-
564
- If( totalRays.greaterThan( int( 4 ) ), () => {
565
-
566
- const stbnInfluence = sampleSTBN2D( pixelCoord, rayIndex, int( 0 ), frame ).mul( 0.1 );
567
- jitter.assign( mix( jitter, stbnInfluence, 0.2 ) );
568
-
569
- } );
570
-
571
- jitter.divAssign( vec2( float( strataX ), float( strataY ) ) );
572
-
573
- result.assign( strataPos.add( jitter ) );
574
-
575
- } );
576
-
577
- return result;
578
-
579
- };
461
+ export const getStratifiedSample = ( pixelCoord, rayIndex, rngState, resolution, frame ) =>
462
+ getRandomSample( pixelCoord, rayIndex, int( 0 ), rngState, int( - 1 ), resolution, frame );
580
463
 
581
464
  // Get decorrelated seed with better mixing
582
465
 
@@ -10,11 +10,11 @@ import {
10
10
  If, Loop, normalize, max, min, exp, log, clamp, dot, length, select, smoothstep, acos,
11
11
  instanceIndex,
12
12
  sampler,
13
- atomicAdd, atomicLoad, atomicStore, uintBitsToFloat,
13
+ atomicLoad, atomicStore,
14
14
  Return,
15
15
  } from 'three/tsl';
16
16
 
17
- import { sampleEnvironment, sampleEquirectProbability, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
17
+ import { sampleEnvironment, sampleEquirect, groundProjectedEnvDir } from './Environment.js';
18
18
  import { getMaterial, powerHeuristic, balanceHeuristic, classifyMaterial, REC709_LUMINANCE_COEFFICIENTS, PI_INV, EPSILON, diffuseGroundMaterial, offsetRayOrigin, SHADOW_END } from './Common.js';
19
19
  import { cosineWeightedSample } from './MaterialSampling.js';
20
20
  import { sampleAllMaterialTextures, processAnisotropyMap, applyExtensionMaps, getTransformedUV, triangleUVTangent } from './TextureSampling.js';
@@ -107,7 +107,7 @@ export function buildShadeKernel( params ) {
107
107
  accumFrame,
108
108
  chunkRowBase, // chunked path pool: global pixel = chunkRowBase·W + localSlot (rayID). See spec.
109
109
  emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
110
- emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
110
+ emissiveBoost, enableEmissiveTriangleSampling,
111
111
  lightBVHNodeCount, reverseMapVec4Offset,
112
112
  maxRayCount,
113
113
  // Aux G-buffer (normal/depth/albedo + surface ID) feeds only the denoiser/OIDN MRT. Gated by a
@@ -201,9 +201,9 @@ export function buildShadeKernel( params ) {
201
201
  const pixelIndex = rayID;
202
202
  const rngState = readRngState( hitBufferRW, rayID ).toVar();
203
203
 
204
- // STBN keyed on (GLOBAL pixel, dimension, frame). pixelIndex is the LOCAL path slot; the global pixel
205
- // = chunkRowBase·W + localSlot, so the blue-noise pattern stays spatially aligned across row-band chunks
206
- // (and matches Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
204
+ // Samples key on the GLOBAL pixel. pixelIndex is the LOCAL path slot; the global pixel
205
+ // = chunkRowBase·W + localSlot, so sequences stay aligned across row-band chunks
206
+ // (and match Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
207
207
  // Hoisted here rather than at the BSDF draw because the ground catcher runs NEE far earlier.
208
208
  const _resX = int( resolution.x ).toVar();
209
209
  const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
package/src/index.js CHANGED
@@ -12,7 +12,7 @@ import './TSL/patches.js';
12
12
  export { PathTracerApp, describeAdapter } from './PathTracerApp.js';
13
13
 
14
14
  // Event types
15
- export { EngineEvents } from './EngineEvents.js';
15
+ export { EngineEvents, LEGACY_EVENT_NAMES } from './EngineEvents.js';
16
16
 
17
17
  // Headless rendering — the supported entry point for a caller with no screen
18
18
  export { renderHeadless, openHeadless, captureHeadless } from './Headless.js';
@@ -25,6 +25,7 @@ export { SETTING_SOURCE } from './RenderSettings.js';
25
25
 
26
26
  // Asset URL / cache namespace overrides (call before constructing PathTracerApp)
27
27
  export { configureAssets, getAssetConfig } from './AssetConfig.js';
28
+ export { configurePlatform, getPlatform } from './Platform.js';
28
29
 
29
30
  // On-disk storage (origin private file system): caches, and areas a host defines for its own data
30
31
  export {