rayzee 5.0.2 → 5.1.1

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.
@@ -25,7 +25,6 @@ import {
25
25
  array,
26
26
  } from 'three/tsl';
27
27
 
28
- import { struct } from './structProxy.js';
29
28
  import { Ray, HitInfo } from './Struct.js';
30
29
  import { getDatafromStorageBuffer, MATERIAL_SLOTS } from './Common.js';
31
30
  import { RandomPointInCircle } from './Random.js';
@@ -34,14 +33,6 @@ import { RandomPointInCircle } from './Random.js';
34
33
  // STRUCTS
35
34
  // ================================================================================
36
35
 
37
- // Combined visibility data structure
38
- export const VisibilityData = struct( {
39
- visible: 'bool',
40
- side: 'int',
41
- transparent: 'bool',
42
- opacity: 'float'
43
- } );
44
-
45
36
  // ================================================================================
46
37
  // CONSTANTS
47
38
  // ================================================================================
@@ -52,6 +43,20 @@ const BVH_STRIDE = 4;
52
43
  const TRI_STRIDE = 8;
53
44
  const HUGE_VAL = 1e8;
54
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
+ }
59
+
55
60
  // ================================================================================
56
61
  // STACK HELPERS (Native WGSL array via TSL ArrayNode)
57
62
  // ================================================================================
