rayzee 6.0.1 → 6.2.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 (43) hide show
  1. package/README.md +5 -0
  2. package/dist/assets/TexturesWorker-DBqGmVdR.js.map +1 -1
  3. package/dist/rayzee.es.js +2421 -2078
  4. package/dist/rayzee.es.js.map +1 -1
  5. package/dist/rayzee.umd.js +55 -52
  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 +4 -6
  20. package/src/TSL/Common.js +1 -1
  21. package/src/TSL/Debugger.js +0 -2
  22. package/src/TSL/EmissiveSampling.js +20 -22
  23. package/src/TSL/Environment.js +60 -14
  24. package/src/TSL/Fresnel.js +13 -4
  25. package/src/TSL/LightsCore.js +238 -5
  26. package/src/TSL/LightsDirect.js +16 -5
  27. package/src/TSL/LightsIndirect.js +4 -37
  28. package/src/TSL/LightsSampling.js +119 -185
  29. package/src/TSL/MaterialEvaluation.js +25 -14
  30. package/src/TSL/MaterialProperties.js +14 -34
  31. package/src/TSL/MaterialTransmission.js +18 -37
  32. package/src/TSL/PathTracer.js +25 -7
  33. package/src/TSL/PathTracerCore.js +144 -139
  34. package/src/TSL/Struct.js +7 -1
  35. package/src/TSL/TextureSampling.js +2 -2
  36. package/src/index.js +2 -0
  37. package/src/managers/AnimationManager.js +3 -6
  38. package/src/managers/DenoisingManager.js +1 -1
  39. package/src/managers/GoboManager.js +277 -0
  40. package/src/managers/IESManager.js +268 -0
  41. package/src/managers/LightManager.js +33 -1
  42. package/src/managers/TransformManager.js +3 -3
  43. 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
 
