rayzee 7.6.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.
@@ -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
  } );
@@ -200,7 +202,6 @@ export class PathTracer extends PathTracerStage {
200
202
  if ( this.shaderBuilder.prevColorTexNode ) {
201
203
 
202
204
  this.shaderBuilder.prevColorTexNode.value = readTextures.color;
203
- this.shaderBuilder.prevNormalDepthTexNode.value = readTextures.normalDepth;
204
205
  this.shaderBuilder.prevAlbedoTexNode.value = readTextures.albedo;
205
206
 
206
207
  }
@@ -450,13 +451,8 @@ export class PathTracer extends PathTracerStage {
450
451
 
451
452
  const mat = this.materialData;
452
453
  if ( ! mat ) return;
453
- if ( mat.albedoMaps && t.albedoMaps ) t.albedoMaps.value = mat.albedoMaps;
454
- if ( mat.normalMaps && t.normalMaps ) t.normalMaps.value = mat.normalMaps;
455
- if ( mat.bumpMaps && t.bumpMaps ) t.bumpMaps.value = mat.bumpMaps;
456
- if ( mat.metalnessMaps && t.metalnessMaps ) t.metalnessMaps.value = mat.metalnessMaps;
457
- if ( mat.roughnessMaps && t.roughnessMaps ) t.roughnessMaps.value = mat.roughnessMaps;
458
- if ( mat.emissiveMaps && t.emissiveMaps ) t.emissiveMaps.value = mat.emissiveMaps;
459
- if ( mat.displacementMaps && t.displacementMaps ) t.displacementMaps.value = mat.displacementMaps;
454
+ refreshBucketTextureNodes( t.srgbBuckets, mat.srgbBuckets );
455
+ refreshBucketTextureNodes( t.linearBuckets, mat.linearBuckets );
460
456
 
461
457
  }
462
458
 
@@ -570,7 +566,6 @@ export class PathTracer extends PathTracerStage {
570
566
  this._wfMaxRayCount.value = maxRays;
571
567
 
572
568
  const prevColor = this.shaderBuilder.prevColorTexNode;
573
- const prevND = this.shaderBuilder.prevNormalDepthTexNode;
574
569
  const prevAlbedo = this.shaderBuilder.prevAlbedoTexNode;
575
570
  const writeTex = this.storageTextures.getWriteTextures();
576
571
 
@@ -663,26 +658,21 @@ export class PathTracer extends PathTracerStage {
663
658
  // Independent texture nodes (never compiled elsewhere) avoid Three.js TextureNode caching across pipelines; refreshed via _refreshWfTextureNodes.
664
659
  const _mat = this.materialData;
665
660
  const _env = this.environment;
666
- const _placeholder = texNodes.albedoMapsTex;
667
- const freshAlbedoMaps = _mat.albedoMaps ? texture( _mat.albedoMaps ) : _placeholder;
668
- const freshNormalMaps = _mat.normalMaps ? texture( _mat.normalMaps ) : texNodes.normalMapsTex;
669
- const freshBumpMaps = _mat.bumpMaps ? texture( _mat.bumpMaps ) : texNodes.bumpMapsTex;
670
- const freshMetalnessMaps = _mat.metalnessMaps ? texture( _mat.metalnessMaps ) : texNodes.metalnessMapsTex;
671
- const freshRoughnessMaps = _mat.roughnessMaps ? texture( _mat.roughnessMaps ) : texNodes.roughnessMapsTex;
672
- const freshEmissiveMaps = _mat.emissiveMaps ? texture( _mat.emissiveMaps ) : texNodes.emissiveMapsTex;
673
- 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 );
674
669
  const freshEnvTex = _env.environmentTexture ? texture( _env.environmentTexture ) : texNodes.envTex;
675
670
 
676
671
  this._wfTexNodes = {
677
672
  envTex: freshEnvTex,
678
673
  envCDFTex: freshEnvCDF,
679
- albedoMaps: freshAlbedoMaps,
680
- normalMaps: freshNormalMaps,
681
- bumpMaps: freshBumpMaps,
682
- metalnessMaps: freshMetalnessMaps,
683
- roughnessMaps: freshRoughnessMaps,
684
- emissiveMaps: freshEmissiveMaps,
685
- displacementMaps: freshDisplacementMaps,
674
+ srgbBuckets: freshSrgbBuckets,
675
+ linearBuckets: freshLinearBuckets,
686
676
  };
687
677
 
688
678
  // Material-coherence sort gate (experiment): only worthwhile above a few materials.
@@ -763,13 +753,6 @@ export class PathTracer extends PathTracerStage {
763
753
  hitBufferRO: pb.hitBuffer.ro,
764
754
  counters,
765
755
  activeIndicesRO: this._sortMaterials ? qm.getSortedRO() : qm.getActiveReadRO(),
766
- albedoMaps: freshAlbedoMaps,
767
- normalMaps: freshNormalMaps,
768
- bumpMaps: freshBumpMaps,
769
- metalnessMaps: freshMetalnessMaps,
770
- roughnessMaps: freshRoughnessMaps,
771
- emissiveMaps: freshEmissiveMaps,
772
- displacementMaps: freshDisplacementMaps,
773
756
  envTexture: freshEnvTex,
774
757
  environmentIntensity: this.environmentIntensity,
775
758
  envMatrix: this.environmentMatrix,
@@ -885,7 +868,6 @@ export class PathTracer extends PathTracerStage {
885
868
  cameraIsMoving: this.cameraIsMoving,
886
869
  transparentBackground: this.transparentBackground,
887
870
  prevAccumTexture: prevColor,
888
- prevNormalDepthTexture: prevND,
889
871
  prevAlbedoTexture: prevAlbedo,
890
872
  renderWidth: this._wfRenderWidth,
891
873
  renderHeight: this._wfRenderHeight,
@@ -929,12 +911,6 @@ export class PathTracer extends PathTracerStage {
929
911
  enableEnvironmentLight: this.enableEnvironment,
930
912
  visMode: this.visMode,
931
913
  debugVisScale: this.debugVisScale,
932
- albedoMaps: freshAlbedoMaps,
933
- normalMaps: freshNormalMaps,
934
- bumpMaps: freshBumpMaps,
935
- metalnessMaps: freshMetalnessMaps,
936
- roughnessMaps: freshRoughnessMaps,
937
- emissiveMaps: freshEmissiveMaps,
938
914
  frame: this.frame,
939
915
  } );
940
916
  this._kernelManager.register( 'debug',
@@ -30,7 +30,6 @@ export function buildDebugKernel( params ) {
30
30
  bvhBuffer, triangleBuffer, materialBuffer,
31
31
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
32
32
  visMode, debugVisScale,
33
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
34
33
  frame,
35
34
  } = params;
36
35
 
@@ -73,7 +72,6 @@ export function buildDebugKernel( params ) {
73
72
  envTexture, environmentMatrix, environmentIntensity, enableEnvironmentLight,
74
73
  visMode, debugVisScale,
75
74
  pixelCoord, resolution,
76
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
77
75
  cameraProjectionMatrix, cameraViewMatrix,
78
76
  frame,
79
77
  ) );
@@ -71,9 +71,7 @@ export const TraceDebugMode = Fn( ( [
71
71
  visMode, debugVisScale,
72
72
  // Screen info
73
73
  pixelCoord, resolution,
74
- // Texture arrays (for MRT debug modes)
75
- albedoMaps, normalMaps, bumpMaps,
76
- metalnessMaps, roughnessMaps, emissiveMaps,
74
+ // Material textures are read from the module-level bucket nodes (setMaterialBucketTextures).
77
75
  // Camera matrices (for depth debug mode)
78
76
  cameraProjectionMatrix, cameraViewMatrix,
79
77
  // Frame counter (for stochastic debug modes)
@@ -204,7 +202,6 @@ export const TraceDebugMode = Fn( ( [
204
202
 
205
203
  // Sample all textures to get emissive
206
204
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
207
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
208
205
  material, hitInfo.uv, hitInfo.normal,
209
206
  ) ).toVar();
210
207
 
@@ -249,7 +246,6 @@ export const TraceDebugMode = Fn( ( [
249
246
 
250
247
  // Get material-mapped normal (same as what's used in main shader)
251
248
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
252
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
253
249
  material, hitInfo.uv, hitInfo.normal,
254
250
  ) ).toVar();
255
251
 
@@ -296,7 +292,6 @@ export const TraceDebugMode = Fn( ( [
296
292
  const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
297
293
 
298
294
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
299
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
300
295
  material, hitInfo.uv, hitInfo.normal,
301
296
  ) ).toVar();
302
297
 
@@ -323,7 +318,6 @@ export const TraceDebugMode = Fn( ( [
323
318
  // Get primary surface material
324
319
  const material = RayTracingMaterial.wrap( getMaterial( hitInfo.materialIndex, materialBuffer ) ).toVar();
325
320
  const matSamples = MaterialSamples.wrap( sampleAllMaterialTextures(
326
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
327
321
  material, hitInfo.uv, hitInfo.normal,
328
322
  ) ).toVar();
329
323
 
@@ -370,7 +364,6 @@ export const TraceDebugMode = Fn( ( [
370
364
  // Bounce hit surface B — evaluate its contribution
371
365
  const materialB = RayTracingMaterial.wrap( getMaterial( bounceHit.materialIndex, materialBuffer ) ).toVar();
372
366
  const matSamplesB = MaterialSamples.wrap( sampleAllMaterialTextures(
373
- albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
374
367
  materialB, bounceHit.uv, bounceHit.normal,
375
368
  ) ).toVar();
376
369
 
@@ -1,8 +1,8 @@
1
- import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, textureSize } from 'three/tsl';
1
+ import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max } from 'three/tsl';
2
2
 
3
3
  import { struct } from './patches.js';
4
4
  import { getDatafromStorageBuffer } from './Common.js';
5
- import { sampleDisplacementMap } from './TextureSampling.js';
5
+ import { sampleDisplacementMap, bucketTexelSize, getLinearBucketTextures } from './TextureSampling.js';
6
6
 
7
7
  // Ray-displacement intersection configuration
8
8
  const MAX_MARCH_STEPS = 32;
@@ -32,7 +32,7 @@ export const DisplacementResult = struct( {
32
32
  * We find dt where h_ray(dt) = h_surf(dt).
33
33
  */
34
34
  export const refineDisplacedIntersection = Fn( ( [
35
- ray, hitInfo, triangleBuffer, displacementMaps, material, bounceIndex
35
+ ray, hitInfo, triangleBuffer, material, bounceIndex
36
36
  ] ) => {
37
37
 
38
38
  const resultHitPoint = hitInfo.hitPoint.toVar();
@@ -135,7 +135,7 @@ export const refineDisplacedIntersection = Fn( ( [
135
135
 
136
136
  // Displaced surface height
137
137
  const heightSample = sampleDisplacementMap(
138
- displacementMaps, material.displacementMapIndex, marchUV, material.displacementTransform,
138
+ material.displacementMapIndex, marchUV, material.displacementTransform,
139
139
  );
140
140
  const surfaceHeight = heightSample.sub( 0.5 ).mul( scale );
141
141
 
@@ -167,7 +167,7 @@ export const refineDisplacedIntersection = Fn( ( [
167
167
  const midUV = hitInfo.uv.add( dUV_dt.mul( midDt ) );
168
168
  const midRayHeight = midDt.mul( dh_ray_dt );
169
169
  const midSample = sampleDisplacementMap(
170
- displacementMaps, material.displacementMapIndex, midUV, material.displacementTransform,
170
+ material.displacementMapIndex, midUV, material.displacementTransform,
171
171
  );
172
172
  const midSurfHeight = midSample.sub( 0.5 ).mul( scale );
173
173
 
@@ -189,19 +189,19 @@ export const refineDisplacedIntersection = Fn( ( [
189
189
  const finalPoint = hitInfo.hitPoint.add( rayDir.mul( finalDt ) );
190
190
 
191
191
  const finalHeight = sampleDisplacementMap(
192
- displacementMaps, material.displacementMapIndex, finalUV, material.displacementTransform,
192
+ material.displacementMapIndex, finalUV, material.displacementTransform,
193
193
  );
194
194
  const displacedHeight = finalHeight.sub( 0.5 ).mul( scale );
195
195
 
196
196
  // Compute displaced normal from height-field gradients using UV tangent vectors.
197
- // Finite-difference step = one texel of the actual displacement-array dimensions.
198
- const texel = vec2( 1.0 ).div( vec2( textureSize( displacementMaps ) ) ).toVar();
197
+ // Finite-difference step = one texel of the SELECTED bucket array's real dimensions.
198
+ const texel = bucketTexelSize( getLinearBucketTextures(), material.displacementMapIndex ).toVar();
199
199
  const hC = finalHeight;
200
200
  const hU = sampleDisplacementMap(
201
- displacementMaps, material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
201
+ material.displacementMapIndex, finalUV.add( vec2( texel.x, 0.0 ) ), material.displacementTransform,
202
202
  );
203
203
  const hV = sampleDisplacementMap(
204
- displacementMaps, material.displacementMapIndex, finalUV.add( vec2( 0.0, texel.y ) ), material.displacementTransform,
204
+ material.displacementMapIndex, finalUV.add( vec2( 0.0, texel.y ) ), material.displacementTransform,
205
205
  );
206
206
 
207
207
  // Perturb normal using actual tangent/bitangent from UV parameterization
@@ -32,7 +32,7 @@ export function buildFinalWriteKernel( params ) {
32
32
  resolution, frame,
33
33
  enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
34
34
  transparentBackground,
35
- prevAccumTexture, prevNormalDepthTexture, prevAlbedoTexture,
35
+ prevAccumTexture, prevAlbedoTexture,
36
36
  renderWidth, renderHeight,
37
37
  visMode,
38
38
  // Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
@@ -80,7 +80,10 @@ export function buildFinalWriteKernel( params ) {
80
80
  finalColor.assign( mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) );
81
81
  If( auxOn, () => {
82
82
 
83
- finalNormalDepth.assign( mix( texture( prevNormalDepthTexture, prevUV, 0 ), finalNormalDepth, accumulationAlpha ) );
83
+ // Albedo averages cleanly (it's a colour). The NORMAL must NOT: averaging jittered
84
+ // unit normals collapses toward the flat geometric mean (worse at distance), which made
85
+ // OIDN smooth bump detail away. Keep this frame's point-sampled normal — it varies with
86
+ // the bump, which is exactly what OIDN's edge-stop needs to preserve it.
84
87
  finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
85
88
 
86
89
  } );
@@ -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