rayzee 11.2.0 → 11.3.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.
Files changed (63) hide show
  1. package/README.md +62 -33
  2. package/dist/ArchiveImporter-D735xX-4.js +7860 -0
  3. package/dist/BidirectionalIntegrator-M89c0AlP.js +954 -0
  4. package/dist/{BufferLayout-D_IBo2ci.js → BufferLayout-xPRH9jYD.js} +41 -5
  5. package/dist/{NeuralRetouch-DFGbnmLb.js → NeuralRetouch-DiBH63kz.js} +1 -1
  6. package/dist/{NeuralSuperRes-BtXvyJQl.js → NeuralSuperRes-CZNQP13W.js} +1 -1
  7. package/dist/{PathTracer-BHXGJtPN.js → PathTracer-BXCCVrPK.js} +4371 -4612
  8. package/dist/{PhysicalSky-DN6u8UX4.js → PhysicalSky-DqmaS0lb.js} +2 -2
  9. package/dist/{RayzeeRenderer-Dw1x8mVL.js → RayzeeRenderer-DUSMFhmc.js} +426 -363
  10. package/dist/SolarLimb-bZE6y63S.js +762 -0
  11. package/dist/addons/archives.es.js +1 -1
  12. package/dist/addons/bidirectional.es.js +1 -1
  13. package/dist/addons/physical-sky.es.js +1 -1
  14. package/dist/rayzee-core.es.js +7 -7
  15. package/dist/rayzee.es.js +472 -468
  16. package/dist/rayzee.umd.js +106 -105
  17. package/dist/sceneCachePolicy-BAib5fCp.js +1602 -0
  18. package/package.json +1 -1
  19. package/src/EngineDefaults.js +1 -1
  20. package/src/PathTracerApp.js +4 -3
  21. package/src/Processor/ArchiveImporter.js +173 -94
  22. package/src/Processor/ArchiveReader.js +20 -23
  23. package/src/Processor/AssetLoader.js +48 -1
  24. package/src/Processor/BVHRefitter.js +109 -13
  25. package/src/Processor/BufferLayout.js +115 -0
  26. package/src/Processor/ChunkedRecords.js +3 -2
  27. package/src/Processor/EmissiveTriangleBuilder.js +6 -6
  28. package/src/Processor/GeometryExtractor.js +189 -79
  29. package/src/Processor/HostMemory.js +39 -9
  30. package/src/Processor/InstanceTable.js +714 -42
  31. package/src/Processor/PBRT/PBRTCurves.js +224 -6
  32. package/src/Processor/PBRT/PBRTMaterials.js +3 -0
  33. package/src/Processor/PBRT/PBRTSceneBuilder.js +82 -25
  34. package/src/Processor/PackedRayBuffer.js +5 -2
  35. package/src/Processor/SceneProcessor.js +747 -141
  36. package/src/Processor/TLASBuilder.js +32 -17
  37. package/src/Processor/USD/USDLayer.js +1149 -0
  38. package/src/Processor/USD/USDScene.js +1655 -0
  39. package/src/Processor/USD/USDStage.js +682 -0
  40. package/src/Processor/USD/USDText.js +674 -0
  41. package/src/Processor/USD/index.js +138 -0
  42. package/src/Processor/Workers/BVHRefitWorker.js +3 -3
  43. package/src/RayzeeRenderer.js +168 -27
  44. package/src/Stages/NormalDepth.js +7 -12
  45. package/src/Stages/PathTracer.js +12 -17
  46. package/src/Stages/PathTracerStage.js +273 -96
  47. package/src/Storage/GeometrySpill.js +46 -14
  48. package/src/Storage/SceneGraphCodec.js +17 -3
  49. package/src/Storage/SpillStore.js +9 -0
  50. package/src/TSL/BVHTraversal.js +183 -7
  51. package/src/TSL/Common.js +57 -13
  52. package/src/TSL/ShadeKernel.js +3 -2
  53. package/src/TSL/TextureSampling.js +11 -8
  54. package/src/TSL/patches.js +84 -0
  55. package/src/integrators/BidirectionalIntegrator.js +20 -2
  56. package/src/managers/DenoisingManager.js +4 -4
  57. package/src/managers/InteractionManager.js +19 -6
  58. package/src/managers/MaterialDataManager.js +22 -5
  59. package/src/managers/UniformManager.js +3 -0
  60. package/dist/ArchiveImporter-eNlu0_6F.js +0 -5198
  61. package/dist/BidirectionalIntegrator-D1-6UySB.js +0 -946
  62. package/dist/SolarLimb-CPaeZVwz.js +0 -721
  63. package/dist/sceneCachePolicy-DA7iGoL4.js +0 -485
