rayzee 8.8.2 → 9.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 (104) hide show
  1. package/README.md +210 -20
  2. package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
  3. package/dist/{NeuralSuperRes-Dc6jRYke.js → NeuralSuperRes-CbNvQjCw.js} +5 -2
  4. package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
  5. package/dist/rayzee.es.js +10795 -5324
  6. package/dist/rayzee.umd.js +279 -268
  7. package/package.json +1 -1
  8. package/src/AssetConfig.js +4 -0
  9. package/src/EngineDefaults.js +60 -76
  10. package/src/EngineEvents.js +7 -0
  11. package/src/EngineIssues.js +6 -0
  12. package/src/Headless.js +3 -1
  13. package/src/Passes/AIUpscaler.js +2 -1
  14. package/src/Passes/OIDNDenoiser.js +3 -6
  15. package/src/Passes/OIDNTemporalHistory.js +25 -7
  16. package/src/PathTracerApp.js +313 -25
  17. package/src/Processor/ArchiveCache.js +120 -0
  18. package/src/Processor/ArchiveReader.js +179 -32
  19. package/src/Processor/AssetLoader.js +523 -76
  20. package/src/Processor/AtmosphereModel.js +465 -0
  21. package/src/Processor/BVHBuilder.js +4 -1
  22. package/src/Processor/BVHLeafFold.js +100 -0
  23. package/src/Processor/BVHRefitter.js +98 -214
  24. package/src/Processor/ChunkedRecords.js +189 -4
  25. package/src/Processor/EmissiveTriangleBuilder.js +8 -0
  26. package/src/Processor/GeometryExtractor.js +238 -40
  27. package/src/Processor/InstanceTable.js +55 -37
  28. package/src/Processor/PBRT/PBRTCurves.js +104 -27
  29. package/src/Processor/PBRT/PBRTParser.js +132 -22
  30. package/src/Processor/PBRT/PBRTSceneBuilder.js +210 -55
  31. package/src/Processor/PBRT/buffers.js +27 -0
  32. package/src/Processor/PBRT/index.js +55 -15
  33. package/src/Processor/PackedRayBuffer.js +10 -13
  34. package/src/Processor/ParallelBVHBuilder.js +36 -15
  35. package/src/Processor/PhysicalSky.js +308 -0
  36. package/src/Processor/QueueManager.js +1 -0
  37. package/src/Processor/SceneProcessor.js +1132 -181
  38. package/src/Processor/SplitBLAS.js +249 -0
  39. package/src/Processor/SunPosition.js +41 -0
  40. package/src/Processor/TextureCreator.js +21 -0
  41. package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
  42. package/src/Processor/Workers/BVHWorker.js +17 -13
  43. package/src/Processor/ZipReader.js +207 -0
  44. package/src/RenderSettings.js +49 -8
  45. package/src/SceneState/SceneState.js +367 -0
  46. package/src/SceneState/portable.js +52 -0
  47. package/src/Stages/MotionVector.js +8 -42
  48. package/src/Stages/NRD.js +30 -21
  49. package/src/Stages/NormalDepth.js +12 -23
  50. package/src/Stages/PathTracer.js +114 -6
  51. package/src/Stages/PathTracerStage.js +251 -104
  52. package/src/Storage/BLASCache.js +190 -0
  53. package/src/Storage/CDFCache.js +77 -0
  54. package/src/Storage/DownloadCache.js +266 -0
  55. package/src/Storage/GeometrySpill.js +217 -0
  56. package/src/Storage/SceneGraphCodec.js +503 -0
  57. package/src/Storage/SpillStore.js +112 -0
  58. package/src/Storage/StorageManager.js +934 -0
  59. package/src/Storage/StorageOps.js +137 -0
  60. package/src/Storage/StorageWorker.js +37 -0
  61. package/src/Storage/events.js +29 -0
  62. package/src/Storage/identity.js +77 -0
  63. package/src/Storage/index.js +9 -0
  64. package/src/Storage/inlineTransport.js +24 -0
  65. package/src/Storage/locks.js +99 -0
  66. package/src/Storage/openStorage.js +87 -0
  67. package/src/Storage/sceneCachePolicy.js +12 -0
  68. package/src/Storage/shared.js +9 -0
  69. package/src/Storage/transport.js +62 -0
  70. package/src/TSL/Atmosphere.js +684 -0
  71. package/src/TSL/BVHTraversal.js +288 -239
  72. package/src/TSL/CameraRay.js +99 -14
  73. package/src/TSL/Common.js +11 -2
  74. package/src/TSL/DebugKernel.js +2 -2
  75. package/src/TSL/Displacement.js +6 -6
  76. package/src/TSL/EmissiveSampling.js +5 -5
  77. package/src/TSL/EnvironmentCDF.js +176 -0
  78. package/src/TSL/GenerateKernel.js +5 -5
  79. package/src/TSL/HitFacet.js +4 -4
  80. package/src/TSL/LightsDirect.js +8 -8
  81. package/src/TSL/LightsSampling.js +55 -37
  82. package/src/TSL/Random.js +1 -0
  83. package/src/TSL/ShadeKernel.js +75 -19
  84. package/src/TSL/Sun.js +40 -0
  85. package/src/TSL/TextureSampling.js +6 -7
  86. package/src/index.js +22 -0
  87. package/src/managers/CameraManager.js +412 -56
  88. package/src/managers/DenoisingManager.js +4 -3
  89. package/src/managers/EnvironmentManager.js +263 -33
  90. package/src/managers/GoboManager.js +19 -8
  91. package/src/managers/IESManager.js +3 -3
  92. package/src/managers/InteractionManager.js +30 -2
  93. package/src/managers/LightManager.js +98 -5
  94. package/src/managers/MaterialDataManager.js +22 -2
  95. package/src/managers/TransformManager.js +26 -0
  96. package/src/managers/UniformManager.js +11 -7
  97. package/src/managers/VideoRenderManager.js +38 -17
  98. package/src/managers/ViewCamera.js +77 -0
  99. package/src/managers/WalkControls.js +262 -0
  100. package/src/managers/helpers/OutlineHelper.js +22 -2
  101. package/src/managers/timeline/CameraTrack.js +235 -0
  102. package/src/managers/timeline/TimelineManager.js +146 -0
  103. package/src/version.js +3 -0
  104. package/src/Processor/ProceduralSky.js +0 -291
