rayzee 7.7.0 → 7.8.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": "7.7.0",
3
+ "version": "7.8.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",
@@ -492,9 +492,53 @@ export const TEXTURE_CONSTANTS = {
492
492
  // guaranteed minimum). The configurable maxTextureSize setting is clamped to this.
493
493
  MAX_TEXTURE_SIZE: 8192,
494
494
  // Default cap applied when no maxTextureSize is supplied (engine standalone use).
495
- DEFAULT_MAX_TEXTURE_SIZE: 4096
495
+ DEFAULT_MAX_TEXTURE_SIZE: 4096,
496
+ // Max layers (textures) per bucket array. Also the packing stride for (bucket, layer).
497
+ MAX_TEXTURES_LIMIT: 128,
498
+ // Size buckets per colorSpace pool. Material maps are grouped into this many
499
+ // longest-edge size classes so a small map no longer pays a large neighbour's
500
+ // footprint. 4 → ~8 bound material arrays (4 sRGB + 4 linear).
501
+ MATERIAL_BUCKET_COUNT: 4,
502
+ // Packing stride: a map's stored index encodes bucketId * BUCKET_LAYER_STRIDE + layer.
503
+ // Also the per-bucket layer cap. Kept at the WebGPU portable maxTextureArrayLayers floor (256)
504
+ // so a consolidated bucket (which merges several map types of one size) stays portable.
505
+ BUCKET_LAYER_STRIDE: 256,
496
506
  };
497
507
 
508
+ // Longest-edge size ladder for a given cap, ascending.
509
+ // cap=4096, count=4 → [512, 1024, 2048, 4096].
510
+ export function getTextureBucketSizes( maxTextureSize, count = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT ) {
511
+
512
+ const sizes = [];
513
+ for ( let i = count - 1; i >= 0; i -- ) {
514
+
515
+ sizes.push( Math.max( TEXTURE_CONSTANTS.MIN_TEXTURE_WIDTH, Math.round( maxTextureSize / Math.pow( 2, i ) ) ) );
516
+
517
+ }
518
+
519
+ return sizes;
520
+
521
+ }
522
+
523
+ // Bucket index for a texture, by its power-of-2 longest edge vs the cap's ladder.
524
+ // Larger-than-cap maps land in the top bucket (downscaled to cap, as before).
525
+ export function getTextureBucketId( width, height, maxTextureSize, count = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT ) {
526
+
527
+ const longest = Math.max( width || 1, height || 1 );
528
+ const pot = Math.pow( 2, Math.ceil( Math.log2( longest ) ) );
529
+ const sizes = getTextureBucketSizes( maxTextureSize, count );
530
+ for ( let i = 0; i < sizes.length; i ++ ) if ( pot <= sizes[ i ] ) return i;
531
+ return sizes.length - 1;
532
+
533
+ }
534
+
535
+ // Pack (bucketId, layer) into the single int slot a material map index occupies.
536
+ export function packTextureIndex( bucketId, layer ) {
537
+
538
+ return bucketId * TEXTURE_CONSTANTS.BUCKET_LAYER_STRIDE + layer;
539
+
540
+ }
541
+
498
542
  // Default texture matrix for materials
499
543
  export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
500
544
 
@@ -9,7 +9,8 @@ import { GeometryExtractor } from './GeometryExtractor.js';
9
9
  import { EmissiveTriangleBuilder } from './EmissiveTriangleBuilder.js';
10
10
  import { updateLoading } from '../Processor/utils.js';
11
11
  import { BuildTimer } from './BuildTimer.js';
12
- import { TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS } from '../EngineDefaults.js';
12
+ import { SRGBColorSpace } from 'three';
13
+ import { TRIANGLE_DATA_LAYOUT, TEXTURE_CONSTANTS, getTextureBucketId, packTextureIndex } from '../EngineDefaults.js';
13
14
  import { fetchAsWorker } from './Workers/fetchAsWorker.js';
