rayzee 7.7.0 → 7.9.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 (39) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/{BVHSubtreeWorker-D9GImjGj.js → BVHSubtreeWorker-sNzvxn66.js} +2 -2
  3. package/dist/assets/{BVHSubtreeWorker-D9GImjGj.js.map → BVHSubtreeWorker-sNzvxn66.js.map} +1 -1
  4. package/dist/assets/{BVHWorker-CNJ0UBQz.js → BVHWorker-CiVdFrwe.js} +2 -2
  5. package/dist/assets/{BVHWorker-CNJ0UBQz.js.map → BVHWorker-CiVdFrwe.js.map} +1 -1
  6. package/dist/rayzee.es.js +2057 -2035
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +55 -55
  9. package/dist/rayzee.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/EngineDefaults.js +45 -1
  12. package/src/EngineEvents.js +1 -0
  13. package/src/Passes/OIDNDenoiser.js +25 -1
  14. package/src/PathTracerApp.js +36 -0
  15. package/src/Processor/AssetLoader.js +3 -0
  16. package/src/Processor/GeometryExtractor.js +35 -9
  17. package/src/Processor/PackedRayBuffer.js +15 -1
  18. package/src/Processor/QueueManager.js +13 -1
  19. package/src/Processor/SceneProcessor.js +176 -113
  20. package/src/Processor/ShaderBuilder.js +6 -24
  21. package/src/Processor/TextureCreator.js +16 -32
  22. package/src/Processor/Workers/BVHWorker.js +6 -1
  23. package/src/Stages/EdgeFilter.js +4 -3
  24. package/src/Stages/MotionVector.js +4 -4
  25. package/src/Stages/NormalDepth.js +20 -30
  26. package/src/Stages/PathTracer.js +20 -40
  27. package/src/TSL/DebugKernel.js +0 -2
  28. package/src/TSL/Debugger.js +1 -8
  29. package/src/TSL/Displacement.js +10 -10
  30. package/src/TSL/GenerateKernel.js +5 -1
  31. package/src/TSL/LightsDirect.js +8 -9
  32. package/src/TSL/LightsIndirect.js +15 -9
  33. package/src/TSL/LightsSampling.js +9 -19
  34. package/src/TSL/Random.js +11 -1
  35. package/src/TSL/ShadeKernel.js +1 -6
  36. package/src/TSL/TextureSampling.js +155 -34
  37. package/src/managers/EnvironmentManager.js +11 -0
  38. package/src/managers/MaterialDataManager.js +71 -39
  39. package/src/managers/RenderTargetManager.js +0 -522
@@ -1,18 +1,19 @@
1
1
  import { Fn, vec3, vec4, float, int, uint, uvec2, uniform, normalize, mat3, storage, If,
2
- texture, textureStore, workgroupId, localId } from 'three/tsl';
2
+ textureStore, workgroupId, localId } from 'three/tsl';
3
3
  import { RenderTarget, StorageTexture } from 'three/webgpu';
4
- import { HalfFloatType, RGBAFormat, NearestFilter, LinearFilter, DataArrayTexture, Matrix4, Box2, Vector2 } from 'three';
4
+ import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
5
5
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
6
6
  import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
7
7
  import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
8
8
  import { traverseBVH } from '../TSL/BVHTraversal.js';
9
9
  import { getMaterial } from '../TSL/Common.js';
