rayzee 7.31.1 → 8.0.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 (51) hide show
  1. package/README.md +164 -0
  2. package/dist/rayzee.es.js +6366 -4361
  3. package/dist/rayzee.umd.js +122 -73
  4. package/package.json +1 -1
  5. package/src/EngineDefaults.js +168 -13
  6. package/src/EngineIssues.js +5 -0
  7. package/src/PathTracerApp.js +120 -19
  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 +577 -92
  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/Stages/NormalDepth.js +6 -2
  35. package/src/Stages/PathTracer.js +5 -5
  36. package/src/Stages/PathTracerStage.js +163 -41
  37. package/src/TSL/BVHTraversal.js +208 -90
  38. package/src/TSL/Common.js +135 -2
  39. package/src/TSL/Displacement.js +21 -11
  40. package/src/TSL/EmissiveSampling.js +49 -25
  41. package/src/TSL/ExtendKernel.js +3 -2
  42. package/src/TSL/LightBVHSampling.js +7 -4
  43. package/src/TSL/LightsDirect.js +30 -29
  44. package/src/TSL/ShadeKernel.js +8 -6
  45. package/src/TSL/Struct.js +3 -0
  46. package/src/TSL/TextureSampling.js +24 -9
  47. package/src/TSL/patches.js +128 -3
  48. package/src/index.js +3 -0
  49. package/src/managers/AnimationManager.js +86 -76
  50. package/src/managers/MaterialDataManager.js +29 -23
  51. package/src/managers/TransformManager.js +9 -192
@@ -1,14 +1,13 @@
1
- import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max } from 'three/tsl';
1
+ import { Fn, float, vec2, int, If, Loop, abs, normalize, dot, max, uintBitsToFloat } from 'three/tsl';
2
2
 
3
3
  import { struct } from './patches.js';
4
- import { getDatafromStorageBuffer } from './Common.js';
4
+ import { getDatafromStorageBuffer, TRI_STRIDE, instanceRows, instanceDirToWorld } from './Common.js';
5
5
  import { sampleDisplacementMap, bucketTexelSize, getLinearBucketTextures } from './TextureSampling.js';
6
6
 
7
7
  // Ray-displacement intersection configuration
8
8
  const MAX_MARCH_STEPS = 32;
9
9
  const MIN_MARCH_STEPS = 16;
10
10
  const BINARY_STEPS = 5;
11
- const TRI_STRIDE = 8;
12
11
 
13
12
  export const DisplacementResult = struct( {
14
13
  hitPoint: 'vec3',
@@ -32,7 +31,7 @@ export const DisplacementResult = struct( {
32
31
  * We find dt where h_ray(dt) = h_surf(dt).
33
32
  */
34
33
  export const refineDisplacedIntersection = Fn( ( [
35
- ray, hitInfo, triangleBuffer, material, bounceIndex
34
+ ray, hitInfo, triangleBuffer, material, bounceIndex, bvhBuffer, instanceLeaf
36
35
  ] ) => {
37
36
 
38
37
  const resultHitPoint = hitInfo.hitPoint.toVar();
@@ -43,21 +42,32 @@ export const refineDisplacedIntersection = Fn( ( [
43
42
  // Fetch triangle vertex data
44
43
  const triIdx = hitInfo.triangleIndex;
45
44
 
46
- const pA = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 0 ), int( TRI_STRIDE ) ).xyz.toVar();
47
- const pB = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 1 ), int( TRI_STRIDE ) ).xyz;
48
- const pC = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 2 ), int( TRI_STRIDE ) ).xyz;
45
+ const pA = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 0 ), int( TRI_STRIDE ) ).xyz ).toVar();
46
+ const pB = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 1 ), int( TRI_STRIDE ) ).xyz );
47
+ const pC = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 2 ), int( TRI_STRIDE ) ).xyz );
49
48
 
50
- const uvData1 = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 6 ), int( TRI_STRIDE ) ).toVar();
51
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, triIdx, int( 7 ), int( TRI_STRIDE ) );
49
+ const uvData1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 3 ), int( TRI_STRIDE ) ) ).toVar();
50
+ const uvData2 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIdx, int( 4 ), int( TRI_STRIDE ) ).xy );
52
51
 
53
52
  const uvA = uvData1.xy.toVar();