@@ -24,10 +24,10 @@ import {
24
24
  bool as tslBool,
25
25
  } from 'three/tsl';
26
26
 
27
- import { BVH_LEAF_MARKERS, BVH_MAX_INDEX, TRI_MATERIAL_MASK, TRI_SIDE_SHIFT, TLAS_LEAF_IDENTITY } from '../EngineDefaults.js';
27
+ import { BVH_LEAF_MARKERS, BVH_MAX_INDEX, BVH_FOLDED_LEAF_MAX, TRI_MATERIAL_MASK, TRI_SIDE_SHIFT, TLAS_LEAF_IDENTITY } from '../EngineDefaults.js';
28
28
  import { HitInfo } from './Struct.js';
29
29
  import {
30
- getDatafromStorageBuffer, instanceRows, instanceNormalToWorld, unpackTriangleNormal, TRI_STRIDE, shadowFlagsSettle
30
+ getDatafromStorageBuffer, instanceRows, instanceNormalToWorld, unpackTriangleNormal, triangleRow, shadowFlagsSettle
31
31
  } from './Common.js';
32
32
 
33
33
  const MAX_STACK_DEPTH = 32;
@@ -46,6 +46,16 @@ const HUGE_VAL = 1e8;
46
46
 
47
47
  const createStack = () => array( 'int', MAX_STACK_DEPTH ).toVar();
48
48
 
49
+ // A negative stack entry is a leaf folded into its parent, `~( first << 4 | count )` as the parent
50
+ // stores it, so child slots are pushed as they are and a leaf needs no fetch.
51
+ const foldedFirst = ( entry ) => int( uint( entry.bitNot() ).shiftRight( uint( 4 ) ) );
52
+ const foldedCount = ( entry ) => int( uint( entry.bitNot() ).bitAnd( uint( BVH_FOLDED_LEAF_MAX ) ) );
53
+
54
+ // Leaf node tags sit from 2^30 up; in a folded BVH only to 2^31, where folded leaves begin.
55
+ const leafTagTest = ( folded ) => ( folded
56
+ ? ( tag ) => tag.shiftRight( uint( 30 ) ).equal( uint( 1 ) )
57
+ : ( tag ) => tag.greaterThanEqual( uint( BVH_MAX_INDEX ) ) );
58
+
49
59
  // Reciprocal ray direction with the axis-aligned cases pinned to a large signed value.
