rayzee 5.4.1 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "5.4.1",
3
+ "version": "5.4.2",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -618,7 +618,22 @@ export class GeometryExtractor {
618
618
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 0 ] = normalA.x;
619
619
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 1 ] = normalA.y;
620
620
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 2 ] = normalA.z;
621
- this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 3 ] = 0; // vec4 padding
621
+ // Repurposed padding: opaque-blocker fast-path flag for shadow rays.
622
+ // 1.0 = surface fully blocks light (no alpha, transmission, or transparency) →
623
+ // traceShadowRay can skip the 7-slot getShadowMaterial fetch.
624
+ // 0.0 = requires full material evaluation.
625
+ {
626
+
627
+ const mat = this.materials[ materialIndex ];
628
+ const isOpaqueBlocker = mat
629
+ && ( mat.alphaMode | 0 ) === 0
630
+ && ( mat.transparent | 0 ) === 0
631
+ && ( mat.transmission || 0 ) === 0
632
+ && ( mat.opacity ?? 1 ) >= 1
633
+ ? 1.0 : 0.0;
634
+ this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_A_OFFSET + 3 ] = isOpaqueBlocker;
635
+
636
+ }
622
637
 
623
638
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_B_OFFSET + 0 ] = normalB.x;
624
639
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_B_OFFSET + 1 ] = normalB.y;
@@ -628,7 +643,9 @@ export class GeometryExtractor {
628
643
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_C_OFFSET + 0 ] = normalC.x;
629
644
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_C_OFFSET + 1 ] = normalC.y;
630
645
  this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_C_OFFSET + 2 ] = normalC.z;
631
- this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_C_OFFSET + 3 ] = 0; // vec4 padding
646
+ // Repurposed padding: per-triangle side flag (0=front, 1=back, 2=double).
647
+ // Lets BVH traversal do side culling without a material-buffer read per hit.
648
+ this.triangleData[ offset + TRIANGLE_DATA_LAYOUT.NORMAL_C_OFFSET + 3 ] = this.materials[ materialIndex ]?.side ?? 0;
632
649
 
633
650
  // UVs and material index (2 vec4s = 8 floats)
634
651
  // First vec4: uvA.x, uvA.y, uvB.x, uvB.y
@@ -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 );
@@ -224,28 +224,34 @@ export const traverseBVH = Fn( ( [
224
224
  const u = triResult.y;
225
225
  const v = triResult.z;
226
226
 
227
- // 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).
228
231
  const nA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 3 ), int( TRI_STRIDE ) ).xyz;
229
232
  const nB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).xyz;
230
- const nC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 5 ), int( TRI_STRIDE ) ).xyz;
231
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 7 ), int( TRI_STRIDE ) );
232
-
233
- 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();
234
236
 
235
237
  // Interpolate normal
236
238
  const w = float( 1.0 ).sub( u ).sub( v );
237
239
  const normal = normalize( nA.mul( w ).add( nB.mul( u ) ).add( nC.mul( v ) ) ).toVar();
238
240
 
