rayzee 5.4.0 → 5.4.2

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.
@@ -114,6 +114,19 @@ export class PathTracer extends RenderStage {
114
114
  // Initialize material data manager
115
115
  this.materialData = new MaterialDataManager( this.sdfs );
116
116
  this.materialData.callbacks.onReset = () => this.reset();
117
+ // Triangle data carries the per-triangle `side` flag (NORMAL_C.w). The
118
+ // authoritative CPU array is triangleStorageAttr.array (not sdfs.triangleData,
119
+ // which isn't populated on the PathTracerApp build path). The patch mutates
120
+ // the array in place — only a dirty flag is needed for GPU re-upload.
121
+ this.materialData.callbacks.getTriangleData = () => ( {
122
+ array: this.triangleStorageAttr?.array,
123
+ count: this.triangleCount,
124
+ } );
125
+ this.materialData.callbacks.onTriangleDataChanged = () => {
126
+
127
+ if ( this.triangleStorageAttr ) this.triangleStorageAttr.needsUpdate = true;
128
+
129
+ };
117
130
 
118
131
  // Initialize environment manager
119
132
  this.environment = new EnvironmentManager( this.scene, this.uniforms );
@@ -180,17 +193,20 @@ export class PathTracer extends RenderStage {
180
193
  // Blue noise
181
194
  this.blueNoiseTexture = null;
182
195
 
183
- // Emissive triangles (storage buffer)initialized with dummy data so TSL compilation never sees null
184
- this.emissiveTriangleStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 4 ), 4 );
185
- this.emissiveTriangleStorageNode = storage( this.emissiveTriangleStorageAttr, 'vec4', 1 ).toReadOnly();
196
+ // Packed light buffer — [lightBVH nodes (4 vec4s each) | emissive triangles (2 vec4s each)]
197
+ // emissiveVec4Offset uniform tracks the vec4-count offset where emissive data starts.
198
+ // Initialized with dummy data so TSL compilation never sees null.
199
+ this.lightStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
200
+ this.lightStorageNode = storage( this.lightStorageAttr, 'vec4', 1 ).toReadOnly();
186
201
 
187
- // Light BVH storage buffer initialized with dummy data
188
- this.lightBVHStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( 16 ), 4 );
189
- this.lightBVHStorageNode = storage( this.lightBVHStorageAttr, 'vec4', 1 ).toReadOnly();
202
+ // Cached CPU-side data — rebuilt into the packed buffer whenever either source changes.
203
+ this._lbvhDataCache = null;
204
+ this._emissiveDataCache = null;
190
205
 
191
- // Per-mesh visibility (storage buffer for TLAS BLAS-pointer skip)
192
- this.meshVisibilityStorageAttr = new StorageInstancedBufferAttribute( new Float32Array( [ 1, 0, 0, 0 ] ), 4 );
193
- this.meshVisibilityStorageNode = storage( this.meshVisibilityStorageAttr, 'vec4', 1 ).toReadOnly();
206
+ // Per-mesh visibility is packed into the TLAS BLAS-pointer leaf's slot [2]
207
+ // (see TLASBuilder.flatten + BVHTraversal.js). The InstanceTable holds the
208
+ // tlasLeafIndex for each mesh so we can patch visibility in place.
209
+ this._instanceTable = null;
194
210
 
195
211
  // Adaptive sampling
196
212
  this.adaptiveSamplingTexture = null;