10
- import { computeUVCache, processNormal, processBump } from '../TSL/TextureSampling.js';
10
+ import { computeUVCache, processNormal, processBump, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
11
11
 
12
12
  /**
13
13
  * NormalDepth — primary-ray G-buffer for SVGF gates.
14
14
  *
15
- * RGB = geometric world normal · 0.5 + 0.5, A = linear ray distance (sky=1e6).
15
+ * RGB = geometric world normal · 0.5 + 0.5, A = linear ray distance (sky=65504, the
16
+ * max HalfFloat value — a finite sentinel so miss−miss depth diffs stay 0, not Inf−Inf=NaN).
16
17
  * Geometric (not shading) normals because shading normals carry sub-pixel
17
18
  * jitter that breaks the temporal gate's same-pixel-across-frames comparison.
18
19
  * The path tracer's MRT already carries shading normals for OIDN; this stage
@@ -104,22 +105,10 @@ export class NormalDepth extends RenderStage {
104
105
  this._computeNode = null;
105
106
  this._computeBuilt = false;
106
107
 
107
- // Normal/bump map array nodes persistent placeholders, value swapped to
108
- // the real DataArrayTextures on model load. processNormal/processBump
109
- // runtime-guard on map indices, so the placeholder is never sampled.
110
- this._normalMapsTex = texture( this._makePlaceholderArray() );
111
- this._bumpMapsTex = texture( this._makePlaceholderArray() );
112
-
113
- }
114
-
115
- _makePlaceholderArray() {
116
-
117
- const t = new DataArrayTexture( new Uint8Array( [ 128, 128, 255, 255 ] ), 1, 1, 1 );
118
- t.minFilter = LinearFilter;
119
- t.magFilter = LinearFilter;
120
- t.generateMipmaps = false;
121
- t.needsUpdate = true;
122
- return t;
108
+ // Independent linear-pool bucket nodes for this pipeline (normal + bump live in the
109
+ // linear pool). Built lazily in _buildCompute from the path tracer's materialData;
110
+ // value-swapped on model load. processNormal/processBump runtime-guard on map indices.
111
+ this._linearBuckets = null;
123
112
 
124
113
  }
125
114
 
@@ -185,10 +174,8 @@ export class NormalDepth extends RenderStage {
185
174
 
186
175
  }
187
176
 
188
- // In-place map swaps (model change) — graph closes over the node, only .value changes.
189
- const md = pt.materialData;
190
- if ( md?.normalMaps ) this._normalMapsTex.value = md.normalMaps;
191
- if ( md?.bumpMaps ) this._bumpMapsTex.value = md.bumpMaps;
177
+ // In-place bucket swaps (model change) — graph closes over the nodes, only .value changes.
178
+ if ( this._linearBuckets ) refreshBucketTextureNodes( this._linearBuckets, pt.materialData?.linearBuckets );
192
179
 
193
180
  this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
194
181
  this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
@@ -203,8 +190,11 @@ export class NormalDepth extends RenderStage {
203
190
  const triStorage = this._triStorageNode;
204
191
  const bvhStorage = this._bvhStorageNode;
205
192
  const matStorage = this._matStorageNode;
206
- const normalMaps = this._normalMapsTex;
207
- const bumpMaps = this._bumpMapsTex;
193
+ // Independent linear-pool bucket nodes for this pipeline (normal + bump). The sRGB pool
194
+ // is never sampled here, so it gets placeholders. Publish to the sampling module before
195
+ // the graph is built so processNormal/processBump bake in THESE (per-pipeline) nodes.
196
+ this._linearBuckets = buildBucketTextureNodes( this.pathTracer?.materialData?.linearBuckets );
197
+ setMaterialBucketTextures( buildBucketTextureNodes( null ), this._linearBuckets );
208
198
  const camWorld = this.cameraWorldMatrix;
209
199
  const camProjInv = this.cameraProjectionMatrixInverse;
210
200
  const resW = this.resolutionWidth;
@@ -248,7 +238,7 @@ export class NormalDepth extends RenderStage {
248
238
 
249
239
  const result = hit.didHit.select(
250
240
  vec4( encodedNormal, depth ),
251
- vec4( 0.0, 0.0, 0.0, float( 1e6 ) )
241
+ vec4( 0.0, 0.0, 0.0, float( 65504.0 ) )
252
242
  );
253
243
 
254
244
  textureStore(
@@ -266,14 +256,14 @@ export class NormalDepth extends RenderStage {
266
256
  getMaterial( hit.materialIndex, matStorage )
267
257
  ).toVar();
268
258
  const uvCache = UVCache.wrap( computeUVCache( hit.uv, material ) ).toVar();
269
- const mapped = processNormal( normalMaps, hit.normal, material, uvCache ).toVar();
270
- shadingNormal.assign( processBump( bumpMaps, mapped, material, uvCache ) );
259
+ const mapped = processNormal( hit.normal, material, uvCache ).toVar();
260
+ shadingNormal.assign( processBump( mapped, material, uvCache ) );
271
261
 
272
262
  } );
273
263
 
274
264
  const shadingResult = hit.didHit.select(
275
265
  vec4( shadingNormal.mul( 0.5 ).add( 0.5 ), depth ),
276
- vec4( 0.0, 0.0, 0.0, float( 1e6 ) )
266
+ vec4( 0.0, 0.0, 0.0, float( 65504.0 ) )
277
267
  );
278
268
 
279
269
  textureStore(
@@ -7,7 +7,7 @@
7
7
  import { uniform, texture, storage } from 'three/tsl';
8
8
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
9
9
  import { PathTracerStage } from './PathTracerStage.js';
10
- import { PackedRayBuffer, GBUFFER_STRIDE } from '../Processor/PackedRayBuffer.js';
10
+ import { PackedRayBuffer, GBUFFER_STRIDE, freeStorageAttribute } from '../Processor/PackedRayBuffer.js';
11
11
  import { QueueManager, COUNTER } from '../Processor/QueueManager.js';
12
12
  import { VRAMTracker } from '../Processor/VRAMTracker.js';
13
13
  import { KernelManager } from '../Processor/KernelManager.js';
@@ -17,6 +17,8 @@ import { buildShadeKernel, SHADE_WG_SIZE } from '../TSL/ShadeKernel.js';
17
17
  import { buildCompactKernel, buildCompactSubgroupKernel, COMPACT_WG_SIZE } from '../TSL/CompactKernel.js';
18
18
  import { buildFinalWriteKernel, FINALWRITE_WG_SIZE } from '../TSL/FinalWriteKernel.js';
19
19
  import { buildDebugKernel, DEBUG_WG_SIZE } from '../TSL/DebugKernel.js';
20
+ import { setMaterialBucketTextures, buildBucketTextureNodes, refreshBucketTextureNodes } from '../TSL/TextureSampling.js';
21
+ import { setShadowAlbedoMaps } from '../TSL/LightsDirect.js';
20
22
  import {
21
23
  buildResetGlobalHistKernel, buildGlobalHistKernel, buildGlobalPrefixKernel, buildGlobalScatterKernel,
22
24
  SORT_GLOBAL_WG_SIZE, SORT_GLOBAL_MAX_BINS,
@@ -126,8 +128,8 @@ export class PathTracer extends PathTracerStage {
126
128
  if ( ! m ) return null;
127
129
  return [
128
130
  m.materialStorageAttr,
129
- m.albedoMaps, m.emissiveMaps, m.normalMaps, m.bumpMaps,
130
- m.roughnessMaps, m.metalnessMaps, m.displacementMaps,
131
+ ...( m.srgbBuckets || [] ).filter( Boolean ),
132
+ ...( m.linearBuckets || [] ).filter( Boolean ),
131
133
  ];
132
134
 
133
135
  } );
@@ -449,13 +451,8 @@ export class PathTracer extends PathTracerStage {
449
451
 
450
452
  const mat = this.materialData;
451
453
  if ( ! mat ) return;
452
- if ( mat.albedoMaps && t.albedoMaps ) t.albedoMaps.value = mat.albedoMaps;
453
- if ( mat.normalMaps && t.normalMaps ) t.normalMaps.value = mat.normalMaps;
454
- if ( mat.bumpMaps && t.bumpMaps ) t.bumpMaps.value = mat.bumpMaps;
455
- if ( mat.metalnessMaps && t.metalnessMaps ) t.metalnessMaps.value = mat.metalnessMaps;
456
- if ( mat.roughnessMaps && t.roughnessMaps ) t.roughnessMaps.value = mat.roughnessMaps;
457
- if ( mat.emissiveMaps && t.emissiveMaps ) t.emissiveMaps.value = mat.emissiveMaps;
458
- if ( mat.displacementMaps && t.displacementMaps ) t.displacementMaps.value = mat.displacementMaps;
454
+ refreshBucketTextureNodes( t.srgbBuckets, mat.srgbBuckets );
455
+ refreshBucketTextureNodes( t.linearBuckets, mat.linearBuckets );
459
456
 
460
457
  }
461
458
 
@@ -527,7 +524,7 @@ export class PathTracer extends PathTracerStage {
527
524
 
528
525
  if ( ! this._packedBuffers ) {
529
526
 
530
- this._packedBuffers = new PackedRayBuffer( maxRays );
527
+ this._packedBuffers = new PackedRayBuffer( maxRays, this.renderer );
531
528
 
532
529
  } else {
533
530
 
@@ -541,13 +538,14 @@ export class PathTracer extends PathTracerStage {
541
538
  // per-pixel, written by Generate/Shade bounce-0 and read only by FinalWrite.
542
539
  // 1.25× margin (same as the per-ray buffers) so it survives the in-place-resize range.
543
540
  const gBufferVec4s = PackedRayBuffer.requiredCapacity( maxRays ) * GBUFFER_STRIDE;
541
+ freeStorageAttribute( this.renderer, this._gBufferAttr ); // free the outgoing G-buffer on capacity growth
544
542
  this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
545
543
  const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
546
544
  const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
547
545
 
548
546
  if ( ! this._queueManager ) {
549
547
 
550
- this._queueManager = new QueueManager( this._packedBuffers.capacity );
548
+ this._queueManager = new QueueManager( this._packedBuffers.capacity, this.renderer );
551
549
 
552
550
  } else {
553
551
 
@@ -661,26 +659,21 @@ export class PathTracer extends PathTracerStage {
661
659
  // Independent texture nodes (never compiled elsewhere) avoid Three.js TextureNode caching across pipelines; refreshed via _refreshWfTextureNodes.
662
660
  const _mat = this.materialData;
663
661
  const _env = this.environment;
664
- const _placeholder = texNodes.albedoMapsTex;
665
- const freshAlbedoMaps = _mat.albedoMaps ? texture( _mat.albedoMaps ) : _placeholder;
666
- const freshNormalMaps = _mat.normalMaps ? texture( _mat.normalMaps ) : texNodes.normalMapsTex;
667
- const freshBumpMaps = _mat.bumpMaps ? texture( _mat.bumpMaps ) : texNodes.bumpMapsTex;
668
- const freshMetalnessMaps = _mat.metalnessMaps ? texture( _mat.metalnessMaps ) : texNodes.metalnessMapsTex;
669
- const freshRoughnessMaps = _mat.roughnessMaps ? texture( _mat.roughnessMaps ) : texNodes.roughnessMapsTex;
670
- const freshEmissiveMaps = _mat.emissiveMaps ? texture( _mat.emissiveMaps ) : texNodes.emissiveMapsTex;
671
- const freshDisplacementMaps = _mat.displacementMaps ? texture( _mat.displacementMaps ) : texNodes.displacementMapsTex;
662
+ // Consolidated size-bucket nodes (K sRGB + K linear). Empty buckets get placeholders so
663
+ // every runtime branch references a valid node. Published to the sampling module before
664
+ // the Shade/Debug graphs are built so they bake in these (per-pipeline) nodes.
665
+ const freshSrgbBuckets = buildBucketTextureNodes( _mat.srgbBuckets );
666
+ const freshLinearBuckets = buildBucketTextureNodes( _mat.linearBuckets );
667
+ setMaterialBucketTextures( freshSrgbBuckets, freshLinearBuckets );
668
+ // Alpha-cutout shadow rays sample albedo (sRGB pool) emitted into the shade graph now.
669
+ setShadowAlbedoMaps( freshSrgbBuckets );
672
670
  const freshEnvTex = _env.environmentTexture ? texture( _env.environmentTexture ) : texNodes.envTex;
673
671
 
674
672
  this._wfTexNodes = {
675
673
  envTex: freshEnvTex,
676
674
  envCDFTex: freshEnvCDF,
677
- albedoMaps: freshAlbedoMaps,
678
- normalMaps: freshNormalMaps,
679
- bumpMaps: freshBumpMaps,
680
- metalnessMaps: freshMetalnessMaps,
681
- roughnessMaps: freshRoughnessMaps,
682
- emissiveMaps: freshEmissiveMaps,
683
- displacementMaps: freshDisplacementMaps,
675
+ srgbBuckets: freshSrgbBuckets,
676
+ linearBuckets: freshLinearBuckets,
684
677
  };
685
678
 
686
679
  // Material-coherence sort gate (experiment): only worthwhile above a few materials.
@@ -761,13 +754,6 @@ export class PathTracer extends PathTracerStage {
761
754
  hitBufferRO: pb.hitBuffer.ro,
762
755
  counters,
763
756
  activeIndicesRO: this._sortMaterials ? qm.getSortedRO() : qm.getActiveReadRO(),
764
- albedoMaps: freshAlbedoMaps,
765
- normalMaps: freshNormalMaps,
766
- bumpMaps: freshBumpMaps,
767
- metalnessMaps: freshMetalnessMaps,
768
- roughnessMaps: freshRoughnessMaps,
769
- emissiveMaps: freshEmissiveMaps,
770
- displacementMaps: freshDisplacementMaps,
771
757
  envTexture: freshEnvTex,
772
758
  environmentIntensity: this.environmentIntensity,
773
759
  envMatrix: this.environmentMatrix,
@@ -926,12 +912,6 @@ export class PathTracer extends PathTracerStage {
926
912
  enableEnvironmentLight: this.enableEnvironment,
927
913
  visMode: this.visMode,
928
914
  debugVisScale: this.debugVisScale,
929
- albedoMaps: freshAlbedoMaps,
930
- normalMaps: freshNormalMaps,
931
- bumpMaps: freshBumpMaps,
932
- metalnessMaps: freshMetalnessMaps,
933
- roughnessMaps: freshRoughnessMaps,
934
- emissiveMaps: freshEmissiveMaps,
935
915
  frame: this.frame,
936
916
  } );
937
917
  this._kernelManager.register( 'debug',
@@ -30,7 +30,6 @@ export function buildDebugKernel( params ) {
30
30
  bvhBuffer, triangleBuffer, materialBuffer,
31
31
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
32
32
  visMode, debugVisScale,
33
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
34
33
  frame,
35
34
  } = params;
36
35
 
@@ -73,7 +72,6 @@ export function buildDebugKernel( params ) {
73
72
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
74
73
  visMode, debugVisScale,
75
74
  pixelCoord, resolution,
76
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
77
75
  cameraProjectionMatrix, cameraViewMatrix,
78
76
  frame,
79
77
  ) );
@@ -71,9 +71,7 @@ export const TraceDebugMode = Fn( ( [
71
71
  visMode, debugVisScale,
72
72
  // Screen info
73
73
  pixelCoord, resolution,
74
- // Texture arrays (for MRT debug modes)
75
- albedoMaps, normalMaps, bumpMaps,
76
- metalnessMaps, roughnessMaps, emissiveMaps,
74
+ // Material textures are read from the module-level bucket nodes (setMaterialBucketTextures).
77
75
  // Camera matrices (for depth debug mode)
78
76
  cameraProjectionMatrix, cameraViewMatrix,
79
77
  // Frame counter (for stochastic debug modes)
@@ -204,7 +202,6 @@ export const TraceDebugMode = Fn( ( [
204
202
 
205
203
  // Sample all textures to get emissive
206
204
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
207
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
208
205
  material, hitInfo.uv, hitInfo.normal,
209
206
  ) ).toVar();
210
207
 
@@ -249,7 +246,6 @@ export const TraceDebugMode = Fn( ( [
249
246
 
250
247
  // Get material-mapped normal (same as what's used in main shader)
251
248
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
252
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
253
249
  material, hitInfo.uv, hitInfo.normal,
254
250
  ) ).toVar();
255
251
 
@@ -296,7 +292,6 @@ export const TraceDebugMode = Fn( ( [
296
292
  const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
297
293
 
298
294
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
299
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
300
295
  material, hitInfo.uv, hitInfo.normal,
301
296
  ) ).toVar();
302
297
 
@@ -323,7 +318,6 @@ export const TraceDebugMode = Fn( ( [
323
318
  // Get primary surface material
324
319
  const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
325
320
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
326
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
327
321
  material, hitInfo.uv, hitInfo.normal,
328
322
  ) ).toVar();
329
323
 
@@ -370,7 +364,6 @@ export const TraceDebugMode = Fn( ( [
370
364
  // Bounce hit surface B — evaluate its contribution
371
365
  const materialB = RayTracingMaterial.wrap( getMaterial( bounceHit.materialIndex, materialBuffer ) ).toVar();
372
366
  const matSamplesB = MaterialSamples.wrap( sampleAllMaterialTextures(
373
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
374
367
  materialB, bounceHit.uv, bounceHit.normal,
375
368
  ) ).toVar();
376
369
 
@@ -1,8 +1,8 @@
1
- import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, textureSize } from 'three/tsl';
1
+ import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max } from 'three/tsl';
2
2
 
3
3
  import { struct } from './patches.js';
4
4
  import { getDatafromStorageBuffer } from './Common.js';
5
- import { sampleDisplacementMap } from './TextureSampling.js';
5
+ import { sampleDisplacementMap, bucketTexelSize, getLinearBucketTextures } from './TextureSampling.js';
6
6
 
7
7
  // Ray-displacement intersection configuration
8
8
  const MAX_MARCH_STEPS = 32;
@@ -32,7 +32,7 @@ export const DisplacementResult = struct( {
32
32
  * We find dt where h_ray(dt) = h_surf(dt).
33
33
  */
34
34
  export const refineDisplacedIntersection = Fn( ( [
35
- ray, hitInfo, triangleBuffer, displacementMaps, material, bounceIndex
35
+ ray, hitInfo, triangleBuffer, material, bounceIndex
36
36
  ] ) => {
37
37
 
38
38
  const resultHitPoint = hitInfo.hitPoint.toVar();
@@ -135,7 +135,7 @@ export const refineDisplacedIntersection = Fn( ( [
135
135
 
136
136
  // Displaced surface height
137
137
  const heightSample = sampleDisplacementMap(
138
- displacementMaps, material.displacementMapIndex, marchUV, material.displacementTransform,
138
+ material.displacementMapIndex, marchUV, material.displacementTransform,
139
139
  );
140
140
  const surfaceHeight = heightSample.sub( 0.5 ).mul( scale );
141
141
 
@@ -167,7 +167,7 @@ export const refineDisplacedIntersection = Fn( ( [
167
167
  const midUV = hitInfo.uv.add( dUV_dt.mul( midDt ) );
168
168
  const midRayHeight = midDt.mul( dh_ray_dt );
169
169
  const midSample = sampleDisplacementMap(
170
- displacementMaps, material.displacementMapIndex, midUV, material.displacementTransform,
170
+ material.displacementMapIndex, midUV, material.displacementTransform,
171
171
  );
172
172
  const midSurfHeight = midSample.sub( 0.5 ).mul( scale );
173
173
 
@@ -189,19 +189,19 @@ export const refineDisplacedIntersection = Fn( ( [
189
189
  const finalPoint = hitInfo.hitPoint.add( rayDir.mul( finalDt ) );
190
190
 
191
191
  const finalHeight = sampleDisplacementMap(
192
- displacementMaps, material.displacementMapIndex, finalUV, material.displacementTransform,
192
+ material.displacementMapIndex, finalUV, material.displacementTransform,
193
193
  );
194
194
  const displacedHeight = finalHeight.sub( 0.5 ).mul( scale );
195
195
 
196
196
  // Compute displaced normal from height-field gradients using UV tangent vectors.
197
- // Finite-difference step = one texel of the actual displacement-array dimensions.
198
- const texel = vec2( 1.0 ).div( vec2( textureSize( displacementMaps ) ) ).toVar();
197
+ // Finite-difference step = one texel of the SELECTED bucket array's real dimensions.
198
+ const texel = bucketTexelSize( getLinearBucketTextures(), material.displacementMapIndex ).toVar();
199
199
  const hC = finalHeight;
200
200
  const hU = sampleDisplacementMap(
201
- displacementMaps, material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
201
+ material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
202
202
  );
203
203
  const hV = sampleDisplacementMap(
204
- displacementMaps, material.displacementMapIndex, finalUV.add( vec2( 0.0, texel.y ) ), material.displacementTransform,
204
+ material.displacementMapIndex, finalUV.add( vec2( 0.0, texel.y ) ), material.displacementTransform,
205
205
  );
206
206
 
207
207
  // Perturb normal using actual tangent/bitangent from UV parameterization
@@ -58,7 +58,11 @@ export function buildGenerateKernel( params ) {
58
58
  const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
59
59
  const seed = pcgHash( { state: baseSeed } ).toVar();
60
60
 
61
- const stratifiedJitter = getStratifiedSample( pixelCoord, int( 0 ), int( 1 ), seed, resolution, frame ).toVar();
61
+ // Sample index 1 (not 0) so the AA sub-pixel jitter draws a DIFFERENT STBN cell
62
+ // than the first-bounce BSDF sample (ShadeKernel uses sampleIndex 0). Every bounce
63
+ // samples at index 0, so index 1 is collision-free — this decorrelates the sub-pixel
64
+ // position from the first scatter direction (they were reading the identical cell).
65
+ const stratifiedJitter = getStratifiedSample( pixelCoord, int( 1 ), int( 1 ), seed, resolution, frame ).toVar();
62
66
 
63
67
  const jitterScale = vec2( 2.0 ).div( resolution );
64
68
  const jitter = stratifiedJitter.sub( 0.5 ).mul( jitterScale );
@@ -22,28 +22,27 @@ import {
22
22
  clamp,
23
23
  smoothstep,
24
24
  select,
25
- texture,
26
25
  } from 'three/tsl';
27
26
 
28
27
  import { Ray, ShadowMaterial, HitInfo } from './Struct.js';
29
28
  import { REC709_LUMINANCE_COEFFICIENTS, getShadowMaterial, getDatafromStorageBuffer } from './Common.js';
30
29
  import { fresnelSchlickFloat, iorToFresnel0 } from './Fresnel.js';
31
30
  import { calculateBeerLawAbsorption } from './MaterialTransmission.js';
32
- import { getTransformedUV } from './TextureSampling.js';
31
+ import { getTransformedUV, sampleBucket } from './TextureSampling.js';
33
32
 
34
33
  // Module-level state for alpha-cutout shadow testing.
35
- // Set by ShaderBuilder before graph construction.
34
+ // Set by PathTracer before shade-graph construction.
36
35
  let _shadowAlbedoMaps = null;
37
36
  let _enableAlphaShadows = null;
38
37
 
39
38
  /**
40
- * Set the albedo texture array node for alpha-aware shadow rays.
41
- * Must be called before the shader graph is constructed.
42
- * @param {TextureNode} maps - TSL texture node for the albedo array
39
+ * Set the sRGB bucket texture node array for alpha-aware shadow rays (albedo alpha).
40
+ * Must be called before the shade graph is constructed.
41
+ * @param {Array} buckets - K sRGB bucket texture nodes
43
42
  */
44
- export function setShadowAlbedoMaps( maps ) {
43
+ export function setShadowAlbedoMaps( buckets ) {
45
44
 
46
- _shadowAlbedoMaps = maps;
45
+ _shadowAlbedoMaps = buckets;
47
46
 
48
47
  }
49
48
 
@@ -137,7 +136,7 @@ export const traceShadowRay = Fn( ( [
137
136
  const uvData2 = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 7 ), TRI_STRIDE );
138
137
  const hitUV = uvData1.xy.mul( baryW ).add( uvData1.zw.mul( baryU ) ).add( uvData2.xy.mul( baryV ) );
139
138
  const albedoUV = getTransformedUV( { uv: hitUV, transform: shadowMaterial.albedoTransform } );
140
- texAlpha.assign( texture( _shadowAlbedoMaps, albedoUV ).depth( int( shadowMaterial.albedoMapIndex ) ).a );
139
+ texAlpha.assign( sampleBucket( _shadowAlbedoMaps, shadowMaterial.albedoMapIndex, albedoUV ).a );
141
140
 
142
141
  } );
143
142
 
@@ -290,6 +290,9 @@ export const calculateIndirectLighting = Fn( ( [
290
290
  const sampleDir = vec3( 0.0 ).toVar();
291
291
  const samplePdf = float( 0.0 ).toVar();
292
292
  const sampleBrdfValue = vec3( 0.0 ).toVar();
293
+ // Transmission lobe uses this weight instead of a reflection-BRDF eval, which is
294
+ // invalid for the below-surface refraction direction.
295
+ const transColorWeight = vec3( 1.0 ).toVar();
293
296
 
294
297
  // Execute selected strategy (chained If/ElseIf/Else for exclusive branches)
295
298
  // Environment removed — handled via deterministic NEE in direct lighting
@@ -312,13 +315,16 @@ export const calculateIndirectLighting = Fn( ( [
312
315
 
313
316
  // Strategy 3: Transmission
314
317
  const entering = dot( V, N ).greaterThan( 0.0 );
315
- // pathWavelength=0 — MIS evaluation reads only direction/PDF, no spectral tint
316
318
  const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
317
319
  V, N, material.ior, material.roughness, entering, material.dispersion, sampleRand, rngState, float( 0.0 )
318
320
  ).toVar() );
319
321
  sampleDir.assign( mtResult.direction );
320
322
  samplePdf.assign( mtResult.pdf );
321
- sampleBrdfValue.assign( evaluateMaterialResponse( V, sampleDir, N, material ) );
323
+ // evaluateMaterialResponse is reflection-only and returns a non-physical value for
324
+ // the below-surface refraction direction (its dots are floored to 0.001). Carry the
325
+ // sampler's spectral tint and apply an energy-consistent transmission throughput
326
+ // below (mirrors handleTransmission's material.color × colorWeight convention).
327
+ transColorWeight.assign( mtResult.colorWeight );
322
328
 
323
329
  } ).Else( () => {
324
330
 
@@ -329,10 +335,7 @@ export const calculateIndirectLighting = Fn( ( [
329
335
 
330
336
  } );
331
337
 
332
- // For transmission directions (below surface), use |cos| instead of max(cos, 0)
333
- const rawNoL = dot( N, sampleDir ).toVar();
334
- const NoL = max( rawNoL, 0.0 ).toVar();
335
- const absNoL = abs( rawNoL );
338
+ const NoL = max( dot( N, sampleDir ), 0.0 ).toVar();
336
339
 
337
340
  // Calculate combined PDF for MIS (material strategies only)
338
341
  const combinedPdf = float( 0.0 ).toVar();
@@ -380,11 +383,14 @@ export const calculateIndirectLighting = Fn( ( [
380
383
  // MIS weight calculation
381
384
  const misWeight = samplePdf.div( combinedPdf ).toVar();
382
385
 
383
- // Throughput calculation: use |cos| for transmission, max(cos,0) for reflection strategies
384
- const cosineWeight = select( selectedStrategy.equal( int( 3 ) ), absNoL, NoL );
386
+ // Reflection lobes: f·NoL·mis/pdf. Transmission lobe: the sampler's energy-consistent
387
+ // weight (material tint × dispersion colorWeight × mis) the reflection BRDF value is
388
+ // invalid below the surface, so it is not used for transmission.
389
+ const reflThroughput = sampleBrdfValue.mul( NoL ).mul( misWeight ).div( samplePdf );
390
+ const transThroughput = material.color.xyz.mul( transColorWeight ).mul( misWeight );
385
391
 
386
392
  r_direction.assign( sampleDir );
387
- r_throughput.assign( sampleBrdfValue.mul( cosineWeight ).mul( misWeight ).div( samplePdf ) );
393
+ r_throughput.assign( select( selectedStrategy.equal( int( 3 ) ), transThroughput, reflThroughput ) );
388
394
  r_misWeight.assign( misWeight );
389
395
  r_pdf.assign( samplePdf );
390
396
  r_combinedPdf.assign( combinedPdf );
@@ -360,7 +360,6 @@ export const sampleLightWithImportance = Fn( ( [
360
360
  If( totalLights.greaterThan( int( 0 ) ), () => {
361
361
 
362
362
  const totalWeight = float( 0.0 ).toVar();
363
- const lightIndex = int( 0 ).toVar();
364
363
 
365
364
  // Reservoir state: winning light's type/index/importance.
366
365
  const selectedType = int( - 1 ).toVar(); // 0=dir, 1=area, 2=point, 3=spot
@@ -375,7 +374,7 @@ export const sampleLightWithImportance = Fn( ( [
375
374
 
376
375
  Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
377
376
 
378
- If( lightIndex.lessThan( int( 16 ) ), () => {
377
+ If( i.lessThan( int( 16 ) ), () => {
379
378
 
380
379
  const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
381
380
  const importance = calculateDirectionalLightImportance( light, normal, material, bounceIndex ).toVar();
@@ -389,7 +388,6 @@ export const sampleLightWithImportance = Fn( ( [
389
388
  selectedImportance.assign( importance );
390
389
 
391
390
  } );
392
- lightIndex.addAssign( 1 );
393
391
 
394
392
  } );
395
393
 
@@ -401,7 +399,7 @@ export const sampleLightWithImportance = Fn( ( [
401
399
 
402
400
  Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
403
401
 
404
- If( lightIndex.lessThan( int( 16 ) ), () => {
402
+ If( i.lessThan( int( 16 ) ), () => {
405
403
 
406
404
  const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
407
405
  const importance = select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ).toVar();
@@ -415,7 +413,6 @@ export const sampleLightWithImportance = Fn( ( [
415
413
  selectedImportance.assign( importance );
416
414
 
417
415
  } );
418
- lightIndex.addAssign( 1 );
419
416
 
420
417
  } );
421
418
 
@@ -427,7 +424,7 @@ export const sampleLightWithImportance = Fn( ( [
427
424
 
428
425
  Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
429
426
 
430
- If( lightIndex.lessThan( int( 16 ) ), () => {
427
+ If( i.lessThan( int( 16 ) ), () => {
431
428
 
432
429
  const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
433
430
  const importance = calculatePointLightImportance( light, rayOrigin, normal, material ).toVar();
@@ -441,7 +438,6 @@ export const sampleLightWithImportance = Fn( ( [
441
438
  selectedImportance.assign( importance );
442
439
 
443
440
  } );
444
- lightIndex.addAssign( 1 );
445
441
 
446
442
  } );
447
443
 
@@ -453,7 +449,7 @@ export const sampleLightWithImportance = Fn( ( [
453
449
 
454
450
  Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
455
451
 
456
- If( lightIndex.lessThan( int( 16 ) ), () => {
452
+ If( i.lessThan( int( 16 ) ), () => {
457
453
 
458
454
  const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
459
455
  const importance = calculateSpotLightImportance( light, rayOrigin, normal, material ).toVar();
@@ -467,7 +463,6 @@ export const sampleLightWithImportance = Fn( ( [
467
463
  selectedImportance.assign( importance );
468
464
 
469
465
  } );
470
- lightIndex.addAssign( 1 );
471
466
 
472
467
  } );
473
468
 
@@ -762,7 +757,7 @@ export const calculateMaterialPDF = Fn( ( [ viewDir, lightDir, normal, material
762
757
  } );
763
758
 
764
759
  // Total light-selection weight at a shading point, replicating the NEE reservoir's
765
- // importance accumulation (same per-type importance fns, same 16-light cap + order).
760
+ // importance accumulation (same per-type importance fns, same per-type 16-light cap + order).
766
761
  // Used to reconstruct the exact NEE selection pdf for MIS on the BSDF-hit path —
767
762
  // the analogue of Cycles' light_tree_pdf re-walk. Without it, the BSDF-hit MIS
768
763
  // partner assumes uniform 1/N selection, which disagrees with the importance-
@@ -776,17 +771,15 @@ export const computeTotalLightImportance = Fn( ( [
776
771
  ] ) => {
777
772
 
778
773
  const totalWeight = float( 0.0 ).toVar();
779
- const lightIndex = int( 0 ).toVar();
780
774
 
781
775
  If( numDirectionalLights.greaterThan( int( 0 ) ), () => {
782
776
 
783
777
  Loop( { start: int( 0 ), end: numDirectionalLights, type: 'int', condition: '<' }, ( { i } ) => {
784
778
 
785
- If( lightIndex.lessThan( int( 16 ) ), () => {
779
+ If( i.lessThan( int( 16 ) ), () => {
786
780
 
787
781
  const light = DirectionalLight.wrap( getDirectionalLight( directionalLightsBuffer, i ) );
788
782
  totalWeight.addAssign( calculateDirectionalLightImportance( light, normal, material, bounceIndex ) );
789
- lightIndex.addAssign( 1 );
790
783
 
791
784
  } );
792
785
 
@@ -798,11 +791,10 @@ export const computeTotalLightImportance = Fn( ( [
798
791
 
799
792
  Loop( { start: int( 0 ), end: numAreaLights, type: 'int', condition: '<' }, ( { i } ) => {
800
793
 
801
- If( lightIndex.lessThan( int( 16 ) ), () => {
794
+ If( i.lessThan( int( 16 ) ), () => {
802
795
 
803
796
  const light = AreaLight.wrap( getAreaLight( areaLightsBuffer, i ) );
804
797
  totalWeight.addAssign( select( light.intensity.greaterThan( 0.0 ), estimateLightImportance( light, rayOrigin, normal, material ), float( 0.0 ) ) );
805
- lightIndex.addAssign( 1 );
806
798
 
807
799
  } );
808
800
 
@@ -814,11 +806,10 @@ export const computeTotalLightImportance = Fn( ( [
814
806
 
815
807
  Loop( { start: int( 0 ), end: numPointLights, type: 'int', condition: '<' }, ( { i } ) => {
816
808
 
817
- If( lightIndex.lessThan( int( 16 ) ), () => {
809
+ If( i.lessThan( int( 16 ) ), () => {
818
810
 
819
811
  const light = PointLight.wrap( getPointLight( pointLightsBuffer, i ) );
820
812
  totalWeight.addAssign( calculatePointLightImportance( light, rayOrigin, normal, material ) );
821
- lightIndex.addAssign( 1 );
822
813
 
823
814
  } );
824
815
 
@@ -830,11 +821,10 @@ export const computeTotalLightImportance = Fn( ( [
830
821
 
831
822
  Loop( { start: int( 0 ), end: numSpotLights, type: 'int', condition: '<' }, ( { i } ) => {
832
823
 
833
- If( lightIndex.lessThan( int( 16 ) ), () => {
824
+ If( i.lessThan( int( 16 ) ), () => {
834
825
 
835
826
  const light = SpotLight.wrap( getSpotLight( spotLightsBuffer, i ) );
836
827
  totalWeight.addAssign( calculateSpotLightImportance( light, rayOrigin, normal, material ) );
837
- lightIndex.addAssign( 1 );
838
828
 
839
829
  } );
840
830
 
package/src/TSL/Random.js CHANGED
@@ -187,7 +187,17 @@ const computeSTBNAtlasCoord = ( pixelCoords, sampleIndex, dimensionIndex, frame
187
187
  export const sampleSTBN2D = ( pixelCoords, sampleIndex, dimensionPairIndex, frame ) => {
188
188
 
189
189
  const coord = computeSTBNAtlasCoord( pixelCoords, sampleIndex, dimensionPairIndex, frame );
190
- return stbnVec2TextureNode.load( coord ).xy;
190
+ const raw = stbnVec2TextureNode.load( coord ).xy;
191
+
192
+ // The atlas has only 64 temporal slices, so frame N and N+64 read the same slice: the
193
+ // sample repeats and accumulation stops improving past 64 frames. Decorrelate across
194
+ // 64-frame cycles with a Cranley-Patterson rotation (toroidal shift) by an R2 offset
195
+ // keyed on the cycle index (frame >> 6). The offset is uniform per cycle, preserving
196
+ // spatial and within-window temporal blue noise; cycle 0's offset is 0, so frames
197
+ // 0-63 stay bit-identical. A toroidal shift of uniform samples stays uniform (unbiased).
198
+ const cycle = float( uint( frame ).shiftRight( uint( 6 ) ) );
199
+ const rotation = fract( vec2( R2_A1, R2_A2 ).mul( cycle ) );
200
+ return fract( raw.add( rotation ) );
191
201
 
192
202
  };
193
203