54
53
  const uvB = uvData1.zw;
55
- const uvC = uvData2.xy;
54
+ const uvC = uvData2;
56
55
 
57
- // Compute tangent vectors from triangle edges + UV differences
56
+ // Compute tangent vectors from triangle edges + UV differences. The ray, the hit point and
57
+ // the normal are all in world space; a triangle that belongs to a placement is not, so its
58
+ // edges are moved before they are used, or the marching frame is half in each space.
58
59
  const edge1 = pB.sub( pA ).toVar();
59
60
  const edge2 = pC.sub( pA ).toVar();
60
61
 
62
+ If( instanceLeaf.greaterThanEqual( int( 0 ) ), () => {
63
+
64
+ const rows = instanceRows( bvhBuffer, instanceLeaf );
65
+ const r = { r0: rows[ 0 ].xyz, r1: rows[ 1 ].xyz, r2: rows[ 2 ].xyz };
66
+ edge1.assign( instanceDirToWorld( { ...r, v: edge1 } ) );
67
+ edge2.assign( instanceDirToWorld( { ...r, v: edge2 } ) );
68
+
69
+ } );
70
+
61
71
  const dUV1 = uvB.sub( uvA ).toVar();
62
72
  const dUV2 = uvC.sub( uvA ).toVar();
63
73
  const det = dUV1.x.mul( dUV2.y ).sub( dUV2.x.mul( dUV1.y ) ).toVar();
@@ -22,10 +22,17 @@ import {
22
22
  cos,
23
23
  acos,
24
24
  atan,
25
+ uint,
26
+ uintBitsToFloat,
25
27
  } from 'three/tsl';
26
28
 
27
29
  import { struct } from './patches.js';
28
- import { MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT, computeDotProductsAniso } from './Common.js';
30
+ import {
31
+ MIN_PDF, getDatafromStorageBuffer, powerHeuristic, MATERIAL_SLOTS, MATERIAL_SLOT,
32
+ computeDotProductsAniso, instanceRows, instanceNormalToWorld, instancePointToWorld,
33
+ unpackTriangleNormal, TRI_STRIDE
34
+ } from './Common.js';
35
+ import { TRI_MATERIAL_MASK } from '../EngineDefaults.js';
29
36
  import { getRandomSample1D, getRandomSample2D } from './Random.js';
30
37
  import { calculateMaterialPDFFromDots } from './LightsSampling.js';
31
38
  import { evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
@@ -272,7 +279,6 @@ export const isEmissive = Fn( ( [ material ] ) => {
272
279
  // TRIANGLE DATA ACCESS
273
280
  // ================================================================================
274
281
 
275
- const TRI_STRIDE = 8;
276
282
  const EMISSIVE_STRIDE = 2; // 2 vec4s per emissive entry
277
283
 
278
284
  export const TriangleData = struct( {
@@ -283,26 +289,40 @@ export const TriangleData = struct( {
283
289
 
284
290
  // Fetch triangle vertices from storage buffer
285
291
  // Returns data packed in a struct-like way
286
- export const fetchTriangleData = Fn( ( [ triangleIndex, triangleBuffer ] ) => {
287
-
288
- // Positions
289
- const pos0 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 0 ), int( TRI_STRIDE ) );
290
- const pos1 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 1 ), int( TRI_STRIDE ) );
291
- const pos2 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 2 ), int( TRI_STRIDE ) );
292
+ /**
293
+ * A triangle's vertices and normals in WORLD space.
294
+ *
295
+ * Storage is per instance in object space, so `instanceLeaf` names the TLAS leaf whose
296
+ * transform brings it back out; pass -1 for geometry that is already in world space.
297
+ */
298
+ export const fetchTriangleData = Fn( ( [ triangleIndex, triangleBuffer, bvhBuffer, instanceLeaf ] ) => {
299
+
300
+ // Positions carry their packed normal in .w
301
+ const pos0 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 0 ), int( TRI_STRIDE ) ).toVar();
302
+ const pos1 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 1 ), int( TRI_STRIDE ) ).toVar();
303
+ const pos2 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 2 ), int( TRI_STRIDE ) ).toVar();
304
+
305
+ const uvMat = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 4 ), int( TRI_STRIDE ) );
306
+
307
+ const v0 = uintBitsToFloat( pos0.xyz ).toVar(), v1 = uintBitsToFloat( pos1.xyz ).toVar(), v2 = uintBitsToFloat( pos2.xyz ).toVar();
308
+ const n0 = unpackTriangleNormal( pos0.w ).toVar(), n1 = unpackTriangleNormal( pos1.w ).toVar(), n2 = unpackTriangleNormal( pos2.w ).toVar();
309
+
310
+ If( instanceLeaf.greaterThanEqual( int( 0 ) ), () => {
311
+
312
+ const rows = instanceRows( bvhBuffer, instanceLeaf );
313
+ v0.assign( instancePointToWorld( rows, v0 ) );
314
+ v1.assign( instancePointToWorld( rows, v1 ) );
315
+ v2.assign( instancePointToWorld( rows, v2 ) );
316
+ n0.assign( normalize( instanceNormalToWorld( rows, n0 ) ) );
317
+ n1.assign( normalize( instanceNormalToWorld( rows, n1 ) ) );
318
+ n2.assign( normalize( instanceNormalToWorld( rows, n2 ) ) );
292
319
 
293
- // Normals
294
- const norm0 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 3 ), int( TRI_STRIDE ) );
295
- const norm1 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 4 ), int( TRI_STRIDE ) );
296
- const norm2 = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 5 ), int( TRI_STRIDE ) );
297
-
298
- // Material index (stored in last vec4)
299
- const uvMat = getDatafromStorageBuffer( triangleBuffer, triangleIndex, int( 7 ), int( TRI_STRIDE ) );
320
+ } );
300
321
 