@@ -454,6 +470,7 @@ export class PathTracer extends RenderStage {
454
470
  // Set data references
455
471
  this.setTriangleData( this.sdfs.triangleData, this.sdfs.triangleCount );
456
472
  this.setBVHData( this.sdfs.bvhData );
473
+ this.setInstanceTable( this.sdfs.instanceTable );
457
474
  this.materialData.setMaterialData( this.sdfs.materialData );
458
475
 
459
476
  // Update triangle count
@@ -768,61 +785,78 @@ export class PathTracer extends RenderStage {
768
785
  }
769
786
 
770
787
  /**
771
- * Build per-mesh visibility storage buffer from mesh world-visibility.
772
- * Each mesh gets one float (1.0 = visible, 0.0 = hidden).
773
- * Padded to vec4 alignment for GPU storage buffer compatibility.
774
- * @param {Array} meshes - Array of Three.js mesh objects
788
+ * Bind the InstanceTable used to locate each mesh's TLAS leaf for in-place
789
+ * visibility patching. Called by SceneProcessor during upload.
790
+ * @param {import('../Processor/InstanceTable.js').InstanceTable} instanceTable
775
791
  */
776
- setMeshVisibilityData( meshes ) {
792
+ setInstanceTable( instanceTable ) {
777
793
 
778
- if ( ! meshes || meshes.length === 0 ) return;
794
+ this._instanceTable = instanceTable;
779
795
 
780
- const meshCount = meshes.length;
781
- // One vec4 per mesh — visibility stored in .x (simple indexing on GPU)
782
- const data = new Float32Array( meshCount * 4 );
796
+ }
783
797
 
784
- for ( let i = 0; i < meshCount; i ++ ) {
798
+ /**
799
+ * Initialize packed visibility for each mesh from current world-visibility.
800
+ * Patches the TLAS leaf slots in the combined BVH buffer that was just uploaded.
801
+ * @param {Array} meshes - Array of Three.js mesh objects, ordered by meshIndex
802
+ */
803
+ setMeshVisibilityData( meshes ) {
785
804
 
786
- data[ i * 4 ] = this._isWorldVisible( meshes[ i ] ) ? 1.0 : 0.0;
805
+ if ( ! meshes || meshes.length === 0 || ! this._instanceTable ) return;
806
+
807
+ for ( let i = 0; i < meshes.length; i ++ ) {
808
+
809
+ this._patchTLASLeafVisibility( i, this._isWorldVisible( meshes[ i ] ) );
787
810
 
788
811
  }
789
812
 
790
- this.meshVisibilityStorageAttr = new StorageInstancedBufferAttribute( data, 4 );
791
- this.meshVisibilityStorageNode.value = this.meshVisibilityStorageAttr;
792
- this.meshVisibilityStorageNode.bufferCount = meshCount;
813
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
793
814
 
794
815
  }
795
816
 
796
817
  /**
797
- * Update visibility for a single mesh in the GPU buffer (no rebuild).
818
+ * Update visibility for a single mesh by patching its TLAS leaf slot [2].
798
819
  * @param {number} meshIndex
799
820
  * @param {boolean} visible
800
821
  */
801
822
  updateMeshVisibility( meshIndex, visible ) {
802
823
 
803
- if ( ! this.meshVisibilityStorageAttr ) return;
804
-
805
- this.meshVisibilityStorageAttr.array[ meshIndex * 4 ] = visible ? 1.0 : 0.0;
806
- this.meshVisibilityStorageAttr.needsUpdate = true;
824
+ if ( ! this._patchTLASLeafVisibility( meshIndex, visible ) ) return;
825
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
807
826
 
808
827
  }
809
828
 
810
829
  /**
811
- * Recompute world-visibility for all meshes and update the GPU buffer.
830
+ * Recompute world-visibility for all meshes and patch TLAS leaves in place.
812
831
  * Call this when group visibility changes at runtime.
813
832
  */
814
833
  updateAllMeshVisibility() {
815
834
 
816
- if ( ! this._meshRefs || ! this.meshVisibilityStorageAttr ) return;
835
+ if ( ! this._meshRefs || ! this._instanceTable ) return;
817
836
 
818
- const data = this.meshVisibilityStorageAttr.array;
819
837
  for ( let i = 0; i < this._meshRefs.length; i ++ ) {
820
838
 
821
- data[ i * 4 ] = this._isWorldVisible( this._meshRefs[ i ] ) ? 1.0 : 0.0;
839
+ this._patchTLASLeafVisibility( i, this._isWorldVisible( this._meshRefs[ i ] ) );
822
840
 
823
841
  }
824
842
 
825
- this.meshVisibilityStorageAttr.needsUpdate = true;
843
+ if ( this.bvhStorageAttr ) this.bvhStorageAttr.needsUpdate = true;
844
+
845
+ }
846
+
847
+ /**
848
+ * Patch a single TLAS leaf's visibility flag in the combined BVH buffer.
849
+ * Returns true if the patch was applied.
850
+ * @private
851
+ */
852
+ _patchTLASLeafVisibility( meshIndex, visible ) {
853
+
854
+ const entry = this._instanceTable?.entries?.[ meshIndex ];
855
+ if ( ! entry || entry.tlasLeafIndex < 0 || ! this.bvhStorageAttr ) return false;
856
+
857
+ entry.visible = visible;
858
+ this.bvhStorageAttr.array[ entry.tlasLeafIndex * 16 + 2 ] = visible ? 1.0 : 0.0;
859
+ return true;
826
860
 
827
861
  }
828
862
 
@@ -1493,18 +1527,43 @@ export class PathTracer extends RenderStage {
1493
1527
 
1494
1528
  }
1495
1529
 
1496
- setEmissiveTriangleData( emissiveData, count, totalPower = 0 ) {
1530
+ /**
1531
+ * Rebuild the packed light buffer from cached lightBVH + emissive data.
1532
+ * Layout: [ lightBVH (LBVH_STRIDE vec4s per node) | emissive (EMISSIVE_STRIDE vec4s per entry) ].
1533
+ * Also updates `emissiveVec4Offset` uniform (in vec4 elements).
1534
+ * @private
1535
+ */
1536
+ _rebuildLightBuffer() {
1497
1537
 
1498
- if ( ! emissiveData ) return;
1538
+ const LBVH_STRIDE = 4; // vec4s per LBVH node — must match LightBVHSampling.js
1539
+ const lbvh = this._lbvhDataCache;
1540
+ const emis = this._emissiveDataCache;
1541
+ const lbvhLen = lbvh ? lbvh.length : 0;
1542
+ const emisLen = emis ? emis.length : 0;
1543
+
1544
+ // Ensure at least a minimal non-empty buffer so GPU allocation remains valid.
1545
+ const totalLen = Math.max( lbvhLen + emisLen, 4 );
1546
+ const combined = new Float32Array( totalLen );
1547
+ if ( lbvh ) combined.set( lbvh, 0 );
1548
+ if ( emis ) combined.set( emis, lbvhLen );
1549
+
1550
+ this.lightStorageAttr = new StorageInstancedBufferAttribute( combined, 4 );
1551
+ this.lightStorageNode.value = this.lightStorageAttr;
1552
+ this.lightStorageNode.bufferCount = combined.length / 4;
1553
+
1554
+ // Offset (in vec4 elements) where emissive data starts.
1555
+ this.emissiveVec4Offset.value = ( this.lightBVHNodeCount.value || 0 ) * LBVH_STRIDE;
1499
1556
 
1500
- const vec4Count = emissiveData.length / 4;
1557
+ }
1558
+
1559
+ setEmissiveTriangleData( emissiveData, count, totalPower = 0 ) {
1501
1560
 
1502
- this.emissiveTriangleStorageAttr = new StorageInstancedBufferAttribute( emissiveData, 4 );
1503
- this.emissiveTriangleStorageNode.value = this.emissiveTriangleStorageAttr;
1504
- this.emissiveTriangleStorageNode.bufferCount = vec4Count;
1561
+ if ( ! emissiveData ) return;
1505
1562
 
1563
+ this._emissiveDataCache = emissiveData;
1506
1564
  this.emissiveTriangleCount.value = count;
1507
1565
  this.emissiveTotalPower.value = totalPower;
1566
+ this._rebuildLightBuffer();
1508
1567
  console.log( `PathTracer: ${count} emissive triangles, totalPower=${totalPower.toFixed( 4 )} (storage buffer)` );
1509
1568
 
1510
1569
  }
@@ -1513,11 +1572,9 @@ export class PathTracer extends RenderStage {
1513
1572
 
1514
1573
  if ( ! nodeData ) return;
1515
1574
 
1516
- const vec4Count = nodeData.length / 4;
1517
- this.lightBVHStorageAttr = new StorageInstancedBufferAttribute( nodeData, 4 );
1518
- this.lightBVHStorageNode.value = this.lightBVHStorageAttr;
1519
- this.lightBVHStorageNode.bufferCount = vec4Count;
1575
+ this._lbvhDataCache = nodeData;
1520
1576
  this.lightBVHNodeCount.value = nodeCount;
1577
+ this._rebuildLightBuffer();
1521
1578
  console.log( `PathTracer: Light BVH ${nodeCount} nodes` );
1522
1579
 
1523
1580
  }
@@ -43,19 +43,8 @@ const BVH_STRIDE = 4;
43
43
  const TRI_STRIDE = 8;
44
44
  const HUGE_VAL = 1e8;
45
45
 
46
- // Per-mesh visibility buffer (set by ShaderBuilder before graph construction)
47
- let _meshVisibilityBuffer = null;
48
-
49
- /**
50
- * Set the per-mesh visibility storage buffer node.
51
- * Must be called before the shader graph is constructed (i.e., before setupCompute).
52
- * @param {StorageNode} buffer - TSL storage node indexed by meshIndex
53
- */
54
- export function setMeshVisibilityBuffer( buffer ) {
55
-
56
- _meshVisibilityBuffer = buffer;
57
-
58
- }
46
+ // Per-mesh visibility is now packed into the TLAS BLAS-pointer leaf's slot [2]
47
+ // by TLASBuilder.flatten() — eliminates the dedicated meshVisibility storage buffer.
59
48
 
60
49
  // ================================================================================
61
50
  // STACK HELPERS (Native WGSL array via TSL ArrayNode)
@@ -235,28 +224,34 @@ export const traverseBVH = Fn( ( [
235
224
  const u = triResult.y;
236
225
  const v = triResult.z;
237
226
 
238
- // Fetch normals + material data for visibility check (4 reads)
227
+ // Fetch normals for side-culling (3 reads). Slot 7 (uvData2,
228
+ // carries matIdx + meshIndex) is deferred to post-traversal —
229
+ // it's only needed for the one winning triangle, not per candidate.
230
+ // normalCData.w carries the per-triangle side flag (0/1/2).
239
231
  const nA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 3 ), int( TRI_STRIDE ) ).xyz;
240
232
  const nB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).xyz;
241
- const nC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 5 ), int( TRI_STRIDE ) ).xyz;
242
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 7 ), int( TRI_STRIDE ) );
243
-
244
- const matIdx = int( uvData2.z );
233
+ const normalCData = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 5 ), int( TRI_STRIDE ) );
234
+ const nC = normalCData.xyz;
235
+ const side = int( normalCData.w ).toVar();
245
236
 