50
60
  const buildInvDir = ( d ) => {
51
61
 
@@ -219,6 +229,10 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
219
229
  // either side to find the exit; exterior rays honor the authored side as before.
220
230
  const inMedium = insideMedium ?? tslBool( false );
221
231
 
232
+ // Folded leaves cost up to 4 % of traversal, so only a BVH built with them gets that code.
233
+ const folded = bvhBuffer.value?.foldedLeaves === true;
234
+ const isLeafTag = leafTagTest( folded );
235
+
222
236
  const closestHit = HitInfo( {
223
237
  didHit: false,
224
238
  dst: float( 1e20 ),
@@ -260,183 +274,200 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
260
274
  const instExit = int( 0 ).toVar();
261
275
  const hitInstLeaf = int( - 1 ).toVar();
262
276
 
277
+ // Emitted once per leaf kind, in its own branch: hoisted below both, the range stayed live
278
+ // through the inner-node path and cost 9–13 % of traversal time.
279
+ const testLeaf = ( first, count ) => {
280
+
281
+ const triStart = int( first ).toVar();
282
+ const triCount = int( count ).toVar();
283
+
284
+ Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
285
+
286
+ if ( trackStats ) closestHit.triTests.addAssign( 1 );
287
+ const triIndex = triStart.add( i ).toVar();
288
+
289
+ // Three fetches carry positions AND the packed normals in their .w lanes.
290
+ const recA = triangleRow( triangleBuffer, triIndex, 0 ).toVar();
291
+ const recB = triangleRow( triangleBuffer, triIndex, 1 ).toVar();
292
+ const recC = triangleRow( triangleBuffer, triIndex, 2 ).toVar();
293
+ const pA = uintBitsToFloat( recA.xyz );
294
+ const pB = uintBitsToFloat( recB.xyz );
295
+ const pC = uintBitsToFloat( recC.xyz );
296
+
297
+ const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
298
+
299
+ // RayTriangleGeometry already guarantees t < closestHit.dst when w > 0.5
300
+ If( triResult.w.greaterThan( 0.5 ), () => {
301
+
302
+ const t = triResult.x;
303
+ const u = triResult.y;
304
+ const v = triResult.z;
305
+
306
+ // Normals came with the positions; only the side flag needs a fetch, and
307
+ // slot 4 also carries matIdx/meshIndex, deferred to post-traversal.
308
+ const nA = unpackTriangleNormal( recA.w );
309
+ const nB = unpackTriangleNormal( recB.w );
310
+ const nC = unpackTriangleNormal( recC.w );
311
+ const flags = triangleRow( triangleBuffer, triIndex, 4 ).z;
312
+ const side = int( flags.shiftRight( uint( TRI_SIDE_SHIFT ) ).bitAnd( uint( 3 ) ) ).toVar();
313
+
314
+ // Interpolate normal for the side-culling dot product (kept local,
315
+ // not stored on closestHit — re-derived post-loop from closestTriIdx).
316
+ const w = float( 1.0 ).sub( u ).sub( v );
317
+ const rayDotNormal = rayDirection.dot(
318
+ normalize( nA.mul( w ).add( nB.mul( u ) ).add( nC.mul( v ) ) )
319
+ );
320
+
321
+ // Side culling (inline; per-mesh visibility is at the BLAS-pointer level).
322
+ // 0=front (reject back-facing), 1=back (reject front-facing), 2=double (pass).
323
+ const sidePass = inMedium.or( side.equal( int( 2 ) ) )
324
+ .or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
325
+ .or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
326
+ If( sidePass, () => {
327
+
328
+ closestHit.didHit.assign( true );
329
+ closestHit.dst.assign( t );
330
+
331
+ // Defer normal/materialIndex/meshIndex/hitPoint/UV to post-traversal
332
+ // (all re-derived from closestTriIdx after the loop exits).
333
+ closestTriIdx.assign( triIndex );
334
+ closestU.assign( u );
335
+ closestV.assign( v );
336
+ hitInstLeaf.assign( instLeaf );
337
+
338
+ } );
339
+
340
+ } );
341
+
342
+ } );
343
+
344
+ // If we found a very close hit, we can terminate early
345
+ If( closestHit.didHit.and( closestHit.dst.lessThan( 0.001 ) ), () => {
346
+
347
+ Break();
348
+
349
+ } );
350
+
351
+ };
352
+
263
353
  const iterCount = int( 0 ).toVar();
264
354
 
265
355
  Loop( stackPtr.greaterThan( int( 0 ) ).and( iterCount.lessThan( int( MAX_BVH_ITERATIONS ) ) ), () => {
266
356
 
267
357
  iterCount.addAssign( 1 );
268
358
  stackPtr.subAssign( 1 );
269
- const nodeIndex = stack.element( stackPtr ).toVar();
359
+ const entry = stack.element( stackPtr ).toVar();
270
360
  instLeaf.assign( select( stackPtr.lessThan( instExit ), int( - 1 ), instLeaf ) );
271
361
 
272
- // New layout: 4 vec4 per node
273
- // Leaf: vec4(0) = [triOffset, triCount, 0, -1]
274
- // Inner: vec4(0) = [leftMin.xyz, leftChild], vec4(1) = [leftMax.xyz, rightChild],
275
- // vec4(2) = [rightMin.xyz, 0], vec4(3) = [rightMax.xyz, 0]
276
- const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
277
- if ( trackStats ) closestHit.boxTests.addAssign( 1 );
278
-
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 ) ), () => {
285
-
286
- If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
287
-
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();
291
-
292
- // Process triangles in leaf
293
- Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
294
-
295
- if ( trackStats ) closestHit.triTests.addAssign( 1 );
296
- const triIndex = triStart.add( i ).toVar();
297
-
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 );
305
-
306
- const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
307
-
308
- // RayTriangleGeometry already guarantees t < closestHit.dst when w > 0.5
309
- If( triResult.w.greaterThan( 0.5 ), () => {
310
-
311
- const t = triResult.x;
312
- const u = triResult.y;
313
- const v = triResult.z;
314
-
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();
322
-
323
- // Interpolate normal for the side-culling dot product (kept local,
324
- // not stored on closestHit — re-derived post-loop from closestTriIdx).
325
- const w = float( 1.0 ).sub( u ).sub( v );
326
- const rayDotNormal = rayDirection.dot(
327
- normalize( nA.mul( w ).add( nB.mul( u ) ).add( nC.mul( v ) ) )
328
- );
329
-
330
- // Side culling (inline; per-mesh visibility is at the BLAS-pointer level).
331
- // 0=front (reject back-facing), 1=back (reject front-facing), 2=double (pass).
332
- const sidePass = inMedium.or( side.equal( int( 2 ) ) )
333
- .or( side.equal( int( 0 ) ).and( rayDotNormal.lessThan( - 0.0001 ) ) )
334
- .or( side.equal( int( 1 ) ).and( rayDotNormal.greaterThan( 0.0001 ) ) );
335
- If( sidePass, () => {
336
-
337
- closestHit.didHit.assign( true );
338
- closestHit.dst.assign( t );
339
-
340
- // Defer normal/materialIndex/meshIndex/hitPoint/UV to post-traversal
341
- // (all re-derived from closestTriIdx after the loop exits).
342
- closestTriIdx.assign( triIndex );
343
- closestU.assign( u );
344
- closestV.assign( v );
345
- hitInstLeaf.assign( instLeaf );
362
+ const visitNode = () => {
346
363
 
347
- } );
364
+ const nodeIndex = entry;
348
365
 
349
- } );
366
+ // 4 vec4 per node
367
+ // Leaf: vec4(0) = [triOffset, triCount, 0, TRIANGLE_LEAF]
368
+ // Inner: vec4(0) = [leftMin.xyz, leftChild], vec4(1) = [leftMax.xyz, rightChild],
369
+ // vec4(2) = [rightMin.xyz, 0], vec4(3) = [rightMax.xyz, 0]
370
+ const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
371
+ if ( trackStats ) closestHit.boxTests.addAssign( 1 );
350
372
 
351
- } );
373
+ // Slot [3] carries a u32 bit pattern: an inner node's left child, or a leaf tag. Stored as
374
+ // float VALUES these rounded past 2^24 and sent the ray to a neighbouring node, which
375
+ // silently erased geometry from large scenes.
376
+ const nodeTag = floatBitsToUint( nodeData0.w ).toVar();
352
377
 
