rayzee 7.11.0 → 7.11.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.11.0",
3
+ "version": "7.11.1",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -19,6 +19,7 @@ export class ShaderBuilder {
19
19
  // Previous-frame texture nodes (sample from MRT RenderTarget)
20
20
  this.prevColorTexNode = null;
21
21
  this.prevAlbedoTexNode = null;
22
+ this.prevNormalDepthTexNode = null;
22
23
 
23
24
  // Scene texture nodes cache (for in-place updates on model change)
24
25
  this._sceneTextureNodes = null;
@@ -98,6 +99,7 @@ export class ShaderBuilder {
98
99
  const readTextures = storageTextures.getReadTextures();
99
100
  this.prevColorTexNode = texture( readTextures.color );
100
101
  this.prevAlbedoTexNode = texture( readTextures.albedo );
102
+ this.prevNormalDepthTexNode = texture( readTextures.normalDepth );
101
103
 
102
104
  const createArrayPlaceholder = () => {
103
105
 
@@ -137,6 +139,7 @@ export class ShaderBuilder {
137
139
 
138
140
  this.prevColorTexNode = null;
139
141
  this.prevAlbedoTexNode = null;
142
+ this.prevNormalDepthTexNode = null;
140
143
  this._sceneTextureNodes = null;
141
144
 
142
145
  }
@@ -46,6 +46,11 @@ export class PathTracer extends PathTracerStage {
46
46
  // uniform — NOT baked — so DenoisingManager can toggle it without a (UI-freezing) kernel rebuild.
47
47
  this._auxGBufferEnabled = false;
48
48
  this._auxGBufferUniform = uniform( 0, 'uint' );
49
+ // Clean-aux mode: temporally accumulate + renormalize the aux NORMAL (like albedo) so a clean-aux
50
+ // OIDN model (calb_cnrm/high, alb_nrm/balanced) gets the prefiltered-ish normal it expects instead
51
+ // of the point-sampled per-frame value that leaks noise. Off for fast/ASVGF (they want the bump normal).
52
+ this._cleanAuxNormalEnabled = false;
53
+ this._cleanAuxNormalUniform = uniform( 0, 'uint' );
49
54
 
50
55
  // CPU sizes per-bounce kernels from last frame's survivor curve; kernels bound on ENTERING_COUNT so over-sizing is safe. (indirect dispatch not viable — three.js doesn't sync compute-written indirect buffers across submissions)
51
56
  this._useDynamicDispatch = true;
@@ -220,6 +225,7 @@ export class PathTracer extends PathTracerStage {
220
225
 
221
226
  this.shaderBuilder.prevColorTexNode.value = readTextures.color;
222
227
  this.shaderBuilder.prevAlbedoTexNode.value = readTextures.albedo;
228
+ this.shaderBuilder.prevNormalDepthTexNode.value = readTextures.normalDepth;
223
229
 
224
230
  }
225
231
 
@@ -410,6 +416,19 @@ export class PathTracer extends PathTracerStage {
410
416
 
411
417
  }
412
418
 
419
+ // Clean-aux normal: when a clean-aux OIDN model is active (calb_cnrm/high, alb_nrm/balanced), FinalWrite
420
+ // temporally accumulates + renormalizes the aux normal so the model isn't fed per-frame point-sampled
421
+ // noise. DenoisingManager calls this on OIDN enable + quality change. Live uniform → value flip + reset.
422
+ setCleanAuxNormal( enabled ) {
423
+
424
+ enabled = !! enabled;
425
+ if ( this._cleanAuxNormalEnabled === enabled ) return;
426
+ this._cleanAuxNormalEnabled = enabled;
427
+ this._cleanAuxNormalUniform.value = enabled ? 1 : 0;
428
+ this.reset();
429
+
430
+ }
431
+
413
432
  // UI-driven resize (Resolution dropdown) — parent bypasses _handleResize(), so hook here too.
414
433
  setSize( width, height ) {
415
434
 
@@ -605,6 +624,7 @@ export class PathTracer extends PathTracerStage {
605
624
 
606
625
  const prevColor = this.shaderBuilder.prevColorTexNode;
607
626
  const prevAlbedo = this.shaderBuilder.prevAlbedoTexNode;
627
+ const prevNormalDepth = this.shaderBuilder.prevNormalDepthTexNode;
608
628
  const writeTex = this.storageTextures.getWriteTextures();
609
629
 
610
630
  const counters = qm.getCounters();
@@ -907,10 +927,12 @@ export class PathTracer extends PathTracerStage {
907
927
  transparentBackground: this.transparentBackground,
908
928
  prevAccumTexture: prevColor,
909
929
  prevAlbedoTexture: prevAlbedo,
930
+ prevNormalDepthTexture: prevNormalDepth,
910
931
  renderWidth: this._wfRenderWidth,
911
932
  renderHeight: this._wfRenderHeight,
912
933
  visMode: this.visMode,
913
934
  auxGBufferEnabled: this._auxGBufferUniform,
935
+ cleanAuxNormalEnabled: this._cleanAuxNormalUniform,
914
936
  } );
915
937
  this._kernelManager.register( 'finalWrite',
916
938
  // Per-pixel (w×h) — kernel averages the S sample-slots internally.
@@ -4,7 +4,7 @@
4
4
 
5
5
  import {
6
6
  Fn, wgslFn, float, vec2, vec4, int, uint, uvec2,
7
- If, mix, select, texture, textureStore,
7
+ If, mix, select, texture, textureStore, length,
8
8
  localId, workgroupId,
9
9
  } from 'three/tsl';
10
10
 
@@ -32,15 +32,19 @@ export function buildFinalWriteKernel( params ) {
32
32
  resolution, frame,
33
33
  enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
34
34
  transparentBackground,
35
- prevAccumTexture, prevAlbedoTexture,
35
+ prevAccumTexture, prevAlbedoTexture, prevNormalDepthTexture,
36
36
  renderWidth, renderHeight,
37
37
  visMode,
38
38
  // Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
39
39
  // on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
40
40
  auxGBufferEnabled,
41
+ // Clean-aux normal (1 = temporally accumulate + renormalize the aux normal). On only for clean-aux
42
+ // OIDN models (calb_cnrm/high, alb_nrm/balanced); off for fast/ASVGF which want the bump normal.
43
+ cleanAuxNormalEnabled,
41
44
  } = params;
42
45
 
43
46
  const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
47
+ const cleanAuxNormalOn = cleanAuxNormalEnabled.greaterThan( uint( 0 ) );
44
48
 
45
49
  const computeFn = Fn( () => {
46
50
 
@@ -80,12 +84,27 @@ export function buildFinalWriteKernel( params ) {
80
84
  finalColor.assign( mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) );
81
85
  If( auxOn, () => {
82
86
 
83
- // Albedo averages cleanly (it's a colour). The NORMAL must NOT: averaging jittered
84
- // unit normals collapses toward the flat geometric mean (worse at distance), which made
85
- // OIDN smooth bump detail away. Keep this frame's point-sampled normal — it varies with
86
- // the bump, which is exactly what OIDN's edge-stop needs to preserve it.
87
+ // Albedo averages cleanly (it's a colour).
87
88
  finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
88
89
 
90
+ // NORMAL: by default keep this frame's POINT-SAMPLED normal — it varies with the bump,
91
+ // which fast/ASVGF want to preserve edge detail. But a CLEAN-AUX OIDN model (calb_cnrm/high,
92
+ // alb_nrm/balanced) trusts the aux and is fed per-frame point-sampled NOISE → leaked noise
93
+ // (high) / over-smoothing (balanced). When cleanAuxNormalOn, temporally accumulate it like
94
+ // the colour. Average the RAW unit normals (decode 0.5+0.5 → [-1,1]) and RENORMALIZE — a
95
+ // plain encoded-space mix would bias toward the flat (0.5,0.5,1) mean and collapse detail.
96
+ // Depth (.w) stays this frame's value; guard the degenerate near-zero mean (opposing normals).
97
+ If( cleanAuxNormalOn, () => {
98
+
99
+ const prevN = texture( prevNormalDepthTexture, prevUV, 0 ).xyz.mul( 2.0 ).sub( 1.0 );
100
+ const curN = finalNormalDepth.xyz.mul( 2.0 ).sub( 1.0 ).toVar();
101
+ const mixedN = mix( prevN, curN, accumulationAlpha ).toVar();
102
+ const len = length( mixedN );
103
+ const avgN = select( len.greaterThan( 1e-4 ), mixedN.div( len ), curN );
104
+ finalNormalDepth.assign( vec4( avgN.mul( 0.5 ).add( 0.5 ), finalNormalDepth.w ) );
105
+
106
+ } );
107
+
89
108
  } );
90
109
 
91
110
  If( transparentBackground, () => {
@@ -54,7 +54,7 @@ import {
54
54
  readHitDistance, readHitBarycentrics, readHitNormal,
55
55
  readHitMaterialIndex, readHitTriangleIndex,
56
56
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
57
- writeGBuffer, readGBuffer, gbDecodeNormalDepth,
57
+ writeGBuffer, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
58
58
  readRayRadiance,
59
59
  } from '../Processor/PackedRayBuffer.js';
60
60
 
@@ -320,6 +320,11 @@ export function buildShadeKernel( params ) {
320
320
  const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
321
321
  const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
322
322
 
323
+ // Fix ①: OIDN clean-aux for a redirected ray that escapes to the environment (e.g. smooth
324
+ // glass over sky). Holds the clamped env colour this ray sees; written into the aux G-buffer
325
+ // before the miss Return below, but only when the pixel still carries the black bounce-0 default.
326
+ const escapedAuxAlbedo = vec3( 0.0 ).toVar();
327
+
323
328
  If( wantBackdrop.or( wantEnvLight ), () => {
324
329
 
325
330
  // Ground projection bends the backdrop-view env lookup onto a projected sphere+disk so the lower
@@ -358,6 +363,10 @@ export function buildShadeKernel( params ) {
358
363
 
359
364
  } );
360
365
 
366
+ // Fix ①: remember the (clamped) env colour this escaped ray sees, so a redirected ray
367
+ // that never captured a surface aux can feed OIDN a meaningful albedo below.
368
+ escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
369
+
361
370
  // MIS weight for implicit env hit — prevents double-counting with NEE
362
371
  const envMisWeight = float( 1.0 ).toVar();
363
372
  If( isBackdropView.not().and( useEnvMapIS ), () => {
@@ -425,6 +434,25 @@ export function buildShadeKernel( params ) {
425
434
 
426
435
  } );
427
436
 
437
+ // Fix ①: fill the black bounce-0 default aux for a redirected ray that reached the
438
+ // environment without ever locking a surface guide (smooth glass / specular transmission
439
+ // over sky). Only when the stored albedo is still the black default (dot < 1e-4), so a real
440
+ // specular-surface aux written upstream is never clobbered. Normal = the escaped ray
441
+ // direction (varies per pixel → OIDN sees structure that tracks the refracted view);
442
+ // albedo = the clamped env colour it sees; depth kept at the primary hit.
443
+ If( auxOn
444
+ .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
445
+ .and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
446
+
447
+ const gPrev = readGBuffer( gBufferRW, pixelIndex );
448
+ If( dot( gbDecodeAlbedo( gPrev ), vec3( 1.0 ) ).lessThan( float( 1e-4 ) ), () => {
449
+
450
+ writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, escapedAuxAlbedo );
451
+
452
+ } );
453
+
454
+ } );
455
+
428
456
  writeRayRadiance( rayBufferRW, rayID, currentRadiance );
429
457
  writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
430
458
  Return();
@@ -607,7 +635,23 @@ export function buildShadeKernel( params ) {
607
635
  // (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
608
636
  // block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
609
637
  // (objectNormal/objectColor stay at their init for transmissive-then-miss).
610
- writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
638
+ //
639
+ // Fix ③: BLEND (semi-transparent) glass instead LOCKS its OWN normal + albedo here. Its per-frame
640
+ // stochastic alpha-skip-vs-opaque-shade outcome (MaterialTransmission.js:522-540) otherwise writes a
641
+ // DIFFERENT surface into the aux each frame (wall-behind on skip vs glass on opaque); with the normal
642
+ // not temporally averaged that becomes the speckle OIDN smears on — the visible artefact on
643
+ // alpha-transparent glass. A deterministic glass-surface guide every frame removes it. MASK / opaque
644
+ // materials keep the default + rely on the downstream aux-extend.
645
+ If( material.alphaMode.equal( int( 2 ) ), () => {
646
+
647
+ writeGBuffer( gBufferRW, pixelIndex, N, linearDepth, albedo.xyz );
648
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
649
+
650
+ } ).Else( () => {
651
+
652
+ writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
653
+
654
+ } );
611
655
 
612
656
  } );
613
657
 
@@ -744,6 +788,26 @@ export function buildShadeKernel( params ) {
744
788
 
745
789
  throughput.mulAssign( interaction.throughput );
746
790
 
791
+ // Fix ②: OIDN clean-aux for STOCHASTIC glass (rough / frosted / dispersive), whose refracted
792
+ // ray hits a different behind-surface every frame. Extending aux through it (the aux-extend on
793
+ // the opaque-hit path) captures that per-frame surface — albedo then temporally averaged, normal
794
+ // point-sampled — an inconsistent guide OIDN smears on. Instead lock the glass surface's OWN
795
+ // normal + tint here (deterministic, spatially coherent). Smooth non-dispersive glass is left
796
+ // alone: its refraction is a per-pixel delta, so the behind-surface aux captured downstream is
797
+ // already stable. Gated to a genuine dielectric event (not alpha-cutout passthrough or SSS).
798
+ If( auxOn
799
+ .and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
800
+ .and( interaction.isTransmissive )
801
+ .and( interaction.isAlphaSkip.not() )
802
+ .and( interaction.isSubsurface.not() )
803
+ .and( material.roughness.greaterThan( 0.05 ).or( material.dispersion.greaterThan( 0.0 ) ) ), () => {
804
+
805
+ const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
806
+ writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
807
+ flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
808
+
809
+ } );
810
+
747
811
  // reflection stays on same side, transmission pushes through
748
812
  const reflectOffsetDir = select( interaction.entering, N, N.negate() );
749
813
  const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
@@ -368,6 +368,10 @@ export class DenoisingManager extends EventDispatcher {
368
368
  // MRT read-targets directly). When none are active the wavefront skips those writes entirely.
369
369
  s.pathTracer?.setAuxGBufferEnabled?.( normalNeeded || !! this.denoiser?.enabled );
370
370
 
371
+ // Clean-aux normal: temporally accumulate the aux normal only for OIDN clean-aux models
372
+ // (balanced/high). 'fast' and the real-time denoisers keep the point-sampled bump normal.
373
+ s.pathTracer?.setCleanAuxNormal?.( !! this.denoiser?.enabled && this.denoiser?.quality !== 'fast' );
374
+
371
375
  // Reclaim VRAM: free the big 2048² StorageTextures of any denoiser/G-buffer stage that ended up
372
376
  // disabled (lazily re-created on the next dispatch after re-enable). Every strategy/denoiser
373
377
  // toggle funnels through here after the enabled flags above are settled, so this is the one
@@ -610,6 +614,8 @@ export class DenoisingManager extends EventDispatcher {
610
614
  setOIDNQuality( quality ) {
611
615
 
612
616
  this.denoiser?.updateQuality( quality );
617
+ // Clean-aux normal follows the model: balanced/high (clean-aux models) → accumulate; fast → keep bump.
618
+ this._stages.pathTracer?.setCleanAuxNormal?.( !! this.denoiser?.enabled && quality !== 'fast' );
613
619
 
614
620
  }
615
621