246
237
  // Interpolate normal
247
238
  const w = float( 1.0 ).sub( u ).sub( v );
248
239
  const normal = normalize( nA.mul( w ).add( nB.mul( u ) ).add( nC.mul( v ) ) ).toVar();
249
240
 
250
- // Side culling check (per-mesh visibility handled at BLAS-pointer level)
251
- If( passesSideCulling( matIdx, rayDirection, normal, materialBuffer ), () => {
241
+ // Side culling (inline; per-mesh visibility is at the BLAS-pointer level).
242
+ // 0=front (reject back-facing), 1=back (reject front-facing), 2=double (pass).
243
+ const rayDotNormal = rayDirection.dot( normal );
244
+ const sidePass = side.equal( int( 2 ) )
245
+ .or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
246
+ .or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
247
+ If( sidePass, () => {
252
248
 
253
249
  closestHit.didHit.assign( true );
254
250
  closestHit.dst.assign( t );
255
251
  closestHit.normal.assign( normal );
256
- closestHit.materialIndex.assign( matIdx );
257
- closestHit.meshIndex.assign( int( uvData2.w ) );
258
252
 
259
- // Defer hitPoint + UV computation to post-traversal
253
+ // Defer materialIndex/meshIndex/hitPoint/UV to post-traversal
254
+ // (all re-derived from closestTriIdx with a single uvData2 fetch below).
260
255
  closestTriIdx.assign( triIndex );
261
256
  closestU.assign( u );
262
257
  closestV.assign( v );
@@ -276,35 +271,17 @@ export const traverseBVH = Fn( ( [
276
271
 
277
272
  } ).Else( () => {
278
273
 
279
- // BLAS-pointer leaf (marker -2) — push BLAS root node onto stack
280
- // nodeData0: [blasRootNodeIndex, meshIndex, 0, -2]
274
+ // BLAS-pointer leaf (marker -2) — push BLAS root onto stack if mesh is visible
275
+ // nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]
276
+ // Visibility is free-fetched with the leaf — no extra storage read.
281
277
  const blasRoot = int( nodeData0.x ).toVar();
282
278
 
283
- if ( _meshVisibilityBuffer ) {
284
-
285
- // Per-mesh visibility check — skip entire BLAS if mesh is hidden
286
- // getDatafromStorageBuffer( buffer, stride=1, sampleIndex=meshIdx, dataOffset=0 )
287
- const meshIdx = int( nodeData0.y ).toVar();
288
- const meshVis = getDatafromStorageBuffer( _meshVisibilityBuffer, int( 1 ), meshIdx, int( 0 ) ).x;
289
-
290
- If( meshVis.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
291
-
292
- stack.element( stackPtr ).assign( blasRoot );
293
- stackPtr.addAssign( 1 );
279
+ If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
294
280
 
295
- } );
296
-
297
- } else {
298
-
299
- // No visibility buffer — push unconditionally (original behavior)
300
- If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
301
-
302
- stack.element( stackPtr ).assign( blasRoot );
303
- stackPtr.addAssign( 1 );
304
-
305
- } );
281
+ stack.element( stackPtr ).assign( blasRoot );
282
+ stackPtr.addAssign( 1 );
306
283
 
307
- }
284
+ } );
308
285
 
309
286
  } );