353
- // If we found a very close hit, we can terminate early
354
- If( closestHit.didHit.and( closestHit.dst.lessThan( 0.001 ) ), () => {
378
+ If( isLeafTag( nodeTag ), () => {
355
379
 
356
- Break();
380
+ If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
357
381
 
358
- } );
382
+ testLeaf( floatBitsToUint( nodeData0.x ), floatBitsToUint( nodeData0.y ) );
359
383
 
360
- } ).Else( () => {
384
+ } ).Else( () => {
361
385
 
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.
365
- If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
386
+ // BLAS-pointer leaf — enter the instance if the mesh is visible.
387
+ // nodeData0: [blasRootNodeIndex, meshIndex, visibility, -2]; slots 4..15 hold
388
+ // the world-to-object matrix. Visibility is free-fetched with the leaf.
389
+ If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
366
390
 
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 ) ), () => {
391
+ // A baked placement already sits in world space; only a transformed one moves the ray.
392
+ If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
369
393
 
370
- const rows = instanceRows( bvhBuffer, nodeIndex );
371
- const localDir = toObjectDir( rows, worldDirection ).toVar();
394
+ const rows = instanceRows( bvhBuffer, nodeIndex );
395
+ const localDir = toObjectDir( rows, worldDirection ).toVar();
372
396
 
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 );
397
+ rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
398
+ rayDirection.assign( localDir );
399
+ const localInv = buildInvDir( localDir ).toVar();
400
+ invDir.assign( localInv );
401
+ woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
402
+ instLeaf.assign( nodeIndex );
403
+ instExit.assign( stackPtr );
380
404
 
