rayzee 7.16.0 → 7.18.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 (47) hide show
  1. package/README.md +1 -1
  2. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js +2 -0
  3. package/dist/assets/BVHSubtreeWorker-D7KMbrQ-.js.map +1 -0
  4. package/dist/assets/BVHWorker-DOaGn6uN.js +2 -0
  5. package/dist/assets/BVHWorker-DOaGn6uN.js.map +1 -0
  6. package/dist/rayzee.es.js +2817 -2537
  7. package/dist/rayzee.es.js.map +1 -1
  8. package/dist/rayzee.umd.js +55 -55
  9. package/dist/rayzee.umd.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/EngineDefaults.js +27 -11
  12. package/src/Passes/OIDNDenoiser.js +17 -14
  13. package/src/PathTracerApp.js +86 -13
  14. package/src/Processor/BVHBuilder.js +11 -7
  15. package/src/Processor/BuildTimer.js +20 -6
  16. package/src/Processor/EmissiveTriangleBuilder.js +11 -7
  17. package/src/Processor/GeometryExtractor.js +11 -5
  18. package/src/Processor/KernelManager.js +48 -1
  19. package/src/Processor/PackedRayBuffer.js +69 -35
  20. package/src/Processor/ParallelBVHBuilder.js +9 -6
  21. package/src/Processor/QueueManager.js +6 -8
  22. package/src/Processor/SceneProcessor.js +47 -21
  23. package/src/Processor/ShaderBuilder.js +4 -1
  24. package/src/Processor/StorageTexturePool.js +4 -1
  25. package/src/Processor/Workers/BVHWorker.js +12 -6
  26. package/src/RenderSettings.js +1 -1
  27. package/src/Stages/ASVGF.js +38 -0
  28. package/src/Stages/BilateralFilter.js +23 -0
  29. package/src/Stages/EdgeFilter.js +18 -0
  30. package/src/Stages/MotionVector.js +23 -0
  31. package/src/Stages/NormalDepth.js +22 -0
  32. package/src/Stages/PathTracer.js +324 -169
  33. package/src/Stages/PathTracerStage.js +17 -14
  34. package/src/Stages/Variance.js +23 -0
  35. package/src/TSL/FinalWriteKernel.js +30 -18
  36. package/src/TSL/GenerateKernel.js +28 -17
  37. package/src/TSL/ShadeKernel.js +186 -87
  38. package/src/TSL/TextureSampling.js +12 -2
  39. package/src/index.js +3 -0
  40. package/src/managers/EnvironmentManager.js +12 -8
  41. package/src/managers/MaterialDataManager.js +12 -9
  42. package/src/managers/UniformManager.js +1 -1
  43. package/src/utils/Logger.js +291 -0
  44. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js +0 -2
  45. package/dist/assets/BVHSubtreeWorker-sNzvxn66.js.map +0 -1
  46. package/dist/assets/BVHWorker-CiVdFrwe.js +0 -2
  47. package/dist/assets/BVHWorker-CiVdFrwe.js.map +0 -1