310
287
 
@@ -353,7 +330,7 @@ export const traverseBVH = Fn( ( [
353
330
 
354
331
  } );
355
332
 
356
- // Deferred: compute hitPoint and UVs once for the final closest hit
333
+ // Deferred: compute hitPoint, UVs, and fetch matIdx/meshIndex once for the final closest hit
357
334
  If( closestHit.didHit, () => {
358
335
 
359
336
  closestHit.hitPoint.assign( ray.origin.add( ray.direction.mul( closestHit.dst ) ) );
@@ -364,6 +341,8 @@ export const traverseBVH = Fn( ( [
364
341
  closestHit.uv.assign(
365
342
  uvData1.xy.mul( w ).add( uvData1.zw.mul( closestU ) ).add( uvData2.xy.mul( closestV ) )
366
343
  );
344
+ closestHit.materialIndex.assign( int( uvData2.z ) );
345
+ closestHit.meshIndex.assign( int( uvData2.w ) );
367
346
  closestHit.triangleIndex.assign( closestTriIdx );
368
347
 
369
348
  } );
@@ -466,35 +445,16 @@ export const traverseBVHShadow = Fn( ( [
466
445
 
467
446
  } ).Else( () => {
468
447
 
469
- // BLAS-pointer leaf (marker -2) — push BLAS root node onto stack
470
- // nodeData0: [blasRootNodeIndex, meshIndex, 0, -2]
448
+ // BLAS-pointer leaf (marker -2) — push BLAS root onto stack if mesh is visible
449
+ // nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]
471
450
  const blasRoot = int( nodeData0.x ).toVar();
472
451
 
473
- if ( _meshVisibilityBuffer ) {
474
-
475
- // Per-mesh visibility check — skip entire BLAS if mesh is hidden
476
- // getDatafromStorageBuffer( buffer, stride=1, sampleIndex=meshIdx, dataOffset=0 )
477
- const meshIdx = int( nodeData0.y ).toVar();
478
- const meshVis = getDatafromStorageBuffer( _meshVisibilityBuffer, int( 1 ), meshIdx, int( 0 ) ).x;
479
-
480
- If( meshVis.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
481
-
482
- stack.element( stackPtr ).assign( blasRoot );
483
- stackPtr.addAssign( 1 );
484
-
485
- } );
486
-
487
- } else {
488
-
489
- // No visibility buffer — push unconditionally (original behavior)
490
- If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
452
+ If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
491
453
 
492
- stack.element( stackPtr ).assign( blasRoot );
493
- stackPtr.addAssign( 1 );
494
-
495
- } );
454
+ stack.element( stackPtr ).assign( blasRoot );
455
+ stackPtr.addAssign( 1 );
496
456
 
497
- }
457
+ } );
498
458
 
499
459
  } );