381
- } ).Else( () => {
405
+ } ).Else( () => {
382
406
 
383
- rayOrigin.assign( worldOrigin );
384
- rayDirection.assign( worldDirection );
385
- invDir.assign( worldInvDir );
386
- woopParams.assign( worldWoop );
387
- instLeaf.assign( int( - 1 ) );
407
+ rayOrigin.assign( worldOrigin );
408
+ rayDirection.assign( worldDirection );
409
+ invDir.assign( worldInvDir );
410
+ woopParams.assign( worldWoop );
411
+ instLeaf.assign( int( - 1 ) );
388
412
 
389
- } );
413
+ } );
414
+
415
+ stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
416
+ stackPtr.addAssign( 1 );
390
417
 
391
- stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
392
- stackPtr.addAssign( 1 );
418
+ } );
393
419
 
394
420
  } );
395
421
 
396
- } );
422
+ } ).Else( () => {
397
423
 
398
- } ).Else( () => {
424
+ // Inner node — child AABBs stored in this node (4 reads total, no child fetches)
425
+ const nodeData1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
426
+ const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
427
+ const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
399
428
 
400
- // Inner node — child AABBs stored in this node (4 reads total, no child fetches)
401
- const nodeData1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
402
- const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
403
- const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
429
+ const leftChild = int( nodeTag ).toVar();
430
+ const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
404
431
 
405
- const leftChild = int( nodeTag ).toVar();
406
- const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
432
+ const inInst = instLeaf.greaterThanEqual( int( 0 ) );
433
+ const boxOrigin = select( inInst, rayOrigin, worldOrigin ).toVar();
434
+ const boxInv = select( inInst, invDir, worldInvDir ).toVar();
435
+ const dstA = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
436
+ const dstB = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
407
437
 
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();
438
+ // Optimized early rejection
439
+ const minDst = min( dstA, dstB );
440
+ If( minDst.lessThan( closestHit.dst ), () => {
413
441
 
414
- // Optimized early rejection
415
- const minDst = min( dstA, dstB );
416
- If( minDst.lessThan( closestHit.dst ), () => {
442
+ // Improved node ordering with fewer conditionals
443
+ const aCloser = dstA.lessThan( dstB );
417
444
 
418
- // Improved node ordering with fewer conditionals
419
- const aCloser = dstA.lessThan( dstB );
445
+ // Push far child first (processed last)
446
+ If( select( aCloser, dstB, dstA ).lessThan( closestHit.dst ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
420
447
 
421
- // Push far child first (processed last)
422
- If( select( aCloser, dstB, dstA ).lessThan( closestHit.dst ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
448
+ stack.element( stackPtr ).assign( select( aCloser, rightChild, leftChild ) );
449
+ stackPtr.addAssign( 1 );
423
450
 
424
- stack.element( stackPtr ).assign( select( aCloser, rightChild, leftChild ) );
425
- stackPtr.addAssign( 1 );
451
+ } );
426
452
 
427
- } );
453
+ // Push near child second (processed first)
454
+ If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
428
455
 
429
- // Push near child second (processed first)
430
- If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
456
+ stack.element( stackPtr ).assign( select( aCloser, leftChild, rightChild ) );
457
+ stackPtr.addAssign( 1 );
431
458
 
432
- stack.element( stackPtr ).assign( select( aCloser, leftChild, rightChild ) );
433
- stackPtr.addAssign( 1 );
459
+ } );
434
460
 
435
461
  } );
436
462
 
437
463
  } );
438
464
 
439
- } );
465
+
466
+ };
467
+
468
+ // Only a folded BVH has negative entries: a leaf folded into its parent, tested with no fetch.
469
+ if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( visitNode );
470
+ else visitNode();
440
471
 
441
472
  } );
442
473
 
@@ -449,9 +480,9 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
449
480
 
450
481
  // Re-fetch the winning triangle's normals — trading 3 storage reads (once)
451
482
  // for ~3 regs freed across every BVH iteration.
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 );
483
+ const nA = unpackTriangleNormal( triangleRow( triangleBuffer, closestTriIdx, 0 ).w );
484
+ const nB = unpackTriangleNormal( triangleRow( triangleBuffer, closestTriIdx, 1 ).w );
485
+ const nC = unpackTriangleNormal( triangleRow( triangleBuffer, closestTriIdx, 2 ).w );
455
486
  const objectNormal = normalize( nA.mul( w ).add( nB.mul( closestU ) ).add( nC.mul( closestV ) ) ).toVar();