@@ -312,63 +310,6 @@ export const generateSampledDirection = Fn( ( [
312
310
 
313
311
  } );
314
312
 
315
- // =============================================================================
316
- // Path Contribution Estimation
317
- // =============================================================================
318
-
319
- export const estimatePathContribution = Fn( ( [
320
- throughput, direction, material, materialIndex,
321
- classificationCached, lastMaterialIndex, cachedClassification,
322
- enableEnvironmentLight, useEnvMapIS,
323
- ] ) => {
324
-
325
- const throughputStrength = max( maxComponent( { v: throughput } ), 0.0 ).toVar();
326
-
327
- // Use cached material classification
328
- const mc = MaterialClassification.wrap( getOrCreateMaterialClassification(
329
- material, materialIndex,
330
- classificationCached, lastMaterialIndex, cachedClassification,
331
- ) ).toVar();
332
-
333
- // Enhanced material importance with interaction bonuses
334
- const materialImportance = mc.complexityScore.toVar();
335
-
336
- // Interaction complexity bonuses
337
- If( mc.isMetallic.and( mc.isSmooth ), () => {
338
-
339
- materialImportance.addAssign( 0.15 );
340
-
341
- } );
342
- If( mc.isTransmissive.and( mc.hasClearcoat ), () => {
343
-
344
- materialImportance.addAssign( 0.12 );
345
-
346
- } );
347
- If( mc.isEmissive, () => {
348
-
349
- materialImportance.addAssign( 0.1 );
350
-
351
- } );
352
- materialImportance.assign( clamp( materialImportance, 0.0, 1.0 ) );
353
-
354
- // Direction importance calculation
355
- const directionImportance = float( 0.5 ).toVar();
356
-
357
- If( enableEnvironmentLight.and( useEnvMapIS ).and( throughputStrength.greaterThan( 0.01 ) ), () => {
358
-
359
- const cosTheta = clamp( direction.y, 0.0, 1.0 );
360
- directionImportance.assign( mix( float( 0.3 ), float( 0.8 ), cosTheta.mul( cosTheta ) ) );
361
-
362
- } );
363
-
364
- // Enhanced weighting
365
- const throughputWeight = smoothstep( float( 0.001 ), float( 0.1 ), throughputStrength );
366
- return throughputStrength.mul(
367
- mix( materialImportance.mul( 0.7 ), directionImportance, 0.3 ),
368
- ).mul( throughputWeight );
369
-
370
- } );
371
-
372
313
  // =============================================================================
373
314
  // Russian Roulette Path Termination
374
315
  // =============================================================================
@@ -376,7 +317,6 @@ export const estimatePathContribution = Fn( ( [
376
317
  export const handleRussianRoulette = Fn( ( [
377
318
  depth, throughput, material, materialIndex, rayDirection, rngState,
378
319
  classificationCached, lastMaterialIndex, cachedClassification,
379
- weightsComputed, pathImportance,
380
320
  enableEnvironmentLight, useEnvMapIS,
381
321
  ] ) => {
382
322
 
@@ -444,22 +384,37 @@ export const handleRussianRoulette = Fn( ( [
444
384
 
445
385
  } ).Else( () => {
446
386
 
447
- // Path importanceused across all depth ranges
448
- 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 ), () => {
449
390
 
450
- If( classificationCached.and( weightsComputed ), () => {
391
+ estMaterialImportance.addAssign( 0.15 );
451
392
 
452
- pathContribution.assign( pathImportance );
393
+ } );
394
+ If( mc.isTransmissive.and( mc.hasClearcoat ), () => {
453
395
 
454
- } ).Else( () => {
396
+ estMaterialImportance.addAssign( 0.12 );
455
397
 
456
- pathContribution.assign( estimatePathContribution(
457
- throughput, rayDirection, material, materialIndex,
458
- classificationCached, lastMaterialIndex, cachedClassification,
459
- enableEnvironmentLight, useEnvMapIS,
460
- ) );
398
+ } );
399
+ If( mc.isEmissive, () => {
400
+
401
+ estMaterialImportance.addAssign( 0.1 );
461
402
 
462
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();
463
418
 
464
419
  // Smooth adaptive continuation probability (no discrete depth brackets)
465
420
  // Early behavior: throughput + material driven, generous
@@ -516,9 +471,10 @@ export const handleRussianRoulette = Fn( ( [
516
471
  // =============================================================================
517
472
 
518
473
  export const sampleBackgroundLighting = Fn( ( [
519
- isPrimaryRay, direction,
474
+ isPrimaryRay, rayOrigin, direction,
520
475
  envTexture, envMatrix, environmentIntensity, enableEnvironmentLight,
521
476
  showBackground, backgroundIntensity,
477
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
522
478
  ] ) => {
523
479
 
524
480
  // Only hide background for primary camera rays when showBackground is false
@@ -531,8 +487,18 @@ export const sampleBackgroundLighting = Fn( ( [
531
487
 
532
488
  } ).Else( () => {
533
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
+
534
500
  const sampled = sampleEnvironment( {
535
- tex: envTexture, samp: sampler( envTexture ), direction, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight,
501
+ tex: envTexture, samp: sampler( envTexture ), direction: effectiveDir, environmentMatrix: envMatrix, environmentIntensity, enableEnvironmentLight,
536
502
  } );
537
503
 
538
504
  If( isPrimaryRay, () => {
@@ -567,7 +533,7 @@ export const regularizePathContribution = /*@__PURE__*/ wgslFn( `
567
533
  // =============================================================================
568
534
 
569
535
  export const Trace = Fn( ( [
570
- ray, rngState, rayIndex, pixelIndex,
536
+ ray, rngState, rayIndex,
571
537
  // BVH / Scene
572
538
  bvhBuffer,
573
539
  triangleBuffer,
@@ -586,11 +552,12 @@ export const Trace = Fn( ( [
586
552
  envCDFBuffer,
587
553
  envTotalSum, envCompensationDelta, envResolution,
588
554
  enableEnvironmentLight, useEnvMapIS,
555
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
589
556
  // Rendering parameters
590
557
  maxBounceCount, transmissiveBounces,
591
558
  backgroundIntensity, showBackground, transparentBackground,
592
559
  fireflyThreshold, globalIlluminationIntensity,
593
- totalTriangleCount, enableEmissiveTriangleSampling,
560
+ enableEmissiveTriangleSampling,
594
561
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower, emissiveBoost,
595
562
  lightBVHBuffer, lightBVHNodeCount,
596
563
  // Per-pixel info
@@ -614,11 +581,25 @@ export const Trace = Fn( ( [
614
581
  // behind glass), not the glass surface itself.
615
582
  const auxLocked = tslBool( false ).toVar();
616
583
 
617
- // 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.
618
587
  const mediumStackDepth = int( 0 ).toVar();
619
588
  const mediumStack_ior_1 = float( 1.0 ).toVar();
620
589
  const mediumStack_ior_2 = float( 1.0 ).toVar();
621
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();
622
603
 
623
604
  // Locked at the first dispersive transmission; reused for subsequent transmissions on
624
605
  // the path so multi-bounce dispersion doesn't collapse under repeated colorWeight ×.
@@ -635,8 +616,6 @@ export const Trace = Fn( ( [
635
616
  const psWeightsComputed = tslBool( false ).toVar();
636
617
  const psClassificationCached = tslBool( false ).toVar();
637
618
  const psMaterialCacheCached = tslBool( false ).toVar();
638
- const psTexturesLoaded = tslBool( false ).toVar();
639
- const psPathImportance = float( 0.0 ).toVar();
640
619
  const psLastMaterialIndex = int( - 1 ).toVar();
641
620
 
642
621
  // Cached classification
@@ -693,16 +672,41 @@ export const Trace = Fn( ( [
693
672
  currentRay,
694
673
  bvhBuffer,
695
674
  triangleBuffer,
696
- materialBuffer,
697
675
  ) ).toVar();
698
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
+
699
702
  If( hitInfo.didHit.not(), () => {
700
703
 
701
704
  // ENVIRONMENT LIGHTING
702
705
  const envColor = sampleBackgroundLighting(
703
- stateIsPrimaryRay, rayDirection,
706
+ stateIsPrimaryRay, rayOrigin, rayDirection,
704
707
  envTexture, envMatrix, environmentIntensity, enableEnvironmentLight,
705
708
  showBackground, backgroundIntensity,
709
+ groundProjectionEnabled, groundProjectionRadius, groundProjectionHeight,
706
710
  );
707
711
 
708
712
  // MIS weight for implicit environment hit — prevents double-counting with NEE.
@@ -804,7 +808,7 @@ export const Trace = Fn( ( [
804
808
 
805
809
  // Handle transparent materials
806
810
  const interaction = MaterialInteractionResult.wrap( handleMaterialTransparency(
807
- currentRay, hitInfo.hitPoint, N, material, rngState,
811
+ currentRay, N, material, rngState,
808
812
  stateTransmissiveTraversals,
809
813
  currentMediumIOR, previousMediumIOR,
810
814
  pathWavelength,
@@ -826,22 +830,39 @@ export const Trace = Fn( ( [
826
830
 
827
831
  If( interaction.entering, () => {
828
832
 
829
- // Push new medium onto stack
833
+ // Push new medium onto stack (IOR + KHR_materials_volume attenuation)
830
834
  If( mediumStackDepth.lessThan( int( 3 ) ), () => {
831
835
 
832
836
  mediumStackDepth.addAssign( 1 );
833
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
+
834
846
  If( mediumStackDepth.equal( int( 1 ) ), () => {
835
847
 
836
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 );
837
852
 
838
853
  } ).ElseIf( mediumStackDepth.equal( int( 2 ) ), () => {
839
854
 
840
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 );
841
859
 
842
860
  } ).ElseIf( mediumStackDepth.equal( int( 3 ) ), () => {
843
861
 
844
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 );
845
866
 
846
867
  } );
847
868
 
@@ -915,6 +936,19 @@ export const Trace = Fn( ( [
915
936
  const V = rayDirection.negate().toVar();
916
937
  material.sheenRoughness.assign( clamp( material.sheenRoughness, MIN_ROUGHNESS, MAX_ROUGHNESS ) );
917
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
+
918
952
  // Create material cache if needed
919
953
  If( psMaterialCacheCached.not(), () => {
920
954
 
@@ -940,9 +974,12 @@ export const Trace = Fn( ( [
940
974
 
941
975
  } ).Else( () => {
942
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.
943
980
  const brdfSample = DirectionSample.wrap( generateSampledDirection(
944
- V, N, material, hitInfo.materialIndex, randomSample, rngState,
945
- psClassificationCached, psLastMaterialIndex, psCachedClassification,
981
+ V, N, material, randomSample, rngState,
982
+ psCachedClassification,
946
983
  psWeightsComputed, psCachedBrdfWeights,
947
984
  psMaterialCacheCached, psCachedMaterialCache,
948
985
  ) );
@@ -950,22 +987,6 @@ export const Trace = Fn( ( [
950
987
  brdfValue.assign( brdfSample.value );
951
988
  brdfPdf.assign( brdfSample.pdf );
952
989
 
953
- // Sync psCachedClassification for downstream consumers (importance sampling, Russian roulette).
954
- // generateSampledDirection computed the correct classification internally via materialIndex
955
- // guard, but TSL Fn can't write back to the caller's variable — update it here.
956
- If( psLastMaterialIndex.notEqual( hitInfo.materialIndex ).or( psClassificationCached.not() ), () => {
957
-
958
- psCachedClassification.assign( classifyMaterial(
959
- material.metalness, material.roughness,
960
- material.transmission, material.clearcoat,
961
- material.emissive,
962
- ) );
963
-
964
- } );
965
-
966
- // Update cache state after generateSampledDirection
967
- psClassificationCached.assign( tslBool( true ) );
968
- psLastMaterialIndex.assign( hitInfo.materialIndex );
969
990
  psWeightsComputed.assign( tslBool( true ) );
970
991
 
971
992
  } );
@@ -1003,7 +1024,7 @@ export const Trace = Fn( ( [
1003
1024
  hitInfo.hitPoint, N, material,
1004
1025
  V,
1005
1026
  brdfDir, brdfPdf, brdfValue,
1006
- rayIndex, bounceIndex, rngState,
1027
+ bounceIndex, rngState,
1007
1028
  directionalLightsBuffer, numDirectionalLights,
1008
1029
  areaLightsBuffer, numAreaLights,
1009
1030
  pointLightsBuffer, numPointLights,
@@ -1024,10 +1045,9 @@ export const Trace = Fn( ( [
1024
1045
  // 2b. EMISSIVE TRIANGLE DIRECT LIGHTING
1025
1046
  If( enableEmissiveTriangleSampling.equal( int( 1 ) ).and( emissiveTriangleCount.greaterThan( int( 0 ) ) ), () => {
1026
1047
 
1027
- // Wrapper binding BVH params (EmissiveSampling expects 4-param callback)
1028
- const traceShadowRayWrapped = Fn( ( [ origin, dir, maxDist, rs ] ) => {
1048
+ const traceShadowRayWrapped = Fn( ( [ origin, dir, maxDist ] ) => {
1029
1049
 
1030
- return traceShadowRay( origin, dir, maxDist, rs, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer );
1050
+ return traceShadowRay( origin, dir, maxDist, traverseBVHShadow, bvhBuffer, triangleBuffer, materialBuffer );
1031
1051
 
1032
1052
  } );
1033
1053
 
@@ -1057,12 +1077,14 @@ export const Trace = Fn( ( [
1057
1077
  const rayOffset = calculateRayOffset( hitInfo.hitPoint, N, material );
1058
1078
  const rayOrigin = hitInfo.hitPoint.add( rayOffset );
1059
1079
  const shadowDist = emissiveSample.distance.sub( 0.001 );
1060
- const visibility = traceShadowRayWrapped( rayOrigin, emissiveSample.direction, shadowDist, rngState );
1080
+ const visibility = traceShadowRayWrapped( rayOrigin, emissiveSample.direction, shadowDist );
1061
1081
 
1062
1082
  If( visibility.greaterThan( 0.0 ), () => {
1063
1083
 
1064
- const brdfValue = evaluateMaterialResponse( V, emissiveSample.direction, N, material );
1065
- 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 );
1066
1088
  const misWeight = select(
1067
1089
  brdfPdf.greaterThan( 0.0 ),
1068
1090
  powerHeuristic( { pdf1: emissiveSample.pdf, pdf2: brdfPdf } ),
@@ -1089,12 +1111,11 @@ export const Trace = Fn( ( [
1089
1111
  // Fallback: flat CDF importance sampling
1090
1112
  const emissiveLight = calculateEmissiveTriangleContribution(
1091
1113
  hitInfo.hitPoint, N, V, material,
1092
- totalTriangleCount, bounceIndex, rngState,
1114
+ bounceIndex, rngState,
1093
1115
  emissiveBoost,
1094
1116
  emissiveTriangleBuffer, emissiveVec4Offset, emissiveTriangleCount, emissiveTotalPower,
1095
1117
  triangleBuffer,
1096
1118
  traceShadowRayWrapped,
1097
- evaluateMaterialResponse,
1098
1119
  calculateRayOffset,
1099
1120
  );
1100
1121
 
@@ -1106,34 +1127,19 @@ export const Trace = Fn( ( [
1106
1127
 
1107
1128
  } );
1108
1129
 
1109
- // Get importance sampling info with caching
1110
- If( psWeightsComputed.not().or( bounceIndex.equal( int( 0 ) ) ), () => {
1111
-
1112
- // Update classification first
1113
- psCachedClassification.assign( MaterialClassification.wrap( getOrCreateMaterialClassification(
1114
- material, hitInfo.materialIndex,
1115
- psClassificationCached, psLastMaterialIndex, psCachedClassification,
1116
- ) ) );
1117
- psClassificationCached.assign( tslBool( true ) );
1118
- psLastMaterialIndex.assign( hitInfo.materialIndex );
1119
-
1120
- } );
1121
-
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.
1122
1133
  const samplingInfo = ImportanceSamplingInfo.wrap( getImportanceSamplingInfo(
1123
1134
  material, bounceIndex, psCachedClassification,
1124
- environmentIntensity, useEnvMapIS, enableEnvironmentLight,
1125
1135
  ) );
1126
1136
 
1127
1137
  // 3. INDIRECT LIGHTING
1128
1138
  const indirectResult = IndirectLightingResult.wrap( calculateIndirectLighting(
1129
1139
  V, N, material,
1130
1140
  brdfDir, brdfPdf, brdfValue,
1131
- rayIndex, bounceIndex,
1132
1141
  rngState,
1133
1142
  samplingInfo,
1134
- envTexture, environmentIntensity, envMatrix,
1135
- envTotalSum, envCompensationDelta, envResolution,
1136
- enableEnvironmentLight, useEnvMapIS,
1137
1143
  ) );
1138
1144
  throughput.mulAssign( indirectResult.throughput );
1139
1145
 
@@ -1192,7 +1198,6 @@ export const Trace = Fn( ( [
1192
1198
  bounceIndex, throughput, material, hitInfo.materialIndex,
1193
1199
  rayDirection, rngState,
1194
1200
  psClassificationCached, psLastMaterialIndex, psCachedClassification,
1195
- psWeightsComputed, psPathImportance,
1196
1201
  enableEnvironmentLight, useEnvMapIS,
1197
1202
  );
1198
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