301
322
  // Return all data as a struct
302
323
  return TriangleData( {
303
- v0: pos0.xyz, v1: pos1.xyz, v2: pos2.xyz,
304
- n0: norm0.xyz, n1: norm1.xyz, n2: norm2.xyz,
305
- materialIndex: int( uvMat.z ),
324
+ v0, v1, v2, n0, n1, n2,
325
+ materialIndex: int( uvMat.z.bitAnd( uint( TRI_MATERIAL_MASK ) ) ),
306
326
  } );
307
327
 
308
328
  } );
@@ -312,9 +332,10 @@ export const fetchTriangleData = Fn( ( [ triangleIndex, triangleBuffer ] ) => {
312
332
  // Uses same heuristic as sampleEmissiveTriangle for MIS consistency
313
333
  export const calculateEmissiveLightPdf = Fn( ( [
314
334
  triangleIndex, hitDistance, rayDir, shadingPoint, triangleBuffer, materialBuffer, emissiveTotalPower,
335
+ bvhBuffer, instanceLeaf,
315
336
  ] ) => {
316
337
 
317
- const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer ) );
338
+ const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer, bvhBuffer, instanceLeaf ) );
318
339
  const area = triangleArea( triData.v0, triData.v1, triData.v2 );
319
340
 
320
341
  // Targeted material read: only fetch emissive data (2 vec4s instead of full 27)
