rayzee 5.4.3 → 5.5.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "5.4.3",
3
+ "version": "5.5.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -2,9 +2,9 @@ import { storage } from 'three/tsl';
2
2
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
3
3
  import {
4
4
  NearestFilter, Vector2, Matrix4,
5
- TextureLoader, RepeatWrapping, FloatType
5
+ TextureLoader, RepeatWrapping
6
6
  } from 'three';
7
- import { blueNoiseTextureNode } from '../TSL/Random.js';
7
+ import { stbnScalarTextureNode, stbnVec2TextureNode } from '../TSL/Random.js';
8
8
 
9
9
  // Pipeline system
10
10
  import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
@@ -26,8 +26,9 @@ import { LightSerializer } from '../Processor/LightSerializer';
26
26
  // Constants
27
27
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
28
28
 
29
- // Blue noise (loaded at runtime from CDN — not inlined to keep bundle small)
30
- const blueNoiseImage = 'https://assets.rayzee.atulmourya.com/noise/simple_bluenoise.png';
29
+ // STBN atlases - original source: https://github.com/NVIDIA-RTX/STBN/blob/main/Assets/STBN.zip
30
+ const stbnScalarAtlas = 'https://assets.rayzee.atulmourya.com/noise/stbn_scalar_atlas.png';
31
+ const stbnVec2Atlas = 'https://assets.rayzee.atulmourya.com/noise/stbn_vec2_atlas.png';
31
32
 
32
33
  /**
33
34
  * Data layout constants
@@ -190,8 +191,9 @@ export class PathTracer extends RenderStage {
190
191
  this.spotLightsData = null;
191
192
  this.areaLightsData = null;
192
193
 
193
- // Blue noise
194
- this.blueNoiseTexture = null;
194
+ // STBN noise textures
195
+ this.stbnScalarTexture = null;
196
+ this.stbnVec2Texture = null;
195
197
 
196
198
  // Packed light buffer — [lightBVH nodes (4 vec4s each) | emissive triangles (2 vec4s each)]
197
199
  // emissiveVec4Offset uniform tracks the vec4-count offset where emissive data starts.
@@ -376,25 +378,38 @@ export class PathTracer extends RenderStage {
376
378
  }
377
379
 
378
380
  /**
379
- * Setup blue noise texture
381
+ * Load STBN (Spatiotemporal Blue Noise) atlas textures.
382
+ * Each atlas is 1024×1024: 8×8 grid of 128×128 tiles, 64 temporal slices.
380
383
  */
381
384
  setupBlueNoise() {
382
385
 
383
386
  const loader = new TextureLoader();
384
387
  loader.setCrossOrigin( 'anonymous' );
385
- loader.load( blueNoiseImage, ( texture ) => {
386
388
 
387
- texture.minFilter = NearestFilter;
388
- texture.magFilter = NearestFilter;
389
- texture.wrapS = RepeatWrapping;
390
- texture.wrapT = RepeatWrapping;
391
- texture.type = FloatType;
392
- texture.generateMipmaps = false;
389
+ const configure = ( tex ) => {
393
390
 
394
- this.blueNoiseTexture = texture;
395
- blueNoiseTextureNode.value = texture;
391
+ tex.minFilter = NearestFilter;
392
+ tex.magFilter = NearestFilter;
393
+ tex.wrapS = RepeatWrapping;
394
+ tex.wrapT = RepeatWrapping;
395
+ tex.generateMipmaps = false;
396
+ return tex;
396
397
 
397
- console.log( `PathTracer: Blue noise loaded ${texture.image.width}x${texture.image.height}` );
398
+ };
399
+
400
+ loader.load( stbnScalarAtlas, ( tex ) => {
401
+
402
+ this.stbnScalarTexture = configure( tex );
403
+ stbnScalarTextureNode.value = tex;
404
+ console.log( `PathTracer: STBN scalar atlas loaded ${tex.image.width}x${tex.image.height}` );
405
+
406
+ } );
407
+
408
+ loader.load( stbnVec2Atlas, ( tex ) => {
409
+
410
+ this.stbnVec2Texture = configure( tex );
411
+ stbnVec2TextureNode.value = tex;
412
+ console.log( `PathTracer: STBN vec2 atlas loaded ${tex.image.width}x${tex.image.height}` );
398
413
 
399
414
  } );
400
415
 
@@ -1521,9 +1536,9 @@ export class PathTracer extends RenderStage {
1521
1536
 
1522
1537
  setBlueNoiseTexture( tex ) {
1523
1538
 
1524
- this.blueNoiseTexture = tex;
1525
- // Update the shared Random.js texture node so TSL shader graph uses the real texture
1526
- if ( tex ) blueNoiseTextureNode.value = tex;
1539
+ // Legacy API — sets the scalar STBN atlas texture
1540
+ this.stbnScalarTexture = tex;
1541
+ if ( tex ) stbnScalarTextureNode.value = tex;
1527
1542
 
1528
1543
  }
1529
1544
 
@@ -1676,7 +1691,8 @@ export class PathTracer extends RenderStage {
1676
1691
  this.storageTextures?.dispose();
1677
1692
 
1678
1693
  // Dispose textures
1679
- this.blueNoiseTexture?.dispose();
1694
+ this.stbnScalarTexture?.dispose();
1695
+ this.stbnVec2Texture?.dispose();
1680
1696
  this.placeholderTexture?.dispose();
1681
1697
 
1682
1698
  // Clear data references
@@ -28,12 +28,11 @@ import {
28
28
  texture,
29
29
  } from 'three/tsl';
30
30
 
31
- import { Ray, ShadowMaterial, HitInfo, DirectionSample, MaterialCache } from './Struct.js';
31
+ import { Ray, ShadowMaterial, HitInfo } from './Struct.js';
32
32
  import { PI, TWO_PI, EPSILON, REC709_LUMINANCE_COEFFICIENTS, powerHeuristic, getShadowMaterial, getDatafromStorageBuffer } from './Common.js';
33
33
  import { fresnelSchlickFloat } from './Fresnel.js';
34
34
  import { iorToFresnel0 } from './Fresnel.js';
35
35
  import {
36
- DirectionalLight, AreaLight, PointLight, SpotLight,
37
36
  sampleCone, intersectAreaLight,
38
37
  } from './LightsCore.js';
39
38
  import { calculateBeerLawAbsorption, calculateShadowTransmittance } from './MaterialTransmission.js';
@@ -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