456
487
  If( hitInstLeaf.greaterThanEqual( int( 0 ) ), () => {
457
488
 
@@ -461,8 +492,8 @@ const makeTraverseBVH = ( trackStats ) => Fn( ( [
461
492
 
462
493
  closestHit.normal.assign( objectNormal );
463
494
 
464
- const uvData1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 3 ), int( TRI_STRIDE ) ) );
465
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, closestTriIdx, int( 4 ), int( TRI_STRIDE ) ).toVar();
495
+ const uvData1 = uintBitsToFloat( triangleRow( triangleBuffer, closestTriIdx, 3 ) );
496
+ const uvData2 = triangleRow( triangleBuffer, closestTriIdx, 4 ).toVar();
466
497
  closestHit.uv.assign(
467
498
  uvData1.xy.mul( w ).add( uvData1.zw.mul( closestU ) ).add( uintBitsToFloat( uvData2.xy ).mul( closestV ) )
468
499
  );
@@ -492,6 +523,9 @@ export const traverseBVHShadow = Fn( ( [
492
523
  maxShadowDist,
493
524
  ] ) => {
494
525
 
526
+ const folded = bvhBuffer.value?.foldedLeaves === true;
527
+ const isLeafTag = leafTagTest( folded );
528
+
495
529
  const closestHit = HitInfo( {
496
530
  didHit: false,
497
531
  dst: maxShadowDist,
@@ -524,152 +558,167 @@ export const traverseBVHShadow = Fn( ( [
524
558
  const instExit = int( 0 ).toVar();
525
559
  const blocked = tslBool( false ).toVar();
526
560
 
527
- const sIterCount = int( 0 ).toVar();
561
+ const testLeaf = ( first, count ) => {
528
562
 
529
- Loop( stackPtr.greaterThan( int( 0 ) ).and( blocked.not() ).and( sIterCount.lessThan( int( MAX_BVH_ITERATIONS ) ) ), () => {
563
+ const triStart = int( first ).toVar();
564
+ const triCount = int( count ).toVar();
530
565
 
531
- sIterCount.addAssign( 1 );
532
- stackPtr.subAssign( 1 );
533
- const nodeIndex = stack.element( stackPtr ).toVar();
534
- instLeaf.assign( select( stackPtr.lessThan( instExit ), int( - 1 ), instLeaf ) );
566
+ Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
535
567
 
536
- const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
568
+ const triIndex = triStart.add( i ).toVar();
537
569
 
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();
570
+ const pA = uintBitsToFloat( triangleRow( triangleBuffer, triIndex, 0 ).xyz );
571
+ const pB = uintBitsToFloat( triangleRow( triangleBuffer, triIndex, 1 ).xyz );
572
+ const pC = uintBitsToFloat( triangleRow( triangleBuffer, triIndex, 2 ).xyz );
542
573
 
543
- If( nodeTag.greaterThanEqual( uint( BVH_MAX_INDEX ) ), () => {
574
+ const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
544
575
 
545
- If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
576
+ If( triResult.w.greaterThan( 0.5 ), () => {
546
577
 
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();
578
+ // Per-mesh visibility is handled at the BLAS-pointer level.
579
+ const uvData2 = triangleRow( triangleBuffer, triIndex, 4 ).toVar();
550
580
 
551
- Loop( { start: int( 0 ), end: triCount }, ( { i } ) => {
581
+ closestHit.didHit.assign( true );
582
+ closestHit.dst.assign( triResult.x );
583
+ closestHit.materialIndex.assign( int( uvData2.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ) );
584
+ closestHit.meshIndex.assign( int( uvData2.w ) );
585
+ closestHit.instanceLeaf.assign( instLeaf );
552
586
 
553
- const triIndex = triStart.add( i ).toVar();
587
+ // Hit point is cheap (origin + dir*t). Geometric normal is deferred
588
+ // to traceShadowRay — only the transmission branch needs it, so we
589
+ // skip the cross+normalize for the (much more common) opaque-blocker
590
+ // and alpha-cutout paths. Normal stays vec3(0) from struct init.
591
+ closestHit.hitPoint.assign( worldOrigin.add( worldDirection.mul( triResult.x ) ) );
554
592
 
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 );
593
+ closestHit.uv.assign( vec2( triResult.y, triResult.z ) );
594
+ closestHit.triangleIndex.assign( triIndex );
558
595
 
559
- const triResult = RayTriangleGeometry( { rayOrigin, rayDir: rayDirection, pA, pB, pC, closestHitDst: closestHit.dst, woopParams } );
596
+ // An opaque blocker settles the ray. A surface light passes through has to be
597
+ // the nearest one, or the layers behind the first find are never counted.
598
+ If( shadowFlagsSettle( uvData2.z ), () => {
560
599
 
561
- If( triResult.w.greaterThan( 0.5 ), () => {
600
+ blocked.assign( true );
601
+ Break();
562
602
 
563
- // Per-mesh visibility is handled at the BLAS-pointer level.
564
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), int( TRI_STRIDE ) ).toVar();
603
+ } );
565
604
 
566
- closestHit.didHit.assign( true );
567
- closestHit.dst.assign( triResult.x );
568
- closestHit.materialIndex.assign( int( uvData2.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ) );
569
- closestHit.meshIndex.assign( int( uvData2.w ) );
570
- closestHit.instanceLeaf.assign( instLeaf );
605
+ } );
571
606
 
572
- // Hit point is cheap (origin + dir*t). Geometric normal is deferred
573
- // to traceShadowRay — only the transmission branch needs it, so we
574
- // skip the cross+normalize for the (much more common) opaque-blocker
575
- // and alpha-cutout paths. Normal stays vec3(0) from struct init.
576
- closestHit.hitPoint.assign( worldOrigin.add( worldDirection.mul( triResult.x ) ) );
607
+ } );
577
608
 
578
- closestHit.uv.assign( vec2( triResult.y, triResult.z ) );
579
- closestHit.triangleIndex.assign( triIndex );
609
+ };
580
610
 
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 ), () => {
611
+ const sIterCount = int( 0 ).toVar();
584
612
 
585
- blocked.assign( true );
586
- Break();
613
+ Loop( stackPtr.greaterThan( int( 0 ) ).and( blocked.not() ).and( sIterCount.lessThan( int( MAX_BVH_ITERATIONS ) ) ), () => {
587
614
 
588
- } );
615
+ sIterCount.addAssign( 1 );
616
+ stackPtr.subAssign( 1 );
617
+ const entry = stack.element( stackPtr ).toVar();
618
+ instLeaf.assign( select( stackPtr.lessThan( instExit ), int( - 1 ), instLeaf ) );
589
619
 
590
- } );
620
+ const visitNode = () => {
591
621
 
592
- } );
622
+ const nodeIndex = entry;
623
+ const nodeData0 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 0 ), int( BVH_STRIDE ) );
593
624
 
594
- } ).Else( () => {
625
+ // Slot [3] carries a u32 bit pattern: an inner node's left child, or a leaf tag. Stored as
626
+ // float VALUES these rounded past 2^24 and sent the ray to a neighbouring node, which
627
+ // silently erased geometry from large scenes.
628
+ const nodeTag = floatBitsToUint( nodeData0.w ).toVar();
595
629
 
596
- // BLAS-pointer leaf — enter the instance if the mesh is visible.
597
- If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
630
+ If( isLeafTag( nodeTag ), () => {
598
631
 
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 ) ), () => {
632
+ If( nodeTag.equal( uint( BVH_LEAF_MARKERS.TRIANGLE_LEAF ) ), () => {
601
633
 
602
- const rows = instanceRows( bvhBuffer, nodeIndex );
603
- const localDir = toObjectDir( rows, worldDirection ).toVar();
634
+ testLeaf( floatBitsToUint( nodeData0.x ), floatBitsToUint( nodeData0.y ) );
604
635
 
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 );
636
+ } ).Else( () => {
612
637
 
613
- } ).Else( () => {
638
+ // BLAS-pointer leaf — enter the instance if the mesh is visible.
639
+ If( nodeData0.z.greaterThan( 0.5 ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
614
640
 
615
- rayOrigin.assign( worldOrigin );
616
- rayDirection.assign( worldDirection );
617
- invDir.assign( worldInvDir );
618
- woopParams.assign( worldWoop );
619
- instLeaf.assign( int( - 1 ) );
641
+ // A baked placement already sits in world space; only a transformed one moves the ray.
642
+ If( floatBitsToUint( nodeData0.y ).bitAnd( uint( TLAS_LEAF_IDENTITY ) ).equal( uint( 0 ) ), () => {
620
643
 
621
- } );
644
+ const rows = instanceRows( bvhBuffer, nodeIndex );
645
+ const localDir = toObjectDir( rows, worldDirection ).toVar();
646
+
647
+ rayOrigin.assign( toObjectPoint( rows, worldOrigin ) );
648
+ rayDirection.assign( localDir );
649
+ const localInv = buildInvDir( localDir ).toVar();
650
+ invDir.assign( localInv );
651
+ woopParams.assign( computeWoopFromInvDir( { rayDir: localDir, invDir: localInv } ) );
652
+ instLeaf.assign( nodeIndex );
653
+ instExit.assign( stackPtr );
654
+
655
+ } ).Else( () => {
656
+
657
+ rayOrigin.assign( worldOrigin );
658
+ rayDirection.assign( worldDirection );
659
+ invDir.assign( worldInvDir );
660
+ woopParams.assign( worldWoop );
661
+ instLeaf.assign( int( - 1 ) );
622
662
 
623
- stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
624
- stackPtr.addAssign( 1 );
663
+ } );
664
+
665
+ stack.element( stackPtr ).assign( int( floatBitsToUint( nodeData0.x ) ) );
666
+ stackPtr.addAssign( 1 );
667
+
668
+ } );
625
669
 
626
670
  } );
627
671
 
628
- } );
672
+ } ).Else( () => {
629
673
 
630
- } ).Else( () => {
674
+ // Inner node — child AABBs stored in this node
675
+ const nodeData1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
676
+ const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
677
+ const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
631
678
 
632
- // Inner node — child AABBs stored in this node
633
- const nodeData1 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 1 ), int( BVH_STRIDE ) );
634
- const nodeData2 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 2 ), int( BVH_STRIDE ) );
635
- const nodeData3 = getDatafromStorageBuffer( bvhBuffer, nodeIndex, int( 3 ), int( BVH_STRIDE ) );
679
+ const leftChild = int( nodeTag ).toVar();
680
+ const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
636
681
 
