rayzee 7.31.1 → 8.1.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 (53) hide show
  1. package/README.md +164 -0
  2. package/dist/rayzee.es.js +6728 -4735
  3. package/dist/rayzee.umd.js +121 -73
  4. package/package.json +2 -2
  5. package/src/EngineDefaults.js +168 -13
  6. package/src/EngineIssues.js +5 -0
  7. package/src/PathTracerApp.js +151 -25
  8. package/src/Processor/ArchiveReader.js +538 -0
  9. package/src/Processor/AssetLoader.js +265 -23
  10. package/src/Processor/BVHBuilder.js +48 -32
  11. package/src/Processor/BVHRefitter.js +244 -99
  12. package/src/Processor/ChunkedRecords.js +302 -0
  13. package/src/Processor/EmissiveTriangleBuilder.js +48 -21
  14. package/src/Processor/GeometryExtractor.js +580 -93
  15. package/src/Processor/HostMemory.js +222 -0
  16. package/src/Processor/InstanceTable.js +548 -97
  17. package/src/Processor/PBRT/EqualAreaOctahedral.js +32 -8
  18. package/src/Processor/PBRT/PBRTCurves.js +249 -0
  19. package/src/Processor/PBRT/PBRTMath.js +19 -0
  20. package/src/Processor/PBRT/PBRTParser.js +187 -41
  21. package/src/Processor/PBRT/PBRTSceneBuilder.js +586 -42
  22. package/src/Processor/PBRT/PBRTTokenizer.js +205 -65
  23. package/src/Processor/PBRT/index.js +257 -50
  24. package/src/Processor/PackedRayBuffer.js +29 -18
  25. package/src/Processor/ParallelBVHBuilder.js +9 -8
  26. package/src/Processor/QueueManager.js +5 -3
  27. package/src/Processor/SceneProcessor.js +1046 -365
  28. package/src/Processor/TLASBuilder.js +546 -201
  29. package/src/Processor/TextureCreator.js +11 -9
  30. package/src/Processor/VRAMTracker.js +12 -3
  31. package/src/Processor/Workers/BVHRefitWorker.js +16 -11
  32. package/src/Processor/Workers/BVHWorker.js +12 -12
  33. package/src/Processor/Workers/TLASWorker.js +37 -0
  34. package/src/Processor/utils.js +8 -20
  35. package/src/Stages/NormalDepth.js +6 -2
  36. package/src/Stages/PathTracer.js +5 -5
  37. package/src/Stages/PathTracerStage.js +163 -41
  38. package/src/TSL/BVHTraversal.js +208 -90
  39. package/src/TSL/Common.js +135 -2
  40. package/src/TSL/Displacement.js +21 -11
  41. package/src/TSL/EmissiveSampling.js +49 -25
  42. package/src/TSL/ExtendKernel.js +3 -2
  43. package/src/TSL/LightBVHSampling.js +7 -4
  44. package/src/TSL/LightsDirect.js +30 -29
  45. package/src/TSL/ShadeKernel.js +8 -6
  46. package/src/TSL/SortGlobalKernels.js +3 -4
  47. package/src/TSL/Struct.js +3 -0
  48. package/src/TSL/TextureSampling.js +24 -9
  49. package/src/TSL/patches.js +149 -84
  50. package/src/index.js +3 -0
  51. package/src/managers/AnimationManager.js +86 -76
  52. package/src/managers/MaterialDataManager.js +29 -23
  53. package/src/managers/TransformManager.js +9 -192