500
460
 
@@ -13,7 +13,7 @@ import {
13
13
  If,
14
14
  } from 'three/tsl';
15
15
 
16
- import { struct } from './structProxy.js';
16
+ import { struct } from './patches.js';
17
17
 
18
18
  import { Ray, HitInfo, RayTracingMaterial, DotProducts } from './Struct.js';
19
19
  import { PI, MIN_CLEARCOAT_ROUGHNESS, computeDotProducts } from './Common.js';
@@ -1,6 +1,6 @@
1
1
  import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max } from 'three/tsl';
2
2
 
3
- import { struct } from './structProxy.js';
3
+ import { struct } from './patches.js';
4
4
  import { getDatafromStorageBuffer } from './Common.js';
5
5
  import { sampleDisplacementMap } from './TextureSampling.js';
6
6
 
@@ -27,7 +27,7 @@ import {
27
27
  atan,
28
28
  } from 'three/tsl';
29
29
 
30
- import { struct } from './structProxy.js';
30
+ import { struct } from './patches.js';
31
31
  import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT } from './Common.js';
32
32
  import { RandomValue } from './Random.js';
33
33
  import { calculateMaterialPDF } from './LightsSampling.js';
@@ -361,8 +361,10 @@ export const calculateEmissiveLightPdf = Fn( ( [
361
361
  // ================================================================================
362
362
 
363
363
  // Binary search in CDF for importance-weighted triangle selection
364
- // CDF values are stored in the .b channel of the emissive buffer
365
- const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveTriangleCount, rand ] ) => {
364
+ // CDF values are stored in the .b channel of the emissive buffer.
365
+ // `emissiveOffset` is the vec4-element offset into the packed light buffer
366
+ // where emissive entries start (0 if using a non-packed buffer).
367
+ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveOffset, emissiveTriangleCount, rand ] ) => {
366
368
 
367
369
  const lo = int( 0 ).toVar();
368
370
  const hi = emissiveTriangleCount.sub( 1 ).toVar();
@@ -370,7 +372,7 @@ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveTriangleCount, r
370
372
  Loop( lo.lessThan( hi ), () => {
371
373
 
372
374
  const mid = lo.add( hi ).div( 2 ).toVar();
373
- const cdfVal = emissiveTriangleBuffer.element( mid.mul( EMISSIVE_STRIDE ) ).b;
375
+ const cdfVal = emissiveTriangleBuffer.element( emissiveOffset.add( mid.mul( int( EMISSIVE_STRIDE ) ) ) ).b;
374
376
 
375
377
  If( cdfVal.lessThan( rand ), () => {
376
378
 
@@ -388,11 +390,13 @@ const binarySearchCDF = Fn( ( [ emissiveTriangleBuffer, emissiveTriangleCount, r
388
390
 
389
391
  } );
390
392
 
391
- // Sample from emissive triangle index using CDF importance sampling
393
+ // Sample from emissive triangle index using CDF importance sampling.
394
+ // `emissiveTriangleBuffer` may be the shared packed light buffer; `emissiveVec4Offset`
395
+ // gives the vec4 offset where emissive entries begin.
392
396
  export const sampleEmissiveTriangle = Fn( ( [
393
397
  hitPoint, surfaceNormal, totalTriangleCount,
394
398
  rngState,
395
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower,
399
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
396
400
  triangleBuffer,
397
401
  ] ) => {
398
402
 
@@ -413,12 +417,12 @@ export const sampleEmissiveTriangle = Fn( ( [
413
417
 
414
418
  // CDF importance-weighted triangle selection (brighter triangles sampled more)
415
419
  const randEmissive = RandomValue( rngState );
416
- const emissiveIndex = binarySearchCDF( emissiveTriangleBuffer, emissiveTriangleCount, randEmissive ).toVar();
420
+ const emissiveIndex = binarySearchCDF( emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, randEmissive ).toVar();
417
421
 
418
- // Fetch emissive triangle data from storage buffer (2 vec4s per entry)
422
+ // Fetch emissive triangle data from packed light buffer (2 vec4s per entry)
419
423
  // vec4[0] = (triangleIndex, power, cdf, selectionPdf)
420
424
  // vec4[1] = (emission.r, emission.g, emission.b, area)
421
- const baseIdx = emissiveIndex.mul( EMISSIVE_STRIDE );
425
+ const baseIdx = emissiveVec4Offset.add( emissiveIndex.mul( int( EMISSIVE_STRIDE ) ) );
422
426
  const emissiveData0 = emissiveTriangleBuffer.element( baseIdx );
423
427
  const emissiveData1 = emissiveTriangleBuffer.element( baseIdx.add( 1 ) );
424
428
  const triangleIndex = int( emissiveData0.r );
@@ -534,7 +538,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
534
538
  hitPoint, normal, viewDir, material,
535
539
  totalTriangleCount, bounceIndex, rngState,
536
540
  emissiveBoost,
537
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower,
541
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
538
542
  triangleBuffer,
539
543
  // Callback functions to avoid circular deps
540
544
  traceShadowRayFn,
@@ -557,7 +561,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
557
561
  // Sample emissive triangle (CDF importance-weighted)
558
562
  const emissiveSample = EmissiveSample.wrap( sampleEmissiveTriangle(
559
563
  hitPoint, normal, totalTriangleCount, rngState,
560
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower,
564
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
561
565
  triangleBuffer,
562
566
  ) );
563
567
 
@@ -619,7 +623,7 @@ export const calculateEmissiveTriangleContribution = Fn( ( [
619
623
  hitPoint, normal, viewDir, material,
620
624
  totalTriangleCount, bounceIndex, rngState,
621
625
  emissiveBoost,
622
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower,
626
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
623
627
  triangleBuffer,
624
628
  traceShadowRayFn,
625
629
  evaluateMaterialResponseFn,
@@ -630,7 +634,7 @@ export const calculateEmissiveTriangleContribution = Fn( ( [
630
634
  hitPoint, normal, viewDir, material,
631
635
  totalTriangleCount, bounceIndex, rngState,
632
636
  emissiveBoost,
633
- emissiveTriangleBuffer, emissiveTriangleCount, emissiveTotalPower,
637
+ emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
634
638
  triangleBuffer,
635
639
  traceShadowRayFn,
636
640
  evaluateMaterialResponseFn,
@@ -82,8 +82,7 @@ export const sampleEquirect = Fn( ( [ environment, direction, environmentMatrix,
82
82
  // Exact implementation from three-gpu-pathtracer
83
83
  export const sampleEquirectProbability = Fn( ( [
84
84
  environment,
85
- envMarginalWeights,
86
- envConditionalWeights,
85
+ envCDFBuffer,
87
86
  environmentMatrix,
88
87
  environmentIntensity,
89
88
  envTotalSum,
@@ -92,15 +91,19 @@ export const sampleEquirectProbability = Fn( ( [
92
91
  colorOutput
93
92
  ] ) => {
94
93
 
95
- // Sample marginal CDF for V coordinate (1D storage buffer, linear interpolation)
94
+ // Packed CDF layout: [marginal (envResolution.y floats) | conditional (envResolution.x * envResolution.y floats)]
95
+ // The conditional offset equals the marginal length, which is envResolution.y.
96
+ const condOffset = int( envResolution.y ).toVar();
97
+
98
+ // Sample marginal CDF for V coordinate (1D, linear interpolation)
96
99
  const marginalSize = envResolution.y;
97
100
  const mIdx = clamp( r.x.mul( marginalSize.sub( 1.0 ) ), 0.0, marginalSize.sub( 1.0 ) );
98
101
  const mI0 = int( floor( mIdx ) );
99
102
  const mI1 = min( mI0.add( 1 ), int( marginalSize ).sub( 1 ) );
100
103
  const mFrac = fract( mIdx );
101
- const v = mix( envMarginalWeights.element( mI0 ), envMarginalWeights.element( mI1 ), mFrac ).toVar();
104
+ const v = mix( envCDFBuffer.element( mI0 ), envCDFBuffer.element( mI1 ), mFrac ).toVar();
102
105
 
103
- // Sample conditional CDF for U coordinate (2D storage buffer, bilinear interpolation)
106
+ // Sample conditional CDF for U coordinate (2D grid, bilinear interpolation)
104
107
  const condW = envResolution.x;
105
108
  const condH = envResolution.y;
106
109
  const cxf = clamp( r.y.mul( condW.sub( 1.0 ) ), 0.0, condW.sub( 1.0 ) );
@@ -112,10 +115,10 @@ export const sampleEquirectProbability = Fn( ( [
112
115
  const fx = fract( cxf );
113
116
  const fy = fract( cyf );
114
117
  const condWi = int( condW );
115
- const v00 = envConditionalWeights.element( cy0.mul( condWi ).add( cx0 ) );
116
- const v10 = envConditionalWeights.element( cy0.mul( condWi ).add( cx1 ) );
117
- const v01 = envConditionalWeights.element( cy1.mul( condWi ).add( cx0 ) );
118
- const v11 = envConditionalWeights.element( cy1.mul( condWi ).add( cx1 ) );
118
+ const v00 = envCDFBuffer.element( condOffset.add( cy0.mul( condWi ).add( cx0 ) ) );
119
+ const v10 = envCDFBuffer.element( condOffset.add( cy0.mul( condWi ).add( cx1 ) ) );
120
+ const v01 = envCDFBuffer.element( condOffset.add( cy1.mul( condWi ).add( cx0 ) ) );
121
+ const v11 = envCDFBuffer.element( condOffset.add( cy1.mul( condWi ).add( cx1 ) ) );
119
122
  const u = mix( mix( v00, v10, fx ), mix( v01, v11, fx ), fy ).toVar();
120
123
 
121
124
  const uv = vec2( u, v ).toVar();
@@ -53,6 +53,7 @@ export const sampleLightBVHTriangle = Fn( ( [
53
53
  rngState,
54
54
  lbvhBuffer,
55
55
  emissiveTriangleBuffer,
56
+ emissiveVec4Offset,
56
57
  triangleBuffer,
57
58
  ] ) => {
58
59
 
@@ -185,7 +186,7 @@ export const sampleLightBVHTriangle = Fn( ( [
185
186
  Loop( { start: int( 0 ), end: emissiveCount }, ( { i } ) => {
186
187
 
187
188
  const entryIdx = emissiveStart.add( i );
188
- const baseIdx = entryIdx.mul( int( EMISSIVE_STRIDE ) );
189
+ const baseIdx = emissiveVec4Offset.add( entryIdx.mul( int( EMISSIVE_STRIDE ) ) );
189
190
  const emData0 = emissiveTriangleBuffer.element( baseIdx );
190
191
  const triPower = max( emData0.g, float( 0.0 ) );
191
192
  cumPower.addAssign( triPower );
@@ -204,7 +205,7 @@ export const sampleLightBVHTriangle = Fn( ( [
204
205
  selectionPdf.mulAssign( selectedPower.div( leafTotalPower ) );
205
206
 
206
207
  // Now sample the selected triangle (same path as flat CDF sampling)
207
- const baseIdx = selectedEmissiveIndex.mul( int( EMISSIVE_STRIDE ) );
208
+ const baseIdx = emissiveVec4Offset.add( selectedEmissiveIndex.mul( int( EMISSIVE_STRIDE ) ) );
208
209
  const emissiveData0 = emissiveTriangleBuffer.element( baseIdx );
209
210
  const emissiveData1 = emissiveTriangleBuffer.element( baseIdx.add( int( 1 ) ) );
210
211
 
@@ -13,7 +13,7 @@ import {
13
13
  abs,
14
14
  } from 'three/tsl';
15
15
 
16
- import { struct } from './structProxy.js';
16
+ import { struct } from './patches.js';
17
17
 
18
18
  // ================================================================================
19
19
  // LIGHT STRUCTURES
@@ -41,7 +41,7 @@ import { RandomValue } from './Random.js';
41
41
  import { getTransformedUV } from './TextureSampling.js';
42
42
 
43
43
  // Module-level state for alpha-cutout shadow testing.
44
- // Set by ShaderBuilder before graph construction (same pattern as _meshVisibilityBuffer in BVHTraversal.js).
44
+ // Set by ShaderBuilder before graph construction.
45
45
  let _shadowAlbedoMaps = null;
46
46
  let _enableAlphaShadows = null;
47
47
 
@@ -130,6 +130,19 @@ export const traceShadowRay = Fn( ( [
130
130
 
131
131
  } );
132
132
 
133
+ // Opaque fast-path: check the per-triangle blocker flag (NORMAL_A.w, set at
134
+ // extraction time when alphaMode/transparent/transmission/opacity all indicate
135
+ // a fully opaque surface). Short-circuits the 7-slot getShadowMaterial fetch
136
+ // and the entire alpha/transmission/transparent decision tree below.
137
+ const TRI_STRIDE_SR = int( 8 );
138
+ const blocker = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 3 ), TRI_STRIDE_SR ).w;
139
+ If( blocker.greaterThan( 0.5 ), () => {
140
+
141
+ transmittance.assign( 0.0 );
142
+ Break();
143
+
144
+ } );
145
+
133
146
  // Fetch material for the hit surface (thin reader: 7 slots instead of 27)
134
147
  const shadowMaterial = ShadowMaterial.wrap( getShadowMaterial( shadowHit.materialIndex, materialBuffer ) );
135
148
 
@@ -273,7 +273,6 @@ export const calculateIndirectLighting = Fn( ( [
273
273
  samplingInfo,
274
274
  // Environment resources
275
275
  envTexture, environmentIntensity, envMatrix,
276
- envMarginalWeights, envConditionalWeights,
277
276
  envTotalSum, envResolution,
278
277
  enableEnvironmentLight, useEnvMapIS,
279
278
  ] ) => {
@@ -934,7 +934,7 @@ export const calculateDirectLightingUnified = Fn( ( [
934
934
  materialBuffer,
935
935
  // Environment resources
936
936
  envTexture, environmentIntensity, envMatrix,
937
- envMarginalWeights, envConditionalWeights,
937
+ envCDFBuffer,
938
938
  envTotalSum, envResolution,
939
939
  enableEnvironmentLight,
940
940
  ] ) => {
@@ -1203,7 +1203,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1203
1203
 
1204
1204
  // Sample direction + PDF + color from importance-sampled environment
1205
1205
  const envSampleResult = sampleEquirectProbability(
1206
- envTexture, envMarginalWeights, envConditionalWeights,
1206
+ envTexture, envCDFBuffer,
1207
1207
  envMatrix, environmentIntensity, envTotalSum, envResolution, envRandom, envColor
1208
1208
  ).toVar();
1209
1209