rayzee 6.0.0 → 6.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 (44) hide show
  1. package/README.md +5 -0
  2. package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -1
  3. package/dist/rayzee.es.js +2396 -2072
  4. package/dist/rayzee.es.js.map +1 -1
  5. package/dist/rayzee.umd.js +49 -53
  6. package/dist/rayzee.umd.js.map +1 -1
  7. package/package.json +2 -2
  8. package/src/EngineDefaults.js +3 -0
  9. package/src/PathTracerApp.js +18 -8
  10. package/src/Pipeline/RenderStage.js +3 -0
  11. package/src/Processor/IESParser.js +340 -0
  12. package/src/Processor/LightSerializer.js +32 -4
  13. package/src/Processor/SceneProcessor.js +0 -1
  14. package/src/Processor/ShaderBuilder.js +40 -1
  15. package/src/Processor/Workers/TexturesWorker.js +1 -1
  16. package/src/RenderSettings.js +3 -0
  17. package/src/Stages/NormalDepth.js +3 -19
  18. package/src/Stages/PathTracer.js +15 -9
  19. package/src/TSL/BVHTraversal.js +5 -18
  20. package/src/TSL/Clearcoat.js +1 -5
  21. package/src/TSL/Common.js +2 -2
  22. package/src/TSL/Debugger.js +0 -14
  23. package/src/TSL/EmissiveSampling.js +20 -22
  24. package/src/TSL/Environment.js +60 -14
  25. package/src/TSL/Fresnel.js +13 -4
  26. package/src/TSL/LightsCore.js +238 -5
  27. package/src/TSL/LightsDirect.js +16 -5
  28. package/src/TSL/LightsIndirect.js +6 -38
  29. package/src/TSL/LightsSampling.js +119 -185
  30. package/src/TSL/MaterialEvaluation.js +25 -14
  31. package/src/TSL/MaterialProperties.js +14 -34
  32. package/src/TSL/MaterialTransmission.js +100 -222
  33. package/src/TSL/PathTracer.js +5 -4
  34. package/src/TSL/PathTracerCore.js +152 -140
  35. package/src/TSL/Struct.js +7 -1
  36. package/src/TSL/TextureSampling.js +2 -2
  37. package/src/index.js +2 -0
  38. package/src/managers/AnimationManager.js +3 -6
  39. package/src/managers/DenoisingManager.js +1 -1
  40. package/src/managers/GoboManager.js +277 -0
  41. package/src/managers/IESManager.js +268 -0
  42. package/src/managers/LightManager.js +33 -1
  43. package/src/managers/TransformManager.js +3 -3
  44. package/src/managers/UniformManager.js +5 -5
@@ -7,8 +7,7 @@
7
7
  * Contains:
8
8
  * - getOrCreateMaterialClassification — cached material classification
9
9
  * - generateSampledDirection — BRDF direction sampling with multi-lobe CDF
10
- * - estimatePathContribution — path importance estimation
11
- * - handleRussianRoulette — adaptive path termination
10
+ * - handleRussianRoulette adaptive path termination (inlines path importance)
12
11
  * - sampleBackgroundLighting — environment background sampling
13
12
  * - regularizePathContribution — firefly suppression
14
13
  * - Trace — main path tracing loop