@@ -2,6 +2,7 @@ import {
2
2
  Fn,
3
3
  wgslFn,
4
4
  vec3,
5
+ vec4,
5
6
  vec2,
6
7
  float,
7
8
  int,
@@ -17,18 +18,23 @@ import {
17
18
  notEqual,
18
19
  lessThan,
19
20
  array,
21
+ uint,
22
+ uintBitsToFloat,
23
+ floatBitsToUint,
20
24
  bool as tslBool,
21
25
  } from 'three/tsl';
22
26
 
27
+ import { BVH_LEAF_MARKERS, BVH_MAX_INDEX, TRI_MATERIAL_MASK, TRI_SIDE_SHIFT, TLAS_LEAF_IDENTITY } from '../EngineDefaults.js';
23
28
  import { HitInfo } from './Struct.js';
24
- import { getDatafromStorageBuffer } from './Common.js';
29
+ import {
30
+ getDatafromStorageBuffer, instanceRows, instanceNormalToWorld, unpackTriangleNormal, TRI_STRIDE, shadowFlagsSettle
31
+ } from './Common.js';
25
32
 
26
33
  const MAX_STACK_DEPTH = 32;
27
34
  // Hang guard only — tripping it reports a MISS, which the shade kernel pays out as
28
35
  // full-intensity environment. San Miguel needs ~1024; cost is flat from 1024 up.
29
36
  const MAX_BVH_ITERATIONS = 4096;
30
37
  const BVH_STRIDE = 4;
31
- const TRI_STRIDE = 8;
32
38
  const HUGE_VAL = 1e8;
33
39
 
34
40
  // Per-mesh visibility is now packed into the TLAS BLAS-pointer leaf's slot [2]
@@ -40,13 +46,37 @@ const HUGE_VAL = 1e8;
40
46
 
41
47
  const createStack = () => array( 'int', MAX_STACK_DEPTH ).toVar();
42
48
 
49
+ // Reciprocal ray direction with the axis-aligned cases pinned to a large signed value.
50
+ const buildInvDir = ( d ) => {
51
+
52
+ const dirSign = mix( vec3( 1.0 ), sign( d ), notEqual( d, vec3( 0.0 ) ) );
53
+ return mix( vec3( 1.0 ).div( d ), vec3( HUGE_VAL ).mul( dirSign ), lessThan( abs( d ), vec3( 1e-8 ) ) );
54
+
55
+ };
56
+
57
+ // The ray direction is deliberately left unnormalised through the transform: t then means
58
+ // the same thing either side of it, so closest-hit comparisons and the world-space hit
59
+ // point both survive without rescaling.
60
+ const toObjectPoint = ( rows, p ) => vec3(
61
+ rows[ 0 ].xyz.dot( p ).add( rows[ 0 ].w ),
62
+ rows[ 1 ].xyz.dot( p ).add( rows[ 1 ].w ),
63
+ rows[ 2 ].xyz.dot( p ).add( rows[ 2 ].w )
64
+ );
65
+
66
+ const toObjectDir = ( rows, d ) => vec3(
67
+ rows[ 0 ].xyz.dot( d ),
68
+ rows[ 1 ].xyz.dot( d ),
69
+ rows[ 2 ].xyz.dot( d )
70
+ );
71
+
72
+
43
73
  // ================================================================================
44
74
  // RAY INTERSECTION HELPERS (inlined for BVH traversal performance)
45
75
  // ================================================================================
46
76
 
47
77
  // Woop watertight intersection (Woop/Benthin/Wald 2013). Eliminates edge leakage
48
78
  // at shared triangle edges that Möller-Trumbore exhibits under FP32. Per-ray shears
49
- // are precomputed once via computeWoopRayParams; per-triangle test is FMA-friendly
79
+ // are precomputed once via computeWoopFromInvDir; per-triangle test is FMA-friendly
50
80
  // and uses sign-aware depth comparison so it works for any det orientation.
51
81
  const RayTriangleGeometry = wgslFn( `
52
82
  fn RayTriangleGeometry( rayOrigin: vec3f, rayDir: vec3f, pA: vec3f, pB: vec3f, pC: vec3f, closestHitDst: f32, woopParams: vec4f ) -> vec4f {
@@ -119,8 +149,10 @@ const RayTriangleGeometry = wgslFn( `
119
149
  // Compute Woop ray-space transform (Woop 2013, §3.1) — runs once per ray and
120
150
  // amortizes across hundreds of triangle tests. Returns Sx/Sy/Sz shears plus the
121
151
  // permuted axis indices packed via bitcast into the .w slot.
122
- const computeWoopRayParams = wgslFn( `
123
- fn computeWoopRayParams( rayDir: vec3f ) -> vec4f {
152
+ // The reciprocal direction is always built first, and its dominant-axis lane IS Sz; the two
153
+ // shears are the other components times it. Three fewer divides per call than dividing afresh.
154
+ const computeWoopFromInvDir = wgslFn( `
155
+ fn computeWoopFromInvDir( rayDir: vec3f, invDir: vec3f ) -> vec4f {
124
156
 
125
157
  let absDir = abs( rayDir );
126
158
 
@@ -139,10 +171,9 @@ const computeWoopRayParams = wgslFn( `
139
171
  ky = tmp;
140
172
  }
141
173
 
142
- let dz = rayDir[ u32( kz ) ];
143
- let Sx = rayDir[ u32( kx ) ] / dz;
144
- let Sy = rayDir[ u32( ky ) ] / dz;
145
- let Sz = 1.0f / dz;
174
+ let Sz = invDir[ u32( kz ) ];
175
+ let Sx = rayDir[ u32( kx ) ] * Sz;
176
+ let Sy = rayDir[ u32( ky ) ] * Sz;
146
177
 
147
178
  let packed = kx | ( ky << 2 ) | ( kz << 4 );
148
179
  return vec4f( Sx, Sy, Sz, f32( packed ) );
@@ -196,6 +227,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
196
227
  uv: vec2( 0.0 ),
197
228
  materialIndex: int( - 1 ),
198
229
  meshIndex: int( - 1 ),
230
+ instanceLeaf: int( - 1 ),
199
231
  boxTests: int( 0 ),
200
232
  triTests: int( 0 ),
201
233
  } ).toVar();
@@ -211,19 +243,22 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
211
243
  const stackPtr = int( 1 ).toVar();
212
244
  stack.element( int( 0 ) ).assign( int( 0 ) ); // Root node
213
245
 
214
- // Compact axis-aligned ray handling with correct sign preservation
215
- const dirSign = mix( vec3( 1.0 ), sign( ray.direction ), notEqual( ray.direction, vec3( 0.0 ) ) );
216
- const invDir = mix(
217
- vec3( 1.0 ).div( ray.direction ),
218
- vec3( HUGE_VAL ).mul( dirSign ),
219
- lessThan( abs( ray.direction ), vec3( 1e-8 ) )
220
- ).toVar();
246
+ // Triangles sit in each instance's own space, so the ray is what moves: `world*` stays
247
+ // put, `ray*`/`invDir`/`woopParams` are rewritten on entering a BLAS and never restored.
248
+ const worldOrigin = vec3( ray.origin ).toVar();
249
+ const worldDirection = vec3( ray.direction ).toVar();
250
+ const worldInvDir = buildInvDir( worldDirection ).toVar();
251
+ const worldWoop = computeWoopFromInvDir( { rayDir: worldDirection, invDir: worldInvDir } ).toVar();
221
252
 
222
- const rayOrigin = ray.origin;
223
- const rayDirection = ray.direction;
253
+ const rayOrigin = vec3( worldOrigin ).toVar();
254
+ const rayDirection = vec3( worldDirection ).toVar();
255
+ const invDir = vec3( worldInvDir ).toVar();
256
+ const woopParams = vec4( worldWoop ).toVar();
224
257
 
225
- // Woop watertight intersection: precompute per-ray shears + axis permutation.
226
- const woopParams = computeWoopRayParams( { rayDir: rayDirection } ).toVar();
258
+ // The TLAS leaf the ray is inside, or -1; dropping below `instExit` on the stack clears it.
259
+ const instLeaf = int( - 1 ).toVar();
260
+ const instExit = int( 0 ).toVar();
261
+ const hitInstLeaf = int( - 1 ).toVar();
227
262
 
228
263
  const iterCount = int( 0 ).toVar();
229
264
 
@@ -232,6 +267,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
232
267
  iterCount.addAssign( 1 );
233
268
  stackPtr.subAssign( 1 );
234
269
  const nodeIndex = stack.element( stackPtr ).toVar();
270
+ instLeaf.assign( select( stackPtr.lessThan( instExit ), int( - 1 ), instLeaf ) );
235
271
 
236
272
  // New layout: 4 vec4 per node
237
273
  // Leaf: vec4(0) = [triOffset, triCount, 0, -1]
@@ -240,14 +276,18 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
240
276
  const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
241
277
  if ( trackStats ) closestHit.boxTests.addAssign( 1 );
242
278
 
243
- If( nodeData0.w.lessThan( 0.0 ), () => {
279
+ // Slot [3] carries a u32 bit pattern: an inner node's left-child index, or a leaf tag
280
+ // above every valid index. Stored as float VALUES these rounded past 2^24 and sent the
281
+ // ray to a neighbouring node, which silently erased geometry from large scenes.
282
+ const nodeTag = floatBitsToUint( nodeData0.w ).toVar();
283
+
284
+ If( nodeTag.greaterThanEqual( uint( BVH_MAX_INDEX ) ), () => {
244
285
 
245
- // Leaf node — distinguish triangle leaf (-1) from BLAS-pointer leaf (-2)
246
- If( nodeData0.w.greaterThan( float( - 1.5 ) ), () => {
286
+ If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
247
287
 
248
- // Triangle leaf (marker -1) — triOffset and triCount packed in vec4(0).xy
249
- const triStart = int( nodeData0.x ).toVar();
250
- const triCount = int( nodeData0.y ).toVar();
288
+ // Triangle leaf — triOffset and triCount packed in vec4(0).xy
289
+ const triStart = int( floatBitsToUint( nodeData0.x ) ).toVar();
290
+ const triCount = int( floatBitsToUint( nodeData0.y ) ).toVar();
251
291
 
252
292
  // Process triangles in leaf
253
293
  Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
@@ -255,10 +295,13 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
255
295
  if ( trackStats ) closestHit.triTests.addAssign( 1 );
256
296
  const triIndex = triStart.add( i ).toVar();
257
297
 
258
- // Fetch geometry first (3 fetches from storage buffer)
259
- const pA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), int( TRI_STRIDE ) ).xyz;
260
- const pB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), int( TRI_STRIDE ) ).xyz;
261
- const pC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), int( TRI_STRIDE ) ).xyz;
298
+ // Three fetches carry positions AND the packed normals in their .w lanes.
299
+ const recA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), int( TRI_STRIDE ) ).toVar();
300
+ const recB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), int( TRI_STRIDE ) ).toVar();
301
+ const recC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), int( TRI_STRIDE ) ).toVar();
302
+ const pA = uintBitsToFloat( recA.xyz );
303
+ const pB = uintBitsToFloat( recB.xyz );
304
+ const pC = uintBitsToFloat( recC.xyz );
262
305
 
263
306
  const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
264
307
 
@@ -269,15 +312,13 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
269
312
  const u = triResult.y;
270
313
  const v = triResult.z;
271
314
 
272
- // Fetch normals for side-culling (3 reads). Slot 7 (uvData2,
273
- // carries matIdx + meshIndex) is deferred to post-traversal —
274
- // it's only needed for the one winning triangle, not per candidate.
275
- // normalCData.w carries the per-triangle side flag (0/1/2).
276
- const nA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 3 ), int( TRI_STRIDE ) ).xyz;
277
- const nB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).xyz;
278
- const normalCData = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 5 ), int( TRI_STRIDE ) );
279
- const nC = normalCData.xyz;
280
- const side = int( normalCData.w ).toVar();
315
+ // Normals came with the positions; only the side flag needs a fetch, and
316
+ // slot 4 also carries matIdx/meshIndex, deferred to post-traversal.
317
+ const nA = unpackTriangleNormal( recA.w );
318
+ const nB = unpackTriangleNormal( recB.w );
319
+ const nC = unpackTriangleNormal( recC.w );
320
+ const flags = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).z;
321
+ const side = int( flags.shiftRight( uint( TRI_SIDE_SHIFT ) ).bitAnd( uint( 3 ) ) ).toVar();
281
322
 
282
323
  // Interpolate normal for the side-culling dot product (kept local,
283
324
  // not stored on closestHit — re-derived post-loop from closestTriIdx).
@@ -301,6 +342,7 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
301
342
  closestTriIdx.assign( triIndex );
302
343
  closestU.assign( u );
303
344
  closestV.assign( v );
345
+ hitInstLeaf.assign( instLeaf );
304
346
 
305
347
  } );
306
348
 
@@ -317,12 +359,36 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
317
359
 
318
360
  } ).Else( () => {
319
361
 
320
- // BLAS-pointer leaf (marker -2) — push BLAS root onto stack if mesh is visible
321
- // nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]
322
- // Visibility is free-fetched with the leaf — no extra storage read.
362
+ // BLAS-pointer leaf — enter the instance if the mesh is visible.
363
+ // nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]; slots 4..15 hold
364
+ // the world-to-object matrix. Visibility is free-fetched with the leaf.
323
365
  If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
324
366
 
325
- stack.element( stackPtr ).assign( int( nodeData0.x ) );
367
+ // A baked placement already sits in world space; only a transformed one moves the ray.
368
+ If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
369
+
370
+ const rows = instanceRows( bvhBuffer, nodeIndex );
371
+ const localDir = toObjectDir( rows, worldDirection ).toVar();
372
+
373
+ rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
374
+ rayDirection.assign( localDir );
375
+ const localInv = buildInvDir( localDir ).toVar();
376
+ invDir.assign( localInv );
377
+ woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
378
+ instLeaf.assign( nodeIndex );
379
+ instExit.assign( stackPtr );
380
+
381
+ } ).Else( () => {
382
+
383
+ rayOrigin.assign( worldOrigin );
384
+ rayDirection.assign( worldDirection );
385
+ invDir.assign( worldInvDir );
386
+ woopParams.assign( worldWoop );
387
+ instLeaf.assign( int( - 1 ) );
388
+
389
+ } );
390
+
391
+ stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
326
392
  stackPtr.addAssign( 1 );
327
393
 
328
394
  } );
@@ -336,11 +402,14 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
336
402
  const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
337
403
  const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
338
404
 
339
- const leftChild = int( nodeData0.w ).toVar();
340
- const rightChild = int( nodeData1.w ).toVar();
405
+ const leftChild = int( nodeTag ).toVar();
406
+ const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
341
407
 
342
- const dstA = fastRayAABBDst( { rayOrigin, invDir, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
343
- const dstB = fastRayAABBDst( { rayOrigin, invDir, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
408
+ const inInst = instLeaf.greaterThanEqual( int( 0 ) );
409
+ const boxOrigin = select( inInst, rayOrigin, worldOrigin ).toVar();
410
+ const boxInv = select( inInst, invDir, worldInvDir ).toVar();
411
+ const dstA = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
412
+ const dstB = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
344
413
 
345
414
  // Optimized early rejection
346
415
  const minDst = min( dstA, dstB );
@@ -380,18 +449,26 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
380
449
 
381
450
  // Re-fetch the winning triangle's normals — trading 3 storage reads (once)
382
451
  // for ~3 regs freed across every BVH iteration.
383
- const nA = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 3 ), int( TRI_STRIDE ) ).xyz;
384
- const nB = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 4 ), int( TRI_STRIDE ) ).xyz;
385
- const nC = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 5 ), int( TRI_STRIDE ) ).xyz;
386
- closestHit.normal.assign( normalize( nA.mul( w ).add( nB.mul( closestU ) ).add( nC.mul( closestV ) ) ) );
452
+ const nA = unpackTriangleNormal( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 0 ), int( TRI_STRIDE ) ).w );
453
+ const nB = unpackTriangleNormal( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 1 ), int( TRI_STRIDE ) ).w );
454
+ const nC = unpackTriangleNormal( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 2 ), int( TRI_STRIDE ) ).w );
455
+ const objectNormal = normalize( nA.mul( w ).add( nB.mul( closestU ) ).add( nC.mul( closestV ) ) ).toVar();
456
+ If( hitInstLeaf.greaterThanEqual( int( 0 ) ), () => {
457
+
458
+ objectNormal.assign( normalize( instanceNormalToWorld( instanceRows( bvhBuffer, hitInstLeaf ), objectNormal ) ) );
387
459
 
388
- const uvData1 = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 6 ), int( TRI_STRIDE ) );
389
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 7 ), int( TRI_STRIDE ) );
460
+ } );
461
+
462
+ closestHit.normal.assign( objectNormal );
463
+
464
+ const uvData1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 3 ), int( TRI_STRIDE ) ) );
465
+ const uvData2 = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 4 ), int( TRI_STRIDE ) ).toVar();
390
466
  closestHit.uv.assign(
391
- uvData1.xy.mul( w ).add( uvData1.zw.mul( closestU ) ).add( uvData2.xy.mul( closestV ) )
467
+ uvData1.xy.mul( w ).add( uvData1.zw.mul( closestU ) ).add( uintBitsToFloat( uvData2.xy ).mul( closestV ) )
392
468
  );
393
- closestHit.materialIndex.assign( int( uvData2.z ) );
469
+ closestHit.materialIndex.assign( int( uvData2.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ) );
394
470
  closestHit.meshIndex.assign( int( uvData2.w ) );
471
+ closestHit.instanceLeaf.assign( hitInstLeaf );
395
472
  closestHit.triangleIndex.assign( closestTriIdx );
396
473
 
397
474
  } );
@@ -423,6 +500,7 @@ export const traverseBVHShadow = Fn( ( [
423
500
  uv: vec2( 0.0 ),
424
501
  materialIndex: int( - 1 ),
425
502
  meshIndex: int( - 1 ),
503
+ instanceLeaf: int( - 1 ),
426
504
  boxTests: int( 0 ),
427
505
  triTests: int( 0 ),
428
506
  } ).toVar();
@@ -431,69 +509,83 @@ export const traverseBVHShadow = Fn( ( [
431
509
  const stackPtr = int( 1 ).toVar();
432
510
  stack.element( int( 0 ) ).assign( int( 0 ) );
433
511
 
434
- const dirSign = mix( vec3( 1.0 ), sign( ray.direction ), notEqual( ray.direction, vec3( 0.0 ) ) );
435
- const invDir = mix(
436
- vec3( 1.0 ).div( ray.direction ),
437
- vec3( HUGE_VAL ).mul( dirSign ),
438
- lessThan( abs( ray.direction ), vec3( 1e-8 ) )
439
- ).toVar();
512
+ // Same instance-space scheme as traverseBVH: the ray moves, the triangles do not.
513
+ const worldOrigin = vec3( ray.origin ).toVar();
514
+ const worldDirection = vec3( ray.direction ).toVar();
515
+ const worldInvDir = buildInvDir( worldDirection ).toVar();
516
+ const worldWoop = computeWoopFromInvDir( { rayDir: worldDirection, invDir: worldInvDir } ).toVar();
517
+
518
+ const rayOrigin = vec3( worldOrigin ).toVar();
519
+ const rayDirection = vec3( worldDirection ).toVar();
520
+ const invDir = vec3( worldInvDir ).toVar();
521
+ const woopParams = vec4( worldWoop ).toVar();
440
522
 
441
- // Woop watertight intersection: precompute per-ray shears + axis permutation.
442
- const woopParams = computeWoopRayParams( { rayDir: ray.direction } ).toVar();
523
+ const instLeaf = int( - 1 ).toVar();
524
+ const instExit = int( 0 ).toVar();
525
+ const blocked = tslBool( false ).toVar();
443
526
 
444
527
  const sIterCount = int( 0 ).toVar();
445
528
 
446
- Loop( stackPtr.greaterThan( int( 0 ) ).and( closestHit.didHit.not() ).and( sIterCount.lessThan( int( MAX_BVH_ITERATIONS ) ) ), () => {
529
+ Loop( stackPtr.greaterThan( int( 0 ) ).and( blocked.not() ).and( sIterCount.lessThan( int( MAX_BVH_ITERATIONS ) ) ), () => {
447
530
 
448
531
  sIterCount.addAssign( 1 );
449
532
  stackPtr.subAssign( 1 );
450
533
  const nodeIndex = stack.element( stackPtr ).toVar();
534
+ instLeaf.assign( select( stackPtr.lessThan( instExit ), int( - 1 ), instLeaf ) );
451
535
 
452
536
  const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
453
537
 
454
- If( nodeData0.w.lessThan( 0.0 ), () => {
538
+ // Slot [3] carries a u32 bit pattern: an inner node's left-child index, or a leaf tag
539
+ // above every valid index. Stored as float VALUES these rounded past 2^24 and sent the
540
+ // ray to a neighbouring node, which silently erased geometry from large scenes.
541
+ const nodeTag = floatBitsToUint( nodeData0.w ).toVar();
455
542
 
456
- // Leaf node — distinguish triangle leaf (-1) from BLAS-pointer leaf (-2)
457
- If( nodeData0.w.greaterThan( float( - 1.5 ) ), () => {
543
+ If( nodeTag.greaterThanEqual( uint( BVH_MAX_INDEX ) ), () => {
458
544
 
459
- // Triangle leaf (marker -1) — triOffset and triCount packed in vec4(0).xy
460
- const triStart = int( nodeData0.x ).toVar();
461
- const triCount = int( nodeData0.y ).toVar();
545
+ If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
546
+
547
+ // Triangle leaf — triOffset and triCount packed in vec4(0).xy
548
+ const triStart = int( floatBitsToUint( nodeData0.x ) ).toVar();
549
+ const triCount = int( floatBitsToUint( nodeData0.y ) ).toVar();
462
550
 
463
551
  Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
464
552
 
465
553
  const triIndex = triStart.add( i ).toVar();
466
554
 
467
- const pA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), int( TRI_STRIDE ) ).xyz;
468
- const pB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), int( TRI_STRIDE ) ).xyz;
469
- const pC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), int( TRI_STRIDE ) ).xyz;
555
+ const pA = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), int( TRI_STRIDE ) ).xyz );
556
+ const pB = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), int( TRI_STRIDE ) ).xyz );
557
+ const pC = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), int( TRI_STRIDE ) ).xyz );
470
558
 
471
- const triResult = RayTriangleGeometry( { rayOrigin: ray.origin, rayDir: ray.direction, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
559
+ const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
472
560
 
473
561
  If( triResult.w.greaterThan( 0.5 ), () => {
474
562
 
475
- // Per-mesh visibility handled at BLAS-pointer level — accept any hit
476
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 7 ), int( TRI_STRIDE ) );
563
+ // Per-mesh visibility is handled at the BLAS-pointer level.
564
+ const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).toVar();
477
565
 
478
566
  closestHit.didHit.assign( true );
479
567
  closestHit.dst.assign( triResult.x );
480
- closestHit.materialIndex.assign( int( uvData2.z ) );
568
+ closestHit.materialIndex.assign( int( uvData2.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ) );
481
569
  closestHit.meshIndex.assign( int( uvData2.w ) );
570
+ closestHit.instanceLeaf.assign( instLeaf );
482
571
 
483
572
  // Hit point is cheap (origin + dir*t). Geometric normal is deferred
484
573
  // to traceShadowRay — only the transmission branch needs it, so we
485
574
  // skip the cross+normalize for the (much more common) opaque-blocker
486
575
  // and alpha-cutout paths. Normal stays vec3(0) from struct init.
487
- closestHit.hitPoint.assign( ray.origin.add( ray.direction.mul( triResult.x ) ) );
576
+ closestHit.hitPoint.assign( worldOrigin.add( worldDirection.mul( triResult.x ) ) );
488
577
 
489
- // Store barycentrics + triangle index for deferred UV computation.
490
- // Actual UV interpolation happens in traceShadowRay only when
491
- // the material needs alpha testing — zero overhead for opaque hits.
492
578
  closestHit.uv.assign( vec2( triResult.y, triResult.z ) );
493
579
  closestHit.triangleIndex.assign( triIndex );
494
580
 
495
- // Shadow ray only needs any hit — skip remaining triangles in leaf
496
- Break();
581
+ // An opaque blocker settles the ray. A surface light passes through has to be
582
+ // the nearest one, or the layers behind the first find are never counted.
583
+ If( shadowFlagsSettle( uvData2.z ), () => {
584
+
585
+ blocked.assign( true );
586
+ Break();
587
+
588
+ } );
497
589
 
498
590
  } );
499
591
 
@@ -501,11 +593,34 @@ export const traverseBVHShadow = Fn( ( [
501
593
 
502
594
  } ).Else( () => {
503
595
 
504
- // BLAS-pointer leaf (marker -2) — push BLAS root onto stack if mesh is visible
505
- // nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]
596
+ // BLAS-pointer leaf — enter the instance if the mesh is visible.
506
597
  If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
507
598
 
508
- stack.element( stackPtr ).assign( int( nodeData0.x ) );
599
+ // A baked placement already sits in world space; only a transformed one moves the ray.
600
+ If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
601
+
602
+ const rows = instanceRows( bvhBuffer, nodeIndex );
603
+ const localDir = toObjectDir( rows, worldDirection ).toVar();
604
+
605
+ rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
606
+ rayDirection.assign( localDir );
607
+ const localInv = buildInvDir( localDir ).toVar();
608
+ invDir.assign( localInv );
609
+ woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
610
+ instLeaf.assign( nodeIndex );
611
+ instExit.assign( stackPtr );
612
+
613
+ } ).Else( () => {
614
+
615
+ rayOrigin.assign( worldOrigin );
616
+ rayDirection.assign( worldDirection );
617
+ invDir.assign( worldInvDir );
618
+ woopParams.assign( worldWoop );
619
+ instLeaf.assign( int( - 1 ) );
620
+
621
+ } );
622
+
623
+ stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
509
624
  stackPtr.addAssign( 1 );
510
625
 
511
626
  } );
@@ -519,11 +634,14 @@ export const traverseBVHShadow = Fn( ( [
519
634
  const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
520
635
  const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
521
636
 
522
- const leftChild = int( nodeData0.w ).toVar();
523
- const rightChild = int( nodeData1.w ).toVar();
637
+ const leftChild = int( nodeTag ).toVar();
638
+ const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
524
639
 
525
- const dstA = fastRayAABBDst( { rayOrigin: ray.origin, invDir, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
526
- const dstB = fastRayAABBDst( { rayOrigin: ray.origin, invDir, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
640
+ const inInst = instLeaf.greaterThanEqual( int( 0 ) );
641
+ const boxOrigin = select( inInst, rayOrigin, worldOrigin ).toVar();
642
+ const boxInv = select( inInst, invDir, worldInvDir ).toVar();
643
+ const dstA = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
644
+ const dstB = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
527
645
 
528
646
  // Distance-ordered traversal — nearer child first for faster any-hit
529
647
  // termination. SA build ordering improves cache locality (larger-SA
package/src/TSL/Common.js CHANGED
@@ -1,4 +1,4 @@
1
- import { Fn, wgslFn, float, vec2, vec3, vec4, int, mat3, If, max, dot, clamp, select } from 'three/tsl';
1
+ import { Fn, wgslFn, float, vec2, vec3, vec4, int, uint, mat3, If, max, dot, clamp, select, bool as tslBool } from 'three/tsl';
2
2
 
3
3
  import {
4
4
  AnisoFrame,
@@ -24,7 +24,7 @@ export const FP16_MAX = 65504.0;
24
24
  export const sanitize1 = ( x, ceiling = 1e7 ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, ceiling );
25
25
  export const sanitizeRGB = ( c, ceiling = 1e7 ) =>
26
26
  vec3( sanitize1( c.x, ceiling ), sanitize1( c.y, ceiling ), sanitize1( c.z, ceiling ) );
27
- import { MATERIAL_DATA_LAYOUT } from '../EngineDefaults.js';
27
+ import { MATERIAL_DATA_LAYOUT, TRI_BLOCKER_SHIFT, TRI_BLOCKER_ALPHA_SHIFT } from '../EngineDefaults.js';
28
28
 
29
29
  export const MATERIAL_SLOTS = MATERIAL_DATA_LAYOUT.SLOTS_PER_MATERIAL;
30
30
  export const MATERIAL_SLOT = MATERIAL_DATA_LAYOUT.SLOT;
@@ -314,6 +314,52 @@ export const getDatafromStorageBuffer = Fn( ( [ buffer, stride, sampleIndex, dat
314
314
 
315
315
  } );
316
316
 
317
+ /** uvec4 lanes per triangle record — see TRIANGLE_DATA_LAYOUT. */
318
+ export const TRI_STRIDE = 5;
319
+
320
+ // Unit normal to an octahedral snorm16 pair; GPU twin of packNormalOct in EngineDefaults.
321
+ export const packNormalOct = /*@__PURE__*/ wgslFn( `
322
+ fn packNormalOct( n: vec3f ) -> u32 {
323
+
324
+ let s = abs( n.x ) + abs( n.y ) + abs( n.z );
325
+ var p = n.xy / max( s, 1e-20 );
326
+
327
+ if ( n.z < 0.0 ) {
328
+
329
+ let px = ( 1.0 - abs( p.y ) ) * select( -1.0, 1.0, p.x >= 0.0 );
330
+ let py = ( 1.0 - abs( p.x ) ) * select( -1.0, 1.0, p.y >= 0.0 );
331
+ p = vec2f( px, py );
332
+
333
+ }
334
+
335
+ return pack2x16snorm( p );
336
+
337
+ }
338
+ ` );
339
+
340
+ // Octahedral snorm16 pair back to a unit normal; mirrors packNormalOct on the CPU.
341
+ export const unpackTriangleNormal = /*@__PURE__*/ wgslFn( `
342
+ fn unpackTriangleNormal( packed: u32 ) -> vec3f {
343
+
344
+ let e = unpack2x16snorm( packed );
345
+ var nx = e.x;
346
+ var ny = e.y;
347
+ let nz = 1.0 - abs( e.x ) - abs( e.y );
348
+
349
+ if ( nz < 0.0 ) {
350
+
351
+ let tx = ( 1.0 - abs( ny ) ) * select( -1.0, 1.0, nx >= 0.0 );
352
+ let ty = ( 1.0 - abs( nx ) ) * select( -1.0, 1.0, ny >= 0.0 );
353
+ nx = tx;
354
+ ny = ty;
355
+
356
+ }
357
+
358
+ return normalize( vec3f( nx, ny, nz ) );
359
+
360
+ }
361
+ ` );
362
+
317
363
  // Reconstruct mat3 from two vec4s — exact port of GLSL
318
364
  export const arrayToMat3 = wgslFn( `
319
365
  fn arrayToMat3( data1: vec4f, data2: vec4f ) -> mat3x3f {
@@ -531,3 +577,90 @@ export const getShadowMaterial = Fn( ( [ materialIndex, materialBuffer ] ) => {
531
577
  } );
532
578
 
533
579
  } );
580
+
581
+ // ================================================================================
582
+ // INSTANCE TRANSFORMS
583
+ // ================================================================================
584
+
585
+ /**
586
+ * The three rows of an instance's world-to-object matrix, read off its TLAS leaf.
587
+ * A BVH node is 16 floats and the BLAS pointer needs four, so the affine inverse
588
+ * rides along in the rest — no second binding, which matters on a backend that
589
+ * allows ten storage buffers per stage.
590
+ */
591
+ // Runtime uniform that toggles alpha-cutout shadows; set by ShaderBuilder before the kernels build.
592
+ let _enableAlphaShadows = null;
593
+
594
+ export function setAlphaShadowsUniform( node ) {
595
+
596
+ _enableAlphaShadows = node;
597
+
598
+ }
599
+
600
+ export const getAlphaShadowsUniform = () => _enableAlphaShadows;
601
+
602
+ // Does a hit on a triangle with these flags settle the shadow ray without a material fetch?
603
+ export const shadowFlagsSettle = ( flags ) => {
604
+
605
+ const bit = ( shift ) => flags.shiftRight( uint( shift ) ).bitAnd( uint( 1 ) ).equal( uint( 1 ) );
606
+ const alphaOff = _enableAlphaShadows ? _enableAlphaShadows.equal( int( 0 ) ) : tslBool( true );
607
+ return bit( TRI_BLOCKER_SHIFT ).or( bit( TRI_BLOCKER_ALPHA_SHIFT ).and( alphaOff ) );
608
+
609
+ };
610
+
611
+ export const instanceRows = ( bvhBuffer, leafIndex ) => [
612
+ getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 1 ), int( 4 ) ),
613
+ getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 2 ), int( 4 ) ),
614
+ getDatafromStorageBuffer( bvhBuffer, leafIndex, int( 3 ), int( 4 ) )
615
+ ];
616
+
617
+ /** Object-space normal to world: transpose of the world-to-object basis. */
618
+ export const instanceNormalToWorld = ( rows, n ) => vec3(
619
+ vec3( rows[ 0 ].x, rows[ 1 ].x, rows[ 2 ].x ).dot( n ),
620
+ vec3( rows[ 0 ].y, rows[ 1 ].y, rows[ 2 ].y ).dot( n ),
621
+ vec3( rows[ 0 ].z, rows[ 1 ].z, rows[ 2 ].z ).dot( n )
622
+ );
623
+
624
+ /**
625
+ * A face normal, built from an object-space cross product, put into world space. It takes the
626
+ * same inverse-transpose as a shading normal, but a mirroring placement reverses which way the
627
+ * triangle winds, so the sign of the placement's determinant has to come with it or the face
628
+ * points into the surface instead of out of it.
629
+ *
630
+ * Inline nodes rather than a wgslFn: a WGSL function here is a real call in the shade kernel and
631
+ * measured 0.6 ms per sample on a scene that never even reaches this branch.
632
+ */
633
+ export const instanceFaceNormalToWorld = ( rows, n ) => {
634
+
635
+ const det = rows[ 0 ].xyz.dot( rows[ 1 ].xyz.cross( rows[ 2 ].xyz ) );
636
+ return instanceNormalToWorld( rows, n ).mul( det.sign() );
637
+
638
+ };
639
+
640
+ /**
641
+ * Object-space direction to world. Tangents transform by the forward matrix, not the
642
+ * inverse-transpose that normals use, so the 3x3 is inverted here — once per shaded
643
+ * hit, which is far cheaper than carrying a second matrix through every BVH node.
644
+ */
645
+ export const instanceDirToWorld = /*@__PURE__*/ wgslFn( `
646
+ fn instanceDirToWorld( r0: vec3f, r1: vec3f, r2: vec3f, v: vec3f ) -> vec3f {
647
+
648
+ // Rows of world-to-object; its inverse has columns cross(r1,r2), cross(r2,r0), cross(r0,r1).
649
+ let c0 = cross( r1, r2 );
650
+ let det = dot( r0, c0 );
651
+ // Relative: for a uniform scale s this determinant is s⁻³, so a fixed floor would call a
652
+ // heavily scaled-up instance singular and leave the direction untransformed.
653
+ let magnitude = length( r0 ) * length( r1 ) * length( r2 );
654
+ if ( abs( det ) <= 1e-12f * magnitude ) { return v; }
655
+
656
+ let inv = 1.0f / det;
657
+ return ( c0 * v.x + cross( r2, r0 ) * v.y + cross( r0, r1 ) * v.z ) * inv;
658
+
659
+ }
660
+ ` );
661
+
662
+ /** Object-space point to world: undo the inverse translation, then the inverse basis. */
663
+ export const instancePointToWorld = ( rows, p ) => instanceDirToWorld( {
664
+ r0: rows[ 0 ].xyz, r1: rows[ 1 ].xyz, r2: rows[ 2 ].xyz,
665
+ v: p.sub( vec3( rows[ 0 ].w, rows[ 1 ].w, rows[ 2 ].w ) )
666
+ } );