@@ -7,7 +7,7 @@
7
7
  import {
8
8
  Fn, float, vec2, vec3, vec4, int, uint,
9
9
  bool as tslBool,
10
- If, Loop, normalize, max, exp, log, clamp, dot, length, select,
10
+ If, Loop, normalize, max, exp, log, clamp, dot, length, select, smoothstep, mix,
11
11
  instanceIndex,
12
12
  sampler,
13
13
  atomicAdd, atomicLoad, atomicStore, uintBitsToFloat,
@@ -27,7 +27,8 @@ import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficient
27
27
  import { calculateIndirectLighting } from './LightsIndirect.js';
28
28
  import { IndirectLightingResult, sampleCone } from './LightsCore.js';
29
29
  import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
30
- import { getImportanceSamplingInfo } from './MaterialProperties.js';
30
+ import { getImportanceSamplingInfo, evaluateDFG } from './MaterialProperties.js';
31
+ import { dielectricF0 } from './Fresnel.js';
31
32
  import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
32
33
  import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
33
34
  import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
@@ -44,6 +45,7 @@ import {
44
45
  BRDFWeights,
45
46
  MaterialCache,
46
47
  DirectLightingDual,
48
+ DFGResult,
47
49
  } from './Struct.js';
48
50
  import { RandomValue, getRandomSample } from './Random.js';
49
51
  import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
@@ -51,11 +53,13 @@ import {
51
53
  readRayOrigin, readRayDirection, readRayBounceFlags, readRayThroughput, readRayPdf,
52
54
  readMediumStack, writeMediumStack, readMediumSigmaA, writeMediumSigmaA,
53
55
  readPathBounces, readSssSteps, readSSSMedium, writeSSSMedium,
56
+ readTransparentCount,
54
57
  readHitDistance, readHitBarycentrics, readHitNormal,
55
58
  readHitMaterialIndex, readHitTriangleIndex,
56
59
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
57
- writeGBuffer, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
60
+ writeGBuffer, readGBuffer, gbDecodeNormalDepth,
58
61
  readRayRadiance,
62
+ readFeatureThroughput, writeFeatureThroughput,
59
63
  } from '../Processor/PackedRayBuffer.js';
60
64
 
61
65
  const WG_SIZE = 256;
@@ -85,11 +89,13 @@ export function buildShadeKernel( params ) {
85
89
  pointLightsBuffer, numPointLights,
86
90
  spotLightsBuffer, numSpotLights,
87
91
  maxBounceCount, maxSubsurfaceSteps,
92
+ maxTransparentBounces, // guard on alpha-skip depth; counter in ORIGIN_META.w
88
93
  currentBounce, // loop iteration = path length (advances on free bounces); drives RR/firefly/giScale
89
94
  transparentBackground, backgroundIntensity, backgroundColor, backgroundBlurriness, backgroundBlurSamples, showBackground,
90
95
  globalIlluminationIntensity,
91
96
  cameraProjectionMatrix, cameraViewMatrix,
92
97
  fireflyThreshold, frame, resolution,
98
+ chunkRowBase, // chunked path pool: global pixel = chunkRowBase·W + localSlot (rayID). See spec.
93
99
  emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
94
100
  emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
95
101
  lightBVHNodeCount, reverseMapVec4Offset,
@@ -183,6 +189,11 @@ export function buildShadeKernel( params ) {
183
189
  const pixelIndex = rayID;
184
190
  const rngState = rngBufferRW.element( rayID ).toVar();
185
191
 
192
+ // DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
193
+ // albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
194
+ // deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
195
+ const featCarry = readFeatureThroughput( rayBufferRW, rayID ).toVar();
196
+
186
197
  const hitDist = readHitDistance( hitBufferRO, rayID ).toVar();
187
198
  const hitNormal = readHitNormal( hitBufferRO, rayID ).toVar();
188
199
  // hitInfo.uv is the interpolated texture UV (not barycentrics)
@@ -195,6 +206,7 @@ export function buildShadeKernel( params ) {
195
206
  // path length = loop iteration (advances every bounce incl. transmissive/SSS); drives RR/firefly/giScale/MIS. Megakernel: loop counter i.
196
207
  const bounceIndex = int( currentBounce ).toVar();
197
208
  const sssSteps = readSssSteps( rayBufferRW, rayID ).toVar();
209
+ const transparentCount = readTransparentCount( rayBufferRW, rayID ).toVar();
198
210
 
199
211
  // ── Analytic ground-plane shadow catcher (primary ray only, no geometry) ──
200
212
  // A horizontal plane at y = groundCatcherHeight. For a bounce-0 ray that crosses it
@@ -309,7 +321,8 @@ export function buildShadeKernel( params ) {
309
321
  } );
310
322
 
311
323
  writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
312
- writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
324
+ // DDFA: the plane's aux (written above) is a committed surface — lock it (cosmetic; ray dies here).
325
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ).bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
313
326
  rngBufferRW.element( rayID ).assign( rngState );
314
327
  Return();
315
328
 
@@ -333,9 +346,8 @@ export function buildShadeKernel( params ) {
333
346
  const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
334
347
  const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
335
348
 
336
- // Fix ①: OIDN clean-aux for a redirected ray that escapes to the environment (e.g. smooth
337
- // glass over sky). Holds the clamped env colour this ray sees; written into the aux G-buffer
338
- // before the miss Return below, but only when the pixel still carries the black bounce-0 default.
349
+ // DDFA: env colour a redirected ray escaping to the environment sees (e.g. smooth glass over
350
+ // sky). Committed (tinted by featCarry) into the aux G-buffer before the miss Return below.
339
351
  const escapedAuxAlbedo = vec3( 0.0 ).toVar();
340
352
 
341
353
  If( wantBackdrop.or( wantEnvLight ), () => {
@@ -376,8 +388,8 @@ export function buildShadeKernel( params ) {
376
388
 
377
389
  } );
378
390
 
379
- // Fix ①: remember the (clamped) env colour this escaped ray sees, so a redirected ray
380
- // that never captured a surface aux can feed OIDN a meaningful albedo below.
391
+ // DDFA: remember the (clamped) env colour this escaped ray sees, for the redirected-escape
392
+ // aux commit below (tinted by the accumulated see-through throughput).
381
393
  escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
382
394
 
383
395
  // MIS weight for implicit env hit — prevents double-counting with NEE
@@ -447,22 +459,18 @@ export function buildShadeKernel( params ) {
447
459
 
448
460
  } );
449
461
 
450
- // Fix ①: fill the black bounce-0 default aux for a redirected ray that reached the
451
- // environment without ever locking a surface guide (smooth glass / specular transmission
452
- // over sky). Only when the stored albedo is still the black default (dot < 1e-4), so a real
453
- // specular-surface aux written upstream is never clobbered. Normal = the escaped ray
454
- // direction (varies per pixel → OIDN sees structure that tracks the refracted view);
455
- // albedo = the clamped env colour it sees; depth kept at the primary hit.
462
+ // DDFA: a redirected ray (specular chain — glass/mirror) that escaped to the environment without
463
+ // ever committing a surface guide commits the env colour it sees, tinted by the accumulated
464
+ // see-through throughput (red glass over sky → sky×red). Normal = the escaped direction (varies
465
+ // per pixel → OIDN sees structure tracking the refracted view); depth kept at the primary hit.
466
+ // AUX_LOCKED-clear alone means "never committed" — the old black-probe is redundant. A direct
467
+ // backdrop (REDIRECTED clear) keeps Generate's black/far default (regression-safe sky guide).
456
468
  If( auxOn
457
469
  .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
458
470
  .and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
459
471
 
460
472
  const gPrev = readGBuffer( gBufferRW, pixelIndex );
461
- If( dot( gbDecodeAlbedo( gPrev ), vec3( 1.0 ) ).lessThan( float( 1e-4 ) ), () => {
462
-
463
- writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, escapedAuxAlbedo );
464
-
465
- } );
473
+ writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, clamp( escapedAuxAlbedo.mul( featCarry ), vec3( 0.0 ), vec3( 1.0 ) ) );
466
474
 
467
475
  } );
