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.
@@ -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
@@ -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
 
@@ -74,9 +74,6 @@ export function buildShadeKernel( params ) {
74
74
  rayBufferRW, rngBufferRW, hitBufferRO, gBufferRW,
75
75
  counters,
76
76
  activeIndicesRO,
77
- albedoMaps, normalMaps, bumpMaps,
78
- metalnessMaps, roughnessMaps, emissiveMaps,
79
- displacementMaps,
80
77
  envTexture, environmentIntensity, envMatrix,
81
78
  enableEnvironmentLight, useEnvMapIS,
82
79
  groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight, groundProjectionLevel,
@@ -533,7 +530,7 @@ export function buildShadeKernel( params ) {
533
530
  boxTests: int( 0 ), triTests: int( 0 ),
534
531
  } );
535
532
  const dispResult = DisplacementResult.wrap( refineDisplacedIntersection(
536
- dispRay, dispHit, triangleBuffer, displacementMaps, material, bounceIndex,
533
+ dispRay, dispHit, triangleBuffer, material, bounceIndex,
537
534
  ) ).toVar();
538
535
  samplingUV.assign( dispResult.uv );
539
536
  displacedNormal.assign( dispResult.normal );
@@ -543,8 +540,6 @@ export function buildShadeKernel( params ) {
543
540
  );
544
541
 
545
542
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
546
- albedoMaps, normalMaps, bumpMaps,
547
- metalnessMaps, roughnessMaps, emissiveMaps,
548
543
  material, samplingUV, N,
549
544
  ) ).toVar();
550
545
 
@@ -1,9 +1,129 @@
1
1
  import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, abs, mix, clamp, texture, textureSize } from 'three/tsl';
2
+ import { DataArrayTexture, LinearFilter } from 'three';
2
3
 
3
4
  import {
4
5
  UVCache,
5
6
  MaterialSamples,
6
7
  } from './Struct.js';