@@ -127,48 +132,17 @@ const fastRayAABBDst = wgslFn( `
127
132
  // VISIBILITY FUNCTIONS
128
133
  // ================================================================================
129
134
 
130
- // Fetch all visibility data in 2 reads
131
- export const getVisibilityData = Fn( ( [ materialIndex, materialBuffer ] ) => {
135
+ // Side culling 1 buffer read (slot 10 only)
136
+ // Per-mesh visibility handled at BLAS-pointer level; material visibility always 1.
137
+ export const passesSideCulling = Fn( ( [ materialIndex, rayDirection, normal, materialBuffer ] ) => {
132
138
 
133
- // Read visibility flag from slot 4
134
- const visData = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 4 ), int( MATERIAL_SLOTS ) );
135
- // Read side and transparency data from slot 10
136
139
  const sideData = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 10 ), int( MATERIAL_SLOTS ) );
137
-
138
- return VisibilityData( {
139
- visible: visData.g.greaterThan( 0.5 ),
140
- opacity: sideData.r,
141
- side: int( sideData.g ),
142
- transparent: sideData.b.greaterThan( 0.5 ),
143
- } );
144
-
145
- } );
146
-
147
- // Fast visibility check using material texture
148
- export const isTriangleVisible = Fn( ( [ materialIndex, materialBuffer ] ) => {
149
-
150
- const visData = getDatafromStorageBuffer( materialBuffer, materialIndex, int( 4 ), int( MATERIAL_SLOTS ) );
151
- return visData.g.greaterThan( 0.5 );
152
-
153
- } );
154
-
155
- // Complete visibility check with side culling
156
- export const isMaterialVisibleOptimized = wgslFn( `
157
- fn isMaterialVisibleOptimized( visible: bool, side: i32, rayDirection: vec3f, normal: vec3f ) -> bool {
158
- if ( !visible ) { return false; }
159
- let rayDotNormal = dot( rayDirection, normal );
160
- let doubleSide = side == 2;
161
- let frontSide = side == 0 && rayDotNormal < -0.0001f;
162
- let backSide = side == 1 && rayDotNormal > 0.0001f;
163
- return doubleSide || frontSide || backSide;
164
- }
165
- ` );
166
-
167
- // Single visibility check with combined data fetch
168
- export const isMaterialVisible = Fn( ( [ materialIndex, rayDirection, normal, materialBuffer ] ) => {
169
-
170
- const vis = VisibilityData.wrap( getVisibilityData( materialIndex, materialBuffer ) );
171
- return isMaterialVisibleOptimized( { visible: vis.visible, side: vis.side, rayDirection, normal } );
140
+ const side = int( sideData.g );
141
+ const rayDotNormal = rayDirection.dot( normal );
142
+ const doubleSide = side.equal( int( 2 ) );
143
+ const frontSide = side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) );
144
+ const backSide = side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) );
145
+ return doubleSide.or( frontSide ).or( backSide );
172
146
 
173
147
  } );
174
148
 
@@ -269,28 +243,23 @@ export const traverseBVH = Fn( ( [
269
243
 
270
244
  const matIdx = int( uvData2.z );
271
245
 
272
- // Early material rejection
273
- If( isTriangleVisible( matIdx, materialBuffer ), () => {
246
+ // Interpolate normal
247
+ const w = float( 1.0 ).sub( u ).sub( v );
248
+ const normal = normalize( nA.mul( w ).add( nB.mul( u ) ).add( nC.mul( v ) ) ).toVar();
274
249
 
275
- // Interpolate normal
276
- const w = float( 1.0 ).sub( u ).sub( v );
277
- const normal = normalize( nA.mul( w ).add( nB.mul( u ) ).add( nC.mul( v ) ) ).toVar();
250
+ // Side culling check (per-mesh visibility handled at BLAS-pointer level)
251
+ If( passesSideCulling( matIdx, rayDirection, normal, materialBuffer ), () => {
278
252
 
279
- // Full material visibility check (culling etc)
280
- If( isMaterialVisible( matIdx, rayDirection, normal, materialBuffer ), () => {
281
-
282
- closestHit.didHit.assign( true );
283
- closestHit.dst.assign( t );
284
- closestHit.normal.assign( normal );
285
- closestHit.materialIndex.assign( matIdx );
286
- closestHit.meshIndex.assign( int( uvData2.w ) );
287
-
288
- // Defer hitPoint + UV computation to post-traversal
289
- closestTriIdx.assign( triIndex );
290
- closestU.assign( u );
291
- closestV.assign( v );
253
+ closestHit.didHit.assign( true );
254
+ closestHit.dst.assign( t );
255
+ closestHit.normal.assign( normal );
256
+ closestHit.materialIndex.assign( matIdx );
257
+ closestHit.meshIndex.assign( int( uvData2.w ) );
292
258
 
293
- } );
259
+ // Defer hitPoint + UV computation to post-traversal
260
+ closestTriIdx.assign( triIndex );
261
+ closestU.assign( u );
262
+ closestV.assign( v );
294
263
 
295
264
  } );
296
265
 
@@ -299,7 +268,7 @@ export const traverseBVH = Fn( ( [
299
268
  } );
300
269
 
301
270
  // If we found a very close hit, we can terminate early
302
- If( closestHit.didHit.and( closestHit.dst.lessThan( 0.001 ) ), () => {
271
+ If( closestHit.didHit.and( closestHit.dst.lessThan( 1e-6 ) ), () => {
303
272
 
304
273
  Break();
305
274
 
@@ -308,13 +277,34 @@ export const traverseBVH = Fn( ( [
308
277
  } ).Else( () => {
309
278
 
310
279
  // BLAS-pointer leaf (marker -2) — push BLAS root node onto stack
280
+ // nodeData0: [blasRootNodeIndex, meshIndex, 0, -2]
311
281
  const blasRoot = int( nodeData0.x ).toVar();
312
- If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
313
282
 
314
- stack.element( stackPtr ).assign( blasRoot );
315
- stackPtr.addAssign( 1 );
283
+ if ( _meshVisibilityBuffer ) {
316
284
 
317
- } );
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 );
294
+
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
+ } );
306
+
307
+ }
318
308
 
319
309
  } );
320
310
 
@@ -390,7 +380,7 @@ export const traverseBVHShadow = Fn( ( [
390
380
  ray,
391
381
  bvhBuffer,
392
382
  triangleBuffer,
393
- materialBuffer,
383
+ _materialBuffer, // eslint-disable-line no-unused-vars -- kept for call-site compatibility
394
384
  maxShadowDist,
395
385
  ] ) => {
396
386
 
@@ -448,25 +438,21 @@ export const traverseBVHShadow = Fn( ( [
448
438
 
449
439
  If( triResult.w.greaterThan( 0.5 ), () => {
450
440
 
441
+ // Per-mesh visibility handled at BLAS-pointer level — accept any hit
451
442
  const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 7 ), int( TRI_STRIDE ) );
452
- const matIdx = int( uvData2.z );
453
-
454
- If( isTriangleVisible( matIdx, materialBuffer ), () => {
455
-
456
- closestHit.didHit.assign( true );
457
- closestHit.dst.assign( triResult.x );
458
- closestHit.materialIndex.assign( matIdx );
459
- closestHit.meshIndex.assign( int( uvData2.w ) );
460
443
 
461
- // Compute hit point and geometric normal -- required for transmissive
462
- // Fresnel in traceShadowRay (cosThetaI needs a real normal, not vec3(0))
463
- closestHit.hitPoint.assign( ray.origin.add( ray.direction.mul( triResult.x ) ) );
464
- closestHit.normal.assign( normalize( cross( pB.sub( pA ), pC.sub( pA ) ) ) );
444
+ closestHit.didHit.assign( true );
445
+ closestHit.dst.assign( triResult.x );
446
+ closestHit.materialIndex.assign( int( uvData2.z ) );
447
+ closestHit.meshIndex.assign( int( uvData2.w ) );
465
448
 
466
- // Shadow ray only needs any hit skip remaining triangles in leaf
467
- Break();
449
+ // Compute hit point and geometric normal -- required for transmissive
450
+ // Fresnel in traceShadowRay (cosThetaI needs a real normal, not vec3(0))
451
+ closestHit.hitPoint.assign( ray.origin.add( ray.direction.mul( triResult.x ) ) );
452
+ closestHit.normal.assign( normalize( cross( pB.sub( pA ), pC.sub( pA ) ) ) );
468
453
 
469
- } );
454
+ // Shadow ray only needs any hit — skip remaining triangles in leaf
455
+ Break();
470
456
 
471
457
  } );
472
458
 
@@ -475,13 +461,34 @@ export const traverseBVHShadow = Fn( ( [
475
461
  } ).Else( () => {
476
462
 
477
463
  // BLAS-pointer leaf (marker -2) — push BLAS root node onto stack
464
+ // nodeData0: [blasRootNodeIndex, meshIndex, 0, -2]
478
465
  const blasRoot = int( nodeData0.x ).toVar();
479
- If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
480
466
 
481
- stack.element( stackPtr ).assign( blasRoot );
482
- stackPtr.addAssign( 1 );
467
+ if ( _meshVisibilityBuffer ) {
483
468
 
484
- } );
469
+ // Per-mesh visibility check — skip entire BLAS if mesh is hidden
470
+ // getDatafromStorageBuffer( buffer, stride=1, sampleIndex=meshIdx, dataOffset=0 )
471
+ const meshIdx = int( nodeData0.y ).toVar();
472
+ const meshVis = getDatafromStorageBuffer( _meshVisibilityBuffer, int( 1 ), meshIdx, int( 0 ) ).x;
473
+
474
+ If( meshVis.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
475
+
476
+ stack.element( stackPtr ).assign( blasRoot );
477
+ stackPtr.addAssign( 1 );
478
+
479
+ } );
480
+
481
+ } else {
482
+
483
+ // No visibility buffer — push unconditionally (original behavior)
484
+ If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
485
+
486
+ stack.element( stackPtr ).assign( blasRoot );
487
+ stackPtr.addAssign( 1 );
488
+
489
+ } );
490
+
491
+ }
485
492
 
486
493
  } );
487
494
 
package/src/TSL/Common.js CHANGED
@@ -353,7 +353,6 @@ export const getMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
353
353
  attenuationColor: data3.rgb,
354
354
  attenuationDistance: data3.a,
355
355
  dispersion: data4.r,
356
- visible: data4.g,
357
356
  sheen: data4.b,
358
357
  sheenRoughness: data4.a,
359
358
  sheenColor: data5.rgb,
@@ -356,7 +356,8 @@ export const TraceDebugMode = Fn( ( [
356
356
  const bounceDir = cosineWeightedSample( { N: normalA, xi } ).toVar();
357
357
 
358
358
  // Trace secondary ray from the hit point (offset along normal to avoid self-intersection)
359
- const bounceOrigin = hitInfo.hitPoint.add( normalA.mul( 0.001 ) ).toVar();
359
+ const debugEps = max( float( 1e-4 ), length( hitInfo.hitPoint ).mul( 1e-6 ) );
360
+ const bounceOrigin = hitInfo.hitPoint.add( normalA.mul( debugEps ) ).toVar();
360
361
  const bounceRay = Ray( { origin: bounceOrigin, direction: bounceDir } );
361
362
 
362
363
  const bounceHit = HitInfo.wrap( traverseBVH(
@@ -257,7 +257,7 @@ export const intersectAreaLight = Fn( ( [ light, rayOrigin, rayDirection ] ) =>
257
257
  const t = dot( light.position.sub( rayOrigin ), normal ).mul( invDenom ).toVar();
258
258
 
259
259
  // Skip intersections behind the ray
260
- If( t.greaterThan( 0.001 ), () => {
260
+ If( t.greaterThan( 1e-5 ), () => {
261
261
 
262
262
  // Optimized rectangle test using vector rejection
263
263
  const hitPoint = rayOrigin.add( rayDirection.mul( t ) );
@@ -142,8 +142,9 @@ export const traceShadowRay = Fn( ( [
142
142
  } );
143
143
 
144
144
  // Continue ray past transmissive surface
145
- rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( 0.001 ) ) );
146
- remainingDist.subAssign( shadowHit.dst.add( 0.001 ) );
145
+ const transEps = max( float( 1e-5 ), length( shadowHit.hitPoint ).mul( 1e-6 ) );
146
+ rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( transEps ) ) );
147
+ remainingDist.subAssign( shadowHit.dst.add( transEps ) );
147
148
 
148
149
  } ).ElseIf( shadowMaterial.transparent, () => {
149
150
 
@@ -158,8 +159,9 @@ export const traceShadowRay = Fn( ( [
158
159
  } );
159
160
 
160
161
  // Continue ray past transparent surface
161
- rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( 0.001 ) ) );
162
- remainingDist.subAssign( shadowHit.dst.add( 0.001 ) );
162
+ const alphaEps = max( float( 1e-5 ), length( shadowHit.hitPoint ).mul( 1e-6 ) );
163
+ rayOrigin.assign( shadowHit.hitPoint.add( dir.mul( alphaEps ) ) );
164
+ remainingDist.subAssign( shadowHit.dst.add( alphaEps ) );
163
165
 
164
166
  } ).Else( () => {
165
167
 
@@ -257,7 +259,7 @@ export const estimateLightImportance = Fn( ( [ light, hitPoint, normal, material
257
259
 
258
260
  If( lightFacing.greaterThan( 0.0 ), () => {
259
261
 
260
- const solidAngle = light.area.div( max( distSq, 0.1 ) );
262
+ const solidAngle = light.area.div( max( distSq, 1e-4 ) );
261
263
  const power = light.intensity.mul( dot( light.color, REC709_LUMINANCE_COEFFICIENTS ) ).mul( light.area );
262
264
 
263
265
  // Material-aware weighting
@@ -659,7 +661,7 @@ export const calculatePointLightContribution = Fn( ( [
659
661
  const rayOffset = calculateRayOffset( hitPoint, normal, material );
660
662
  const rayOrigin = hitPoint.add( rayOffset );
661
663
 
662
- const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.sub( 0.001 ), rngState );
664
+ const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.mul( 0.999 ), rngState );
663
665
 
664
666
  If( visibility.greaterThan( 0.0 ), () => {
665
667
 
@@ -711,7 +713,7 @@ export const calculateSpotLightContribution = Fn( ( [
711
713
  const rayOffset = calculateRayOffset( hitPoint, normal, material );
712
714
  const rayOrigin = hitPoint.add( rayOffset );
713
715
 
714
- const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.sub( 0.001 ), rngState );
716
+ const visibility = traceShadowRayFn( rayOrigin, lightDir, distance.mul( 0.999 ), rngState );
715
717
 
716
718
  If( visibility.greaterThan( 0.0 ), () => {
717
719
 
@@ -71,6 +71,7 @@ import {
71
71
  calculatePointLightImportance,
72
72
  calculateSpotLightImportance,
73
73
  traceShadowRay,
74
+ calculateRayOffset,
74
75
  } from './LightsDirect.js';
75
76
 
76
77
  import { traverseBVHShadow } from './BVHTraversal.js';
@@ -940,7 +941,7 @@ export const calculateDirectLightingUnified = Fn( ( [
940
941
  ] ) => {
941
942
 
942
943
  const totalContribution = vec3( 0.0 ).toVar();
943
- const rayOrigin = hitPoint.add( hitNormal.mul( 0.001 ) ).toVar();
944
+ const rayOrigin = hitPoint.add( calculateRayOffset( hitPoint, hitNormal, material ) ).toVar();
944
945
 
945
946
  // Early exit for highly emissive surfaces
946
947
  If( material.emissiveIntensity.lessThanEqual( 10.0 ), () => {
@@ -1041,7 +1042,7 @@ export const calculateDirectLightingUnified = Fn( ( [
1041
1042
 
1042
1043
  If( NoL.greaterThan( 0.0 ).and( lightImportance.mul( NoL ).greaterThan( importanceThreshold ) ).and( isDirectionValid( { direction: lightSample.direction, surfaceNormal: hitNormal } ) ), () => {
1043
1044
 
1044
- const shadowDistance = min( lightSample.distance.sub( 0.001 ), float( 1000.0 ) ).toVar();
1045
+ const shadowDistance = min( lightSample.distance.mul( 0.999 ), float( 1000.0 ) ).toVar();
1045
1046
  const visibility = traceShadowRay(
1046
1047
  rayOrigin, lightSample.direction, shadowDistance, rngState,
1047
1048
  traverseBVHShadow,
@@ -880,7 +880,8 @@ export const Trace = Fn( ( [
880
880
  // For transmission: offset along the old ray direction to push through the surface
881
881
  const reflectOffsetDir = select( interaction.entering, N, N.negate() );
882
882
  const offsetDir = select( interaction.didReflect, reflectOffsetDir, rayDirection );
883
- rayOrigin.assign( hitInfo.hitPoint.add( offsetDir.mul( 0.001 ) ) );
883
+ const bounceEps = max( float( 1e-4 ), length( hitInfo.hitPoint ).mul( 1e-6 ) );
884
+ rayOrigin.assign( hitInfo.hitPoint.add( offsetDir.mul( bounceEps ) ) );
884
885
  rayDirection.assign( interaction.direction );
885
886
 
886
887
  stateIsPrimaryRay.assign( tslBool( false ) );
@@ -1054,7 +1055,7 @@ export const Trace = Fn( ( [
1054
1055
 
1055
1056
  const rayOffset = calculateRayOffset( hitInfo.hitPoint, N, material );
1056
1057
  const rayOrigin = hitInfo.hitPoint.add( rayOffset );
1057
- const shadowDist = emissiveSample.distance.sub( 0.001 );
1058
+ const shadowDist = emissiveSample.distance.mul( 0.999 );
1058
1059
  const visibility = traceShadowRayWrapped( rayOrigin, emissiveSample.direction, shadowDist, rngState );
1059
1060
 
1060
1061
  If( visibility.greaterThan( 0.0 ), () => {
@@ -1137,7 +1138,7 @@ export const Trace = Fn( ( [
1137
1138
  throughput.mulAssign( indirectResult.throughput );
1138
1139
 
1139
1140
  // Prepare for next bounce
1140
- rayOrigin.assign( hitInfo.hitPoint.add( N.mul( 0.001 ) ) );
1141
+ rayOrigin.assign( hitInfo.hitPoint.add( calculateRayOffset( hitInfo.hitPoint, N, material ) ) );
1141
1142
  rayDirection.assign( indirectResult.direction );
1142
1143
  prevBouncePdf.assign( indirectResult.combinedPdf );
1143
1144
 
package/src/TSL/Struct.js CHANGED
@@ -30,7 +30,6 @@ export const RayTracingMaterial = struct( {
30
30
  alphaMode: 'int', // 0: OPAQUE, 1: MASK, 2: BLEND
31
31
  side: 'int',
32
32
  depthWrite: 'int',
33
- visible: 'bool',
34
33
  albedoMapIndex: 'int',
35
34
  emissiveMapIndex: 'int',
36
35
  normalMapIndex: 'int',
@@ -312,7 +312,7 @@ export class EnvironmentManager {
312
312
  const startTime = performance.now();
313
313
  const textureForCDF = this.scene.environment;
314
314
 
315
- if ( ! textureForCDF.image || ! textureForCDF.image.data ) {
315
+ if ( ! textureForCDF.image ) {
316
316
 
317
317
  this._updateCDFStorageBuffers();
318
318
  this.uniforms.set( 'envTotalSum', 0.0 );
@@ -229,7 +229,6 @@ export class MaterialDataManager {
229
229
  break;
230
230
  case 'attenuationDistance': data[ stride + 15 ] = value; break;
231
231
  case 'dispersion': data[ stride + 16 ] = value; break;
232
- case 'visible': data[ stride + 17 ] = value; break;
233
232
  case 'sheen': data[ stride + 18 ] = value; break;
234
233
  case 'sheenRoughness': data[ stride + 19 ] = value; break;
235
234
  case 'sheenColor':
@@ -378,7 +377,7 @@ export class MaterialDataManager {
378
377
 
379
378
  data[ stride + 15 ] = materialData.attenuationDistance ?? Infinity;
380
379
  data[ stride + 16 ] = materialData.dispersion ?? 0;
381
- data[ stride + 17 ] = materialData.visible ?? 1;
380
+ data[ stride + 17 ] = 1; // Reserved slot (per-mesh visibility handled at BLAS-pointer level)
382
381
  data[ stride + 18 ] = materialData.sheen ?? 0;
383
382
  data[ stride + 19 ] = materialData.sheenRoughness ?? 1;
384
383