637
- const leftChild = int( nodeTag ).toVar();
638
- const rightChild = int( floatBitsToUint( nodeData1.w ) ).toVar();
682
+ const inInst = instLeaf.greaterThanEqual( int( 0 ) );
683
+ const boxOrigin = select( inInst, rayOrigin, worldOrigin ).toVar();
684
+ const boxInv = select( inInst, invDir, worldInvDir ).toVar();
685
+ const dstA = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData0.xyz, boxMax: nodeData1.xyz } ).toVar();
686
+ const dstB = fastRayAABBDst( { rayOrigin: boxOrigin, invDir: boxInv, boxMin: nodeData2.xyz, boxMax: nodeData3.xyz } ).toVar();
639
687
 
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();
688
+ // Distance-ordered traversal — nearer child first for faster any-hit
689
+ // termination. SA build ordering improves cache locality (larger-SA
690
+ // child = left = first in DFS flat layout).
691
+ const minDst = min( dstA, dstB );
692
+ If( minDst.lessThan( closestHit.dst ), () => {
645
693
 
646
- // Distance-ordered traversal — nearer child first for faster any-hit
647
- // termination. SA build ordering improves cache locality (larger-SA
648
- // child = left = first in DFS flat layout).
649
- const minDst = min( dstA, dstB );
650
- If( minDst.lessThan( closestHit.dst ), () => {
694
+ const aCloser = dstA.lessThan( dstB );
651
695
 
652
- const aCloser = dstA.lessThan( dstB );
696
+ // Push far child first (processed last)
697
+ If( select( aCloser, dstB, dstA ).lessThan( closestHit.dst ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
653
698
 
654
- // Push far child first (processed last)
655
- If( select( aCloser, dstB, dstA ).lessThan( closestHit.dst ).and( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ) ), () => {
699
+ stack.element( stackPtr ).assign( select( aCloser, rightChild, leftChild ) );
700
+ stackPtr.addAssign( 1 );
656
701
 
657
- stack.element( stackPtr ).assign( select( aCloser, rightChild, leftChild ) );
658
- stackPtr.addAssign( 1 );
702
+ } );
659
703
 
660
- } );
704
+ // Push near child second (processed first)
705
+ If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
661
706
 
662
- // Push near child second (processed first)
663
- If( stackPtr.lessThan( int( MAX_STACK_DEPTH ) ), () => {
707
+ stack.element( stackPtr ).assign( select( aCloser, leftChild, rightChild ) );
708
+ stackPtr.addAssign( 1 );
664
709
 
665
- stack.element( stackPtr ).assign( select( aCloser, leftChild, rightChild ) );
666
- stackPtr.addAssign( 1 );
710
+ } );
667
711
 
668
712
  } );
669
713
 
670
714
  } );
671
715
 
672
- } );
716
+
717
+ };
718
+
719
+ // Only a folded BVH has negative entries: a leaf folded into its parent, tested with no fetch.
720
+ if ( folded ) If( entry.lessThan( int( 0 ) ), () => testLeaf( foldedFirst( entry ), foldedCount( entry ) ) ).Else( visitNode );
721
+ else visitNode();
673
722
 
674
723
  } );
675
724