8
+ import { TEXTURE_CONSTANTS } from '../EngineDefaults.js';
9
+
10
+ // ================================================================================
11
+ // CONSOLIDATED SIZE-BUCKETED MATERIAL TEXTURES
12
+ // ================================================================================
13
+ // Material maps are packed into two colorSpace pools (sRGB: albedo+emissive; linear:
14
+ // normal/bump/roughness/metalness/displacement), each split into MATERIAL_BUCKET_COUNT
15
+ // longest-edge size buckets so a small map no longer pays a large neighbour's footprint.
16
+ // A map's stored index encodes (bucket, layer) as bucket * BUCKET_LAYER_STRIDE + layer.
17
+ //
18
+ // The bucket texture nodes live at module level (same pattern as gobo/IES/shadowAlbedo):
19
+ // each stage sets them via setMaterialBucketTextures() right before building its graph,
20
+ // so each pipeline bakes in its own fresh nodes (avoiding cross-pipeline TextureNode
21
+ // caching, just like the per-type nodes did before).
22
+
23
+ const _STRIDE = TEXTURE_CONSTANTS.BUCKET_LAYER_STRIDE;
24
+
25
+ // Array<MATERIAL_BUCKET_COUNT> of texture nodes (never null — empty buckets get placeholders).
26
+ let _srgbBuckets = null;
27
+ let _linearBuckets = null;
28
+
29
+ /**
30
+ * Set the bucket texture node arrays read by the sampling functions. Call before building
31
+ * any graph that samples material textures (Shade / NormalDepth / Debug kernels).
32
+ * @param {Array} srgb sRGB pool nodes (albedo + emissive)
33
+ * @param {Array} linear linear pool nodes (normal/bump/roughness/metalness/displacement)
34
+ */
35
+ export function setMaterialBucketTextures( srgb, linear ) {
36
+
37
+ _srgbBuckets = srgb;
38
+ _linearBuckets = linear;
39
+
40
+ }
41
+
42
+ // Run `fn(node, layer)` inside a runtime branch that selects the bucket node a packed
43
+ // index points to. Emits one If/ElseIf arm per bucket; only the matching arm executes.
44
+ // Caller must guard packedIndex >= 0.
45
+ const withBucket = ( buckets, packedIndex, fn ) => {
46
+
47
+ const bucket = packedIndex.div( int( _STRIDE ) ).toVar();
48
+ const layer = packedIndex.sub( bucket.mul( int( _STRIDE ) ) ).toVar();
49
+ let chain = If( bucket.equal( int( 0 ) ), () => fn( buckets[ 0 ], layer ) );
50
+ for ( let i = 1; i < buckets.length; i ++ ) {
51
+
52
+ chain = chain.ElseIf( bucket.equal( int( i ) ), () => fn( buckets[ i ], layer ) );
53
+
54
+ }
55
+
56
+ return chain;
57
+
58
+ };
59
+
60
+ // Sample the bucket a packed index points to. Caller must guard packedIndex >= 0.
61
+ export const sampleBucket = ( buckets, packedIndex, uv ) => {
62
+
63
+ const result = vec4( 0.0 ).toVar();
64
+ withBucket( buckets, packedIndex, ( node, layer ) => result.assign( texture( node, uv ).depth( layer ) ) );
65
+ return result;
66
+
67
+ };
68
+
69
+ // Per-axis texel size (1/dims) of the bucket a packed displacement/bump index points to,
70
+ // for finite-difference derivative steps. Caller must guard packedIndex >= 0.
71
+ export const bucketTexelSize = ( buckets, packedIndex ) => {
72
+
73
+ const ts = vec2( 1.0 ).toVar();
74
+ withBucket( buckets, packedIndex, node => ts.assign( vec2( 1.0 ).div( vec2( textureSize( node ) ) ) ) );
75
+ return ts;
76
+
77
+ };
78
+
79
+ // The linear-pool node set (displacement lives here) — for Displacement.js texel sizing.
80
+ export function getLinearBucketTextures() {
81
+
82
+ return _linearBuckets;
83
+
84
+ }
85
+
86
+ // 1×1 white placeholder array so empty-bucket branches always reference a valid node.
87
+ export function makeBucketPlaceholder() {
88
+
89
+ const t = new DataArrayTexture( new Uint8Array( [ 255, 255, 255, 255 ] ), 1, 1, 1 );
90
+ t.minFilter = LinearFilter;
91
+ t.magFilter = LinearFilter;
92
+ t.generateMipmaps = false;
93
+ t.needsUpdate = true;
94
+ return t;
95
+
96
+ }
97
+
98
+ // Build MATERIAL_BUCKET_COUNT texture nodes from a bucket array (DataArrayTexture | null per
99
+ // bucket). Null buckets get a fresh placeholder. Each caller builds its OWN nodes so each
100
+ // pipeline bakes in independent TextureNodes (no cross-pipeline caching).
101
+ export function buildBucketTextureNodes( bucketArrays ) {
102
+
103
+ const K = TEXTURE_CONSTANTS.MATERIAL_BUCKET_COUNT;
104
+ const nodes = [];
105
+ for ( let i = 0; i < K; i ++ ) {
106
+
107
+ const arr = bucketArrays && bucketArrays[ i ];
108
+ nodes.push( texture( arr || makeBucketPlaceholder() ) );
109
+
110
+ }
111
+
112
+ return nodes;
113
+
114
+ }
115
+
116
+ // Update in-place the .value of bucket nodes from a fresh bucket array (model change).
117
+ export function refreshBucketTextureNodes( nodes, bucketArrays ) {
118
+
119
+ if ( ! nodes || ! bucketArrays ) return;
120
+ for ( let i = 0; i < nodes.length; i ++ ) {
121
+
122
+ if ( bucketArrays[ i ] && nodes[ i ] ) nodes[ i ].value = bucketArrays[ i ];
123
+
124
+ }
125
+
126
+ }
7
127
 
8
128
  // ================================================================================
9
129
  // FAST UTILITY FUNCTIONS