468
476
 
@@ -496,7 +504,14 @@ export function buildShadeKernel( params ) {
496
504
  If( max( max( mSigmaS.x, mSigmaS.y ), mSigmaS.z ).lessThanEqual( 0.0 ), () => {
497
505
 
498
506
  // glass: Beer-Lambert absorption
499
- throughput.mulAssign( exp( mSigmaA.mul( hitDist ).negate() ) );
507
+ const beer = exp( mSigmaA.mul( hitDist ).negate() ).toVar();
508
+ throughput.mulAssign( beer );
509
+ // DDFA: colored-glass volume tints the deferred aux guide by the same absorption.
510
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
511
+
512
+ featCarry.mulAssign( beer );
513
+
514
+ } );
500
515
 
501
516
  } ).Else( () => {
502
517
 
@@ -534,7 +549,7 @@ export function buildShadeKernel( params ) {
534
549
  // free-bounce continuation: ray stays in the same medium, so medium stack + coeffs persist
535
550
  // SSS scatter changes direction → no longer the direct backdrop view.
536
551
  flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
537
- writeRayOriginMeta( rayBufferRW, rayID, scatterPoint, cameraDepth, sssSteps );
552
+ writeRayOriginMeta( rayBufferRW, rayID, scatterPoint, cameraDepth, sssSteps, transparentCount );
538
553
  writeRayDirFlags( rayBufferRW, rayID, newDir, flags );
539
554
  // Free bounce: preserve prevBouncePdf (megakernel leaves it untouched across SSS scatter,
540
555
  // PathTracerCore.js:1272 sets it only after an opaque scatter). Writing 1.0 here spuriously
@@ -585,6 +600,10 @@ export function buildShadeKernel( params ) {
585
600
  material, samplingUV, N,
586
601
  ) ).toVar();
587
602
 
603
+ // DDFA: snapshot the UNCLAMPED roughness (before the min-0.05 clamp below) for the specular↔diffuse
604
+ // classification, so a perfect mirror reads 0 (fully specular → defers).
605
+ const rawRough = matSamples.roughness.toVar();
606
+
588
607
  // BRDF functions read material.color/metalness/roughness, so apply samples here
589
608
  material.color.assign( matSamples.albedo );
590
609
  material.metalness.assign( matSamples.metalness.clamp( 0.0, 1.0 ) );
@@ -635,36 +654,55 @@ export function buildShadeKernel( params ) {
635
654
 
636
655
  } );
637
656
 
638
- // first-hit MRT data (bounce 0 only)
639
- If( bounceIndex.equal( 0 ).and( auxOn ), () => {
657
+ // ─── DDFA classification (Cycles smoothstep-by-roughness, not a hard binary) ───
658
+ // nonspec fraction over a 2-pseudo-closure model: a Lambert closure (weight 1-metal, fully diffuse)
659
+ // + a glossy/glass closure (weight metal·(1-trans)+trans, classified by roughness). Commit the aux at
660
+ // the first surface with a meaningful diffuse response (nonspec≥0.25); smooth glass/mirror defers.
661
+ // featPrefix = the tint accumulated BEFORE this surface (read-only for this bounce's commits).
662
+ const metal = material.metalness.toVar();
663
+ const trans = material.transmission.toVar();
664
+ const roughFrac = smoothstep( float( 0.0 ), float( 0.15 ), rawRough ).toVar();
665
+ const auxCommit = clamp(
666
+ float( 1.0 ).sub( metal ).mul( float( 1.0 ).sub( trans ) )
667
+ .add( roughFrac.mul( metal.mul( float( 1.0 ).sub( trans ) ).add( trans ) ) ),
668
+ 0.0, 1.0,
669
+ ).greaterThanEqual( float( 0.25 ) ).toVar();
670
+ const featPrefix = featCarry.toVar();
671
+
672
+ // DDFA terminal fallback: a ray dying while still deferring (e.g. a mirror maze) commits its last
673
+ // surface instead of leaving a black guide OIDN would demod-amplify.
674
+ // Plain JS inliner, not an Fn — it closes over albedo/featPrefix/flags .toVar()s.
675
+ const commitDeferredAux = ( normal ) => {
676
+
677
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
678
+
679
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
680
+ writeGBuffer( gBufferRW, pixelIndex, normal, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
640
681
 
641
- const linearDepth = computeNDCDepth( {
642
- worldPos: hitPoint,
643
- cameraProjectionMatrix,
644
- cameraViewMatrix,
645
682
  } );
646
- // G-buffer is per-pixel (rayID == pixelIndex). writeGBuffer half-packs (normal/depth/albedo).
647
- // Write the primary DEPTH now with the miss-default aux; the real normal/albedo are captured below
648
- // (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
649
- // block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
650
- // (objectNormal/objectColor stay at their init for transmissive-then-miss).
651
- //
652
- // Fix ③: BLEND (semi-transparent) glass instead LOCKS its OWN normal + albedo here. Its per-frame
653
- // stochastic alpha-skip-vs-opaque-shade outcome (MaterialTransmission.js:522-540) otherwise writes a
654
- // DIFFERENT surface into the aux each frame (wall-behind on skip vs glass on opaque); with the normal
655
- // not temporally averaged that becomes the speckle OIDN smears on — the visible artefact on
656
- // alpha-transparent glass. A deterministic glass-surface guide every frame removes it. MASK / opaque
657
- // materials keep the default + rely on the downstream aux-extend.
658
- If( material.alphaMode.equal( int( 2 ) ), () => {
659
683
 
660
- writeGBuffer( gBufferRW, pixelIndex, N, linearDepth, albedo.xyz );
661
- flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
684
+ };
662
685
 
663
- } ).Else( () => {
686
+ // Deactivate the ray, keeping the radiance already gathered at this vertex.
687
+ const terminatePath = () => {
688
+
689
+ writeRayRadiance( rayBufferRW, rayID, currentRadiance );
690
+ writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
691
+ rngBufferRW.element( rayID ).assign( rngState );
692
+
693
+ };
664
694
 
665
- writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
695
+ // first-hit MRT data (bounce 0 only): write the primary DEPTH now with the default normal/albedo.
696
+ // The real normal/albedo are committed by the DDFA decision blocks below (which re-pack this depth);
697
+ // they may defer through smooth glass/mirror and commit at the first diffuse-enough surface or the env.
698
+ If( bounceIndex.equal( 0 ).and( auxOn ), () => {
666
699
 
700
+ const linearDepth = computeNDCDepth( {
701
+ worldPos: hitPoint,
702
+ cameraProjectionMatrix,
703
+ cameraViewMatrix,
667
704
  } );
705
+ writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
668
706
 
669
707
  } );
670
708
 
@@ -698,6 +736,53 @@ export function buildShadeKernel( params ) {
698
736
  // persist any wavelength locked on a fresh dispersive transmission; identity write otherwise
699
737
  pathWavelength.assign( interaction.pathWavelength );
700
738
 
739
+ // ─── DDFA aux decision at a transmissive / alpha / SSS interaction. Runs for BOTH the continuing and
740
+ // the BLEND fall-through paths, so it sits before If(continueRay). If/ElseIf chaining so BLEND wins
741
+ // over the transmission branch (fixes the BLEND-glass per-frame flicker + the BLEND+transmission case). ───
742
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
743
+
744
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
745
+ // N here is the mapped normal — faceforward it (a ray exiting glass sees a back-facing N).
746
+ const Nff = select( dot( N, direction.negate() ).lessThan( 0.0 ), N.negate(), N ).toVar();
747
+ const auxAlbedo = clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ).toVar();
748
+
749
+ If( material.alphaMode.equal( int( 2 ) ), () => {
750
+
751
+ // BLEND: deterministic own-surface commit on BOTH stochastic skip + shade frames (no flicker).
752
+ writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
753
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
754
+
755
+ } ).ElseIf( interaction.isAlphaSkip, () => {
756
+
757
+ // MASK cutout passthrough: keep deferring, tint unchanged (backdrop through the hole commits later).
758
+
759
+ } ).ElseIf( interaction.isSubsurface, () => {
760
+
761
+ // SSS boundary: force-commit the skin surface as diffuse.
762
+ writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
763
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
764
+
765
+ } ).ElseIf( interaction.isTransmissive, () => {
766
+
767
+ If( auxCommit.or( material.dispersion.greaterThan( 0.0 ) ), () => {
768
+
769
+ // frosted / rough / dispersive glass: commit its own surface (deterministic per frame).
770
+ writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
771
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
772
+
773
+ } ).Else( () => {
774
+
775
+ // smooth glass: DEFER — tint by the glass colour (matches the transmission throughput,
776
+ // which multiplies material.color at both reflect and refract; achromatic eta²/Fr excluded).
777
+ featCarry.mulAssign( clamp( albedo, vec3( 0.0 ), vec3( 1.0 ) ) );
778
+
779
+ } );
780
+
781
+ } );
782
+ // else (opaque non-skip): the opaque decision block below handles it (needs the viewer-facing N).
783
+
784
+ } );
785
+
701
786
  If( interaction.continueRay, () => {
702
787
 
703
788
  // update medium stack for transmission (not reflection/TIR)
@@ -801,26 +886,6 @@ export function buildShadeKernel( params ) {
801
886
 
802
887
  throughput.mulAssign( interaction.throughput );
803
888
 
804
- // Fix ②: OIDN clean-aux for STOCHASTIC glass (rough / frosted / dispersive), whose refracted
805
- // ray hits a different behind-surface every frame. Extending aux through it (the aux-extend on
806
- // the opaque-hit path) captures that per-frame surface — albedo then temporally averaged, normal
807
- // point-sampled — an inconsistent guide OIDN smears on. Instead lock the glass surface's OWN
808
- // normal + tint here (deterministic, spatially coherent). Smooth non-dispersive glass is left
809
- // alone: its refraction is a per-pixel delta, so the behind-surface aux captured downstream is
810
- // already stable. Gated to a genuine dielectric event (not alpha-cutout passthrough or SSS).
811
- If( auxOn
812
- .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
813
- .and( interaction.isTransmissive )
814
- .and( interaction.isAlphaSkip.not() )
815
- .and( interaction.isSubsurface.not() )
816
- .and( material.roughness.greaterThan( 0.05 ).or( material.dispersion.greaterThan( 0.0 ) ) ), () => {
817
-
818
- const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
819
- writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
820
- flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
821
-
822
- } );
823
-
824
889
  // reflection stays on same side, transmission pushes through
825
890
  const reflectOffsetDir = select( interaction.entering, N, N.negate() );
826
891
  const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
@@ -835,8 +900,21 @@ export function buildShadeKernel( params ) {
835
900
 
836
901
  flags.assign( flags.bitOr( uint( RAY_FLAG.REDIRECTED ) ) );
837
902
 
903
+ } ).Else( () => {
904
+
905
+ // Alpha passthrough: charge the guard. N is faceforwarded for the aux commit (a ray
906
+ // exiting a back-facing skip would otherwise write an inverted normal).
907
+ transparentCount.addAssign( 1 );
908
+ If( transparentCount.greaterThan( maxTransparentBounces ), () => {
909
+
910
+ commitDeferredAux( select( dot( N, direction.negate() ).lessThan( 0.0 ), N.negate(), N ) );
911
+ terminatePath();
912
+ Return();
913
+
914
+ } );
915
+
838
916
  } );
839
- writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps );
917
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth, sssSteps, transparentCount );
840
918
  writeRayDirFlags( rayBufferRW, rayID, interaction.direction, flags );
841
919
  // Free bounce: preserve prevBouncePdf (megakernel keeps the last opaque-scatter pdf across
842
920
  // transmission/alpha-skip/SSS-boundary). Writing 1.0 corrupts the next bounce's env/emissive MIS,
@@ -844,6 +922,13 @@ export function buildShadeKernel( params ) {
844
922
  writeRayThroughputPdf( rayBufferRW, rayID, throughput, readRayPdf( rayBufferRW, rayID ) );
845
923
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
846
924
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
925
+ // DDFA: persist the (possibly tinted) see-through throughput for the next bounce. MUST run after
926
+ // the writeMediumSigmaA above — both RMW slot 5 (sigmaA.xyz / featTP.w). Gated AUX_LOCKED-clear.
927
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
928
+
929
+ writeFeatureThroughput( rayBufferRW, rayID, featCarry );
930
+
931
+ } );
847
932
  rngBufferRW.element( rayID ).assign( rngState );
848
933
  Return();
849
934
 
@@ -930,22 +1015,29 @@ export function buildShadeKernel( params ) {
930
1015
 
931
1016
  } );
932
1017
 
933
- // OIDN clean-aux (megakernel parity: PathTracerCore.js:1300): keep overwriting the per-pixel normal/
934
- // albedo through mirror/glass until the first non-specular hit, so aux describes what's actually
935
- // visible (the surface reflected in a mirror / seen behind glass), not the specular surface. Glass
936
- // Returns at the transparency block above, so its aux is replaced by the surface behind it. Depth
937
- // stays at the primary hit (read back + re-packed; the snorm depth re-pack is idempotent — no drift).
938
- If( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ).and( auxOn ), () => {
1018
+ // ─── DDFA opaque aux decision: commit at the first diffuse-enough surface, defer through smooth
1019
+ // mirror/metal so the guide describes what the mirror reflects, not the mirror itself. N is already
1020
+ // viewer-facing (two-sided flip above); depth read back + re-packed (idempotent snorm — no drift). ───
1021
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
939
1022
 
940
1023
  const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
941
- writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
942
-
943
- const auxIsMirror = material.metalness.greaterThan( 0.7 ).and( material.roughness.lessThan( 0.3 ) );
944
- const auxIsTransmissive = material.transmission.greaterThan( 0.5 );
945
- If( auxIsMirror.or( auxIsTransmissive ).not(), () => {
1024
+ If( auxCommit, () => {
946
1025
 
1026
+ // diffuse / glossy / rough-metal: commit the surface guide, tinted by the see-through prefix.
1027
+ writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
947
1028
  flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
948
1029
 
1030
+ } ).Else( () => {
1031
+
1032
+ // smooth mirror / metal: DEFER — tint by the specular directional albedo (metal colour),
1033
+ // matching the opaque throughput (≈ F at the near-delta mirror lobe). Achromatic energy excluded.
1034
+ const NoV = max( dot( N, V ), float( 1e-3 ) );
1035
+ const F0 = clamp(
1036
+ mix( dielectricF0( material.ior ).mul( material.specularColor ), albedo, material.metalness ).mul( material.specularIntensity ),
1037
+ vec3( 0.0 ), vec3( 1.0 ),
1038
+ );
1039
+ featCarry.mulAssign( DFGResult.wrap( evaluateDFG( F0, NoV, max( rawRough, float( 0.02 ) ) ) ).E_total );
1040
+
949
1041
  } );
950
1042
 
951
1043
  } );
@@ -955,11 +1047,14 @@ export function buildShadeKernel( params ) {
955
1047
  material.clearcoat, material.emissive, material.subsurface,
956
1048
  ) ).toVar();
957
1049
 
958
- // STBN keyed on (pixel, bounceIndex, frame).
1050
+ // STBN keyed on (GLOBAL pixel, bounceIndex, frame). pixelIndex is the LOCAL path slot; the global pixel
1051
+ // = chunkRowBase·W + localSlot, so the blue-noise pattern stays spatially aligned across row-band chunks
1052
+ // (and matches Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
959
1053
  const _resX = int( resolution.x ).toVar();
1054
+ const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
960
1055
  const _pixelCoord = vec2(
961
- float( int( pixelIndex ).mod( _resX ) ).add( 0.5 ),
962
- float( int( pixelIndex ).div( _resX ) ).add( 0.5 ),
1056
+ float( _globalPixel.mod( _resX ) ).add( 0.5 ),
1057
+ float( _globalPixel.div( _resX ) ).add( 0.5 ),
963
1058
  );
964
1059
  const xi = getRandomSample( _pixelCoord, int( 0 ), bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
965
1060
  const emptyWeights = BRDFWeights( {
@@ -1174,9 +1269,8 @@ export function buildShadeKernel( params ) {
1174
1269
  ).toVar();
1175
1270
  If( rrSurvival.lessThanEqual( 0.0 ), () => {
1176
1271
 
1177
- writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1178
- writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
1179
- rngBufferRW.element( rayID ).assign( rngState );
1272
+ commitDeferredAux( N );
1273
+ terminatePath();
1180
1274
  Return();
1181
1275
 
1182
1276
  } );
@@ -1185,9 +1279,8 @@ export function buildShadeKernel( params ) {
1185
1279
  // Terminate on CAMERA depth (opaque scatter count), not path length — glass/SSS free bounces no longer burn the maxBounces budget (gap #4).
1186
1280
  If( cameraDepth.greaterThanEqual( maxBounceCount ), () => {
1187
1281
 
1188
- writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1189
- writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
1190
- rngBufferRW.element( rayID ).assign( rngState );
1282
+ commitDeferredAux( N );
1283
+ terminatePath();
1191
1284
  Return();
1192
1285
 
1193
1286
  } );
@@ -1195,11 +1288,17 @@ export function buildShadeKernel( params ) {
1195
1288
  const newOrigin = hitPoint.add( N.mul( 0.001 ) );
1196
1289
 
1197
1290
  // Opaque scatter: the only bounce that advances camera depth.
1198
- writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth.add( 1 ), sssSteps );
1291
+ writeRayOriginMeta( rayBufferRW, rayID, newOrigin, cameraDepth.add( 1 ), sssSteps, transparentCount );
1199
1292
  writeRayDirFlags( rayBufferRW, rayID, bounceDir, flags );
1200
1293
  writeRayThroughputPdf( rayBufferRW, rayID, throughput, bouncePdf );
1201
1294
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
1202
1295
  writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
1296
+ // DDFA: persist the (possibly mirror-tinted) see-through throughput for the next bounce.
1297
+ If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
1298
+
1299
+ writeFeatureThroughput( rayBufferRW, rayID, featCarry );
1300
+
1301
+ } );
1203
1302
  rngBufferRW.element( rayID ).assign( rngState );
1204
1303
 
1205
1304
  } );
@@ -47,10 +47,20 @@ const withBucket = ( buckets, packedIndex, fn ) => {
47
47
 
48
48
  const bucket = packedIndex.div( int( _STRIDE ) ).toVar();
49
49
  const layer = packedIndex.sub( bucket.mul( int( _STRIDE ) ) ).toVar();
50
- let chain = If( bucket.equal( int( 0 ) ), () => fn( buckets[ 0 ], layer ) );
50
+ // Block bodies, not concise arrows: an If() callback that yields a value makes TSL emit a
51
+ // `return` inside an inline Fn(), which it comments out and warns about on every arm.
52
+ let chain = If( bucket.equal( int( 0 ) ), () => {
53
+
54
+ fn( buckets[ 0 ], layer );
55
+
56
+ } );
51
57
  for ( let i = 1; i < buckets.length; i ++ ) {
52
58
 
53
- chain = chain.ElseIf( bucket.equal( int( i ) ), () => fn( buckets[ i ], layer ) );
59
+ chain = chain.ElseIf( bucket.equal( int( i ) ), () => {
60
+
61
+ fn( buckets[ i ], layer );
62
+
63
+ } );
54
64
 
55
65
  }
56
66
 
package/src/index.js CHANGED
@@ -17,6 +17,9 @@ export { EngineEvents } from './EngineEvents.js';
17
17
  // Asset URL / cache namespace overrides (call before constructing PathTracerApp)
18
18
  export { configureAssets, getAssetConfig } from './AssetConfig.js';
19
19
 
20
+ // Logging — leveled/namespaced console output shared with the workers
21
+ export { Logger, createLogger, fmt, LOG_LEVELS } from './utils/Logger.js';
22
+
20
23
  // Configuration defaults and presets
21
24
  export {
22
25
  ENGINE_DEFAULTS,
@@ -13,6 +13,9 @@ import {
13
13
  import { EquirectHDRInfo } from '../Processor/EquirectHDRInfo.js';
14
14
  import { ProceduralSky } from '../Processor/ProceduralSky.js';
15
15
  import { SimpleSky } from '../Processor/SimpleSky.js';
16
+ import { createLogger, fmt } from '../utils/Logger.js';
17
+
18
+ const log = createLogger( 'env' );
16
19
  import { ENGINE_DEFAULTS as DEFAULT_STATE } from '../EngineDefaults.js';
17
20
 
18
21
  export class EnvironmentManager {
@@ -231,8 +234,6 @@ export class EnvironmentManager {
231
234
  this.environmentTexture = envTex;
232
235
  this.envTexSize.set( envTex.image.width, envTex.image.height );
233
236
 
234
- console.log( `EnvironmentManager: Environment map ${envTex.image.width}x${envTex.image.height}` );
235
-
236
237
  }
237
238
 
238
239
  /**
@@ -317,11 +318,14 @@ export class EnvironmentManager {
317
318
 
318
319
  }
319
320
 
320
- console.log( `Environment CDF built in ${this.cdfBuildTime.toFixed( 2 )}ms (worker: ${useWorker})` );
321
+ log.info( fmt.list( [
322
+ fmt.px( this.envTexSize.x, this.envTexSize.y ),
323
+ `CDF ${fmt.ms( this.cdfBuildTime )}${useWorker ? '' : ' (main thread)'}`,
324
+ ] ) );
321
325
 
322
326
  } catch ( error ) {
323
327
 
324
- console.error( 'Error building environment CDF:', error );
328
+ log.error( 'CDF build failed:', error );
325
329
  this.uniforms.set( 'useEnvMapIS', 0 );
326
330
  this.uniforms.set( 'envTotalSum', 0.0 );
327
331
  this.uniforms.set( 'envCompensationDelta', 0.0 );
@@ -437,7 +441,7 @@ export class EnvironmentManager {
437
441
 
438
442
  } catch ( error ) {
439
443
 
440
- console.error( 'Error generating gradient sky:', error );
444
+ log.error( 'gradient sky generation failed:', error );
441
445
 
442
446
  }
443
447
 
@@ -467,7 +471,7 @@ export class EnvironmentManager {
467
471
 
468
472
  } catch ( error ) {
469
473
 
470
- console.error( 'Error generating solid color sky:', error );
474
+ log.error( 'solid color sky generation failed:', error );
471
475
 
472
476
  }
473
477
 
@@ -503,11 +507,11 @@ export class EnvironmentManager {
503
507
  this.uniforms.set( 'sunAngularSize', 0.0087 );
504
508
  this.uniforms.set( 'hasSun', 1 );
505
509
 
506
- console.log( `Sun parameters synced: dir=${this.envParams.skySunDirection.toArray().map( v => v.toFixed( 2 ) ).join( ',' )}` );
510
+ log.debug( `sun synced · dir ${this.envParams.skySunDirection.toArray().map( v => v.toFixed( 2 ) ).join( ',' )}` );
507
511
 
508
512
  } catch ( error ) {
509
513
 
510
- console.error( 'Error generating procedural sky:', error );
514
+ log.error( 'procedural sky generation failed:', error );
511
515
 
512
516
  }
513
517
 
@@ -10,6 +10,9 @@
10
10
  import { StorageInstancedBufferAttribute } from 'three/webgpu';
11
11
  import { storage } from 'three/tsl';
12
12
  import { MATERIAL_DATA_LAYOUT as M, TRIANGLE_DATA_LAYOUT as T } from '../EngineDefaults.js';
13
+ import { createLogger, fmt } from '../utils/Logger.js';
14
+
15
+ const log = createLogger( 'material' );
13
16
 
14
17
  const PIXELS_PER_MATERIAL = M.SLOTS_PER_MATERIAL;
15
18
  // Per-triangle float offsets used by _patchTriangleSideForMaterial / _patchTriangleBlockerForMaterial.
@@ -84,7 +87,7 @@ export class MaterialDataManager {
84
87
  }
85
88
 
86
89
  this.materialCount = Math.floor( vec4Count / PIXELS_PER_MATERIAL );
87
- console.log( `MaterialDataManager: ${this.materialCount} materials (storage buffer)` );
90
+ log.debug( `${fmt.n( this.materialCount )} materials (storage buffer)` );
88
91
 
89
92
  }
90
93
 
@@ -186,7 +189,7 @@ export class MaterialDataManager {
186
189
 
187
190
  if ( ! this.materialStorageAttr ) {
188
191
 
189
- console.warn( 'Material storage buffer not available' );
192
+ log.warn( 'material storage buffer not available' );
190
193
  return;
191
194
 
192
195
  }
@@ -364,7 +367,7 @@ export class MaterialDataManager {
364
367
 
365
368
  break;
366
369
  default:
367
- console.warn( `Unknown material property: ${property}` );
370
+ log.warn( `unknown material property: ${property}` );
368
371
  return;
369
372
 
370
373
  }
@@ -403,7 +406,7 @@ export class MaterialDataManager {
403
406
 
404
407
  if ( ! this.materialStorageAttr ) {
405
408
 
406
- console.warn( 'Material storage buffer not available' );
409
+ log.warn( 'material storage buffer not available' );
407
410
  return;
408
411
 
409
412
  }
@@ -630,7 +633,7 @@ export class MaterialDataManager {
630
633
 
631
634
  if ( ! this.materialStorageAttr ) {
632
635
 
633
- console.warn( 'Material storage buffer not available' );
636
+ log.warn( 'material storage buffer not available' );
634
637
  return;
635
638
 
636
639
  }
@@ -651,7 +654,7 @@ export class MaterialDataManager {
651
654
  const offset = transformOffsets[ textureName ];
652
655
  if ( offset === undefined ) {
653
656
 
654
- console.warn( `Unknown texture name for transform update: ${textureName}` );
657
+ log.warn( `unknown texture name for transform update: ${textureName}` );
655
658
  return;
656
659
 
657
660
  }
@@ -683,7 +686,7 @@ export class MaterialDataManager {
683
686
 
684
687
  if ( ! this.materialStorageAttr?.array ) {
685
688
 
686
- console.warn( '[MaterialDataManager] Material storage buffer not available for feature scanning' );
689
+ log.warn( 'material storage buffer not available for feature scanning' );
687
690
  return false;
688
691
 
689
692
  }
@@ -763,7 +766,7 @@ export class MaterialDataManager {
763
766
 
764
767
  if ( ! features ) {
765
768
 
766
- console.warn( '[MaterialDataManager] No sceneFeatures detected, skipping define injection' );
769
+ log.warn( 'no sceneFeatures detected, skipping define injection' );
767
770
  return;
768
771
 
769
772
  }
@@ -780,7 +783,7 @@ export class MaterialDataManager {
780
783
  // For TSL, we can't inject defines into the shader at runtime
781
784
  // Instead, we would need to conditionally generate the shader
782
785
  // For now, log the features for debugging
783
- console.log( '[MaterialDataManager] Material features:', features );
786
+ log.debug( 'material features:', features );
784
787
 
785
788
  this.compiledFeatures = featuresJSON;
786
789