14
15
  import BVH_WORKER_URL from './Workers/BVHWorker.js?worker&url';
15
16
  import BVH_REFIT_WORKER_URL from './Workers/BVHRefitWorker.js?worker&url';
@@ -79,14 +80,13 @@ export class SceneProcessor {
79
80
  this._rebuildGeneration = 0; // Monotonic counter to discard stale background rebuilds
80
81
  this._pendingRebuilds = new Map(); // meshIndex → worker
81
82
 
82
- // Initialize texture references
83
- this.albedoTextures = null;
84
- this.normalTextures = null;
85
- this.bumpTextures = null;
86
- this.roughnessTextures = null;
87
- this.metalnessTextures = null;
88
- this.emissiveTextures = null;
89
- this.displacementTextures = null;
83
+ // Initialize texture references.
84
+ // Material maps are packed into consolidated size-bucketed arrays (see _bucketTextures):
85
+ // srgbBucketTextures[K] — albedo + emissive (SRGBColorSpace)
86
+ // linearBucketTextures[K] normal/bump/roughness/metalness/displacement
87
+ // A material's per-map index encodes (bucket, layer) via packTextureIndex.
88
+ this.srgbBucketTextures = null;
89
+ this.linearBucketTextures = null;
90
90
  this.emissiveTriangleData = null;
91
91
  this.emissiveTriangleCount = 0;
92
92
  this.lightBVHNodeData = null;
@@ -762,40 +762,45 @@ export class SceneProcessor {
762
762
 
763
763
  try {
764
764
 
765
- // Material raw data for storage buffers (sync, ~1-5ms)
765
+ // Group the extractor's per-type arrays into consolidated colorSpace×size-bucket
766
+ // pools, and rewrite each material's per-map index to the packed (bucket, layer)
767
+ // form. Must run BEFORE createMaterialRawData (which reads mat.map etc.).
768
+ const { srgbLists, linearLists, remap } = this._bucketTextures();
769
+ this._remapMaterialTextureIndices( remap );
770
+
771
+ // Material raw data for storage buffers (sync, ~1-5ms) — now holds packed indices.
766
772
  if ( this.materials?.length ) {
767
773
 
768
774
  this.materialData = this.textureCreator.createMaterialRawData( this.materials );
769
775
 
770
776
  }
771
777
 
772
- // Material texture arrays GPU DataArrayTextures
773
- // All 7 map types are independent process in parallel
774
- const mapTypesList = [
775
- { data: this.maps, prop: 'albedoTextures' },
776
- { data: this.normalMaps, prop: 'normalTextures' },
777
- { data: this.bumpMaps, prop: 'bumpTextures' },
778
- { data: this.roughnessMaps, prop: 'roughnessTextures' },
779
- { data: this.metalnessMaps, prop: 'metalnessTextures' },
780
- { data: this.emissiveMaps, prop: 'emissiveTextures' },
781
- { data: this.displacementMaps, prop: 'displacementTextures' },
782
- ];
783
-
784
- await Promise.all(
785
- mapTypesList
786
- .filter( ( { data } ) => data?.length > 0 )
787
- .map( ( { data, prop } ) =>
788
- this.textureCreator.createTexturesToDataTexture( data )
789
- .then( result => {
790
-
791
- this[ prop ] = result;
792
-
793
- } )
794
- )
795
- );
778
+ // One DataArrayTexture per non-empty bucket. The sRGB pool (albedo + emissive — both
779
+ // authored in sRGB per glTF) carries SRGBColorSpace so the GPU decodes sRGB→linear
780
+ // before lighting; the linear pool (normal/roughness/metalness/bump/displacement —
781
+ // data textures) stays linear. Applied consistently across load AND rebuildMaterials
782
+ // (the prior model-load path omitted this, leaving albedo un-decoded / too bright).
783
+ const buildBucket = ( list, srgb ) => list.length === 0
784
+ ? Promise.resolve( null )
785
+ : this.textureCreator.createTexturesToDataTexture( list ).then( tex => {
786
+
787
+ if ( tex && srgb ) tex.colorSpace = SRGBColorSpace;
788
+ return tex;
789
+
790
+ } );
791
+
792
+ const [ srgbTextures, linearTextures ] = await Promise.all( [
793
+ Promise.all( srgbLists.map( list => buildBucket( list, true ) ) ),
794
+ Promise.all( linearLists.map( list => buildBucket( list, false ) ) ),
795
+ ] );
796
+
797
+ this.srgbBucketTextures = srgbTextures;
798
+ this.linearBucketTextures = linearTextures;
796
799
 
797
800
  this._log( 'Material textures complete', {
798
801
  materialData: !! this.materialData,
802
+ srgbBuckets: srgbTextures.map( t => ( t ? `${t.image.width}x${t.image.height}x${t.image.depth}` : '-' ) ).join( ',' ),
803
+ linearBuckets: linearTextures.map( t => ( t ? `${t.image.width}x${t.image.height}x${t.image.depth}` : '-' ) ).join( ',' ),
799
804
  } );
800
805
 
801
806
  } catch ( error ) {
@@ -807,6 +812,96 @@ export class SceneProcessor {
807
812
 
808
813
  }
809
814
 
815
+ /**
816
+ * Group the extractor's seven per-type texture arrays into two consolidated colorSpace
817
+ * pools (sRGB: albedo+emissive; linear: normal/bump/roughness/metalness/displacement),
818
+ * each split into MATERIAL_BUCKET_COUNT longest-edge size buckets. Textures are deduped
819
+ * across types within a (pool, bucket) so a shared image (e.g. ORM) costs one layer.
820
+ * @returns {{ srgbLists: Array<Array>, linearLists: Array<Array>, remap: Object }}
821
+ * bucket lists + per-type remap arrays (old per-type layer → packed bucket index).
822
+ * @private
823
+ */
824
+ _bucketTextures() {
825
+
826
+ const cap = this.config.maxTextureSize;
827
+ const K = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT;
828
+ const STRIDE = TEXTURE_CONSTANTS.BUCKET_LAYER_STRIDE;
829
+
830
+ const srgbLists = Array.from( { length: K }, () => [] );
831
+ const linearLists = Array.from( { length: K }, () => [] );
832
+ const srgbDedup = Array.from( { length: K }, () => new Map() );
833
+ const linearDedup = Array.from( { length: K }, () => new Map() );
834
+
835
+ // Persistent uuid → packed maps so runtime material edits (updateMaterial) can re-pack
836
+ // a texture's index against the CURRENT bucket layout instead of the stale per-type index.
837
+ this._srgbTexPacked = new Map();
838
+ this._linearTexPacked = new Map();
839
+
840
+ // Assign one texture to its (bucket, layer) within a pool; dedup by source uuid.
841
+ const assign = ( tex, lists, dedup, flat ) => {
842
+
843
+ if ( ! tex || ! tex.image ) return - 1;
844
+ const bucket = getTextureBucketId( tex.image.width, tex.image.height, cap, K );
845
+ const uuid = tex.source?.uuid ?? tex.uuid;
846
+ const seen = dedup[ bucket ].get( uuid );
847
+ if ( seen !== undefined ) return packTextureIndex( bucket, seen );
848
+ if ( lists[ bucket ].length >= STRIDE ) {
849
+
850
+ console.warn( `[SceneProcessor] Texture bucket ${bucket} full (${STRIDE}); dropping a map.` );
851
+ return - 1;
852
+
853
+ }
854
+
855
+ lists[ bucket ].push( tex );
856
+ const layer = lists[ bucket ].length - 1;
857
+ dedup[ bucket ].set( uuid, layer );
858
+ const packed = packTextureIndex( bucket, layer );
859
+ flat.set( uuid, packed );
860
+ return packed;
861
+
862
+ };
863
+
864
+ // Per-type arrays hold unique textures indexed by the layer the extractor assigned
865
+ // (= array position), so remap[type][oldLayer] = packed index.
866
+ const remapType = ( arr, lists, dedup, flat ) => ( arr || [] ).map( tex => assign( tex, lists, dedup, flat ) );
867
+
868
+ const remap = {
869
+ albedo: remapType( this.maps, srgbLists, srgbDedup, this._srgbTexPacked ),
870
+ emissive: remapType( this.emissiveMaps, srgbLists, srgbDedup, this._srgbTexPacked ),
871
+ normal: remapType( this.normalMaps, linearLists, linearDedup, this._linearTexPacked ),
872
+ bump: remapType( this.bumpMaps, linearLists, linearDedup, this._linearTexPacked ),
873
+ roughness: remapType( this.roughnessMaps, linearLists, linearDedup, this._linearTexPacked ),
874
+ metalness: remapType( this.metalnessMaps, linearLists, linearDedup, this._linearTexPacked ),
875
+ displacement: remapType( this.displacementMaps, linearLists, linearDedup, this._linearTexPacked ),
876
+ };
877
+
878
+ return { srgbLists, linearLists, remap };
879
+
880
+ }
881
+
882
+ /**
883
+ * Rewrite each material's per-map index from the extractor's per-type layer to the
884
+ * packed (bucket, layer) index. Idempotency is NOT guaranteed — call exactly once per
885
+ * extraction (materials are freshly extracted on each process/rebuild).
886
+ * @private
887
+ */
888
+ _remapMaterialTextureIndices( remap ) {
889
+
890
+ const fix = ( v, table ) => ( v >= 0 && v < table.length ? table[ v ] : - 1 );
891
+ for ( const mat of this.materials ) {
892
+
893
+ mat.map = fix( mat.map, remap.albedo );
894
+ mat.emissiveMap = fix( mat.emissiveMap, remap.emissive );
895
+ mat.normalMap = fix( mat.normalMap, remap.normal );
896
+ mat.bumpMap = fix( mat.bumpMap, remap.bump );
897
+ mat.roughnessMap = fix( mat.roughnessMap, remap.roughness );
898
+ mat.metalnessMap = fix( mat.metalnessMap, remap.metalness );
899
+ mat.displacementMap = fix( mat.displacementMap, remap.displacement );
900
+
901
+ }
902
+
903
+ }
904
+
810
905
  /**
811
906
  * Extract emissive triangles and build Light BVH.
812
907
  * MUST run after BVH reordering — emissive data stores triangle indices
@@ -905,27 +1000,42 @@ export class SceneProcessor {
905
1000
  */
906
1001
  _disposeTextures() {
907
1002
 
908
- const textureProps = [
909
- 'albedoTextures', 'normalTextures', 'bumpTextures', 'roughnessTextures',
910
- 'metalnessTextures', 'emissiveTextures', 'displacementTextures'
911
- ];
1003
+ this._disposeBucketTextures();
912
1004
 
913
- // Dispose each texture if it exists
914
- textureProps.forEach( prop => {
1005
+ }
915
1006
 
916
- if ( this[ prop ] ) {
1007
+ /**
1008
+ * Dispose the consolidated bucket arrays (srgb/linear), each an Array<K> of
1009
+ * DataArrayTexture | null.
1010
+ * @private
1011
+ */
1012
+ _disposeBucketTextures() {
917
1013
 
918
- if ( typeof this[ prop ].dispose === 'function' ) {
1014
+ for ( const prop of [ 'srgbBucketTextures', 'linearBucketTextures' ] ) {
919
1015
 
920
- this[ prop ].dispose();
1016
+ const arr = this[ prop ];
1017
+ if ( ! arr ) continue;
1018
+ for ( const tex of arr ) {
921
1019
 
922
- }
1020
+ if ( tex && typeof tex.dispose === 'function' ) {
923
1021
 
924
- this[ prop ] = null;
1022
+ try {
1023
+
1024
+ tex.dispose();
1025
+
1026
+ } catch ( error ) {
1027
+
1028
+ console.warn( `[SceneProcessor] Error disposing ${prop}:`, error );
1029
+
1030
+ }
1031
+
1032
+ }
925
1033
 
926
1034
  }
927
1035
 
928
- } );
1036
+ this[ prop ] = null;
1037
+
1038
+ }
929
1039
 
930
1040
  }
931
1041
 
@@ -969,34 +1079,9 @@ export class SceneProcessor {
969
1079
  this.displacementMaps = extractedData.displacementMaps;
970
1080
  this.sceneFeatures = extractedData.sceneFeatures; // Update material feature flags
971
1081
 
972
- // Create new material and texture data only
973
- const params = {
974
- materials: this.materials,
975
- triangles: this.triangleData, // Reuse existing triangle data
976
- maps: this.maps,
977
- normalMaps: this.normalMaps,
978
- bumpMaps: this.bumpMaps,
979
- roughnessMaps: this.roughnessMaps,
980
- metalnessMaps: this.metalnessMaps,
981
- emissiveMaps: this.emissiveMaps,
982
- displacementMaps: this.displacementMaps,
983
- bvhRoot: this.bvhRoot // Reuse existing BVH
984
- };
985
-
986
- // Create only material and texture-related textures
987
- const textures = await this.textureCreator.createMaterialTextures( params );
988
-
989
- // Regenerate raw material data for storage buffers
990
- this.materialData = this.textureCreator.createMaterialRawData( this.materials );
991
-
992
- // Update texture references (keep triangle and BVH data unchanged)
993
- this.albedoTextures = textures.albedoTexture;
994
- this.normalTextures = textures.normalTexture;
995
- this.bumpTextures = textures.bumpTexture;
996
- this.roughnessTextures = textures.roughnessTexture;
997
- this.metalnessTextures = textures.metalnessTexture;
998
- this.emissiveTextures = textures.emissiveTexture;
999
- this.displacementTextures = textures.displacementTexture;
1082
+ // Bucket textures, remap material indices, regenerate raw material data, and
1083
+ // build the consolidated bucket arrays — same path as the initial build.
1084
+ await this._createMaterialTextures();
1000
1085
 
1001
1086
  const duration = performance.now() - startTime;
1002
1087
  this._log( `Material rebuild complete (${duration.toFixed( 2 )}ms)`, {
@@ -1026,37 +1111,7 @@ export class SceneProcessor {
1026
1111
  */
1027
1112
  _disposeMaterialTextures() {
1028
1113
 
1029
- const materialTextureProps = [
1030
- 'albedoTextures', 'normalTextures',
1031
- 'bumpTextures', 'roughnessTextures', 'metalnessTextures', 'emissiveTextures',
1032
- 'displacementTextures'
1033
- ];
1034
-
1035
- materialTextureProps.forEach( prop => {
1036
-
1037
- if ( this[ prop ] ) {
1038
-
1039
- try {
1040
-
1041
- if ( typeof this[ prop ].dispose === 'function' ) {
1042
-
1043
- this[ prop ].dispose();
1044
-
1045
- }
1046
-
1047
- } catch ( error ) {
1048
-
1049
- console.warn( `[SceneProcessor] Error disposing ${prop}:`, error );
1050
-
1051
- } finally {
1052
-
1053
- this[ prop ] = null;
1054
-
1055
- }
1056
-
1057
- }
1058
-
1059
- } );
1114
+ this._disposeBucketTextures();
1060
1115
 
1061
1116
  // Clear texture creator cache to prevent stale references
1062
1117
  if ( this.textureCreator && this.textureCreator.textureCache ) {
@@ -1407,14 +1462,12 @@ export class SceneProcessor {
1407
1462
  }
1408
1463
 
1409
1464
  pathTracer.materialData.setMaterialTextures( {
1410
- albedoMaps: this.albedoTextures,
1411
- normalMaps: this.normalTextures,
1412
- bumpMaps: this.bumpTextures,
1413
- roughnessMaps: this.roughnessTextures,
1414
- metalnessMaps: this.metalnessTextures,
1415
- emissiveMaps: this.emissiveTextures,
1416
- displacementMaps: this.displacementTextures,
1465
+ srgbBuckets: this.srgbBucketTextures,
1466
+ linearBuckets: this.linearBucketTextures,
1417
1467
  } );
1468
+ // Hand the uuid→packed maps to materialData so runtime edits (updateMaterial) can
1469
+ // re-pack a texture's index against this scene's bucket layout.
1470
+ pathTracer.materialData.setTexturePackMaps?.( this._srgbTexPacked, this._linearTexPacked );
1418
1471
 
1419
1472
  if ( this.emissiveTriangleData ) {
1420
1473
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { texture } from 'three/tsl';
11
11
  import { LinearFilter, DataArrayTexture } from 'three';
12
- import { setShadowAlbedoMaps, setAlphaShadowsUniform } from '../TSL/LightsDirect.js';
12
+ import { setAlphaShadowsUniform } from '../TSL/LightsDirect.js';
13
13
  import { setGoboMapsTexture, setIESProfilesTexture } from '../TSL/LightsCore.js';
14
14
 
15
15
  export class ShaderBuilder {
@@ -30,7 +30,6 @@ export class ShaderBuilder {
30
30
  const nodes = this._sceneTextureNodes;
31
31
 
32
32
  const env = stage.environment;
33
- const mat = stage.materialData;
34
33
 
35
34
  if ( env.environmentTexture && nodes.envTex ) {
36
35
 
@@ -38,13 +37,8 @@ export class ShaderBuilder {
38
37
 
39
38
  }
40
39
 
41
- if ( mat.albedoMaps && nodes.albedoMapsTex ) nodes.albedoMapsTex.value = mat.albedoMaps;
42
- if ( mat.normalMaps && nodes.normalMapsTex ) nodes.normalMapsTex.value = mat.normalMaps;
43
- if ( mat.bumpMaps && nodes.bumpMapsTex ) nodes.bumpMapsTex.value = mat.bumpMaps;
44
- if ( mat.metalnessMaps && nodes.metalnessMapsTex ) nodes.metalnessMapsTex.value = mat.metalnessMaps;
45
- if ( mat.roughnessMaps && nodes.roughnessMapsTex ) nodes.roughnessMapsTex.value = mat.roughnessMaps;
46
- if ( mat.emissiveMaps && nodes.emissiveMapsTex ) nodes.emissiveMapsTex.value = mat.emissiveMaps;
47
- if ( mat.displacementMaps && nodes.displacementMapsTex ) nodes.displacementMapsTex.value = mat.displacementMaps;
40
+ // Material bucket arrays are owned by PathTracer's independent wavefront nodes
41
+ // (_refreshWfTextureNodes); nothing to swap here.
48
42
  if ( stage.goboMaps && nodes.goboMapsTex ) nodes.goboMapsTex.value = stage.goboMaps;
49
43
  if ( stage.iesProfiles && nodes.iesProfilesTex ) nodes.iesProfilesTex.value = stage.iesProfiles;
50
44
 
@@ -116,14 +110,9 @@ export class ShaderBuilder {
116
110
 
117
111
  };
118
112
 
119
- const mat = stage.materialData;
120
- const albedoMapsTex = mat.albedoMaps ? texture( mat.albedoMaps ) : createArrayPlaceholder();
121
- const normalMapsTex = mat.normalMaps ? texture( mat.normalMaps ) : createArrayPlaceholder();
122
- const bumpMapsTex = mat.bumpMaps ? texture( mat.bumpMaps ) : createArrayPlaceholder();
123
- const metalnessMapsTex = mat.metalnessMaps ? texture( mat.metalnessMaps ) : createArrayPlaceholder();
124
- const roughnessMapsTex = mat.roughnessMaps ? texture( mat.roughnessMaps ) : createArrayPlaceholder();
125
- const emissiveMapsTex = mat.emissiveMaps ? texture( mat.emissiveMaps ) : createArrayPlaceholder();
126
- const displacementMapsTex = mat.displacementMaps ? texture( mat.displacementMaps ) : createArrayPlaceholder();
113
+ // Material map arrays (consolidated size buckets) are owned by PathTracer's
114
+ // independent wavefront nodes + setMaterialBucketTextures/setShadowAlbedoMaps
115
+ // see PathTracer._buildWavefrontKernels. Nothing material-map related is bound here.
127
116
 
128
117
  // Spot light gobo array — placeholder until GoboManager populates it.
129
118
  const goboMapsTex = stage.goboMaps ? texture( stage.goboMaps ) : createArrayPlaceholder();
@@ -133,16 +122,9 @@ export class ShaderBuilder {
133
122
  const iesProfilesTex = stage.iesProfiles ? texture( stage.iesProfiles ) : createArrayPlaceholder();
134
123
  setIESProfilesTexture( iesProfilesTex );
135
124
 
136
- // Set albedo texture array for alpha-aware shadow rays (module-level in LightsDirect.js).
137
- // Always pass the texture node (real or placeholder) so alpha-cutout code is emitted
138
- // into the shader at graph construction time. Runtime albedoMapIndex >= 0 guards sampling.
139
- setShadowAlbedoMaps( albedoMapsTex );
140
-
141
125
  const result = {
142
126
  triStorage, bvhStorage, matStorage, lightBufferStorage,
143
127
  envTex,
144
- albedoMapsTex, normalMapsTex, bumpMapsTex,
145
- metalnessMapsTex, roughnessMapsTex, emissiveMapsTex, displacementMapsTex,
146
128
  goboMapsTex, iesProfilesTex,
147
129
  };
148
130
 
@@ -1,13 +1,13 @@
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.
@@ -104,22 +104,10 @@ export class NormalDepth extends RenderStage {
104
104
  this._computeNode = null;
105
105
  this._computeBuilt = false;
106
106
 
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;
107
+ // Independent linear-pool bucket nodes for this pipeline (normal + bump live in the
108
+ // linear pool). Built lazily in _buildCompute from the path tracer's materialData;
109
+ // value-swapped on model load. processNormal/processBump runtime-guard on map indices.
110
+ this._linearBuckets = null;
123
111
 
124
112
  }
125
113
 
@@ -185,10 +173,8 @@ export class NormalDepth extends RenderStage {
185
173
 
186
174
  }
187
175
 
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;
176
+ // In-place bucket swaps (model change) — graph closes over the nodes, only .value changes.
177
+ if ( this._linearBuckets ) refreshBucketTextureNodes( this._linearBuckets, pt.materialData?.linearBuckets );
192
178
 
193
179
  this._lastTriAttr = pt.triangleStorageAttr || this._lastTriAttr;
194
180
  this._lastBvhAttr = pt.bvhStorageAttr || this._lastBvhAttr;
@@ -203,8 +189,11 @@ export class NormalDepth extends RenderStage {
203
189
  const triStorage = this._triStorageNode;
204
190
  const bvhStorage = this._bvhStorageNode;
205
191
  const matStorage = this._matStorageNode;
206
- const normalMaps = this._normalMapsTex;
207
- const bumpMaps = this._bumpMapsTex;
192
+ // Independent linear-pool bucket nodes for this pipeline (normal + bump). The sRGB pool
193
+ // is never sampled here, so it gets placeholders. Publish to the sampling module before
194
+ // the graph is built so processNormal/processBump bake in THESE (per-pipeline) nodes.
195
+ this._linearBuckets = buildBucketTextureNodes( this.pathTracer?.materialData?.linearBuckets );
196
+ setMaterialBucketTextures( buildBucketTextureNodes( null ), this._linearBuckets );
208
197
  const camWorld = this.cameraWorldMatrix;
209
198
  const camProjInv = this.cameraProjectionMatrixInverse;
210
199
  const resW = this.resolutionWidth;
@@ -266,8 +255,8 @@ export class NormalDepth extends RenderStage {
266
255
  getMaterial( hit.materialIndex, matStorage )
267
256
  ).toVar();
268
257
  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 ) );
258
+ const mapped = processNormal( hit.normal, material, uvCache ).toVar();
259
+ shadingNormal.assign( processBump( mapped, material, uvCache ) );
271
260
 
272
261
  } );
273
262
 
@@ -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
 
@@ -661,26 +658,21 @@ export class PathTracer extends PathTracerStage {
661
658
  // Independent texture nodes (never compiled elsewhere) avoid Three.js TextureNode caching across pipelines; refreshed via _refreshWfTextureNodes.
662
659
  const _mat = this.materialData;
663
660
  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;
661
+ // Consolidated size-bucket nodes (K sRGB + K linear). Empty buckets get placeholders so
662
+ // every runtime branch references a valid node. Published to the sampling module before
663
+ // the Shade/Debug graphs are built so they bake in these (per-pipeline) nodes.
664
+ const freshSrgbBuckets = buildBucketTextureNodes( _mat.srgbBuckets );
665
+ const freshLinearBuckets = buildBucketTextureNodes( _mat.linearBuckets );
666
+ setMaterialBucketTextures( freshSrgbBuckets, freshLinearBuckets );
667
+ // Alpha-cutout shadow rays sample albedo (sRGB pool) emitted into the shade graph now.
668
+ setShadowAlbedoMaps( freshSrgbBuckets );
672
669
  const freshEnvTex = _env.environmentTexture ? texture( _env.environmentTexture ) : texNodes.envTex;
673
670
 
674
671
  this._wfTexNodes = {
675
672
  envTex: freshEnvTex,
676
673
  envCDFTex: freshEnvCDF,
677
- albedoMaps: freshAlbedoMaps,
678
- normalMaps: freshNormalMaps,
679
- bumpMaps: freshBumpMaps,
680
- metalnessMaps: freshMetalnessMaps,
681
- roughnessMaps: freshRoughnessMaps,
682
- emissiveMaps: freshEmissiveMaps,
683
- displacementMaps: freshDisplacementMaps,
674
+ srgbBuckets: freshSrgbBuckets,
675
+ linearBuckets: freshLinearBuckets,
684
676
  };
685
677
 
686
678
  // Material-coherence sort gate (experiment): only worthwhile above a few materials.
@@ -761,13 +753,6 @@ export class PathTracer extends PathTracerStage {
761
753
  hitBufferRO: pb.hitBuffer.ro,
762
754
  counters,
763
755
  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
756
  envTexture: freshEnvTex,
772
757
  environmentIntensity: this.environmentIntensity,
773
758
  envMatrix: this.environmentMatrix,
@@ -926,12 +911,6 @@ export class PathTracer extends PathTracerStage {
926
911
  enableEnvironmentLight: this.enableEnvironment,
927
912
  visMode: this.visMode,
928
913
  debugVisScale: this.debugVisScale,
929
- albedoMaps: freshAlbedoMaps,
930
- normalMaps: freshNormalMaps,
931
- bumpMaps: freshBumpMaps,
932
- metalnessMaps: freshMetalnessMaps,
933
- roughnessMaps: freshRoughnessMaps,
934
- emissiveMaps: freshEmissiveMaps,
935
914
  frame: this.frame,
936
915
  } );
937
916
  this._kernelManager.register( 'debug',