@@ -26,6 +25,7 @@ import {
26
25
  max,
27
26
  min,
28
27
  exp,
28
+ log,
29
29
  clamp,
30
30
  mix,
31
31
  dot,
@@ -57,6 +57,7 @@ import {
57
57
  getMaterial,
58
58
  powerHeuristic,
59
59
  balanceHeuristic,
60
+ computeDotProducts,
60
61
  } from './Common.js';
61
62
  import {
62
63
  DirectionSample,
@@ -68,10 +69,11 @@ import {
68
69
  MaterialSamples,
69
70
  RayTracingMaterial,
70
71
  ImportanceSamplingInfo,
72
+ DotProducts,
71
73
  } from './Struct.js';
72
74
  import { RandomValue, getRandomSample } from './Random.js';
73
75
  import { traverseBVH } from './BVHTraversal.js';
74
- import { sampleEnvironment, sampleEquirect } from './Environment.js';
76
+ import { sampleEnvironment, sampleEquirect, getGroundProjectedDirection } from './Environment.js';
75
77
  import { sampleAllMaterialTextures } from './TextureSampling.js';
76
78
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
77
79
  import { handleMaterialTransparency, MaterialInteractionResult, sampleMicrofacetTransmission, MicrofacetTransmissionResult } from './MaterialTransmission.js';
@@ -82,7 +84,7 @@ import {
82
84
  createMaterialCache,
83
85
  getImportanceSamplingInfo,
84
86
  } from './MaterialProperties.js';
85
- import { evaluateMaterialResponse } from './MaterialEvaluation.js';
87
+ import { evaluateMaterialResponse, evaluateMaterialResponseFromDots } from './MaterialEvaluation.js';
86
88
  import { dielectricF0 } from './Fresnel.js';
87
89
  import {
88
90
  ImportanceSampleCosine,
@@ -90,7 +92,7 @@ import {
90
92
  sampleGGXVNDF,
91
93
  } from './MaterialSampling.js';
92
94
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
93
- import { calculateDirectLightingUnified, calculateMaterialPDF } from './LightsSampling.js';
95
+ import { calculateDirectLightingUnified, calculateMaterialPDFFromDots } from './LightsSampling.js';
94
96
  import { calculateIndirectLighting } from './LightsIndirect.js';
95
97
  import { IndirectLightingResult } from './LightsCore.js';
96
98
  import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
@@ -152,9 +154,11 @@ export const getOrCreateMaterialClassification = Fn( ( [
152
154
  // =============================================================================
153
155
 
154
156
  export const generateSampledDirection = Fn( ( [
155
- V, N, material, materialIndex, xi, rngState,
156
- // PathState cache fields
157
- classificationCached, lastMaterialIndex, cachedClassification,
157
+ V, N, material, xi, rngState,
158
+ // Caller-resolved material classification (avoids redundant classifyMaterial —
159
+ // TSL Fn can't write back to caller variables, so the caller is responsible
160
+ // for keeping psCachedClassification current and passes it in here).
161
+ mc,
158
162
  weightsComputed, cachedBrdfWeights,
159
163
  materialCacheCached, cachedMaterialCache,
160
164
  ] ) => {
@@ -163,12 +167,6 @@ export const generateSampledDirection = Fn( ( [
163
167
  const resultValue = vec3( 0.0 ).toVar();
164
168
  const resultPdf = float( 0.0 ).toVar();
165
169
 
166
- // Get material classification (cached or computed)
167
- const mc = MaterialClassification.wrap( getOrCreateMaterialClassification(
168
- material, materialIndex,
169
- classificationCached, lastMaterialIndex, cachedClassification,
170
- ) ).toVar();
171
-
172
170
  // Compute BRDF weights
173
171
  const weights = cachedBrdfWeights.toVar();
174
172
 
@@ -291,8 +289,9 @@ export const generateSampledDirection = Fn( ( [
291
289
  If( sampled.not(), () => {
292
290
 
293
291
  const entering = dot( V, N ).greaterThan( 0.0 ).toVar();
292
+ // pathWavelength=0 — only direction/PDF are consumed here, throughput goes via handleTransmission
294
293
  const mtResult = MicrofacetTransmissionResult.wrap( sampleMicrofacetTransmission(
295
- V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState,
294
+ V, N, material.ior, material.roughness, entering, material.dispersion, xi, rngState, float( 0.0 ),
296
295
  ) );
297
296
  resultDirection.assign( mtResult.direction );
298
297
  resultPdf.assign( max( mtResult.pdf, MIN_PDF ) );
@@ -311,63 +310,6 @@ export const generateSampledDirection = Fn( ( [
311
310
 
312
311
  } );
313
312
 
314
- // =============================================================================
315
- // Path Contribution Estimation
316
- // =============================================================================
317
-
318
- export const estimatePathContribution = Fn( ( [
319
- throughput, direction, material, materialIndex,
320
- classificationCached, lastMaterialIndex, cachedClassification,
321
- enableEnvironmentLight, useEnvMapIS,
322
- ] ) => {
323
-
324
- const throughputStrength = max( maxComponent( { v: throughput } ), 0.0 ).toVar();
325
-
326
- // Use cached material classification
327
- const mc = MaterialClassification.wrap( getOrCreateMaterialClassification(
328
- material, materialIndex,
329
- classificationCached, lastMaterialIndex, cachedClassification,
330
- ) ).toVar();
331
-
332
- // Enhanced material importance with interaction bonuses
333
- const materialImportance = mc.complexityScore.toVar();
334
-
335
- // Interaction complexity bonuses
336
- If( mc.isMetallic.and( mc.isSmooth ), () => {
337
-
338
- materialImportance.addAssign( 0.15 );
339
-
340
- } );
341
- If( mc.isTransmissive.and( mc.hasClearcoat ), () => {
342
-
343
- materialImportance.addAssign( 0.12 );
344
-
345
- } );
346
- If( mc.isEmissive, () => {
347
-
348
- materialImportance.addAssign( 0.1 );
349
-
350
- } );
351
- materialImportance.assign( clamp( materialImportance, 0.0, 1.0 ) );
352
-
353
- // Direction importance calculation
354
- const directionImportance = float( 0.5 ).toVar();
355
-
356
- If( enableEnvironmentLight.and( useEnvMapIS ).and( throughputStrength.greaterThan( 0.01 ) ), () => {
357
-
358
- const cosTheta = clamp( direction.y, 0.0, 1.0 );
359
- directionImportance.assign( mix( float( 0.3 ), float( 0.8 ), cosTheta.mul( cosTheta ) ) );
360
-
361
- } );
362
-
363
- // Enhanced weighting
364
- const throughputWeight = smoothstep( float( 0.001 ), float( 0.1 ), throughputStrength );
365
- return throughputStrength.mul(
366
- mix( materialImportance.mul( 0.7 ), directionImportance, 0.3 ),
367
- ).mul( throughputWeight );
368
-
369
- } );
370
-
371
313
  // =============================================================================
372
314
  // Russian Roulette Path Termination
373
315
  // =============================================================================
@@ -375,7 +317,6 @@ export const estimatePathContribution = Fn( ( [
375
317
  export const handleRussianRoulette = Fn( ( [
376
318
  depth, throughput, material, materialIndex, rayDirection, rngState,
377
319
  classificationCached, lastMaterialIndex, cachedClassification,
378
- weightsComputed, pathImportance,
379
320
  enableEnvironmentLight, useEnvMapIS,
380
321
  ] ) => {
381
322
 
@@ -443,22 +384,37 @@ export const handleRussianRoulette = Fn( ( [
443
384
 
444
385
  } ).Else( () => {
445
386
 
446
- // Path importanceused across all depth ranges
447
- const pathContribution = float( 0.0 ).toVar();
387
+ // Path contribution estimate reuses throughputStrength + mc from outer scope.
388
+ const estMaterialImportance = mc.complexityScore.toVar();
389
+ If( mc.isMetallic.and( mc.isSmooth ), () => {
448
390
 
449
- If( classificationCached.and( weightsComputed ), () => {
391
+ estMaterialImportance.addAssign( 0.15 );
450
392
 
451
- pathContribution.assign( pathImportance );
393
+ } );
394
+ If( mc.isTransmissive.and( mc.hasClearcoat ), () => {
452
395
 
453
- } ).Else( () => {
396
+ estMaterialImportance.addAssign( 0.12 );
454
397
 
455
- pathContribution.assign( estimatePathContribution(
456
- throughput, rayDirection, material, materialIndex,
457
- classificationCached, lastMaterialIndex, cachedClassification,
458
- enableEnvironmentLight, useEnvMapIS,
459
- ) );
398
+ } );
399
+ If( mc.isEmissive, () => {
400
+
401
+ estMaterialImportance.addAssign( 0.1 );
460
402
 
461
403
  } );
404
+ estMaterialImportance.assign( clamp( estMaterialImportance, 0.0, 1.0 ) );
405
+
406
+ const directionImportance = float( 0.5 ).toVar();
407
+ If( enableEnvironmentLight.and( useEnvMapIS ).and( throughputStrength.greaterThan( 0.01 ) ), () => {
408
+
409
+ const cosTheta = clamp( rayDirection.y, 0.0, 1.0 );
410
+ directionImportance.assign( mix( float( 0.3 ), float( 0.8 ), cosTheta.mul( cosTheta ) ) );
411
+
412
+ } );
413
+
414
+ const throughputWeight = smoothstep( float( 0.001 ), float( 0.1 ), throughputStrength );
415
+ const pathContribution = throughputStrength.mul(
416
+ mix( estMaterialImportance.mul( 0.7 ), directionImportance, 0.3 ),
417
+ ).mul( throughputWeight ).toVar();
462
418
 
463
419
  // Smooth adaptive continuation probability (no discrete depth brackets)
464
420
  // Early behavior: throughput + material driven, generous
@@ -515,9 +471,10 @@ export const handleRussianRoulette = Fn( ( [
515
471
  // =============================================================================
516
472
 
517
473
  export const sampleBackgroundLighting = Fn( ( [
518
- isPrimaryRay, direction,
474
+ isPrimaryRay, rayOrigin, direction,
519
475
  envTexture, envMatrix, environmentIntensity, enableEnvironmentLight,
520
476
  showBackground, backgroundIntensity,
477
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
521
478
  ] ) => {
522
479
 
523
480
  // Only hide background for primary camera rays when showBackground is false
@@ -530,8 +487,18 @@ export const sampleBackgroundLighting = Fn( ( [
530
487
 
531
488
  } ).Else( () => {
532
489
 
490
+ // Primary-ray only: indirect bounces must see the raw envmap so shading stays physically correct.
491
+ const effectiveDir = direction.toVar();
492
+ If( isPrimaryRay.and( groundProjectionEnabled ), () => {
493
+
494
+ effectiveDir.assign( getGroundProjectedDirection(
495
+ rayOrigin, direction, groundProjectionRadius, groundProjectionHeight,
496
+ ) );
497
+
498
+ } );
499
+
533
500
  const sampled = sampleEnvironment( {
534
- tex: envTexture, samp: sampler( envTexture ), direction, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight,
501
+ tex: envTexture, samp: sampler( envTexture ), direction: effectiveDir, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight,
535
502
  } );
536
503
 
537
504
  If( isPrimaryRay, () => {
@@ -566,7 +533,7 @@ export const regularizePathContribution = /*@__PURE__*/ wgslFn( `
566
533
  // =============================================================================
567
534
 
568
535
  export const Trace = Fn( ( [
569
- ray, rngState, rayIndex, pixelIndex,
536
+ ray, rngState, rayIndex,
570
537
  // BVH / Scene
571
538
  bvhBuffer,
572
539
  triangleBuffer,
@@ -585,11 +552,12 @@ export const Trace = Fn( ( [
585
552
  envCDFBuffer,
586
553
  envTotalSum, envCompensationDelta, envResolution,
587
554
  enableEnvironmentLight, useEnvMapIS,
555
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
588
556
  // Rendering parameters
589
557
  maxBounceCount, transmissiveBounces,
590
558
  backgroundIntensity, showBackground, transparentBackground,
591
559
  fireflyThreshold, globalIlluminationIntensity,
592
- totalTriangleCount, enableEmissiveTriangleSampling,
560
+ enableEmissiveTriangleSampling,
593
561
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
594
562
  lightBVHBuffer, lightBVHNodeCount,
595
563
  // Per-pixel info
@@ -613,11 +581,29 @@ export const Trace = Fn( ( [
613
581
  // behind glass), not the glass surface itself.
614
582
  const auxLocked = tslBool( false ).toVar();
615
583
 
616
- // Medium stack for transmission (per-slot IOR, slots 1-3 for nested media, depth 0 = air)
584
+ // Medium stack for transmission (slots 1-3 for nested media, depth 0 = air).
585
+ // Each slot tracks IOR plus KHR_materials_volume attenuation (color + distance)
586
+ // so per-bounce in-volume absorption uses the actual ray path length.
617
587
  const mediumStackDepth = int( 0 ).toVar();
618
588
  const mediumStack_ior_1 = float( 1.0 ).toVar();
619
589
  const mediumStack_ior_2 = float( 1.0 ).toVar();
620
590
  const mediumStack_ior_3 = float( 1.0 ).toVar();
591
+ const mediumStack_attColor_1 = vec3( 1.0 ).toVar();
592
+ const mediumStack_attColor_2 = vec3( 1.0 ).toVar();
593
+ const mediumStack_attColor_3 = vec3( 1.0 ).toVar();
594
+ const mediumStack_attDist_1 = float( 0.0 ).toVar();
595
+ const mediumStack_attDist_2 = float( 0.0 ).toVar();
596
+ const mediumStack_attDist_3 = float( 0.0 ).toVar();
597
+ // Precomputed Beer-Lambert absorption coefficient sigma_a = -log(attColor)/attDist.
598
+ // Stored at push time so per-bounce absorption inside a medium becomes a single
599
+ // exp(-sigma_a * thickness) instead of a log + div + exp every bounce.
600
+ const mediumStack_sigmaA_1 = vec3( 0.0 ).toVar();
601
+ const mediumStack_sigmaA_2 = vec3( 0.0 ).toVar();
602
+ const mediumStack_sigmaA_3 = vec3( 0.0 ).toVar();
603
+
604
+ // Locked at the first dispersive transmission; reused for subsequent transmissions on
605
+ // the path so multi-bounce dispersion doesn't collapse under repeated colorWeight ×.
606
+ const pathWavelength = float( 0.0 ).toVar();
621
607
 
622
608
  // Render state
623
609
  const stateTraversals = maxBounceCount.toVar();
@@ -630,8 +616,6 @@ export const Trace = Fn( ( [
630
616
  const psWeightsComputed = tslBool( false ).toVar();
631
617
  const psClassificationCached = tslBool( false ).toVar();
632
618
  const psMaterialCacheCached = tslBool( false ).toVar();
633
- const psTexturesLoaded = tslBool( false ).toVar();
634
- const psPathImportance = float( 0.0 ).toVar();
635
619
  const psLastMaterialIndex = int( - 1 ).toVar();
636
620
 
637
621
  // Cached classification
@@ -688,16 +672,41 @@ export const Trace = Fn( ( [
688
672
  currentRay,
689
673
  bvhBuffer,
690
674
  triangleBuffer,
691
- materialBuffer,
692
675
  ) ).toVar();
693
676
 
677
+ // KHR_materials_volume: apply Beer's law over the actual distance the ray
678
+ // traveled inside the current medium. Top-of-stack holds the medium the ray
679
+ // is currently in — depth==0 means air (no absorption). sigma_a was
680
+ // precomputed at push time, so this collapses to a single exp().
681
+ If( hitInfo.didHit.and( mediumStackDepth.greaterThan( int( 0 ) ) ), () => {
682
+
683
+ const mSigmaA = vec3( 0.0 ).toVar();
684
+ If( mediumStackDepth.equal( int( 1 ) ), () => {
685
+
686
+ mSigmaA.assign( mediumStack_sigmaA_1 );
687
+
688
+ } ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
689
+
690
+ mSigmaA.assign( mediumStack_sigmaA_2 );
691
+
692
+ } ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
693
+
694
+ mSigmaA.assign( mediumStack_sigmaA_3 );
695
+
696
+ } );
697
+
698
+ throughput.mulAssign( exp( mSigmaA.mul( hitInfo.dst ).negate() ) );
699
+
700
+ } );
701
+
694
702
  If( hitInfo.didHit.not(), () => {
695
703
 
696
704
  // ENVIRONMENT LIGHTING
697
705
  const envColor = sampleBackgroundLighting(
698
- stateIsPrimaryRay, rayDirection,
706
+ stateIsPrimaryRay, rayOrigin, rayDirection,
699
707
  envTexture, envMatrix, environmentIntensity, enableEnvironmentLight,
700
708
  showBackground, backgroundIntensity,
709
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
701
710
  );
702
711
 
703
712
  // MIS weight for implicit environment hit — prevents double-counting with NEE.
@@ -799,10 +808,12 @@ export const Trace = Fn( ( [
799
808
 
800
809
  // Handle transparent materials
801
810
  const interaction = MaterialInteractionResult.wrap( handleMaterialTransparency(
802
- currentRay, hitInfo.hitPoint, N, material, rngState,
811
+ currentRay, N, material, rngState,
803
812
  stateTransmissiveTraversals,
804
813
  currentMediumIOR, previousMediumIOR,
814
+ pathWavelength,
805
815
  ) ).toVar();
816
+ pathWavelength.assign( interaction.pathWavelength );
806
817
 
807
818
  If( interaction.continueRay, () => {
808
819
 
@@ -819,22 +830,39 @@ export const Trace = Fn( ( [
819
830
 
820
831
  If( interaction.entering, () => {
821
832
 
822
- // Push new medium onto stack
833
+ // Push new medium onto stack (IOR + KHR_materials_volume attenuation)
823
834
  If( mediumStackDepth.lessThan( int( 3 ) ), () => {
824
835
 
825
836
  mediumStackDepth.addAssign( 1 );
826
837
 
838
+ // Precompute sigma_a = -log(attColor)/attDist once at push time.
839
+ // attDist==0 means "no absorption" — store sigma_a=0 so exp() returns 1.
840
+ const mSigmaA = select(
841
+ material.attenuationDistance.greaterThan( 0.0 ),
842
+ log( max( material.attenuationColor, vec3( 0.001 ) ) ).negate().div( material.attenuationDistance ),
843
+ vec3( 0.0 )
844
+ ).toVar();
845
+
827
846
  If( mediumStackDepth.equal( int( 1 ) ), () => {
828
847
 
829
848
  mediumStack_ior_1.assign( material.ior );
849
+ mediumStack_attColor_1.assign( material.attenuationColor );
850
+ mediumStack_attDist_1.assign( material.attenuationDistance );
851
+ mediumStack_sigmaA_1.assign( mSigmaA );
830
852
 
831
853
  } ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
832
854
 
833
855
  mediumStack_ior_2.assign( material.ior );
856
+ mediumStack_attColor_2.assign( material.attenuationColor );
857
+ mediumStack_attDist_2.assign( material.attenuationDistance );
858
+ mediumStack_sigmaA_2.assign( mSigmaA );
834
859
 
835
860
  } ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
836
861
 
837
862
  mediumStack_ior_3.assign( material.ior );
863
+ mediumStack_attColor_3.assign( material.attenuationColor );
864
+ mediumStack_attDist_3.assign( material.attenuationDistance );
865
+ mediumStack_sigmaA_3.assign( mSigmaA );
838
866
 
839
867
  } );
840
868
 
@@ -908,6 +936,19 @@ export const Trace = Fn( ( [
908
936
  const V = rayDirection.negate().toVar();
909
937
  material.sheenRoughness.assign( clamp( material.sheenRoughness, MIN_ROUGHNESS, MAX_ROUGHNESS ) );
910
938
 
939
+ // Sync material classification cache up front — the materialCache, BRDF
940
+ // sample, importance sampling, and Russian roulette all consume it.
941
+ // getOrCreateMaterialClassification is a cache hit when materialIndex
942
+ // matches the previous bounce; otherwise it runs classifyMaterial once.
943
+ // Doing this here eliminates a redundant classifyMaterial that previously
944
+ // fired after generateSampledDirection to "sync" the caller's variable.
945
+ psCachedClassification.assign( MaterialClassification.wrap( getOrCreateMaterialClassification(
946
+ material, hitInfo.materialIndex,
947
+ psClassificationCached, psLastMaterialIndex, psCachedClassification,
948
+ ) ) );
949
+ psClassificationCached.assign( tslBool( true ) );
950
+ psLastMaterialIndex.assign( hitInfo.materialIndex );
951
+
911
952
  // Create material cache if needed
912
953
  If( psMaterialCacheCached.not(), () => {
913
954
 
@@ -933,9 +974,12 @@ export const Trace = Fn( ( [
933
974
 
934
975
  } ).Else( () => {
935
976
 
977
+ // Classification was already synced at the top of the bounce — pass
978
+ // psCachedClassification directly so generateSampledDirection doesn't
979
+ // have to call classifyMaterial again internally.
936
980
  const brdfSample = DirectionSample.wrap( generateSampledDirection(
937
- V, N, material, hitInfo.materialIndex, randomSample, rngState,
938
- psClassificationCached, psLastMaterialIndex, psCachedClassification,
981
+ V, N, material, randomSample, rngState,
982
+ psCachedClassification,
939
983
  psWeightsComputed, psCachedBrdfWeights,
940
984
  psMaterialCacheCached, psCachedMaterialCache,
941
985
  ) );
@@ -943,22 +987,6 @@ export const Trace = Fn( ( [
943
987
  brdfValue.assign( brdfSample.value );
944
988
  brdfPdf.assign( brdfSample.pdf );
945
989
 
946
- // Sync psCachedClassification for downstream consumers (importance sampling, Russian roulette).
947
- // generateSampledDirection computed the correct classification internally via materialIndex
948
- // guard, but TSL Fn can't write back to the caller's variable — update it here.
949
- If( psLastMaterialIndex.notEqual( hitInfo.materialIndex ).or( psClassificationCached.not() ), () => {
950
-
951
- psCachedClassification.assign( classifyMaterial(
952
- material.metalness, material.roughness,
953
- material.transmission, material.clearcoat,
954
- material.emissive,
955
- ) );
956
-
957
- } );
958
-
959
- // Update cache state after generateSampledDirection
960
- psClassificationCached.assign( tslBool( true ) );
961
- psLastMaterialIndex.assign( hitInfo.materialIndex );
962
990
  psWeightsComputed.assign( tslBool( true ) );
963
991
 
964
992
  } );
@@ -996,7 +1024,7 @@ export const Trace = Fn( ( [
996
1024
  hitInfo.hitPoint, N, material,
997
1025
  V,
998
1026
  brdfDir, brdfPdf, brdfValue,
999
- rayIndex, bounceIndex, rngState,
1027
+ bounceIndex, rngState,
1000
1028
  directionalLightsBuffer, numDirectionalLights,
1001
1029
  areaLightsBuffer, numAreaLights,
1002
1030
  pointLightsBuffer, numPointLights,
@@ -1017,10 +1045,9 @@ export const Trace = Fn( ( [
1017
1045
  // 2b. EMISSIVE TRIANGLE DIRECT LIGHTING
1018
1046
  If( enableEmissiveTriangleSampling.equal( int( 1 ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
1019
1047
 
1020
- // Wrapper binding BVH params (EmissiveSampling expects 4-param callback)
1021
- const traceShadowRayWrapped = Fn( ( [ origin, dir, maxDist, rs ] ) => {
1048
+ const traceShadowRayWrapped = Fn( ( [ origin, dir, maxDist ] ) => {
1022
1049
 
1023
- return traceShadowRay( origin, dir, maxDist, rs, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer );
1050
+ return traceShadowRay( origin, dir, maxDist, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer );
1024
1051
 
1025
1052
  } );
1026
1053
 
@@ -1050,12 +1077,14 @@ export const Trace = Fn( ( [
1050
1077
  const rayOffset = calculateRayOffset( hitInfo.hitPoint, N, material );
1051
1078
  const rayOrigin = hitInfo.hitPoint.add( rayOffset );
1052
1079
  const shadowDist = emissiveSample.distance.sub( 0.001 );
1053
- const visibility = traceShadowRayWrapped( rayOrigin, emissiveSample.direction, shadowDist, rngState );
1080
+ const visibility = traceShadowRayWrapped( rayOrigin, emissiveSample.direction, shadowDist );
1054
1081
 
1055
1082
  If( visibility.greaterThan( 0.0 ), () => {
1056
1083
 
1057
- const brdfValue = evaluateMaterialResponse( V, emissiveSample.direction, N, material );
1058
- const brdfPdf = calculateMaterialPDF( V, emissiveSample.direction, N, material );
1084
+ // Share H + dot products between BRDF eval and PDF eval.
1085
+ const emisDots = DotProducts.wrap( computeDotProducts( N, V, emissiveSample.direction ) );
1086
+ const brdfValue = evaluateMaterialResponseFromDots( material, emisDots );
1087
+ const brdfPdf = calculateMaterialPDFFromDots( material, emisDots );
1059
1088
  const misWeight = select(
1060
1089
  brdfPdf.greaterThan( 0.0 ),
1061
1090
  powerHeuristic( { pdf1: emissiveSample.pdf, pdf2: brdfPdf } ),
@@ -1082,12 +1111,11 @@ export const Trace = Fn( ( [
1082
1111
  // Fallback: flat CDF importance sampling
1083
1112
  const emissiveLight = calculateEmissiveTriangleContribution(
1084
1113
  hitInfo.hitPoint, N, V, material,
1085
- totalTriangleCount, bounceIndex, rngState,
1114
+ bounceIndex, rngState,
1086
1115
  emissiveBoost,
1087
1116
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1088
1117
  triangleBuffer,
1089
1118
  traceShadowRayWrapped,
1090
- evaluateMaterialResponse,
1091
1119
  calculateRayOffset,
1092
1120
  );
1093
1121
 
@@ -1099,34 +1127,19 @@ export const Trace = Fn( ( [
1099
1127
 
1100
1128
  } );
1101
1129
 
1102
- // Get importance sampling info with caching
1103
- If( psWeightsComputed.not().or( bounceIndex.equal( int( 0 ) ) ), () => {
1104
-
1105
- // Update classification first
1106
- psCachedClassification.assign( MaterialClassification.wrap( getOrCreateMaterialClassification(
1107
- material, hitInfo.materialIndex,
1108
- psClassificationCached, psLastMaterialIndex, psCachedClassification,
1109
- ) ) );
1110
- psClassificationCached.assign( tslBool( true ) );
1111
- psLastMaterialIndex.assign( hitInfo.materialIndex );
1112
-
1113
- } );
1114
-
1130
+ // Classification was already synced at the top of the bounce loop, so
1131
+ // psCachedClassification is current here regardless of which BRDF sample
1132
+ // path ran above.
1115
1133
  const samplingInfo = ImportanceSamplingInfo.wrap( getImportanceSamplingInfo(
1116
1134
  material, bounceIndex, psCachedClassification,
1117
- environmentIntensity, useEnvMapIS, enableEnvironmentLight,
1118
1135
  ) );
1119
1136
 
1120
1137
  // 3. INDIRECT LIGHTING
1121
1138
  const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
1122
1139
  V, N, material,
1123
1140
  brdfDir, brdfPdf, brdfValue,
1124
- rayIndex, bounceIndex,
1125
1141
  rngState,
1126
1142
  samplingInfo,
1127
- envTexture, environmentIntensity, envMatrix,
1128
- envTotalSum, envCompensationDelta, envResolution,
1129
- enableEnvironmentLight, useEnvMapIS,
1130
1143
  ) );
1131
1144
  throughput.mulAssign( indirectResult.throughput );
1132
1145
 
@@ -1185,7 +1198,6 @@ export const Trace = Fn( ( [
1185
1198
  bounceIndex, throughput, material, hitInfo.materialIndex,
1186
1199
  rayDirection, rngState,
1187
1200
  psClassificationCached, psLastMaterialIndex, psCachedClassification,
1188
- psWeightsComputed, psPathImportance,
1189
1201
  enableEnvironmentLight, useEnvMapIS,
1190
1202
  );
1191
1203
  If( rrSurvivalProb.lessThanEqual( 0.0 ), () => {
package/src/TSL/Struct.js CHANGED
@@ -127,7 +127,6 @@ export const ImportanceSamplingInfo = struct( {
127
127
  specularImportance: 'float',
128
128
  transmissionImportance: 'float',
129
129
  clearcoatImportance: 'float',
130
- envmapImportance: 'float',
131
130
  } );
132
131
 
133
132
  export const DotProducts = struct( {
@@ -138,6 +137,13 @@ export const DotProducts = struct( {
138
137
  LoH: 'float', // Light • Half
139
138
  } );
140
139
 
140
+ // Kulla-Conty DFG approximation outputs (computed once, consumed by both
141
+ // the multiscatter compensation factor and the total directional albedo).
142
+ export const DFGResult = struct( {
143
+ compensation: 'vec3', // 1 + F0 * (1/Ew - 1)
144
+ E_total: 'vec3', // clamp(E_ss * compensation, 0, 1)
145
+ } );
146
+
141
147
  export const MaterialSamples = struct( {
142
148
  albedo: 'vec4',
143
149
  emissive: 'vec3',
@@ -307,7 +307,7 @@ export const processBump = Fn( ( [ bumpMaps, currentNormal, material, uvCache ]
307
307
 
308
308
  } );
309
309
 
310
- export const processEmissive = Fn( ( [ emissiveMaps, material, albedoColor, uvCache ] ) => {
310
+ export const processEmissive = Fn( ( [ emissiveMaps, material, uvCache ] ) => {
311
311
 
312
312
  const emissionBase = material.emissive.mul( material.emissiveIntensity ).toVar();
313
313
 
@@ -361,7 +361,7 @@ export const sampleAllMaterialTextures = Fn( ( [
361
361
  const currentNormal = processNormal( normalMaps, geometryNormal, material, uvCache ).toVar();
362
362
  normal.assign( processBump( bumpMaps, currentNormal, material, uvCache ) );
363
363
 
364
- emissive.assign( processEmissive( emissiveMaps, material, albedo, uvCache ) );
364
+ emissive.assign( processEmissive( emissiveMaps, material, uvCache ) );
365
365
 
366
366
  } );
367
367
 
package/src/index.js CHANGED
@@ -39,6 +39,8 @@ export {
39
39
  export { RenderSettings } from './RenderSettings.js';
40
40
  export { CameraManager } from './managers/CameraManager.js';
41
41
  export { LightManager } from './managers/LightManager.js';
42
+ export { GoboManager } from './managers/GoboManager.js';
43
+ export { IESManager } from './managers/IESManager.js';
42
44
  export { DenoisingManager } from './managers/DenoisingManager.js';
43
45
  export { OverlayManager } from './managers/OverlayManager.js';
44
46
 
@@ -27,7 +27,6 @@ export class AnimationManager extends EventDispatcher {
27
27
  this._posBuffer = null; // Float32Array(triCount * 9) — reused each frame
28
28
  this._tempVec = new Vector3();
29
29
  this._skinnedCache = null; // per-mesh Float32Array for skinned vertex positions
30
- this._totalTriangleCount = 0;
31
30
  this._clipsCache = null;
32
31
  this._savedTimeScale = 1;
33
32
  this.onFinished = null; // callback when a non-looping clip ends
@@ -45,9 +44,8 @@ export class AnimationManager extends EventDispatcher {
45
44
  * @param {Object3D} mixerRoot - GLTF model root (for animation track name resolution)
46
45
  * @param {Mesh[]} meshes - SceneProcessor.meshes (extraction order)
47
46
  * @param {AnimationClip[]} animations - GLTF animation clips
48
- * @param {number} triangleCount - Total triangle count
49
47
  */
50
- init( scene, mixerRoot, meshes, animations, triangleCount ) {
48
+ init( scene, mixerRoot, meshes, animations ) {
51
49
 
52
50
  this.dispose();
53
51
 
@@ -56,7 +54,6 @@ export class AnimationManager extends EventDispatcher {
56
54
  this._scene = scene;
57
55
  this._mixerRoot = mixerRoot;
58
56
  this._meshes = meshes;
59
- this._totalTriangleCount = triangleCount;
60
57
 
61
58
  // Try mixerRoot (GLTF model root) first for track resolution.
62
59
  // Fall back to scene if no tracks bind successfully.
@@ -121,10 +118,10 @@ export class AnimationManager extends EventDispatcher {
121
118
  }
122
119
 
123
120
  // Allocate reusable output buffer
124
- this._posBuffer = new Float32Array( triangleCount * 9 );
121
+ this._posBuffer = new Float32Array( offset * 9 );
125
122
 
126
123
  const skinnedCount = meshes.filter( m => m.isSkinnedMesh ).length;
127
- console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${triangleCount} triangles` );
124
+ console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles` );
128
125
 
129
126
  }
130
127
 
@@ -575,7 +575,7 @@ export class DenoisingManager extends EventDispatcher {
575
575
  */
576
576
  setAdaptiveSamplingParams( params ) {
577
577
 
578
- if ( params.min !== undefined ) this._stages.pathTracer?.setAdaptiveSamplingMin( params.min );
578
+ if ( params.min !== undefined ) this._stages.pathTracer?.setUniform( 'adaptiveSamplingMin', params.min );
579
579
  if ( params.adaptiveSamplingMax !== undefined ) this._settings?.set( 'adaptiveSamplingMax', params.adaptiveSamplingMax );
580
580
  this._stages.adaptiveSampling?.setAdaptiveSamplingParameters( params );
581
581