@@ -391,7 +412,7 @@ export const sampleEmissiveTriangle = Fn( ( [
391
412
  rngState,
392
413
  pixelCoord, resolution, frame, dimBase,
393
414
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
394
- triangleBuffer,
415
+ triangleBuffer, bvhBuffer,
395
416
  ] ) => {
396
417
 
397
418
  const result = EmissiveSample( {
@@ -423,9 +444,12 @@ export const sampleEmissiveTriangle = Fn( ( [
423
444
  const samplePower = max( emissiveData0.g, float( 1e-10 ) );
424
445
  const emission = emissiveData1.xyz;
425
446
  const area = emissiveData1.w;
447
+ // Slot 3 carries the emitter's TLAS leaf; the selection pdf it used to hold is
448
+ // recomputed from power and the total, so the word was free.
449
+ const emissiveInstance = int( emissiveData0.a );
426
450
 
427
451
  // Fetch triangle geometry
428
- const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer ) );
452
+ const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer, bvhBuffer, emissiveInstance ) );
429
453
 
430
454
  const xi = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame ).toVar();
431
455
 
@@ -532,7 +556,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
532
556
  pixelCoord, resolution, frame, dimBase,
533
557
  emissiveBoost,
534
558
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
535
- triangleBuffer,
559
+ triangleBuffer, bvhBuffer,
536
560
  // Callback functions to avoid circular deps
537
561
  traceShadowRayFn,
538
562
  calculateRayOffsetFn,
@@ -553,7 +577,7 @@ export const calculateEmissiveTriangleContributionDebug = Fn( ( [
553
577
  hitPoint, normal, rngState,
554
578
  pixelCoord, resolution, frame, dimBase,
555
579
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
556
- triangleBuffer,
580
+ triangleBuffer, bvhBuffer,
557
581
  ) );
558
582
 
559
583
  If( emissiveSample.valid.and( emissiveSample.pdf.greaterThan( 0.0 ) ), () => {
@@ -614,7 +638,7 @@ export const calculateEmissiveTriangleContribution = Fn( ( [
614
638
  pixelCoord, resolution, frame, dimBase,
615
639
  emissiveBoost,
616
640
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
617
- triangleBuffer,
641
+ triangleBuffer, bvhBuffer,
618
642
  traceShadowRayFn,
619
643
  calculateRayOffsetFn,
620
644
  ] ) => {
@@ -625,7 +649,7 @@ export const calculateEmissiveTriangleContribution = Fn( ( [
625
649
  pixelCoord, resolution, frame, dimBase,
626
650
  emissiveBoost,
627
651
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
628
- triangleBuffer,
652
+ triangleBuffer, bvhBuffer,
629
653
  traceShadowRayFn,
630
654
  calculateRayOffsetFn,
631
655
  ) );
@@ -3,7 +3,7 @@
3
3
  */
4
4
 
5
5
  import {
6
- Fn, uint,
6
+ Fn, uint, int,
7
7
  If,
8
8
  instanceIndex,
9
9
  atomicLoad,
@@ -71,7 +71,8 @@ export function buildExtendKernel( params ) {
71
71
  hitInfo.uv.x, hitInfo.uv.y,
72
72
  hitInfo.normal,
73
73
  uint( hitInfo.materialIndex ),
74
- uint( hitInfo.meshIndex ),
74
+ // Biased by one: leaf 0 is real, so 0 has to mean "no instance".
75
+ uint( hitInfo.instanceLeaf.add( int( 1 ) ) ),
75
76
  );
76
77
 
77
78
  } );
@@ -110,7 +110,7 @@ export const sampleLightBVHTriangle = Fn( ( [
110
110
  lbvhBuffer,
111
111
  emissiveTriangleBuffer,
112
112
  emissiveVec4Offset,
113
- triangleBuffer,
113
+ triangleBuffer, bvhBuffer,
114
114
  ] ) => {
115
115
 
116
116
  const result = EmissiveSample( {
@@ -256,9 +256,10 @@ export const sampleLightBVHTriangle = Fn( ( [
256
256
  const triangleIndex = int( emissiveData0.r );
257
257
  const emission = emissiveData1.xyz;
258
258
  const area = emissiveData1.w;
259
+ const emissiveInstance = int( emissiveData0.a );
259
260
 
260
261
  // Fetch triangle geometry
261
- const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer ) );
262
+ const triData = TriangleData.wrap( fetchTriangleData( triangleIndex, triangleBuffer, bvhBuffer, emissiveInstance ) );
262
263
 
263
264
  const xi = getRandomSample2D( pixelCoord, int( 0 ), dimBase.add( int( 3 ) ), rngState, resolution, frame ).toVar();
264
265
 
@@ -361,7 +362,7 @@ export const sampleLightBVHTriangle = Fn( ( [
361
362
  // float per absolute triangleIndex (4 packed per vec4): the root→leaf left(0)/right(1) choices.
362
363
  export const calculateLightBVHPdf = Fn( ( [
363
364
  triangleIndex, hitDistance, rayDir, shadingPoint,
364
- lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer,
365
+ lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer, bvhBuffer,
365
366
  ] ) => {
366
367
 
367
368
  const result = float( 0.0 ).toVar();
@@ -462,6 +463,7 @@ export const calculateLightBVHPdf = Fn( ( [
462
463
  const leafTotalPower = max( d0.w, float( 1e-10 ) );
463
464
 
464
465
  const targetPower = float( 0.0 ).toVar();
466
+ const targetInstance = int( - 1 ).toVar();
465
467
  Loop( { start: int( 0 ), end: emissiveCount }, ( { i } ) => {
466
468
 
467
469
  const entryIdx = emissiveStart.add( i );
@@ -469,6 +471,7 @@ export const calculateLightBVHPdf = Fn( ( [
469
471
  If( int( emData0.r ).equal( triIdx ), () => {
470
472
 
471
473
  targetPower.assign( max( emData0.g, float( 0.0 ) ) );
474
+ targetInstance.assign( int( emData0.a ) );
472
475
  Break();
473
476
 
474
477
  } );
@@ -478,7 +481,7 @@ export const calculateLightBVHPdf = Fn( ( [
478
481
  selectionPdf.mulAssign( targetPower.div( leafTotalPower ) );
479
482
 
480
483
  // Convert selection pdf → solid-angle measure using the SAME heuristic as the sampler.
481
- const triData = TriangleData.wrap( fetchTriangleData( triIdx, triangleBuffer ) );
484
+ const triData = TriangleData.wrap( fetchTriangleData( triIdx, triangleBuffer, bvhBuffer, targetInstance ) );
482
485
  If( useSphericalSampling( triData.v0, triData.v1, triData.v2, shadingPoint ), () => {
483
486
 
484
487
  const solidAngle = sphericalTriangleSolidAngle( triData.v0, triData.v1, triData.v2, shadingPoint );
@@ -22,10 +22,14 @@ import {
22
22
  clamp,
23
23
  smoothstep,
24
24
  select,
25
+ uintBitsToFloat,
25
26
  } from 'three/tsl';
26
27
 
27
28
  import { Ray, ShadowMaterial, HitInfo } from './Struct.js';
28
- import { REC709_LUMINANCE_COEFFICIENTS, getShadowMaterial, getDatafromStorageBuffer } from './Common.js';
29
+ import {
30
+ REC709_LUMINANCE_COEFFICIENTS, getShadowMaterial, getDatafromStorageBuffer, instanceRows,
31
+ instanceFaceNormalToWorld, TRI_STRIDE, getAlphaShadowsUniform, shadowFlagsSettle
32
+ } from './Common.js';
29
33
  import { fresnelSchlickFloat, iorToFresnel0 } from './Fresnel.js';
30
34
  import { calculateBeerLawAbsorption } from './MaterialTransmission.js';
31
35
  import { getTransformedUV, sampleBucket } from './TextureSampling.js';
@@ -33,7 +37,8 @@ import { getTransformedUV, sampleBucket } from './TextureSampling.js';
33
37
  // Module-level state for alpha-cutout shadow testing.
34
38
  // Set by PathTracer before shade-graph construction.
35
39
  let _shadowAlbedoMaps = null;
36
- let _enableAlphaShadows = null;
40
+
41
+ export { setAlphaShadowsUniform } from './Common.js';
37
42
 
38
43
  /**
39
44
  * Set the sRGB bucket texture node array for alpha-aware shadow rays (albedo alpha).
@@ -46,16 +51,6 @@ export function setShadowAlbedoMaps( buckets ) {
46
51
 
47
52
  }
48
53
 
49
- /**
50
- * Set the runtime uniform node that toggles alpha-cutout shadows.
51
- * @param {UniformNode} node - TSL int uniform (0 = disabled, 1 = enabled)
52
- */
53
- export function setAlphaShadowsUniform( node ) {
54
-
55
- _enableAlphaShadows = node;
56
-
57
- }
58
-
59
54
  // ================================================================================
60
55
  // SHADOW RAY TRACING
61
56
  // ================================================================================
@@ -94,13 +89,11 @@ export const traceShadowRay = Fn( ( [
94
89
 
95
90
  } );
96
91
 
97
- // Opaque fast-path: check the per-triangle blocker flag (NORMAL_A.w, set at
98
- // extraction time when alphaMode/transparent/transmission/opacity all indicate
99
- // a fully opaque surface). Short-circuits the 7-slot getShadowMaterial fetch
100
- // and the entire alpha/transmission/transparent decision tree below.
101
- const TRI_STRIDE_SR = int( 8 );
102
- const blocker = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 3 ), TRI_STRIDE_SR ).w;
103
- If( blocker.greaterThan( 0.5 ), () => {
92
+ // Opaque fast-path: check the per-triangle blocker bit, set at extraction time when
93
+ // alphaMode/transparent/transmission/opacity all indicate a fully opaque surface.
94
+ // Short-circuits the 7-slot getShadowMaterial fetch and the whole alpha decision tree.
95
+ const flags = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 4 ), int( TRI_STRIDE ) ).z;
96
+ If( shadowFlagsSettle( flags ), () => {
104
97
 
105
98
  transmittance.assign( 0.0 );
106
99
  Break();
@@ -118,7 +111,8 @@ export const traceShadowRay = Fn( ( [
118
111
  // ---------------------------------------------------------------
119
112
  const alphaCutout = tslBool( false ).toVar();
120
113
 
121
- if ( _enableAlphaShadows ) If( _enableAlphaShadows.equal( int( 1 ) ), () => {
114
+ const alphaShadows = getAlphaShadowsUniform();
115
+ if ( alphaShadows ) If( alphaShadows.equal( int( 1 ) ), () => {
122
116
 
123
117
  // Sample texture alpha once (shared by MASK and BLEND paths).
124
118
  // Deferred UV: barycentrics in shadowHit.uv, triangle index in shadowHit.triangleIndex.
@@ -131,10 +125,9 @@ export const traceShadowRay = Fn( ( [
131
125
  const baryU = shadowHit.uv.x;
132
126
  const baryV = shadowHit.uv.y;
133
127
  const baryW = float( 1.0 ).sub( baryU ).sub( baryV );
134
- const TRI_STRIDE = int( 8 );
135
- const uvData1 = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 6 ), TRI_STRIDE );
136
- const uvData2 = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 7 ), TRI_STRIDE );
137
- const hitUV = uvData1.xy.mul( baryW ).add( uvData1.zw.mul( baryU ) ).add( uvData2.xy.mul( baryV ) );
128
+ const uvData1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 3 ), int( TRI_STRIDE ) ) );
129
+ const uvData2 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 4 ), int( TRI_STRIDE ) ).xy );
130
+ const hitUV = uvData1.xy.mul( baryW ).add( uvData1.zw.mul( baryU ) ).add( uvData2.mul( baryV ) );
138
131
  const albedoUV = getTransformedUV( { uv: hitUV, transform: shadowMaterial.albedoTransform } );
139
132
  texAlpha.assign( sampleBucket( _shadowAlbedoMaps, shadowMaterial.albedoMapIndex, albedoUV ).a );
140
133
 
@@ -188,11 +181,19 @@ export const traceShadowRay = Fn( ( [
188
181
  // Deferred geometric-normal compute — refetch triangle positions and
189
182
  // derive the normal here so opaque/alpha-cutout shadow hits don't pay
190
183
  // the cross+normalize cost in BVH traversal.
191
- const TRI_STRIDE_N = int( 8 );
192
- const pA = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 0 ), TRI_STRIDE_N ).xyz;
193
- const pB = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 1 ), TRI_STRIDE_N ).xyz;
194
- const pC = getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 2 ), TRI_STRIDE_N ).xyz;
195
- const geomNormal = normalize( cross( pB.sub( pA ), pC.sub( pA ) ) );
184
+ const pA = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 0 ), int( TRI_STRIDE ) ).xyz );
185
+ const pB = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 1 ), int( TRI_STRIDE ) ).xyz );
186
+ const pC = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, shadowHit.triangleIndex, int( 2 ), int( TRI_STRIDE ) ).xyz );
187
+ // Positions are in the blocker's object space; the cross product is a normal, so
188
+ // it rides the inverse-transpose back out rather than the forward matrix.
189
+ const objNormal = normalize( cross( pB.sub( pA ), pC.sub( pA ) ) ).toVar();
190
+ If( shadowHit.instanceLeaf.greaterThanEqual( int( 0 ) ), () => {
191
+
192
+ objNormal.assign( normalize( instanceFaceNormalToWorld( instanceRows( bvhBuffer, shadowHit.instanceLeaf ), objNormal ) ) );
193
+
194
+ } );
195
+
196
+ const geomNormal = objNormal;
196
197
  shadowHit.normal.assign( geomNormal );
197
198
 
198
199
  const entering = dot( dir, geomNormal ).lessThan( 0.0 );
@@ -56,7 +56,7 @@ import {
56
56
  readTransparentCount,
57
57
  readMisRayT,
58
58
  readHitDistance, readHitBarycentrics, readHitNormal,
59
- readHitMaterialIndex, readHitTriangleIndex,
59
+ readHitMaterialIndex, readHitTriangleIndex, readHitInstanceLeaf,
60
60
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
61
61
  writeGBuffer, writeGBufferHitDist, readGBuffer, gbDecodeNormalDepth,
62
62
  readRayRadiance,
@@ -219,6 +219,7 @@ export function buildShadeKernel( params ) {
219
219
  const hitUV = readHitBarycentrics( hitBufferRO, rayID ).toVar();
220
220
  const hitMatIdx = readHitMaterialIndex( hitBufferRO, rayID ).toVar();
221
221
  const hitTriIdx = readHitTriangleIndex( hitBufferRO, rayID ).toVar();
222
+ const hitInstance = readHitInstanceLeaf( hitBufferRO, rayID ).toVar();
222
223
 
223
224
  // per-ray camera-bounce depth — advances ONLY on opaque scatter (free bounces don't); drives termination (maxBounces). Megakernel: effectiveBounces.
224
225
  const cameraDepth = readPathBounces( rayBufferRW, rayID ).toVar();
@@ -620,7 +621,7 @@ export function buildShadeKernel( params ) {
620
621
  boxTests: int( 0 ), triTests: int( 0 ),
621
622
  } );
622
623
  const dispResult = DisplacementResult.wrap( refineDisplacedIntersection(
623
- dispRay, dispHit, triangleBuffer, material, bounceIndex,
624
+ dispRay, dispHit, triangleBuffer, material, bounceIndex, bvhBuffer, hitInstance,
624
625
  ) ).toVar();
625
626
  samplingUV.assign( dispResult.uv );
626
627
  displacedNormal.assign( dispResult.normal );
@@ -634,7 +635,7 @@ export function buildShadeKernel( params ) {
634
635
  const uvTangent = vec4( 0.0 ).toVar();
635
636
  If( material.normalMapIndex.greaterThanEqual( int( 0 ) ), () => {
636
637
 
637
- uvTangent.assign( triangleUVTangent( triangleBuffer, int( hitTriIdx ), N, material.normalTransform ) );
638
+ uvTangent.assign( triangleUVTangent( triangleBuffer, int( hitTriIdx ), N, material.normalTransform, bvhBuffer, hitInstance ) );
638
639
 
639
640
  } );
640
641
 
@@ -1024,7 +1025,7 @@ export function buildShadeKernel( params ) {
1024
1025
 
1025
1026
  lightPdf.assign( calculateLightBVHPdf(
1026
1027
  int( hitTriIdx ), misDist, direction, misOrigin,
1027
- lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer,
1028
+ lightBuffer, emissiveVec4Offset, reverseMapVec4Offset, triangleBuffer, bvhBuffer,
1028
1029
  ) );
1029
1030
 
1030
1031
  } ).Else( () => {
@@ -1032,6 +1033,7 @@ export function buildShadeKernel( params ) {
1032
1033
  lightPdf.assign( calculateEmissiveLightPdf(
1033
1034
  int( hitTriIdx ), misDist, direction, misOrigin,
1034
1035
  triangleBuffer, materialBuffer, emissiveTotalPower,
1036
+ bvhBuffer, hitInstance,
1035
1037
  ) );
1036
1038
 
1037
1039
  } );
@@ -1217,7 +1219,7 @@ export function buildShadeKernel( params ) {
1217
1219
  lightBuffer,
1218
1220
  lightBuffer,
1219
1221
  emissiveVec4Offset,
1220
- triangleBuffer,
1222
+ triangleBuffer, bvhBuffer,
1221
1223
  ) );
1222
1224
 
1223
1225
  // No rough-diffuse secondary-bounce skip here: dropping NEE while the emissive-hit
@@ -1274,7 +1276,7 @@ export function buildShadeKernel( params ) {
1274
1276
  _pixelCoord, resolution, frame, dimBase,
1275
1277
  emissiveBoost,
1276
1278
  lightBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1277
- triangleBuffer,
1279
+ triangleBuffer, bvhBuffer,
1278
1280
  traceShadowRayWrapped,
1279
1281
  calculateRayOffset,
1280
1282
  );
package/src/TSL/Struct.js CHANGED
@@ -122,6 +122,9 @@ export const HitInfo = struct( {
122
122
  uv: 'vec2',
123
123
  materialIndex: 'int',
124
124
  meshIndex: 'int',
125
+ // TLAS leaf that owns the hit: triangles are stored per instance in object space, so
126
+ // anything that needs them in world space reads the transform back off this node.
127
+ instanceLeaf: 'int',
125
128
  triangleIndex: 'int',
126
129
  boxTests: 'int',
127
130
  triTests: 'int',
@@ -1,4 +1,4 @@
1
- import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, dot, length, sign, abs, atan, mix, clamp, texture, textureSize } from 'three/tsl';
1
+ import { Fn, wgslFn, float, vec2, vec3, vec4, int, If, normalize, cross, dot, length, sign, abs, atan, mix, clamp, texture, textureSize, uintBitsToFloat } from 'three/tsl';
2
2
  import { DataArrayTexture, LinearFilter } from 'three';
3
3
 
4
4
  import {
@@ -6,7 +6,7 @@ import {
6
6
  MaterialSamples,
7
7
  ExtMapResult,
8
8
  } from './Struct.js';
9
- import { getDatafromStorageBuffer } from './Common.js';
9
+ import { getDatafromStorageBuffer, instanceRows, instanceDirToWorld, TRI_STRIDE } from './Common.js';
10
10
  import { TEXTURE_CONSTANTS } from '../EngineDefaults.js';
11
11
 
12
12
  // ================================================================================
@@ -405,15 +405,22 @@ const uvTransformJacobian = /*@__PURE__*/ wgslFn( `
405
405
  * @param {Node} geometryNormal shading-space normal, for the handedness test
406
406
  * @param {Node} transform the normal map's mat3 UV transform
407
407
  */
408
- export const triangleUVTangent = Fn( ( [ triangleBuffer, triIndex, geometryNormal, transform ] ) => {
408
+ /**
409
+ * Tangent frame for a triangle, in world space.
410
+ *
411
+ * Positions are stored per instance in object space, so T and B come out in that space
412
+ * and are carried across before they meet the world-space geometric normal — otherwise
413
+ * the handedness test mixes two frames and normal maps light from the wrong side.
414
+ */
415
+ export const triangleUVTangent = Fn( ( [ triangleBuffer, triIndex, geometryNormal, transform, bvhBuffer, instanceLeaf ] ) => {
409
416
 
410
- const S = int( 8 );
411
- const pA = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), S ).xyz;
412
- const e1 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), S ).xyz.sub( pA );
413
- const e2 = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), S ).xyz.sub( pA );
417
+ const S = int( TRI_STRIDE );
418
+ const pA = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 0 ), S ).xyz );
419
+ const e1 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 1 ), S ).xyz ).sub( pA );
420
+ const e2 = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 2 ), S ).xyz ).sub( pA );
414
421
 
415
- const uvAB = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 6 ), S );
416
- const uvC = getDatafromStorageBuffer( triangleBuffer, triIndex, int( 7 ), S ).xy;
422
+ const uvAB = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 3 ), S ) );
423
+ const uvC = uintBitsToFloat( getDatafromStorageBuffer( triangleBuffer, triIndex, int( 4 ), S ).xy );
417
424
  const d1 = uvAB.zw.sub( uvAB.xy );
418
425
  const d2 = uvC.sub( uvAB.xy );
419
426
 
@@ -436,6 +443,14 @@ export const triangleUVTangent = Fn( ( [ triangleBuffer, triIndex, geometryNorma
436
443
 
437
444
  } );
438
445
 
446
+ If( instanceLeaf.greaterThanEqual( int( 0 ) ), () => {
447
+
448
+ const rows = instanceRows( bvhBuffer, instanceLeaf );
449
+ T.assign( instanceDirToWorld( { r0: rows[ 0 ].xyz, r1: rows[ 1 ].xyz, r2: rows[ 2 ].xyz, v: T } ) );
450
+ B.assign( instanceDirToWorld( { r0: rows[ 0 ].xyz, r1: rows[ 1 ].xyz, r2: rows[ 2 ].xyz, v: B } ) );
451
+
452
+ } );
453
+
439
454
  If( length( T ).greaterThan( 1e-12 ), () => {
440
455
 
441
456
  result.assign( vec4( normalize( T ), sign( dot( cross( geometryNormal, T ), B ) ) ) );