239
- // Side culling check (per-mesh visibility handled at BLAS-pointer level)
240
- 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, () => {
241
248
 
242
249
  closestHit.didHit.assign( true );
243
250
  closestHit.dst.assign( t );
244
251
  closestHit.normal.assign( normal );
245
- closestHit.materialIndex.assign( matIdx );
246
- closestHit.meshIndex.assign( int( uvData2.w ) );
247
252
 
248
- // 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).
249
255
  closestTriIdx.assign( triIndex );
250
256
  closestU.assign( u );
251
257
  closestV.assign( v );
@@ -324,7 +330,7 @@ export const traverseBVH = Fn( ( [
324
330
 
325
331
  } );
326
332
 
327
- // 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
328
334
  If( closestHit.didHit, () => {
329
335
 
330
336
  closestHit.hitPoint.assign( ray.origin.add( ray.direction.mul( closestHit.dst ) ) );
@@ -335,6 +341,8 @@ export const traverseBVH = Fn( ( [
335
341
  closestHit.uv.assign(
336
342
  uvData1.xy.mul( w ).add( uvData1.zw.mul( closestU ) ).add( uvData2.xy.mul( closestV ) )
337
343
  );
344
+ closestHit.materialIndex.assign( int( uvData2.z ) );
345
+ closestHit.meshIndex.assign( int( uvData2.w ) );
338
346
  closestHit.triangleIndex.assign( closestTriIdx );
339
347
 
340
348
  } );
@@ -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
 
@@ -281,7 +281,7 @@ export const processBump = Fn( ( [ bumpMaps, currentNormal, material, uvCache ]
281
281
 
282
282
  const result = currentNormal.toVar();
283
283
 
284
- If( material.bumpMapIndex.greaterThanEqual( int( 0 ) ), () => {
284
+ If( material.bumpMapIndex.greaterThanEqual( int( 0 ) ).and( material.bumpScale.greaterThan( 0.0 ) ), () => {
285
285
 
286
286
  // Approximate texel size
287
287
  const texelSize = vec2( 1.0 / 1024.0 ).toVar();
@@ -9,9 +9,16 @@
9
9
 
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
11
  import { storage } from 'three/tsl';
12
- import { TEXTURE_CONSTANTS, MATERIAL_DATA_LAYOUT as M } from '../EngineDefaults.js';
12
+ import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T } from '../EngineDefaults.js';
13
13
 
14
14
  const PIXELS_PER_MATERIAL = M.SLOTS_PER_MATERIAL;
15
+ // Per-triangle float offsets used by _patchTriangleSideForMaterial / _patchTriangleBlockerForMaterial.
16
+ const TRI_MAT_IDX_OFFSET = T.UV_C_MAT_OFFSET + 2; // uvData2.z in shader
17
+ const TRI_SIDE_OFFSET = T.NORMAL_C_OFFSET + 3; // normalCData.w in shader
18
+ const TRI_BLOCKER_OFFSET = T.NORMAL_A_OFFSET + 3; // nA.w in shader (opaque-blocker fast path)
19
+
20
+ // Material properties that affect the shadow-ray opaque-blocker flag.
21
+ const BLOCKER_PROPS = new Set( [ 'transmission', 'transparent', 'opacity', 'alphaMode' ] );
15
22
 
16
23
  export class MaterialDataManager {
17
24
 
@@ -41,7 +48,7 @@ export class MaterialDataManager {
41
48
 
42
49
  /**
43
50
  * Optional callbacks set by the owning stage.
44
- * @type {{ onReset?: Function, onFeaturesChanged?: Function }}
51
+ * @type {{ onReset?: Function, onFeaturesChanged?: Function, getTriangleData?: Function, onTriangleDataChanged?: Function }}
45
52
  */
46
53
  this.callbacks = {};
47
54
 
@@ -275,7 +282,11 @@ export class MaterialDataManager {
275
282
  case 'clearcoat': data[ stride + M.CLEARCOAT ] = value; break;
276
283
  case 'clearcoatRoughness': data[ stride + M.CLEARCOAT_ROUGHNESS ] = value; break;
277
284
  case 'opacity': data[ stride + M.OPACITY ] = value; break;
278
- case 'side': data[ stride + M.SIDE ] = value; break;
285
+ case 'side': data[ stride + M.SIDE ] = value;
286
+ // Side is also mirrored into per-triangle data (NORMAL_C.w) so BVH
287
+ // traversal can do side culling without reading the material buffer.
288
+ this._patchTriangleSideForMaterial( materialIndex, value );
289
+ break;
279
290
  case 'transparent': data[ stride + M.TRANSPARENT ] = value; break;
280
291
  case 'alphaTest': data[ stride + M.ALPHA_TEST ] = value; break;
281
292
  case 'alphaMode': data[ stride + M.ALPHA_MODE ] = value; break;
@@ -304,6 +315,13 @@ export class MaterialDataManager {
304
315
 
305
316
  this.materialStorageAttr.needsUpdate = true;
306
317
 
318
+ // Recompute triangle-data opaque-blocker flag when any input to it changes.
319
+ if ( BLOCKER_PROPS.has( property ) ) {
320
+
321
+ this._recomputeOpaqueBlockerForMaterial( materialIndex );
322
+
323
+ }
324
+
307
325
  const featureProperties = [ 'transmission', 'clearcoat', 'sheen', 'iridescence', 'dispersion', 'transparent', 'opacity', 'alphaTest' ];
308
326
  if ( featureProperties.includes( property ) ) {
309
327
 
@@ -414,6 +432,10 @@ export class MaterialDataManager {
414
432
  data[ stride + M.CLEARCOAT_ROUGHNESS ] = materialData.clearcoatRoughness ?? 0;
415
433
  data[ stride + M.OPACITY ] = materialData.opacity ?? 1;
416
434
  data[ stride + M.SIDE ] = materialData.side ?? 0;
435
+ // Mirror side into per-triangle data so BVH traversal avoids a material-buffer read.
436
+ this._patchTriangleSideForMaterial( materialIndex, materialData.side ?? 0 );
437
+ // Recompute shadow-ray opaque-blocker flag (reads alphaMode/transparent/transmission/opacity from buffer).
438
+ this._recomputeOpaqueBlockerForMaterial( materialIndex );
417
439
  data[ stride + M.TRANSPARENT ] = materialData.transparent ?? 0;
418
440
  data[ stride + M.ALPHA_TEST ] = materialData.alphaTest ?? 0;
419
441
  data[ stride + M.ALPHA_MODE ] = materialData.alphaMode ?? 0;
@@ -657,6 +679,74 @@ export class MaterialDataManager {
657
679
 
658
680
  }
659
681
 
682
+ /**
683
+ * Rewrite the per-triangle `side` flag (NORMAL_C.w) for every triangle whose
684
+ * materialIndex matches. Linear over triangles because there's no reverse
685
+ * index — side edits are a rare UI action so the scan cost is acceptable.
686
+ * @private
687
+ */
688
+ /**
689
+ * Re-derive the shadow-ray opaque-blocker flag for a material from its
690
+ * current buffer values and patch NORMAL_A.w on every matching triangle.
691
+ * Kept in sync with the blocker definition in GeometryExtractor.
692
+ * @private
693
+ */
694
+ _recomputeOpaqueBlockerForMaterial( materialIndex ) {
695
+
696
+ const matBuf = this.materialStorageAttr?.array;
697
+ if ( ! matBuf ) return;
698
+
699
+ const matStride = materialIndex * M.FLOATS_PER_MATERIAL;
700
+ const alphaMode = matBuf[ matStride + M.ALPHA_MODE ] | 0;
701
+ const transparent = matBuf[ matStride + M.TRANSPARENT ] | 0;
702
+ const transmission = matBuf[ matStride + M.TRANSMISSION ] || 0;
703
+ const opacity = matBuf[ matStride + M.OPACITY ] ?? 1;
704
+ const isOpaqueBlocker = ( alphaMode === 0 && transparent === 0 && transmission === 0 && opacity >= 1 ) ? 1.0 : 0.0;
705
+
706
+ this._patchTriangleFlagForMaterial( materialIndex, TRI_BLOCKER_OFFSET, isOpaqueBlocker );
707
+
708
+ }
709
+
710
+ /**
711
+ * Generic helper: patch a single per-triangle float at `triOffset` for every
712
+ * triangle whose materialIndex matches, then fire onTriangleDataChanged.
713
+ * @private
714
+ */
715
+ _patchTriangleFlagForMaterial( materialIndex, triOffset, value ) {
716
+
717
+ const triInfo = this.callbacks.getTriangleData?.();
718
+ const triData = triInfo?.array;
719
+ const triCount = triInfo?.count | 0;
720
+ if ( ! triData || triCount === 0 ) return;
721
+
722
+ const stride = T.FLOATS_PER_TRIANGLE;
723
+ let patched = 0;
724
+ for ( let i = 0; i < triCount; i ++ ) {
725
+
726
+ const base = i * stride;
727
+ if ( triData[ base + TRI_MAT_IDX_OFFSET ] === materialIndex ) {
728
+
729
+ triData[ base + triOffset ] = value;
730
+ patched ++;
731
+
732
+ }
733
+
734
+ }
735
+
736
+ if ( patched > 0 && this.callbacks.onTriangleDataChanged ) {
737
+
738
+ this.callbacks.onTriangleDataChanged();
739
+
740
+ }
741
+
742
+ }
743
+
744
+ _patchTriangleSideForMaterial( materialIndex, sideValue ) {
745
+
746
+ this._patchTriangleFlagForMaterial( materialIndex, TRI_SIDE_OFFSET, sideValue );
747
+
748
+ }
749
+
660
750
  // ===== DISPOSAL =====
661
751
 
662
752
  dispose() {