@@ -201,14 +321,14 @@ export const computeUVCache = Fn( ( [ baseUV, material ] ) => {
201
321
  // PROCESSING FUNCTIONS
202
322
  // ================================================================================
203
323
 
204
- export const processAlbedo = Fn( ( [ albedoMaps, material, uvCache ] ) => {
324
+ export const processAlbedo = Fn( ( [ material, uvCache ] ) => {
205
325
 
206
326
  const result = material.color.toVar();
207
327
 
208
328
  If( material.albedoMapIndex.greaterThanEqual( int( 0 ) ), () => {
209
329
 
210
- const albedoSample = texture( albedoMaps, uvCache.albedoUV ).depth( int( material.albedoMapIndex ) ).toVar();
211
- // sRGB→linear handled by GPU hardware (texture.colorSpace = SRGBColorSpace → rgba8unorm-srgb format)
330
+ const albedoSample = sampleBucket( _srgbBuckets, material.albedoMapIndex, uvCache.albedoUV ).toVar();
331
+ // sRGB→linear handled by GPU hardware (sRGB bucket arrays carry SRGBColorSpace → rgba8unorm-srgb)
212
332
  result.assign( vec4( material.color.rgb.mul( albedoSample.rgb ), material.color.a.mul( albedoSample.a ) ) );
213
333
 
214
334
  } );
@@ -217,15 +337,15 @@ export const processAlbedo = Fn( ( [ albedoMaps, material, uvCache ] ) => {
217
337
 
218
338
  } );
219
339
 
220
- export const processMetalnessRoughness = Fn( ( [ metalnessMaps, roughnessMaps, material, uvCache ] ) => {
340
+ export const processMetalnessRoughness = Fn( ( [ material, uvCache ] ) => {
221
341
 
222
342
  const metalness = material.metalness.toVar();
223
343
  const roughness = material.roughness.toVar();
224
344
 
225
345
  If( material.metalnessMapIndex.greaterThanEqual( int( 0 ) ).and( material.metalnessMapIndex.equal( material.roughnessMapIndex ) ), () => {
226
346
 
227
- // Same texture for both
228
- const sample = texture( metalnessMaps, uvCache.metalnessUV ).depth( int( material.metalnessMapIndex ) );
347
+ // Same packed index → same bucket layer (e.g. ORM) → sample once.
348
+ const sample = sampleBucket( _linearBuckets, material.metalnessMapIndex, uvCache.metalnessUV );
229
349
  metalness.assign( material.metalness.mul( sample.b ) );
230
350
  roughness.assign( material.roughness.mul( sample.g ) );
231
351
 
@@ -233,14 +353,14 @@ export const processMetalnessRoughness = Fn( ( [ metalnessMaps, roughnessMaps, m
233
353
 
234
354
  If( material.metalnessMapIndex.greaterThanEqual( int( 0 ) ), () => {
235
355
 
236
- const metSample = texture( metalnessMaps, uvCache.metalnessUV ).depth( int( material.metalnessMapIndex ) );
356
+ const metSample = sampleBucket( _linearBuckets, material.metalnessMapIndex, uvCache.metalnessUV );
237
357
  metalness.assign( material.metalness.mul( metSample.b ) );
238
358
 
239
359
  } );
240
360
 
241
361
  If( material.roughnessMapIndex.greaterThanEqual( int( 0 ) ), () => {
242
362
 
243
- const rghSample = texture( roughnessMaps, uvCache.roughnessUV ).depth( int( material.roughnessMapIndex ) );
363
+ const rghSample = sampleBucket( _linearBuckets, material.roughnessMapIndex, uvCache.roughnessUV );
244
364
  roughness.assign( material.roughness.mul( rghSample.g ) );
245
365
 
246
366
  } );
@@ -251,13 +371,13 @@ export const processMetalnessRoughness = Fn( ( [ metalnessMaps, roughnessMaps, m
251
371
 
252
372
  } );
253
373
 
254
- export const processNormal = Fn( ( [ normalMaps, geometryNormal, material, uvCache ] ) => {
374
+ export const processNormal = Fn( ( [ geometryNormal, material, uvCache ] ) => {
255
375
 
256
376
  const result = geometryNormal.toVar();
257
377
 
258
378
  If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
259
379
 
260
- const normalSample = texture( normalMaps, uvCache.normalUV ).depth( int( material.normalMapIndex ) );
380
+ const normalSample = sampleBucket( _linearBuckets, material.normalMapIndex, uvCache.normalUV );
261
381
  const normalMap = normalSample.xyz.mul( 2.0 ).sub( 1.0 ).toVar();
262
382
  normalMap.x.mulAssign( material.normalScale.x );
263
383
  normalMap.y.assign( normalMap.y.negate().mul( material.normalScale.x ) );
@@ -277,21 +397,25 @@ export const processNormal = Fn( ( [ normalMaps, geometryNormal, material, uvCac
277
397
 
278
398
  } );
279
399
 
280
- export const processBump = Fn( ( [ bumpMaps, currentNormal, material, uvCache ] ) => {
400
+ export const processBump = Fn( ( [ currentNormal, material, uvCache ] ) => {
281
401
 
282
402
  const result = currentNormal.toVar();
283
403
 
284
404
  If( material.bumpMapIndex.greaterThanEqual( int( 0 ) ).and( material.bumpScale.greaterThan( 0.0 ) ), () => {
285
405
 
286
- // Texel size from the actual bump-array dimensions (finite-difference step).
287
- const texelSize = vec2( 1.0 ).div( vec2( textureSize( bumpMaps ) ) ).toVar();
406
+ // Taps + texel size come from the SELECTED bucket node (its real dimensions), so the
407
+ // finite-difference step is correct per bucket done inside one bucket-branch.
408
+ const bumpNormal = vec3( 0.0, 0.0, 1.0 ).toVar();
409
+ withBucket( _linearBuckets, material.bumpMapIndex, ( node, layer ) => {
288
410
 
289
- const h_c = texture( bumpMaps, uvCache.bumpUV ).depth( int( material.bumpMapIndex ) ).r;
290
- const h_u = texture( bumpMaps, vec2( uvCache.bumpUV.x.add( texelSize.x ), uvCache.bumpUV.y ) ).depth( int( material.bumpMapIndex ) ).r;
291
- const h_v = texture( bumpMaps, vec2( uvCache.bumpUV.x, uvCache.bumpUV.y.add( texelSize.y ) ) ).depth( int( material.bumpMapIndex ) ).r;
411
+ const texelSize = vec2( 1.0 ).div( vec2( textureSize( node ) ) ).toVar();
412
+ const h_c = texture( node, uvCache.bumpUV ).depth( layer ).r;
413
+ const h_u = texture( node, vec2( uvCache.bumpUV.x.add( texelSize.x ), uvCache.bumpUV.y ) ).depth( layer ).r;
414
+ const h_v = texture( node, vec2( uvCache.bumpUV.x, uvCache.bumpUV.y.add( texelSize.y ) ) ).depth( layer ).r;
415
+ const gradient = vec2( h_u.sub( h_c ), h_v.sub( h_c ) ).mul( material.bumpScale );
416
+ bumpNormal.assign( normalize( vec3( gradient.x.negate(), gradient.y.negate(), 1.0 ) ) );
292
417
 
293
- const gradient = vec2( h_u.sub( h_c ), h_v.sub( h_c ) ).mul( material.bumpScale );
294
- const bumpNormal = normalize( vec3( gradient.x.negate(), gradient.y.negate(), 1.0 ) );
418
+ } );
295
419
 
296
420
  // Build TBN matrix using current normal
297
421
  const up = abs( currentNormal.z ).lessThan( 0.999 ).select( vec3( 0.0, 0.0, 1.0 ), vec3( 1.0, 0.0, 0.0 ) );
@@ -307,14 +431,14 @@ export const processBump = Fn( ( [ bumpMaps, currentNormal, material, uvCache ]
307
431
 
308
432
  } );
309
433
 
310
- export const processEmissive = Fn( ( [ emissiveMaps, material, uvCache ] ) => {
434
+ export const processEmissive = Fn( ( [ material, uvCache ] ) => {
311
435
 
312
436
  const emissionBase = material.emissive.mul( material.emissiveIntensity ).toVar();
313
437
 
314
438
  If( material.emissiveMapIndex.greaterThanEqual( int( 0 ) ), () => {
315
439
 
316
- const emissiveSample = texture( emissiveMaps, uvCache.emissiveUV ).depth( int( material.emissiveMapIndex ) ).toVar();
317
- // sRGB→linear handled by GPU hardware (texture.colorSpace = SRGBColorSpace → rgba8unorm-srgb format)
440
+ const emissiveSample = sampleBucket( _srgbBuckets, material.emissiveMapIndex, uvCache.emissiveUV ).toVar();
441
+ // sRGB→linear handled by GPU hardware (sRGB bucket arrays carry SRGBColorSpace → rgba8unorm-srgb)
318
442
  emissionBase.assign( emissionBase.mul( emissiveSample.rgb ) );
319
443
 
320
444
  } );
@@ -327,10 +451,7 @@ export const processEmissive = Fn( ( [ emissiveMaps, material, uvCache ] ) => {
327
451
  // MAIN BATCHED SAMPLING FUNCTION
328
452
  // ================================================================================
329
453
 
330
- export const sampleAllMaterialTextures = Fn( ( [
331
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
332
- material, uv, geometryNormal
333
- ] ) => {
454
+ export const sampleAllMaterialTextures = Fn( ( [ material, uv, geometryNormal ] ) => {
334
455
 
335
456
  const albedo = vec4( 0.0 ).toVar();
336
457
  const emissive = vec3( 0.0 ).toVar();
@@ -351,17 +472,17 @@ export const sampleAllMaterialTextures = Fn( ( [
351
472
  // Compute optimized UV cache with redundancy detection
352
473
  const uvCache = UVCache.wrap( computeUVCache( uv, material ) ).toVar();
353
474
 
354
- // Process samples
355
- albedo.assign( processAlbedo( albedoMaps, material, uvCache ) );
475
+ // Process samples (bucket nodes read from module-level state)
476
+ albedo.assign( processAlbedo( material, uvCache ) );
356
477
 
357
- const metalRough = processMetalnessRoughness( metalnessMaps, roughnessMaps, material, uvCache );
478
+ const metalRough = processMetalnessRoughness( material, uvCache );
358
479
  metalness.assign( metalRough.x );
359
480
  roughness.assign( metalRough.y );
360
481
 
361
- const currentNormal = processNormal( normalMaps, geometryNormal, material, uvCache ).toVar();
362
- normal.assign( processBump( bumpMaps, currentNormal, material, uvCache ) );
482
+ const currentNormal = processNormal( geometryNormal, material, uvCache ).toVar();
483
+ normal.assign( processBump( currentNormal, material, uvCache ) );
363
484
 
364
- emissive.assign( processEmissive( emissiveMaps, material, uvCache ) );
485
+ emissive.assign( processEmissive( material, uvCache ) );
365
486
 
366
487
  } );
367
488
 
@@ -369,15 +490,15 @@ export const sampleAllMaterialTextures = Fn( ( [
369
490
 
370
491
  } );
371
492
 
372
- // Sample displacement map at given UV coordinates
373
- export const sampleDisplacementMap = Fn( ( [ displacementMaps, displacementMapIndex, uv, transform ] ) => {
493
+ // Sample displacement map (linear pool) at given UV coordinates.
494
+ export const sampleDisplacementMap = Fn( ( [ displacementMapIndex, uv, transform ] ) => {
374
495
 
375
496
  const result = float( 0.0 ).toVar();
376
497
 
377
498
  If( displacementMapIndex.greaterThanEqual( int( 0 ) ), () => {
378
499
 
379
500
  const transformedUV = getTransformedUV( { uv, transform } );
380
- result.assign( texture( displacementMaps, transformedUV ).depth( int( displacementMapIndex ) ).r );
501
+ result.assign( sampleBucket( _linearBuckets, displacementMapIndex, transformedUV ).r );
381
502
 
382
503
  } );
383
504
 
@@ -34,14 +34,18 @@ export class MaterialDataManager {
34
34
  this.materialStorageNode = null;
35
35
  this.materialCount = 0;
36
36
 
37
- // Material texture arrays
38
- this.albedoMaps = null;
39
- this.emissiveMaps = null;
40
- this.normalMaps = null;
41
- this.bumpMaps = null;
42
- this.roughnessMaps = null;
43
- this.metalnessMaps = null;
44
- this.displacementMaps = null;
37
+ // Consolidated size-bucketed material texture arrays (see SceneProcessor._bucketTextures):
38
+ // srgbBuckets[K] — albedo + emissive (SRGBColorSpace)
39
+ // linearBuckets[K] normal/bump/roughness/metalness/displacement
40
+ // Each entry is a DataArrayTexture | null (null = empty bucket).
41
+ this.srgbBuckets = null;
42
+ this.linearBuckets = null;
43
+
44
+ // uuid → packed (bucket,layer) index maps for the current scene, handed over by the
45
+ // SceneProcessor that built the buckets. Let runtime material edits (updateMaterial)
46
+ // re-pack a texture's index against the current bucket layout.
47
+ this._srgbTexPacked = null;
48
+ this._linearTexPacked = null;
45
49
 
46
50
  // Compiled features cache (for change detection)
47
51
  this.compiledFeatures = null;
@@ -112,45 +116,60 @@ export class MaterialDataManager {
112
116
  */
113
117
  setMaterialTextures( textures ) {
114
118
 
115
- if ( textures.albedoMaps ) this.albedoMaps = textures.albedoMaps;
116
- if ( textures.emissiveMaps ) this.emissiveMaps = textures.emissiveMaps;
117
- if ( textures.normalMaps ) this.normalMaps = textures.normalMaps;
118
- if ( textures.bumpMaps ) this.bumpMaps = textures.bumpMaps;
119
- if ( textures.roughnessMaps ) this.roughnessMaps = textures.roughnessMaps;
120
- if ( textures.metalnessMaps ) this.metalnessMaps = textures.metalnessMaps;
121
- if ( textures.displacementMaps ) this.displacementMaps = textures.displacementMaps;
119
+ if ( textures.srgbBuckets ) this.srgbBuckets = textures.srgbBuckets;
120
+ if ( textures.linearBuckets ) this.linearBuckets = textures.linearBuckets;
122
121
 
123
122
  }
124
123
 
125
124
  /**
126
- * Load texture arrays from sdfs.
125
+ * Receive the scene's uuid→packed texture-index maps (from the SceneProcessor that bucketed).
126
+ * @param {Map|null} srgb
127
+ * @param {Map|null} linear
128
+ */
129
+ setTexturePackMaps( srgb, linear ) {
130
+
131
+ this._srgbTexPacked = srgb || null;
132
+ this._linearTexPacked = linear || null;
133
+
134
+ }
135
+
136
+ /**
137
+ * Packed (bucket, layer) index for a Three.js texture against the current bucket layout,
138
+ * or -1 if it isn't bucketed (a genuinely new texture → needs rebuildMaterials).
139
+ * @param {import('three').Texture|null} texture
140
+ * @param {boolean} isSrgb - true for albedo/emissive pool, false for the linear pool
141
+ * @returns {number}
142
+ */
143
+ getPackedTextureIndex( texture, isSrgb ) {
144
+
145
+ if ( ! texture ) return - 1;
146
+ const uuid = texture.source?.uuid ?? texture.uuid;
147
+ const map = isSrgb ? this._srgbTexPacked : this._linearTexPacked;
148
+ const packed = map?.get( uuid );
149
+ return packed === undefined ? - 1 : packed;
150
+
151
+ }
152
+
153
+ /**
154
+ * Load consolidated bucket arrays + pack maps from the SceneProcessor.
127
155
  */
128
156
  loadTexturesFromSdfs() {
129
157
 
130
- this.albedoMaps = this.sdfs.albedoTextures;
131
- this.emissiveMaps = this.sdfs.emissiveTextures;
132
- this.normalMaps = this.sdfs.normalTextures;
133
- this.bumpMaps = this.sdfs.bumpTextures;
134
- this.roughnessMaps = this.sdfs.roughnessTextures;
135
- this.metalnessMaps = this.sdfs.metalnessTextures;
136
- this.displacementMaps = this.sdfs.displacementTextures;
158
+ this.srgbBuckets = this.sdfs.srgbBucketTextures;
159
+ this.linearBuckets = this.sdfs.linearBucketTextures;
160
+ this.setTexturePackMaps( this.sdfs._srgbTexPacked, this.sdfs._linearTexPacked );
137
161
 
138
162
  }
139
163
 
140
164
  /**
141
- * Get all texture arrays.
142
- * @returns {Object}
165
+ * Get the consolidated bucket arrays.
166
+ * @returns {{ srgbBuckets: Array, linearBuckets: Array }}
143
167
  */
144
168
  getTextureArrays() {
145
169
 
146
170
  return {
147
- albedoMaps: this.albedoMaps,
148
- emissiveMaps: this.emissiveMaps,
149
- normalMaps: this.normalMaps,
150
- bumpMaps: this.bumpMaps,
151
- roughnessMaps: this.roughnessMaps,
152
- metalnessMaps: this.metalnessMaps,
153
- displacementMaps: this.displacementMaps,
171
+ srgbBuckets: this.srgbBuckets,
172
+ linearBuckets: this.linearBuckets,
154
173
  };
155
174
 
156
175
  }
@@ -550,6 +569,22 @@ export class MaterialDataManager {
550
569
  updateMaterial( materialIndex, material ) {
551
570
 
552
571
  const completeMaterialData = this.sdfs.geometryExtractor.createMaterialObject( material );
572
+
573
+ // createMaterialObject returns stale per-type indices; re-pack each map to the packed
574
+ // (bucket, layer) index for the CURRENT bucket layout. -1 for a texture not yet bucketed
575
+ // (a genuinely new map → the caller must rebuildMaterials to add it to a bucket array).
576
+ if ( this._srgbTexPacked || this._linearTexPacked ) {
577
+
578
+ completeMaterialData.map = this.getPackedTextureIndex( material.map, true );
579
+ completeMaterialData.emissiveMap = this.getPackedTextureIndex( material.emissiveMap, true );
580
+ completeMaterialData.normalMap = this.getPackedTextureIndex( material.normalMap, false );
581
+ completeMaterialData.bumpMap = this.getPackedTextureIndex( material.bumpMap, false );
582
+ completeMaterialData.roughnessMap = this.getPackedTextureIndex( material.roughnessMap, false );
583
+ completeMaterialData.metalnessMap = this.getPackedTextureIndex( material.metalnessMap, false );
584
+ completeMaterialData.displacementMap = this.getPackedTextureIndex( material.displacementMap, false );
585
+
586
+ }
587
+
553
588
  this.updateMaterialDataFromObject( materialIndex, completeMaterialData );
554
589
 
555
590
  }
@@ -813,13 +848,10 @@ export class MaterialDataManager {
813
848
  this.materialStorageAttr = null;
814
849
  this.materialStorageNode = null;
815
850
  this.materialCount = 0;
816
- this.albedoMaps = null;
817
- this.emissiveMaps = null;
818
- this.normalMaps = null;
819
- this.bumpMaps = null;
820
- this.roughnessMaps = null;
821
- this.metalnessMaps = null;
822
- this.displacementMaps = null;
851
+ this.srgbBuckets = null;
852
+ this.linearBuckets = null;
853
+ this._srgbTexPacked = null;
854
+ this._linearTexPacked = null;
823
855
  this.compiledFeatures = null;
824
856
 
825
857
  }