@@ -4,6 +4,16 @@ const PACK_BYTES = 32 * 1024 * 1024;
4
4
  const OWN_WRITE_BYTES = PACK_BYTES / 4;
5
5
  const READ_WINDOW = 64 * 1024 * 1024;
6
6
  const EMPTY = new Map();
7
+ // Buffers a restore made: nothing else holds them, so once on disk again they are let go at once rather than at the
8
+ // next major collection, which an idle page may not reach for minutes.
9
+ const RESTORED = new WeakSet();
10
+
11
+ function release( buffer ) {
12
+
13
+ RESTORED.delete( buffer );
14
+ buffer.transfer?.( 0 );
15
+
16
+ }
7
17
 
8
18
  function empty( Type ) {
9
19
 
@@ -61,13 +71,25 @@ export class GeometrySpill {
61
71
 
62
72
  }
63
73
 
64
- /** Moves a geometry's arrays to disk, leaving empty arrays of the same type in their place. */
74
+ /** Moves a geometry's arrays to disk, leaving empty arrays of the same type in their place; its bounds stay. */
65
75
  add( geometry ) {
66
76
 
67
- const byArray = new Map();
77
+ if ( ! geometry.boundingBox ) geometry.computeBoundingBox();
78
+ if ( ! geometry.boundingSphere ) geometry.computeBoundingSphere();
79
+
68
80
  const attributes = Object.values( geometry.attributes );
69
81
  if ( geometry.index ) attributes.push( geometry.index );
82
+ this.addAttributes( attributes );
83
+
84
+ // What the bounds were with the arrays in place: anything computed from the empty ones must not outlive the restore.
85
+ this._geometries.push( { geometry, box: geometry.boundingBox.clone(), sphere: geometry.boundingSphere.clone() } );
86
+
87
+ }
70
88
 
89
+ /** Moves attributes' arrays to disk, each array once however many attributes share it. */
90
+ addAttributes( attributes ) {
91
+
92
+ const byArray = new Map();
71
93
  for ( const attribute of attributes ) {
72
94
 
73
95
  const array = attribute.array;
@@ -79,9 +101,6 @@ export class GeometrySpill {
79
101
 
80
102
  }
81
103
 
82
- // Bounds computed from an empty array would outlive the restore.
83
- this._geometries.push( { geometry, box: geometry.boundingBox !== null, sphere: geometry.boundingSphere !== null } );
84
-
85
104
  }
86
105
 
87
106
  /** @private */
@@ -116,8 +135,9 @@ export class GeometrySpill {
116
135
  record.at = this._packAt + this._packUsed;
117
136
  record.pack = this._pack;
118
137
  record.offset = this._packUsed;
119
- this._packUsed += bytes.byteLength;
138
+ this._packUsed += record.byteLength;
120
139
  this._packRecords.push( record );
140
+ if ( RESTORED.has( array.buffer ) ) release( array.buffer );
121
141
  return record;
122
142
 
123
143
  }
@@ -126,26 +146,35 @@ export class GeometrySpill {
126
146
  _flushPack() {
127
147
 
128
148
  if ( ! this._pack ) return;
129
- this._queue( this._packAt, this._pack.subarray( 0, this._packUsed ), this._packRecords );
149
+ this._queue( this._packAt, this._pack.subarray( 0, this._packUsed ), this._packRecords, true );
130
150
  this._pack = null;
131
151
  this._packRecords = [];
132
152
 
133
153
  }
134
154
 
135
- /** @private */
136
- _queue( at, bytes, records ) {
155
+ /** `own`: the buffer is this spill's (a pack), let go once written. @private */
156
+ _queue( at, bytes, records, own = false ) {
137
157
 
138
- this._queued += bytes.byteLength;
158
+ const length = bytes.byteLength;
159
+ this._queued += length;
139
160
  this._chain = this._chain
140
161
  .then( async () => {
141
162
 
142
163
  if ( this.error ) return;
143
164
  await this._store.writeAt( at, bytes );
144
- for ( const record of records ) record.array = record.pack = null;
165
+ for ( const record of records ) {
166
+
167
+ const buffer = record.array?.buffer;
168
+ record.array = record.pack = null;
169
+ if ( RESTORED.has( buffer ) ) release( buffer );
170
+
171
+ }
172
+
173
+ if ( own ) bytes.buffer.transfer?.( 0 );
145
174
 
146
175
  } )
147
176
  .catch( error => void ( this.error ??= error ) )
148
- .finally( () => void ( this._queued -= bytes.byteLength ) );
177
+ .finally( () => void ( this._queued -= length ) );
149
178
 
150
179
  }
151
180
 
@@ -176,6 +205,7 @@ export class GeometrySpill {
176
205
 
177
206
  if ( ! window || record.at + record.byteLength > windowAt + window.byteLength ) {
178
207
 
208
+ if ( window ) release( window );
179
209
  windowAt = record.at;
180
210
  window = await this._store.readAt( record.at, READ_WINDOW );
181
211
 
@@ -186,14 +216,16 @@ export class GeometrySpill {
186
216
  }
187
217
 
188
218
  if ( array.byteLength !== record.byteLength ) throw new Error( 'geometry spill: an array came back short' );
219
+ if ( array !== record.array ) RESTORED.add( array.buffer );
189
220
  for ( const attribute of record.attributes ) attribute.array = array;
190
221
 
191
222
  }
192
223
 
224
+ if ( window ) release( window );
193
225
  for ( const { geometry, box, sphere } of this._geometries ) {
194
226
 
195
- if ( ! box ) geometry.boundingBox = null;
196
- if ( ! sphere ) geometry.boundingSphere = null;
227
+ geometry.boundingBox = box;
228
+ geometry.boundingSphere = sphere;
197
229
 
198
230
  }
199
231
 
@@ -192,6 +192,8 @@ export function encodeSceneGraph( root, { environment = null, animations = [], s
192
192
 
193
193
  const sections = new SectionWriter();
194
194
  const nodes = [];
195
+ // Instanced meshes sharing one matrix attribute are placed as one object; stored once, they come back shared.
196
+ const instanceSections = new Map();
195
197
  const geometries = new Map();
196
198
  const materials = new Map();
197
199
  const textures = new Map();
@@ -228,7 +230,9 @@ export function encodeSceneGraph( root, { environment = null, animations = [], s
228
230
  if ( object.isInstancedMesh ) {
229
231
 
230
232
  node.count = object.count;
231
- node.instanceMatrix = sections.add( object.instanceMatrix.array );
233
+ let matrices = instanceSections.get( object.instanceMatrix );
234
+ if ( ! matrices ) instanceSections.set( object.instanceMatrix, matrices = sections.add( object.instanceMatrix.array ) );
235
+ node.instanceMatrix = matrices;
232
236
  node.instanceColor = object.instanceColor ? sections.add( object.instanceColor.array ) : null;
233
237
 
234
238
  }
@@ -355,9 +359,16 @@ async function readSections( file, sections ) {
355
359
  function sectionsOf( manifest ) {
356
360
 
357
361
  const all = [];
362
+ const seen = new Set();
358
363
  for ( const node of manifest.nodes ) {
359
364
 
360
- if ( node.instanceMatrix ) all.push( node.instanceMatrix );
365
+ if ( node.instanceMatrix && ! seen.has( node.instanceMatrix.offset ) ) {
366
+
367
+ seen.add( node.instanceMatrix.offset );
368
+ all.push( node.instanceMatrix );
369
+
370
+ }
371
+
361
372
  if ( node.instanceColor ) all.push( node.instanceColor );
362
373
 
363
374
  }
@@ -408,6 +419,7 @@ export async function decodeSceneGraph( manifest, data, { loadTexture } ) {
408
419
 
409
420
  const arrays = await readSections( data, sectionsOf( manifest ) );
410
421
  const section = ( record ) => arrays.get( record.offset );
422
+ const instanceMatrices = new Map();
411
423
 
412
424
  const textures = {};
413
425
  for ( const record of manifest.textures ) {
@@ -476,7 +488,9 @@ export async function decodeSceneGraph( manifest, data, { loadTexture } ) {
476
488
 
477
489
  // Built for one instance, then given the stored matrices: no second full-size array.
478
490
  object = new InstancedMesh( geometries[ node.geometry ], material, 1 );
479
- object.instanceMatrix = new InstancedBufferAttribute( section( node.instanceMatrix ), 16 );
491
+ let matrices = instanceMatrices.get( node.instanceMatrix.offset );
492
+ if ( ! matrices ) instanceMatrices.set( node.instanceMatrix.offset, matrices = new InstancedBufferAttribute( section( node.instanceMatrix ), 16 ) );
493
+ object.instanceMatrix = matrices;
480
494
  object.count = node.count;
481
495
  if ( node.instanceColor ) object.instanceColor = new InstancedBufferAttribute( section( node.instanceColor ), 3 );
482
496
  break;
@@ -99,6 +99,15 @@ export class SpillStore {
99
99
 
100
100
  }
101
101
 
102
+ /** Reads chunk `k` into `target` (bytes) and lets the read buffer go at once, not at the next major collection. */
103
+ async readInto( k, target ) {
104
+
105
+ const buffer = await this.read( k, target.byteLength );
106
+ target.set( new Uint8Array( buffer ) );
107
+ buffer.transfer?.( 0 );
108
+
109
+ }
110
+
102
111
  async dispose() {
103
112
 
104
113
  if ( this._writer ) await this._writer.abort();
@@ -10,6 +10,7 @@ import {
10
10
  If,
11
11
  Loop,
12
12
  Break,
13
+ Continue,
13
14
  select,
14
15
  abs,
15
16
  sign,
@@ -27,7 +28,7 @@ import {
27
28
 
28
29
  import {
29
30
  TRI_MATERIAL_MASK, TRI_SIDE_SHIFT, BVH_LEAF_MARKERS, BVH_MAX_INDEX, BVH_FOLDED_LEAF_MAX,
30
- TLAS_LEAF_IDENTITY,
31
+ TLAS_LEAF_IDENTITY, COPY_ENTRY, CLUSTER_FIRST_MASK, CLUSTER_COUNT_SHIFT, CLUSTER_SIZE, CLUSTER_ROOT_MASK, CLUSTER_COPY_HIDDEN,
31
32
  } from '../Processor/BufferLayout.js';
32
33
  import { HitInfo } from './Struct.js';
33
34
  import {
@@ -83,6 +84,61 @@ const toObjectDir = ( rows, d ) => vec3(
83
84
  rows[ 2 ].xyz.dot( d )
84
85
  );
85
86
 
87
+ // Copy clusters (BufferLayout CLUSTER_LEAF): a leaf pushes a COPY_ENTRY for each copy whose byte box the ray reaches,
88
+ // farthest first so the nearest is entered first, as an inner node orders its children; popping one moves the ray into
89
+ // that copy and pushes its object's root.
90
+
91
+ // Compare-swaps sorting up to four ascending.
92
+ const SORT_NETWORKS = [[], [], [[ 0, 1 ]], [[ 0, 1 ], [ 1, 2 ], [ 0, 1 ]], [[ 0, 1 ], [ 2, 3 ], [ 0, 2 ], [ 1, 3 ], [ 1, 2 ]]];
93
+
94
+ const pushHitCopies = ( bvhBuffer, nodeIndex, nodeData0, origin, invDir, stack, stackPtr, limit ) => {
95
+
96
+ const d1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
97
+ const d2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
98
+ const base = vec3( nodeData0.z, d1.x, d1.y ).toVar();
99
+ const exps = floatBitsToUint( nodeData0.y ).toVar();
100
+ const stepOf = ( a ) => uintBitsToFloat( exps.shiftRight( uint( a * 8 ) ).bitAnd( uint( 255 ) ).shiftLeft( uint( 23 ) ) );
101
+ const step = vec3( stepOf( 0 ), stepOf( 1 ), stepOf( 2 ) ).toVar();
102
+ const lo = [ floatBitsToUint( d1.z ).toVar(), floatBitsToUint( d1.w ).toVar(), floatBitsToUint( d2.x ).toVar() ];
103
+ const hi = [ floatBitsToUint( d2.y ).toVar(), floatBitsToUint( d2.z ).toVar(), floatBitsToUint( d2.w ).toVar() ];
104
+ const count = int( floatBitsToUint( nodeData0.x ).shiftRight( uint( CLUSTER_COUNT_SHIFT ) ).bitAnd( uint( 3 ) ) ).add( 1 ).toVar();
105
+ const byte = ( word, k ) => float( word.shiftRight( uint( k * 8 ) ).bitAnd( uint( 255 ) ) );
106
+
107
+ const dist = [], slot = [];
108
+ for ( let k = 0; k < CLUSTER_SIZE; k ++ ) {
109
+
110
+ const boxMin = base.add( vec3( byte( lo[ 0 ], k ), byte( lo[ 1 ], k ), byte( lo[ 2 ], k ) ).mul( step ) );
111
+ const boxMax = base.add( vec3( byte( hi[ 0 ], k ), byte( hi[ 1 ], k ), byte( hi[ 2 ], k ) ).mul( step ) );
112
+ const d = fastRayAABBDst( { rayOrigin: origin, invDir, boxMin, boxMax } );
113
+ dist.push( select( int( k ).lessThan( count ).and( d.lessThan( limit ) ), d, float( 1e30 ) ).toVar() );
114
+ slot.push( int( k ).toVar() );
115
+
116
+ }
117
+
118
+ for ( const [ a, b ] of SORT_NETWORKS[ CLUSTER_SIZE ] ) {
119
+
120
+ If( dist[ b ].lessThan( dist[ a ] ), () => {
121
+
122
+ const td = float( dist[ a ] ).toVar(), ts = int( slot[ a ] ).toVar();
123
+ dist[ a ].assign( dist[ b ] ); slot[ a ].assign( slot[ b ] );
124
+ dist[ b ].assign( td ); slot[ b ].assign( ts );
125
+
126
+ } );
127
+
128
+ }
129
+
130
+ for ( let k = CLUSTER_SIZE - 1; k >= 0; k -- ) {
131
+
132
+ If( dist[ k ].lessThan( limit ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
133
+
134
+ stack.element( stackPtr ).assign( int( uint( COPY_ENTRY ).bitOr( uint( nodeIndex ).shiftLeft( uint( 2 ) ) ).bitOr( uint( slot[ k ] ) ) ) );
135
+ stackPtr.addAssign( 1 );
136
+
137
+ } );
138
+
139
+ }
140
+
141
+ };
86
142
 
87
143
  // ================================================================================
88
144
  // RAY INTERSECTION HELPERS (inlined for BVH traversal performance)
@@ -301,6 +357,49 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
301
357
  const instExit = int( 0 ).toVar();
302
358
  const hitInstLeaf = int( - 1 ).toVar();
303
359
 
360
+ // Built over copy clusters: the copies' transforms are records from vec4 `copyRecordBase` (BufferLayout).
361
+ const clustered = bvhBuffer.value?.copyRecordBase !== undefined;
362
+ const enterCopy = ( entry ) => {
363
+
364
+ const leaf = int( uint( entry ).bitAnd( uint( ~ COPY_ENTRY >>> 0 ) ).shiftRight( uint( 2 ) ) );
365
+ const k = uint( entry ).bitAnd( uint( 3 ) ).toVar();
366
+ const head = getDatafromStorageBuffer( bvhBuffer, leaf, int( 0 ), int( BVH_STRIDE ) );
367
+ const words = floatBitsToUint( getDatafromStorageBuffer( bvhBuffer, leaf, int( 3 ), int( BVH_STRIDE ) ) ).toVar();
368
+ const word = select( k.equal( uint( 0 ) ), words.x, select( k.equal( uint( 1 ) ), words.y, select( k.equal( uint( 2 ) ), words.z, words.w ) ) ).toVar();
369
+
370
+ If( word.bitAnd( uint( CLUSTER_COPY_HIDDEN ) ).equal( uint( 0 ) ), () => {
371
+
372
+ If( word.bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
373
+
374
+ const record = int( floatBitsToUint( head.x ).bitAnd( uint( CLUSTER_FIRST_MASK ) ).add( k ) ).toVar();
375
+ const rows = instanceRows( bvhBuffer, record );
376
+ const localDir = toObjectDir( rows, worldDirection ).toVar();
377
+
378
+ rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
379
+ rayDirection.assign( localDir );
380
+ const localInv = buildInvDir( localDir ).toVar();
381
+ invDir.assign( localInv );
382
+ woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
383
+ instLeaf.assign( record );
384
+ instExit.assign( stackPtr );
385
+
386
+ } ).Else( () => {
387
+
388
+ rayOrigin.assign( worldOrigin );
389
+ rayDirection.assign( worldDirection );
390
+ invDir.assign( worldInvDir );
391
+ woopParams.assign( worldWoop );
392
+ instLeaf.assign( int( - 1 ) );
393
+
394
+ } );
395
+
396
+ stack.element( stackPtr ).assign( int( word.bitAnd( uint( CLUSTER_ROOT_MASK ) ) ) );
397
+ stackPtr.addAssign( 1 );
398
+
399
+ } );
400
+
401
+ };
402
+
304
403
  // Emitted once per leaf kind, in its own branch: hoisted below both, the range stayed live
305
404
  // through the inner-node path and cost 9–13 % of traversal time.
306
405
  const testLeaf = ( first, count ) => {
@@ -403,7 +502,11 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
403
502
 
404
503
  testLeaf( floatBitsToUint( nodeData0.x ), floatBitsToUint( nodeData0.y ) );
405
504
 
406
- } ).Else( () => {
505
+ } ).Else( clustered ? () => {
506
+
507
+ pushHitCopies( bvhBuffer, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
508
+
509
+ } : () => {
407
510
 
408
511
  // BLAS-pointer leaf — enter the instance if the mesh is visible.
409
512
  // nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]; slots 4..15 hold
@@ -487,9 +590,22 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
487
590
 
488
591
  };
489
592
 
593
+ // A copy continues the loop: the visit nested in an Else overflowed the JS stack building the island's kernels.
594
+ const dispatch = clustered ? () => {
595
+
596
+ If( entry.greaterThanEqual( int( COPY_ENTRY ) ), () => {
597
+
598
+ enterCopy( entry );
599
+ Continue();
600
+
601
+ } );
602
+ visitNode();
603
+
604
+ } : visitNode;
605
+
490
606
  // Only a folded BVH has negative entries: a leaf folded into its parent, tested with no fetch.
491
- if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( visitNode );
492
- else visitNode();
607
+ if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( dispatch );
608
+ else dispatch();
493
609
 
494
610
  } );
495
611
 
@@ -583,6 +699,49 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
583
699
  const instExit = int( 0 ).toVar();
584
700
  const blocked = tslBool( false ).toVar();
585
701
 
702
+ // Built over copy clusters: the copies' transforms are records from vec4 `copyRecordBase` (BufferLayout).
703
+ const clustered = bvhBuffer.value?.copyRecordBase !== undefined;
704
+ const enterCopy = ( entry ) => {
705
+
706
+ const leaf = int( uint( entry ).bitAnd( uint( ~ COPY_ENTRY >>> 0 ) ).shiftRight( uint( 2 ) ) );
707
+ const k = uint( entry ).bitAnd( uint( 3 ) ).toVar();
708
+ const head = getDatafromStorageBuffer( bvhBuffer, leaf, int( 0 ), int( BVH_STRIDE ) );
709
+ const words = floatBitsToUint( getDatafromStorageBuffer( bvhBuffer, leaf, int( 3 ), int( BVH_STRIDE ) ) ).toVar();
710
+ const word = select( k.equal( uint( 0 ) ), words.x, select( k.equal( uint( 1 ) ), words.y, select( k.equal( uint( 2 ) ), words.z, words.w ) ) ).toVar();
711
+
712
+ If( word.bitAnd( uint( CLUSTER_COPY_HIDDEN ) ).equal( uint( 0 ) ), () => {
713
+
714
+ If( word.bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
715
+
716
+ const record = int( floatBitsToUint( head.x ).bitAnd( uint( CLUSTER_FIRST_MASK ) ).add( k ) ).toVar();
717
+ const rows = instanceRows( bvhBuffer, record );
718
+ const localDir = toObjectDir( rows, worldDirection ).toVar();
719
+
720
+ rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
721
+ rayDirection.assign( localDir );
722
+ const localInv = buildInvDir( localDir ).toVar();
723
+ invDir.assign( localInv );
724
+ woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
725
+ instLeaf.assign( record );
726
+ instExit.assign( stackPtr );
727
+
728
+ } ).Else( () => {
729
+
730
+ rayOrigin.assign( worldOrigin );
731
+ rayDirection.assign( worldDirection );
732
+ invDir.assign( worldInvDir );
733
+ woopParams.assign( worldWoop );
734
+ instLeaf.assign( int( - 1 ) );
735
+
736
+ } );
737
+
738
+ stack.element( stackPtr ).assign( int( word.bitAnd( uint( CLUSTER_ROOT_MASK ) ) ) );
739
+ stackPtr.addAssign( 1 );
740
+
741
+ } );
742
+
743
+ };
744
+
586
745
  const testLeaf = ( first, count ) => {
587
746
 
588
747
  const triStart = int( first ).toVar();
@@ -672,7 +831,11 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
672
831
 
673
832
  testLeaf( floatBitsToUint( nodeData0.x ), floatBitsToUint( nodeData0.y ) );
674
833
 
675
- } ).Else( () => {
834
+ } ).Else( clustered ? () => {
835
+
836
+ pushHitCopies( bvhBuffer, nodeIndex, nodeData0, worldOrigin, worldInvDir, stack, stackPtr, closestHit.dst );
837
+
838
+ } : () => {
676
839
 
677
840
  // BLAS-pointer leaf — enter the instance if the mesh is visible.
678
841
  If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
@@ -755,9 +918,22 @@ const makeTraverseBVHShadow = ( cameraCulled ) => Fn( ( [
755
918
 
756
919
  };
757
920
 
921
+ // A copy continues the loop: the visit nested in an Else overflowed the JS stack building the island's kernels.
922
+ const dispatch = clustered ? () => {
923
+
924
+ If( entry.greaterThanEqual( int( COPY_ENTRY ) ), () => {
925
+
926
+ enterCopy( entry );
927
+ Continue();
928
+
929
+ } );
930
+ visitNode();
931
+
932
+ } : visitNode;
933
+
758
934
  // Only a folded BVH has negative entries: a leaf folded into its parent, tested with no fetch.
759
- if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( visitNode );
760
- else visitNode();
935
+ if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( dispatch );
936
+ else dispatch();
761
937
 
762
938
  } );
763
939
 
package/src/TSL/Common.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Fn, wgslFn, float, vec2, vec3, vec4, int, uint, mat3, If, max, dot, clamp, select, bool as tslBool } from 'three/tsl';
1
+ import { Fn, wgslFn, float, vec2, vec3, vec4, uvec2, uvec4, int, uint, mat3, If, max, dot, clamp, select, bool as tslBool } from 'three/tsl';
2
2
 
3
3
  import {
4
4
  AnisoFrame,
@@ -322,12 +322,41 @@ export const classifyMaterial = Fn( ( [ metalness, roughness, transmission, clea
322
322
 
323
323
  // Storage buffer access — flat 1D indexing (WebGPU native)
324
324
  // No 2D coordinate math needed: directly indexes into the buffer
325
- export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dataOffset ] ) => {
325
+ /**
326
+ * One storage array split over several buffers, for a tree or triangle store past maxStorageBufferBindingSize: element
327
+ * e lives in part ⌊e / perPart⌋. A single node needs no split and is returned as it is, so a scene that fits one
328
+ * buffer compiles exactly the code it always did.
329
+ * @param {Array} parts - storage nodes, all but the last `perPart` vec4 elements long
330
+ * @param {number} perPart
331
+ * @param {'vec4'|'uvec4'} type
332
+ */
333
+ export function splitStorage( parts, perPart, type ) {
326
334
 
327
- const elementIndex = stride.mul( dataOffset ).add( sampleIndex );
328
- return buffer.element( elementIndex );
335
+ return parts.length === 1 ? parts[ 0 ] : { isSplitStorage: true, parts, perPart, type, value: parts[ 0 ].value };
329
336
 
330
- } );
337
+ }
338
+
339
+ /** Element `index` of a storage node or of a {@link splitStorage}. */
340
+ export function storageElement( buffer, index ) {
341
+
342
+ if ( ! buffer.isSplitStorage ) return buffer.element( index );
343
+
344
+ const { parts, perPart } = buffer;
345
+ const e = int( index ).toVar();
346
+ const out = ( { uvec4, uvec2 }[ buffer.type ] ?? vec4 )( 0 ).toVar();
347
+ const read = ( k ) => () => out.assign( parts[ k ].element( k === 0 ? e : e.sub( int( perPart * k ) ) ) );
348
+ let branch = If( e.lessThan( int( perPart ) ), read( 0 ) );
349
+ for ( let k = 1; k < parts.length - 1; k ++ ) branch = branch.ElseIf( e.lessThan( int( perPart * ( k + 1 ) ) ), read( k ) );
350
+ branch.Else( read( parts.length - 1 ) );
351
+ return out;
352
+
353
+ }
354
+
355
+ const asNode = ( v ) => ( v?.isNode ? v : int( v ) );
356
+
357
+ // Element `index · stride + offset`.
358
+ export const getDatafromStorageBuffer = ( buffer, index, offset, stride ) =>
359
+ storageElement( buffer, asNode( stride ).mul( asNode( index ) ).add( asNode( offset ) ) );
331
360
 
332
361
  /**
333
362
  * The five uvec4 rows of a triangle record (TRIANGLE_DATA_LAYOUT) live in two GPU buffers: rows
@@ -337,9 +366,17 @@ export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dat
337
366
  export const TRI_GEO_ROWS = 3;
338
367
  export const TRI_SHADE_ROWS = 2;
339
368
 
340
- export const triangleRow = ( tris, triIndex, row ) => row < TRI_GEO_ROWS
341
- ? getDatafromStorageBuffer( tris.geo, triIndex, int( row ), int( TRI_GEO_ROWS ) )
342
- : getDatafromStorageBuffer( tris.shade, triIndex, int( row - TRI_GEO_ROWS ), int( TRI_SHADE_ROWS ) );
369
+ // Without texture coordinates (no material samples a texture) the shade buffer holds only flags and mesh index, one
370
+ // uvec2 a triangle: rows 3–4 read as zero UVs around them, 24 B a triangle saved.
371
+ export const triangleRow = ( tris, triIndex, row ) => {
372
+
373
+ if ( row < TRI_GEO_ROWS ) return getDatafromStorageBuffer( tris.geo, triIndex, int( row ), int( TRI_GEO_ROWS ) );
374
+ if ( ! tris.withoutUV ) return getDatafromStorageBuffer( tris.shade, triIndex, int( row - TRI_GEO_ROWS ), int( TRI_SHADE_ROWS ) );
375
+ if ( row === TRI_GEO_ROWS ) return uvec4( 0 );
376
+ const flagsMesh = storageElement( tris.shade, asNode( triIndex ) );
377
+ return uvec4( 0, 0, flagsMesh.x, flagsMesh.y );
378
+
379
+ };
343
380
 
344
381
  // Unit normal to an octahedral snorm16 pair; GPU twin of packNormalOct in EngineDefaults.
345
382
  export const packNormalOct = /*@__PURE__*/ wgslFn( `
@@ -637,11 +674,18 @@ export const shadowFlagsSettle = ( flags, alphaShadows ) => {
637
674
 
638
675
  };
639
676
 
640
- export const instanceRows = ( bvhBuffer, leafIndex ) => [
641
- getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 1 ), int( 4 ) ),
642
- getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 2 ), int( 4 ) ),
643
- getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 3 ), int( 4 ) )
644
- ];
677
+ export const instanceRows = ( bvhBuffer, leafIndex ) => {
678
+
679
+ // Built over copy clusters, an instance is a copy record (3 vec4 from `copyRecordBase`), not a TLAS leaf.
680
+ const records = bvhBuffer.value?.copyRecordBase;
681
+ if ( records !== undefined ) return [ 0, 1, 2 ].map( ( row ) => getDatafromStorageBuffer( bvhBuffer, leafIndex, int( records + row ), int( 3 ) ) );
682
+ return [
683
+ getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 1 ), int( 4 ) ),
684
+ getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 2 ), int( 4 ) ),
685
+ getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 3 ), int( 4 ) )
686
+ ];
687
+
688
+ };
645
689
 
646
690
  /** Object-space normal to world: transpose of the world-to-object basis. */
647
691
  export const instanceNormalToWorld = ( rows, n ) => vec3(
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * ShadeKernel.js — wavefront material eval + bounce generation. 256×1 workgroup, 1D dispatch.
3
- * 10 storage-buffer bindings: bvh, triGeo, triShade, mat, light, ray, hit (+ RNG state), gBuffer,
4
- * counters, activeIndices (at the device per-stage limit of 10; envCDF is a texture, not a storage buffer).
3
+ * 8 storage-buffer bindings: bvh, triGeo, triShade, scene data (materials, then lights), ray, hit (+ RNG state and the
4
+ * G-buffer), counters, activeIndices; envCDF is a texture. The device allows 10: the other two are for the BVH and
5
+ * triangle stores' parts past one buffer's limit (PathTracerStage._assertBindings).
5
6
  */
6
7
 
7
8
  import {
@@ -363,11 +363,11 @@ export const processMetalnessRoughness = Fn( ( [ material, uvCache ], builder )
363
363
 
364
364
  } );
365
365
 
366
- // (t[1][1], t[0][1]) — the row of the inverse 2x2 linear part that maps dP/du onto dP/du',
367
- // up to the determinant, which drops out under the normalize that follows.
368
- const uvTransformJacobian = /*@__PURE__*/ wgslFn( `
369
- fn uvTransformJacobian( t: mat3x3f ) -> vec2f {
370
- return vec2f( t[1][1], t[0][1] );
366
+ // The inverse of the transform's 2x2 linear part times |det|: .xy takes (dP/du, dP/dv) to dP/du', .zw to dP/dv'.
367
+ // Times det instead, a mirrored transform turned both round.
368
+ const uvTransformInverse = /*@__PURE__*/ wgslFn( `
369
+ fn uvTransformInverse( t: mat3x3f ) -> vec4f {
370
+ return vec4f( t[1][1], - t[0][1], - t[1][0], t[0][0] ) * sign( t[0][0] * t[1][1] - t[1][0] * t[0][1] );
371
371
  }
372
372
  ` );
373
373
 
@@ -411,13 +411,16 @@ export const triangleUVTangent = Fn( ( [ triangleBuffer, triIndex, geometryNorma
411
411
  const T = e1.mul( d2.y ).sub( e2.mul( d1.y ) ).mul( r ).toVar();
412
412
  const B = e2.mul( d1.x ).sub( e1.mul( d2.x ) ).mul( r ).toVar();
413
413
 
414
- // Re-express in the map's transformed UV space (inverse of the 2x2 linear part).
415
- const uvJ = uvTransformJacobian( { t: transform } );
416
- const Tt = T.mul( uvJ.x ).sub( B.mul( uvJ.y ) ).toVar();
414
+ // Re-express both in the map's transformed UV space: the handedness test below needs B there too, or a
415
+ // mirrored map, or one turned past 90°, gets its bitangent reversed.
416
+ const inv = uvTransformInverse( { t: transform } ).toVar();
417
+ const Tt = T.mul( inv.x ).add( B.mul( inv.y ) ).toVar();
418
+ const Bt = T.mul( inv.z ).add( B.mul( inv.w ) ).toVar();
417
419
 
418
420
  If( length( Tt ).greaterThan( 1e-12 ), () => {
419
421
 
420
422
  T.assign( Tt );
423
+ B.assign( Bt );
421
424
 
422
425
  } );
423
426