rayzee 5.4.3 → 5.6.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.
@@ -5,7 +5,7 @@ import {
5
5
  MaterialClassification,
6
6
  MaterialCache,
7
7
  ImportanceSamplingInfo,
8
- MaterialSamples,
8
+
9
9
  } from './Struct.js';
10
10
 
11
11
  import {
@@ -394,22 +394,13 @@ export const getImportanceSamplingInfo = Fn( ( [
394
394
  const tempMaxSheenColor = max( material.sheenColor.r, max( material.sheenColor.g, material.sheenColor.b ) );
395
395
 
396
396
  const tempCache = MaterialCache( {
397
+ F0: dielectricF0( material.ior ),
397
398
  NoV: float( 0.5 ),
399
+ diffuseColor: material.color.rgb,
398
400
  isPurelyDiffuse: false,
399
- isMetallic: mc.isMetallic,
400
- hasSpecialFeatures: false,
401
401
  alpha: material.roughness.mul( material.roughness ),
402
- alpha2: material.roughness.mul( material.roughness ).mul( material.roughness ).mul( material.roughness ),
403
402
  k: material.roughness.add( 1.0 ).mul( material.roughness.add( 1.0 ) ).div( 8.0 ),
404
- F0: dielectricF0( material.ior ),
405
- diffuseColor: material.color.rgb,
406
- specularColor: material.color.rgb,
407
- tsAlbedo: material.color,
408
- tsEmissive: material.emissive,
409
- tsMetalness: material.metalness,
410
- tsRoughness: material.roughness,
411
- tsNormal: vec3( 0.0, 1.0, 0.0 ),
412
- tsHasTextures: false,
403
+ alpha2: material.roughness.mul( material.roughness ).mul( material.roughness ).mul( material.roughness ),
413
404
  invRoughness: tempInvRoughness,
414
405
  metalFactor: tempMetalFactor,
415
406
  iorFactor: tempIorFactor,
@@ -500,13 +491,6 @@ export const createMaterialCache = Fn( ( [ N, V, material, samples, mc ] ) => {
500
491
  .and( material.clearcoat.equal( 0.0 ) )
501
492
  .toVar();
502
493
 
503
- const isMetallic = mc.isMetallic.toVar();
504
-
505
- const hasSpecialFeatures = mc.isTransmissive.or( mc.hasClearcoat )
506
- .or( material.sheen.greaterThan( 0.0 ) )
507
- .or( material.iridescence.greaterThan( 0.0 ) )
508
- .toVar();
509
-
510
494
  const alpha = samples.roughness.mul( samples.roughness ).toVar();
511
495
  const alpha2 = alpha.mul( alpha ).toVar();
512
496
  const r = samples.roughness.add( 1.0 );
@@ -515,7 +499,6 @@ export const createMaterialCache = Fn( ( [ N, V, material, samples, mc ] ) => {
515
499
  const baseF0 = dielectricF0( material.ior ).mul( material.specularColor );
516
500
  const F0 = mix( baseF0, samples.albedo.rgb, samples.metalness ).mul( material.specularIntensity ).toVar();
517
501
  const diffuseColor = samples.albedo.rgb.mul( float( 1.0 ).sub( samples.metalness ) ).toVar();
518
- const specularColor = samples.albedo.rgb.toVar();
519
502
 
520
503
  const invRoughness = float( 1.0 ).sub( samples.roughness ).toVar();
521
504
  const metalFactor = float( 0.5 ).add( float( 0.5 ).mul( samples.metalness ) ).toVar();
@@ -523,89 +506,13 @@ export const createMaterialCache = Fn( ( [ N, V, material, samples, mc ] ) => {
523
506
  const maxSheenColor = max( material.sheenColor.r, max( material.sheenColor.g, material.sheenColor.b ) ).toVar();
524
507
 
525
508
  return MaterialCache( {
526
- NoV,
527
- isPurelyDiffuse,
528
- isMetallic,
529
- hasSpecialFeatures,
530
- alpha,
531
- alpha2,
532
- k,
533
509
  F0,
534
- diffuseColor,
535
- specularColor,
536
- tsAlbedo: samples.albedo,
537
- tsEmissive: samples.emissive,
538
- tsMetalness: samples.metalness,
539
- tsRoughness: samples.roughness,
540
- tsNormal: samples.normal,
541
- tsHasTextures: samples.hasTextures,
542
- invRoughness,
543
- metalFactor,
544
- iorFactor,
545
- maxSheenColor,
546
- } );
547
-
548
- } );
549
-
550
- export const createMaterialCacheLegacy = Fn( ( [ N, V, material ] ) => {
551
-
552
- const NoV = max( dot( N, V ), 0.001 ).toVar();
553
-
554
- const isPurelyDiffuse = material.roughness.greaterThan( 0.98 )
555
- .and( material.metalness.lessThan( 0.02 ) )
556
- .and( material.transmission.equal( 0.0 ) )
557
- .and( material.clearcoat.equal( 0.0 ) )
558
- .toVar();
559
-
560
- const isMetallic = material.metalness.greaterThan( 0.7 ).toVar();
561
-
562
- const hasSpecialFeatures = material.transmission.greaterThan( 0.0 )
563
- .or( material.clearcoat.greaterThan( 0.0 ) )
564
- .or( material.sheen.greaterThan( 0.0 ) )
565
- .or( material.iridescence.greaterThan( 0.0 ) )
566
- .toVar();
567
-
568
- const alpha = material.roughness.mul( material.roughness ).toVar();
569
- const alpha2 = alpha.mul( alpha ).toVar();
570
- const r = material.roughness.add( 1.0 );
571
- const k = r.mul( r ).div( 8.0 ).toVar();
572
-
573
- const baseF0 = dielectricF0( material.ior ).mul( material.specularColor );
574
- const F0 = mix( baseF0, material.color.rgb, material.metalness ).mul( material.specularIntensity ).toVar();
575
- const diffuseColor = material.color.rgb.mul( float( 1.0 ).sub( material.metalness ) ).toVar();
576
- const specularColor = material.color.rgb.toVar();
577
-
578
- const dummySamples = MaterialSamples( {
579
- albedo: material.color,
580
- emissive: material.emissive.mul( material.emissiveIntensity ),
581
- metalness: material.metalness,
582
- roughness: material.roughness,
583
- normal: N,
584
- hasTextures: false,
585
- } );
586
-
587
- const invRoughness = float( 1.0 ).sub( material.roughness ).toVar();
588
- const metalFactor = float( 0.5 ).add( float( 0.5 ).mul( material.metalness ) ).toVar();
589
- const iorFactor = min( float( 2.0 ).div( material.ior ), 1.0 ).toVar();
590
- const maxSheenColor = max( material.sheenColor.r, max( material.sheenColor.g, material.sheenColor.b ) ).toVar();
591
-
592
- return MaterialCache( {
593
510
  NoV,
511
+ diffuseColor,
594
512
  isPurelyDiffuse,
595
- isMetallic,
596
- hasSpecialFeatures,
597
513
  alpha,
598
- alpha2,
599
514
  k,
600
- F0,
601
- diffuseColor,
602
- specularColor,
603
- tsAlbedo: dummySamples.albedo,
604
- tsEmissive: dummySamples.emissive,
605
- tsMetalness: dummySamples.metalness,
606
- tsRoughness: dummySamples.roughness,
607
- tsNormal: dummySamples.normal,
608
- tsHasTextures: dummySamples.hasTextures,
515
+ alpha2,
609
516
  invRoughness,
610
517
  metalFactor,
611
518
  iorFactor,
@@ -613,3 +520,4 @@ export const createMaterialCacheLegacy = Fn( ( [ N, V, material ] ) => {
613
520
  } );
614
521
 
615
522
  } );
523
+
@@ -1,6 +1,5 @@
1
1
  import {
2
- Fn, wgslFn, float, vec3, vec2, int, uint,
3
- If, max, min, abs, normalize, reflect, refract, dot, pow
2
+ Fn, wgslFn, float, vec3, If, max, min, abs, normalize, reflect, refract, dot, pow
4
3
  } from 'three/tsl';
5
4
 
6
5
  import {
@@ -8,8 +7,7 @@ import {
8
7
  } from './Struct.js';
9
8
 
10
9
  import {
11
- PI, PI_INV, MIN_PDF, EPSILON,
12
- classifyMaterial, square,
10
+ PI, PI_INV, MIN_PDF, classifyMaterial,
13
11
  } from './Common.js';
14
12
  import { dielectricF0 } from './Fresnel.js';
15
13
 
@@ -116,22 +114,13 @@ export const calculateSamplingWeights = Fn( ( [ V, N, material ] ) => {
116
114
 
117
115
  // Create temporary cache for calculations
118
116
  const tempCache = MaterialCache( {
117
+ F0: dielectricF0( material.ior ),
119
118
  NoV: float( 0.5 ),
119
+ diffuseColor: material.color.rgb,
120
120
  isPurelyDiffuse: false,
121
- isMetallic: mc.isMetallic,
122
- hasSpecialFeatures: false,
123
121
  alpha: material.roughness.mul( material.roughness ),
124
- alpha2: material.roughness.mul( material.roughness ).mul( material.roughness ).mul( material.roughness ),
125
122
  k: material.roughness.add( 1.0 ).mul( material.roughness.add( 1.0 ) ).div( 8.0 ),
126
- F0: dielectricF0( material.ior ),
127
- diffuseColor: material.color.rgb,
128
- specularColor: material.color.rgb,
129
- tsAlbedo: material.color, // placeholder
130
- tsEmissive: vec3( 0.0 ),
131
- tsMetalness: float( 0.0 ),
132
- tsRoughness: material.roughness,
133
- tsNormal: vec3( 0.0, 1.0, 0.0 ),
134
- tsHasTextures: false,
123
+ alpha2: material.roughness.mul( material.roughness ).mul( material.roughness ).mul( material.roughness ),
135
124
  invRoughness: tempInvRoughness,
136
125
  metalFactor: tempMetalFactor,
137
126
  iorFactor: tempIorFactor,
@@ -184,19 +184,10 @@ export const generateSampledDirection = Fn( ( [
184
184
  F0: dielectricF0( material.ior ),
185
185
  NoV: float( 1.0 ),
186
186
  diffuseColor: vec3( 0.0 ),
187
- specularColor: vec3( 0.0 ),
188
- isMetallic: false,
189
187
  isPurelyDiffuse: false,
190
- hasSpecialFeatures: false,
191
188
  alpha: float( 0.0 ),
192
189
  k: float( 0.0 ),
193
190
  alpha2: float( 0.0 ),
194
- tsAlbedo: vec4( 0.0 ),
195
- tsEmissive: vec3( 0.0 ),
196
- tsMetalness: float( 0.0 ),
197
- tsRoughness: float( 0.0 ),
198
- tsNormal: vec3( 0.0 ),
199
- tsHasTextures: false,
200
191
  invRoughness: float( 1.0 ).sub( material.roughness ),
201
192
  metalFactor: float( 0.5 ).add( float( 0.5 ).mul( material.metalness ) ),
202
193
  iorFactor: min( float( 2.0 ).div( material.ior ), 1.0 ),
@@ -655,12 +646,8 @@ export const Trace = Fn( ( [
655
646
  // Cached material cache
656
647
  const psCachedMaterialCache = MaterialCache( {
657
648
  F0: vec3( 0.04 ), NoV: float( 1.0 ),
658
- diffuseColor: vec3( 0.0 ), specularColor: vec3( 0.0 ),
659
- isMetallic: false, isPurelyDiffuse: false, hasSpecialFeatures: false,
649
+ diffuseColor: vec3( 0.0 ), isPurelyDiffuse: false,
660
650
  alpha: float( 0.0 ), k: float( 0.0 ), alpha2: float( 0.0 ),
661
- tsAlbedo: vec4( 0.0 ), tsEmissive: vec3( 0.0 ),
662
- tsMetalness: float( 0.0 ), tsRoughness: float( 0.0 ),
663
- tsNormal: vec3( 0.0 ), tsHasTextures: false,
664
651
  invRoughness: float( 1.0 ), metalFactor: float( 0.5 ),
665
652
  iorFactor: float( 1.0 ), maxSheenColor: float( 0.0 ),
666
653
  } ).toVar();
package/src/TSL/Random.js CHANGED
@@ -1,6 +1,6 @@
1
1
  // Three.js Transpiler r182
2
2
 
3
- import { uniform, texture, textureSize, float, If, Fn, wgslFn, uint, TWO_PI, cos, sin, vec2, sqrt, fract, mod, floor, ivec2, select, Switch, max, int, vec4, mix } from 'three/tsl';
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
4
  import { DataTexture, FloatType } from 'three';
5
5
 
6
6
  // -----------------------------------------------------------------------------
@@ -12,20 +12,30 @@ const samplingTechnique = samplingTechniqueUniform;
12
12
 
13
13
  // 0: PCG, 1: Halton, 2: Sobol, 3: Blue Noise
14
14
 
15
- // 1x1 placeholder — real texture assigned later via blueNoiseTextureNode.value = ...
15
+ // 1x1 placeholder — real texture assigned later via .value = ...
16
16
  const _placeholderData = new Float32Array( [ 0.5, 0.5, 0.5, 1.0 ] );
17
- const _placeholderTex = new DataTexture( _placeholderData, 1, 1 );
18
- _placeholderTex.type = FloatType;
19
- _placeholderTex.needsUpdate = true;
20
17
 
21
- export const blueNoiseTextureNode = texture( _placeholderTex );
22
- blueNoiseTextureNode.setUpdateMatrix( false ); // No UV transform — we provide our own integer coords
23
- const blueNoiseTextureSize = vec2( textureSize( blueNoiseTextureNode ) );
18
+ const _placeholderScalar = new DataTexture( _placeholderData, 1, 1 );
19
+ _placeholderScalar.type = FloatType;
20
+ _placeholderScalar.needsUpdate = true;
24
21
 
25
- // Golden ratio constants for dimension decorrelation
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;
26
25
 
27
- const INV_PHI = float( 0.61803398875 );
28
- const INV_PHI2 = float( 0.38196601125 );
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 );
29
39
 
30
40
  // Sobol sequence direction vectors using function lookup for compatibility
31
41
 
@@ -160,126 +170,49 @@ export const RandomPointInCircle = ( rngState ) => {
160
170
  };
161
171
 
162
172
  // -----------------------------------------------------------------------------
163
- // Blue noise sampling with proper multi-dimensional support
173
+ // STBN atlas sampling proper spatiotemporal blue noise
164
174
  // -----------------------------------------------------------------------------
165
- // Cranley-Patterson rotation for decorrelation
166
-
167
- export const cranleyPatterson2D = /*@__PURE__*/ wgslFn( `
168
- fn cranleyPatterson2D( p: vec2f, offset: vec2f ) -> vec2f {
169
-
170
- return fract( p + offset );
171
-
172
- }
173
- ` );
174
-
175
- // Improved blue noise sampling that properly uses all parameters
176
-
177
- export const sampleBlueNoiseRaw = /*@__PURE__*/ Fn( ( [ pixelCoords, sampleIndex, bounceIndex, frame ] ) => {
178
-
179
- // Create dimension-specific offsets using golden ratio
180
-
181
- const dimensionOffset = vec2( fract( float( sampleIndex ).mul( INV_PHI ) ), fract( float( bounceIndex ).mul( INV_PHI2 ) ) );
182
-
183
- // Frame-based decorrelation with better hash
184
-
185
- const frameHash = wang_hash( { seed: pcgHash( { state: uint( frame ) } ) } );
186
- const frameOffset = vec2( float( frameHash.bitAnd( 0xFFFF ) ).div( 65536.0 ), float( frameHash.shiftRight( 16 ).bitAnd( 0xFFFF ) ).div( 65536.0 ) );
187
-
188
- // Scale offsets to texture size
189
-
190
- const scaledDimOffset = dimensionOffset.mul( vec2( blueNoiseTextureSize ) );
191
- const scaledFrameOffset = frameOffset.mul( vec2( blueNoiseTextureSize ) );
192
-
193
- // Combine all offsets with proper toroidal wrapping
194
-
195
- const coords = mod( pixelCoords.add( scaledDimOffset ).add( scaledFrameOffset ), vec2( blueNoiseTextureSize ) );
196
-
197
- // Ensure positive coordinates and fetch
198
- // .load() → textureLoad() in WGSL: exact integer texel fetch, no filtering (≡ GLSL texelFetch)
199
-
200
- const texCoord = ivec2( floor( coords ) );
201
-
202
- return blueNoiseTextureNode.load( texCoord );
203
-
204
- }, { pixelCoords: 'vec2', sampleIndex: 'int', bounceIndex: 'int', frame: 'int', return: 'vec4' } );
205
-
206
- // Get 2D blue noise sample with dimension offset
207
-
208
- export const sampleBlueNoise2D = /*@__PURE__*/ Fn( ( [ pixelCoords, sampleIndex, dimensionBase, frame ] ) => {
209
-
210
- // For 2D sampling, we need to carefully select components to maintain blue noise properties
211
-
212
- const noise = sampleBlueNoiseRaw( pixelCoords, sampleIndex, dimensionBase.div( int( 2 ) ), frame );
213
-
214
- // Use different component pairs based on dimension
215
-
216
- const pairIndex = mod( dimensionBase.div( int( 2 ) ), int( 6 ) );
175
+ // Atlas layout: 8×8 grid of 128×128 tiles = 1024×1024 texture.
176
+ // Temporal axis: frame % 64 selects tile (true STBN temporal decorrelation).
177
+ // Spatial decorrelation: R2 quasi-random offset keyed on dimension + sample index.
217
178
 
218
- const result = vec2( 0.0 ).toVar();
219
-
220
- Switch( pairIndex )
221
- .Case( 0, () => {
222
-
223
- result.assign( noise.xy );
224
-
225
- } ).Case( 1, () => {
226
-
227
- result.assign( noise.zw );
228
-
229
- } ).Case( 2, () => {
230
-
231
- result.assign( noise.xz );
179
+ const computeSTBNAtlasCoord = ( pixelCoords, sampleIndex, dimensionIndex, frame ) => {
232
180
 
233
- } ).Case( 3, () => {
181
+ // Temporal slice true STBN temporal axis
182
+ const slice = uint( frame ).bitAnd( uint( 63 ) ); // frame % 64
234
183
 
235
- result.assign( noise.yw );
184
+ // R2 quasi-random spatial offset for per-dimension/per-sample decorrelation
185
+ const n = float( dimensionIndex ).add( float( sampleIndex ).mul( 7.0 ) );
186
+ const offsetX = int( fract( n.mul( R2_A1 ).add( 0.5 ) ).mul( 128.0 ) );
187
+ const offsetY = int( fract( n.mul( R2_A2 ).add( 0.5 ) ).mul( 128.0 ) );
236
188
 
237
- } ).Case( 4, () => {
189
+ // Pixel within 128×128 tile (toroidal wrap via bitmask)
190
+ const px = int( pixelCoords.x ).add( offsetX ).bitAnd( int( 127 ) );
191
+ const py = int( pixelCoords.y ).add( offsetY ).bitAnd( int( 127 ) );
238
192
 
239
- result.assign( noise.xw );
193
+ // Atlas tile position from slice index
194
+ const tileCol = int( slice ).bitAnd( int( 7 ) ); // slice % 8
195
+ const tileRow = int( slice ).shiftRight( int( 3 ) ); // slice / 8
240
196
 
241
- } ).Case( 5, () => {
242
-
243
- result.assign( noise.yz );
244
-
245
- } ).Default( () => {
246
-
247
- result.assign( noise.xy );
248
-
249
- } );
250
-
251
- return result;
197
+ return ivec2( tileCol.mul( int( 128 ) ).add( px ), tileRow.mul( int( 128 ) ).add( py ) );
252
198
 
253
- }, { pixelCoords: 'vec2', sampleIndex: 'int', dimensionBase: 'int', return: 'vec2', frame: 'int' } );
254
-
255
- // Progressive blue noise sampling for temporal accumulation
256
-
257
- export const sampleProgressiveBlueNoise = /*@__PURE__*/ Fn( ( [ pixelCoords, currentSample, maxSamples, frame ] ) => {
258
-
259
- // Determine which "slice" of the blue noise we're in
260
-
261
- const progress = float( currentSample ).div( max( 1.0, float( maxSamples ) ) );
262
- const temporalSlice = int( progress.mul( 16.0 ) );
263
-
264
- // 16 temporal slices
265
- // Use different regions of blue noise for different sample counts
266
-
267
- const sliceOffset = vec2( float( mod( temporalSlice, int( 4 ) ) ).mul( 0.25 ), float( temporalSlice.div( int( 4 ) ) ).mul( 0.25 ) );
199
+ };
268
200
 
269
- // Scale to texture space and add pixel-specific offset
201
+ // Sample 1D scalar STBN value in [0,1]
202
+ export const sampleSTBNScalar = ( pixelCoords, sampleIndex, dimensionIndex, frame ) => {
270
203
 
271
- const scaledOffset = sliceOffset.mul( vec2( blueNoiseTextureSize ) );
272
- const coords = mod( pixelCoords.add( scaledOffset ), vec2( blueNoiseTextureSize ) );
273
- const noise = sampleBlueNoiseRaw( coords, currentSample, int( 0 ), frame );
204
+ const coord = computeSTBNAtlasCoord( pixelCoords, sampleIndex, dimensionIndex, frame );
205
+ return stbnScalarTextureNode.load( coord ).x;
274
206
 
275
- // Apply additional Cranley-Patterson rotation for better distribution
207
+ };
276
208
 
277
- const seed = pcgHash( { state: uint( currentSample ).bitXor( wang_hash( { seed: uint( maxSamples ) } ) ) } );
278
- const rotation = vec2( float( seed.bitAnd( 0xFFFF ) ).div( 65536.0 ), float( seed.shiftRight( 16 ).bitAnd( 0xFFFF ) ).div( 65536.0 ) );
209
+ // Sample decorrelated 2D STBN pair in [0,1]²
210
+ export const sampleSTBN2D = ( pixelCoords, sampleIndex, dimensionPairIndex, frame ) => {
279
211
 
280
- return cranleyPatterson2D( { p: noise.xy, offset: rotation } );
212
+ const coord = computeSTBNAtlasCoord( pixelCoords, sampleIndex, dimensionPairIndex, frame );
213
+ return stbnVec2TextureNode.load( coord ).xy;
281
214
 
282
- }, { pixelCoords: 'vec2', currentSample: 'int', maxSamples: 'int', return: 'vec2', frame: 'int' } );
215
+ };
283
216
 
284
217
  // -----------------------------------------------------------------------------
285
218
  // Low-discrepancy sequence generators
@@ -490,19 +423,20 @@ export const getRandomSampleND = ( pixelCoord, sampleIndex, bounceIndex, rngStat
490
423
 
491
424
  } ).Else( () => {
492
425
 
493
- // Blue Noise (technique 3)
494
- const dimensionOffset = bounceIndex.mul( 4 );
426
+ // STBN — Spatiotemporal Blue Noise (technique 3)
427
+ // Each bounce uses a block of 4 dimension indices for decorrelation
428
+ const dimBase = bounceIndex.mul( int( 4 ) );
495
429
 
496
430
  If( dimensions.lessThanEqual( int( 2 ) ), () => {
497
431
 
498
- const _sample = sampleBlueNoise2D( pixelCoord, sampleIndex, dimensionOffset, frame );
432
+ const _sample = sampleSTBN2D( pixelCoord, sampleIndex, dimBase, frame );
499
433
  result.x.assign( _sample.x );
500
434
  result.y.assign( _sample.y );
501
435
 
502
436
  } ).Else( () => {
503
437
 
504
- const _sample1 = sampleBlueNoise2D( pixelCoord, sampleIndex, dimensionOffset, frame );
505
- const _sample2 = sampleBlueNoise2D( pixelCoord, sampleIndex, dimensionOffset.add( 2 ), frame );
438
+ const _sample1 = sampleSTBN2D( pixelCoord, sampleIndex, dimBase, frame );
439
+ const _sample2 = sampleSTBN2D( pixelCoord, sampleIndex, dimBase.add( int( 1 ) ), frame );
506
440
  result.assign( vec4( _sample1, _sample2 ) );
507
441
 
508
442
  } );
@@ -551,33 +485,26 @@ export const getStratifiedSample = ( pixelCoord, rayIndex, totalRays, rngState,
551
485
 
552
486
  const strataPos = vec2( float( sx ), float( sy ) ).div( vec2( float( strataX ), float( strataY ) ) );
553
487
 
554
- // Enhanced jitter based on sampling technique with blue noise fallback for better convergence
488
+ // Jitter via STBN or fast RNG fallback
555
489
 
556
490
  const jitter = vec2( 0.0 ).toVar();
557
491
 
558
492
  If( samplingTechnique.greaterThanEqual( int( 3 ) ), () => {
559
493
 
560
- // Blue noise - improved progressive sampling
561
-
562
- jitter.assign( sampleProgressiveBlueNoise( pixelCoord, rayIndex, totalRays, frame ) );
494
+ // STBN true spatiotemporal blue noise jitter
495
+ jitter.assign( sampleSTBN2D( pixelCoord, rayIndex, int( 0 ), frame ) );
563
496
 
564
497
  } ).Else( () => {
565
498
 
566
- // Enhanced fallback: use fast sampling with slight blue noise influence for better convergence
567
- // .toVar() on each call to capture value before next state advance
568
-
499
+ // Fast RNG with subtle STBN influence for better convergence
569
500
  const j1 = RandomValueFast( rngState ).toVar();
570
501
  const j2 = RandomValueFast( rngState ).toVar();
571
502
  jitter.assign( vec2( j1, j2 ) );
572
503
 
573
- // Add subtle blue noise influence even for non-blue-noise techniques
574
-
575
504
  If( totalRays.greaterThan( int( 4 ) ), () => {
576
505
 
577
- // Only for multi-sample scenarios
578
-
579
- const blueNoiseInfluence = sampleBlueNoise2D( pixelCoord, rayIndex, int( 0 ), frame ).mul( 0.1 );
580
- jitter.assign( mix( jitter, blueNoiseInfluence, 0.2 ) );
506
+ const stbnInfluence = sampleSTBN2D( pixelCoord, rayIndex, int( 0 ), frame ).mul( 0.1 );
507
+ jitter.assign( mix( jitter, stbnInfluence, 0.2 ) );
581
508
 
582
509
  } );
583
510
 
package/src/TSL/Struct.js CHANGED
@@ -172,47 +172,25 @@ export const UVCache = struct( {
172
172
  allSameUV: 'bool',
173
173
  } );
174
174
 
175
- // Enhanced material cache
175
+ // Material cache — precomputed BRDF terms for the current surface hit.
176
+ // Fields are split into two groups:
177
+ // 1. BRDF evaluation: F0, NoV, diffuseColor, isPurelyDiffuse, alpha, k, alpha2
178
+ // 2. BRDF weight calc: invRoughness, metalFactor, iorFactor, maxSheenColor
176
179
  export const MaterialCache = struct( {
177
180
  F0: 'vec3', // Base reflectance
178
181
  NoV: 'float', // Normal dot View
179
- diffuseColor: 'vec3', // Precomputed diffuse color
180
- specularColor: 'vec3', // Precomputed specular color
181
- isMetallic: 'bool', // metalness > 0.7
182
+ diffuseColor: 'vec3', // Precomputed diffuse color (only for isPurelyDiffuse fast path)
182
183
  isPurelyDiffuse: 'bool', // Optimized path flag
183
- hasSpecialFeatures: 'bool', // Has transmission, clearcoat, etc.
184
184
  alpha: 'float', // roughness squared
185
185
  k: 'float', // Geometry term constant
186
186
  alpha2: 'float', // roughness to the fourth power
187
- // Flattened texture samples (TSL doesn't support nested struct types)
188
- tsAlbedo: 'vec4',
189
- tsEmissive: 'vec3',
190
- tsMetalness: 'float',
191
- tsRoughness: 'float',
192
- tsNormal: 'vec3',
193
- tsHasTextures: 'bool',
194
-
195
- // BRDF optimization: precomputed shared values
187
+ // BRDF weight calculation: precomputed shared values
196
188
  invRoughness: 'float', // 1.0 - roughness
197
189
  metalFactor: 'float', // 0.5 + 0.5 * metalness
198
190
  iorFactor: 'float', // min(2.0 / ior, 1.0)
199
191
  maxSheenColor: 'float', // max component of sheen color
200
192
  } );
201
193
 
202
- // Update PathState to include texture samples
203
- export const PathState = struct( {
204
- brdfWeights: BRDFWeights, // Cached BRDF weights
205
- samplingInfo: ImportanceSamplingInfo, // Cached importance sampling info
206
- materialCache: MaterialCache, // Cached material properties
207
- materialClass: MaterialClassification, // Cached material classification
208
- weightsComputed: 'bool', // Flag to track if weights are computed
209
- texturesLoaded: 'bool', // Flag to track if textures are loaded
210
- classificationCached: 'bool', // Flag for material classification
211
- materialCacheCached: 'bool', // Flag for material cache creation
212
- pathImportance: 'float', // Cached path importance estimate
213
- lastMaterialIndex: 'int', // Track material changes to preserve cache
214
- } );
215
-
216
194
  export const SamplingStrategyWeights = struct( {
217
195
  envWeight: 'float',
218
196
  specularWeight: 'float',