rayzee 7.3.0 → 7.4.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.
@@ -17,7 +17,6 @@ import { buildShadeKernel, SHADE_WG_SIZE } from '../TSL/ShadeKernel.js';
17
17
  import { buildCompactKernel, buildCompactSubgroupKernel, COMPACT_WG_SIZE } from '../TSL/CompactKernel.js';
18
18
  import { buildFinalWriteKernel, FINALWRITE_WG_SIZE } from '../TSL/FinalWriteKernel.js';
19
19
  import { buildDebugKernel, DEBUG_WG_SIZE } from '../TSL/DebugKernel.js';
20
- import { ENGINE_DEFAULTS } from '../EngineDefaults.js';
21
20
  import {
22
21
  Fn, uint, atomicStore, atomicLoad, instanceIndex, If, Return,
23
22
  } from 'three/tsl';
@@ -35,16 +34,18 @@ export class PathTracer extends PathTracerStage {
35
34
  this._gBufferAttr = null; // per-pixel first-hit MRT (ND + albedo); see _buildWavefrontKernels
36
35
  this._wavefrontReady = false;
37
36
 
37
+ // Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. When no denoiser is active the
38
+ // wavefront skips those writes (Generate/Shade G-buffer + FinalWrite stores). Gated by a live
39
+ // uniform — NOT baked — so DenoisingManager can toggle it without a (UI-freezing) kernel rebuild.
40
+ this._auxGBufferEnabled = false;
41
+ this._auxGBufferUniform = uniform( 0, 'uint' );
42
+
38
43
  // 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)
39
44
  this._useDynamicDispatch = true;
40
45
 
41
46
  // Flag-gated off: perf-neutral vs atomic-append and adds a 'subgroups' feature dependency.
42
47
  this._useSubgroupCompact = false;
43
48
 
44
- // Multi-sample pool: S=samplesPerPixel primary rays/pixel/frame (interactive-only, ≤ the pixel cap; else S=1). FinalWrite averages the S slots. Baked into kernels; _ensureSamplesPerPass() rebuilds on change.
45
- this._multiSampleMaxPixels = ENGINE_DEFAULTS.wavefrontMultiSampleMaxPixels ?? 589824; // 768²
46
- this._samplesPerPass = 1;
47
-
48
49
  this._lastBounceCounts = null;
49
50
  // maxBounces the curve was measured at; the curve is ignored once this no longer matches (-1 = none).
50
51
  this._lastBounceCountsBudget = - 1;
@@ -63,7 +64,7 @@ export class PathTracer extends PathTracerStage {
63
64
 
64
65
  // VRAM accounting — providers are thunks reading CURRENT live resources,
65
66
  // so they survive buffer/texture reallocation (resize, scene/material reload).
66
- this.vramTracker = new VRAMTracker();
67
+ this.vramTracker = new VRAMTracker( this.renderer );
67
68
  this._registerVRAMProviders();
68
69
 
69
70
  console.log( 'PathTracer: initialized (wavefront)' );
@@ -164,19 +165,15 @@ export class PathTracer extends PathTracerStage {
164
165
  const renderMode = this.renderMode.value;
165
166
 
166
167
  let originalMaxBounces = null;
167
- let originalSamplesPerPixel = null;
168
168
 
169
169
  if ( renderMode === 1 && frameValue === 0 ) {
170
170
 
171
171
  originalMaxBounces = this.maxBounces.value;
172
- originalSamplesPerPixel = this.samplesPerPixel.value;
173
172
  this.maxBounces.value = 1;
174
- this.samplesPerPixel.value = 1;
175
173
 
176
174
  }
177
175
 
178
176
  this._handleResize();
179
- this._ensureSamplesPerPass();
180
177
  this.manageASVGFForRenderMode( renderMode );
181
178
 
182
179
  // Full-frame render is always a complete cycle (PER_CYCLE stages gate on this).
@@ -218,7 +215,6 @@ export class PathTracer extends PathTracerStage {
218
215
  if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
219
216
 
220
217
  if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
221
- if ( originalSamplesPerPixel !== null ) this.samplesPerPixel.value = originalSamplesPerPixel;
222
218
 
223
219
  this.performanceMonitor?.end();
224
220
  return;
@@ -326,7 +322,9 @@ export class PathTracer extends PathTracerStage {
326
322
 
327
323
  this._maybeReadbackCounters();
328
324
 
329
- this.storageTextures.copyToReadTargets( this.renderer );
325
+ // Skip the normalDepth/albedo copies when aux is off — the wavefront didn't write them and
326
+ // no stage reads them; saves two full-res GPU copies/frame in the default interactive path.
327
+ this.storageTextures.copyToReadTargets( this.renderer, this._auxGBufferEnabled );
330
328
 
331
329
  const readTex = this.storageTextures.getReadTextures();
332
330
  if ( context ) this._publishTexturesToContext( context, readTex );
@@ -336,7 +334,6 @@ export class PathTracer extends PathTracerStage {
336
334
  if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
337
335
 
338
336
  if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
339
- if ( originalSamplesPerPixel !== null ) this.samplesPerPixel.value = originalSamplesPerPixel;
340
337
 
341
338
  this.performanceMonitor?.end();
342
339
 
@@ -354,27 +351,16 @@ export class PathTracer extends PathTracerStage {
354
351
 
355
352
  }
356
353
 
357
- // S=samplesPerPixel for interactive within the pixel cap; production/tiled and high-res get S=1.
358
- _resolveSamplesPerPass( w, h ) {
354
+ // Aux MRT (normalDepth/albedo) is needed only by the denoiser/OIDN; DenoisingManager calls this to
355
+ // turn the wavefront's aux writes on/off. It's a live uniform, so toggling is just a value flip +
356
+ // accumulation reset — no kernel rebuild, no UI freeze.
357
+ setAuxGBufferEnabled( enabled ) {
359
358
 
360
- const interactive = this.renderMode.value === 0;
361
- const within = ( w * h ) <= this._multiSampleMaxPixels;
362
- return ( interactive && within ) ? Math.max( 1, this.samplesPerPixel.value | 0 ) : 1;
363
-
364
- }
365
-
366
- // S is baked at build but samplesPerPixel/mode can change without a resize; rebuild when the implied S differs.
367
- _ensureSamplesPerPass() {
368
-
369
- if ( ! this._wavefrontReady ) return;
370
- const w = this.storageTextures.renderWidth;
371
- const h = this.storageTextures.renderHeight;
372
- if ( this._resolveSamplesPerPass( w, h ) !== this._samplesPerPass ) {
373
-
374
- this._wavefrontReady = false;
375
- this._buildWavefrontKernels();
376
-
377
- }
359
+ enabled = !! enabled;
360
+ if ( this._auxGBufferEnabled === enabled ) return;
361
+ this._auxGBufferEnabled = enabled;
362
+ this._auxGBufferUniform.value = enabled ? 1 : 0;
363
+ this.reset();
378
364
 
379
365
  }
380
366
 
@@ -464,12 +450,10 @@ export class PathTracer extends PathTracerStage {
464
450
  this._readbackFrameCounter = 0;
465
451
  this._readbackGeneration ++;
466
452
 
467
- // Recompile only when buffers reallocate (capacity grows) or S changes; otherwise resize uniforms in place.
468
- const newS = this._resolveSamplesPerPass( newW, newH );
469
- const neededCap = PackedRayBuffer.requiredCapacity( newW * newH * newS );
453
+ // Recompile only when buffers reallocate (capacity grows); otherwise resize uniforms in place.
454
+ const neededCap = PackedRayBuffer.requiredCapacity( newW * newH );
470
455
  const mustRebuild = ! this._packedBuffers
471
- || neededCap > this._packedBuffers.capacity
472
- || newS !== this._samplesPerPass;
456
+ || neededCap > this._packedBuffers.capacity;
473
457
 
474
458
  if ( mustRebuild ) {
475
459
 
@@ -485,10 +469,10 @@ export class PathTracer extends PathTracerStage {
485
469
 
486
470
  }
487
471
 
488
- // Same-capacity, same-S resize: update render-size uniforms + early-exit threshold, no recompile.
472
+ // Same-capacity resize: update render-size uniforms + early-exit threshold, no recompile.
489
473
  _resizeWavefrontInPlace( w, h ) {
490
474
 
491
- const maxRays = w * h * this._samplesPerPass;
475
+ const maxRays = w * h;
492
476
  this._wfRenderWidth.value = w;
493
477
  this._wfRenderHeight.value = h;
494
478
  this._wfMaxRayCount.value = maxRays;
@@ -509,11 +493,7 @@ export class PathTracer extends PathTracerStage {
509
493
 
510
494
  const w = this.storageTextures.renderWidth;
511
495
  const h = this.storageTextures.renderHeight;
512
- // maxRays = pool capacity (pixels × S); all downstream sizing scales off it, so S propagates for free.
513
- this._samplesPerPass = this._resolveSamplesPerPass( w, h );
514
- const S = this._samplesPerPass | 0;
515
- const maxRaysPerSample = w * h;
516
- const maxRays = maxRaysPerSample * S;
496
+ const maxRays = w * h;
517
497
 
518
498
  if ( this._bounceEarlyExitThreshold !== - 1 ) {
519
499
 
@@ -534,9 +514,9 @@ export class PathTracer extends PathTracerStage {
534
514
  // Per-pixel G-buffer (first-hit MRT: ND + albedo), 1 uvec4/pixel — half-precision packed (pack2x16).
535
515
  // uint (not f32) buffer: packed lanes can hit the NaN exponent range (e.g. snorm 1.0 → 0x7FFF), which a
536
516
  // GPU may canonicalize through f32 storage; u32 stores the bits verbatim. Separate from RAY — it's
537
- // per-pixel (not per-ray×S), written by Generate/Shade bounce-0 and read only by FinalWrite.
517
+ // per-pixel, written by Generate/Shade bounce-0 and read only by FinalWrite.
538
518
  // 1.25× margin (same as the per-ray buffers) so it survives the in-place-resize range.
539
- const gBufferVec4s = PackedRayBuffer.requiredCapacity( maxRaysPerSample ) * GBUFFER_STRIDE;
519
+ const gBufferVec4s = PackedRayBuffer.requiredCapacity( maxRays ) * GBUFFER_STRIDE;
540
520
  this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
541
521
  const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
542
522
  const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
@@ -639,14 +619,13 @@ export class PathTracer extends PathTracerStage {
639
619
  anamorphicRatio: this.anamorphicRatio,
640
620
  renderWidth: this._wfRenderWidth,
641
621
  renderHeight: this._wfRenderHeight,
642
- samplesPerPass: S,
643
622
  transmissiveBounces: this.transmissiveBounces,
644
623
  transparentBackground: this.transparentBackground,
624
+ auxGBufferEnabled: this._auxGBufferUniform,
645
625
  } );
646
626
  this._kernelManager.register( 'generate',
647
627
  genFn().compute(
648
- // Multi-sample: dispatch covers h·S rows (each sub-sample is a row band).
649
- [ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( ( h * S ) / GENERATE_WG_SIZE ), 1 ],
628
+ [ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( h / GENERATE_WG_SIZE ), 1 ],
650
629
  [ GENERATE_WG_SIZE, GENERATE_WG_SIZE, 1 ]
651
630
  )
652
631
  );
@@ -743,6 +722,9 @@ export class PathTracer extends PathTracerStage {
743
722
  maxSubsurfaceSteps: this.maxSubsurfaceSteps,
744
723
  transparentBackground: this.transparentBackground,
745
724
  backgroundIntensity: this.backgroundIntensity,
725
+ backgroundColor: this.backgroundColor,
726
+ backgroundBlurriness: this.backgroundBlurriness,
727
+ backgroundBlurSamples: this.backgroundBlurSamples,
746
728
  showBackground: this.showBackground,
747
729
  globalIlluminationIntensity: this.globalIlluminationIntensity,
748
730
  cameraProjectionMatrix: this.cameraProjectionMatrix,
@@ -760,6 +742,7 @@ export class PathTracer extends PathTracerStage {
760
742
  reverseMapVec4Offset: this.reverseMapVec4Offset,
761
743
  currentBounce: this._wfCurrentBounce,
762
744
  maxRayCount: this._wfMaxRayCount,
745
+ auxGBufferEnabled: this._auxGBufferUniform,
763
746
  } );
764
747
  this._kernelManager.register( 'shade',
765
748
  shadeFn().compute(
@@ -833,8 +816,8 @@ export class PathTracer extends PathTracerStage {
833
816
  prevAlbedoTexture: prevAlbedo,
834
817
  renderWidth: this._wfRenderWidth,
835
818
  renderHeight: this._wfRenderHeight,
836
- samplesPerPass: S,
837
819
  visMode: this.visMode,
820
+ auxGBufferEnabled: this._auxGBufferUniform,
838
821
  } );
839
822
  this._kernelManager.register( 'finalWrite',
840
823
  // Per-pixel (w×h) — kernel averages the S sample-slots internally.
@@ -873,7 +856,6 @@ export class PathTracer extends PathTracerStage {
873
856
  enableEnvironmentLight: this.enableEnvironment,
874
857
  visMode: this.visMode,
875
858
  debugVisScale: this.debugVisScale,
876
- samplesPerPass: this._samplesPerPass,
877
859
  albedoMaps: freshAlbedoMaps,
878
860
  normalMaps: freshNormalMaps,
879
861
  bumpMaps: freshBumpMaps,
@@ -898,11 +880,10 @@ export class PathTracer extends PathTracerStage {
898
880
 
899
881
  const w = this._wfRenderWidth.value;
900
882
  const h = this._wfRenderHeight.value;
901
- const S = this._samplesPerPass | 0;
902
883
 
903
884
  this._kernelManager.setDispatchCount( 'generate', [
904
885
  Math.ceil( w / GENERATE_WG_SIZE ),
905
- Math.ceil( ( h * S ) / GENERATE_WG_SIZE ), 1
886
+ Math.ceil( h / GENERATE_WG_SIZE ), 1
906
887
  ] );
907
888
  this._kernelManager.setDispatchCount( 'finalWrite', [
908
889
  Math.ceil( w / FINALWRITE_WG_SIZE ),
@@ -279,18 +279,6 @@ export class PathTracerStage extends RenderStage {
279
279
 
280
280
  }
281
281
  },
282
- numRaysPerPixel: {
283
- get value() {
284
-
285
- return self.samplesPerPixel.value;
286
-
287
- },
288
- set value( v ) {
289
-
290
- self.samplesPerPixel.value = v;
291
-
292
- }
293
- },
294
282
  useEnvMapIS: {
295
283
  get value() {
296
284
 
@@ -346,7 +334,6 @@ export class PathTracerStage extends RenderStage {
346
334
  enabled: DEFAULT_STATE.interactionModeEnabled,
347
335
  qualitySettings: {
348
336
  maxBounceCount: 1,
349
- numRaysPerPixel: 1,
350
337
  useEnvMapIS: false,
351
338
  enableAccumulation: false,
352
339
  enableEmissiveTriangleSampling: false,
@@ -171,6 +171,22 @@ export class SSRC extends RenderStage {
171
171
 
172
172
  }
173
173
 
174
+ // Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
175
+ // after re-enable, and reset() clears the temporal cache. See ASVGF.releaseGPUMemory.
176
+ releaseGPUMemory() {
177
+
178
+ this._cacheTexA?.dispose();
179
+ this._cacheTexB?.dispose();
180
+ this._prevNDTexA?.dispose();
181
+ this._prevNDTexB?.dispose();
182
+ this._outputTex?.dispose();
183
+ // Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
184
+ this.context?.removeTexture( 'ssrc:output' );
185
+ this.outputTarget?.texture?.dispose();
186
+ this.reset();
187
+
188
+ }
189
+
174
190
  reset() {
175
191
 
176
192
  this._resetCache();
@@ -363,6 +363,19 @@ export class Variance extends RenderStage {
363
363
 
364
364
  }
365
365
 
366
+ // Free the 2048² StorageTextures when disabled; three.js re-creates them on the next dispatch
367
+ // after re-enable, and reset() re-anchors the EMA. See ASVGF.releaseGPUMemory.
368
+ releaseGPUMemory() {
369
+
370
+ this._storageTexA?.dispose();
371
+ this._storageTexB?.dispose();
372
+ // Render-res RT texture (dispose .texture, not the RT — RT.dispose() doesn't free it here).
373
+ this.context?.removeTexture( 'variance:output' );
374
+ this._outputTarget?.texture?.dispose();
375
+ this.reset();
376
+
377
+ }
378
+
366
379
  reset() {
367
380
 
368
381
  this.currentMoments = 0;
@@ -32,7 +32,6 @@ export function buildDebugKernel( params ) {
32
32
  visMode, debugVisScale,
33
33
  albedoMaps, normalMaps, bumpMaps, metalnessMaps, roughnessMaps, emissiveMaps,
34
34
  frame,
35
- samplesPerPass = 1,
36
35
  } = params;
37
36
 
38
37
  const computeFn = Fn( () => {
@@ -61,8 +60,8 @@ export function buildDebugKernel( params ) {
61
60
  // Mode 9: visualize the stratified AA-jitter pattern (R,G = jitter).
62
61
  If( visMode.equal( int( 9 ) ), () => {
63
62
 
64
- // Use the real per-frame sample count so >1 SPP shows the stratified lattice (totalRays≤1 → plain random).
65
- const jitter = getStratifiedSample( pixelCoord, int( 0 ), int( samplesPerPass ), seed, resolution, frame );
63
+ // One ray per pixel — plain per-frame jitter (totalRays = 1 random, no stratified lattice).
64
+ const jitter = getStratifiedSample( pixelCoord, int( 0 ), int( 1 ), seed, resolution, frame );
66
65
  color.assign( vec4( jitter, 1.0, 1.0 ) );
67
66
 
68
67
  } ).Else( () => {
@@ -34,12 +34,13 @@ export function buildFinalWriteKernel( params ) {
34
34
  transparentBackground,
35
35
  prevAccumTexture, prevNormalDepthTexture, prevAlbedoTexture,
36
36
  renderWidth, renderHeight,
37
- // Multi-sample: average S sample-slots per pixel (slot = pixel + k*w*h, w*h from the resolution uniform).
38
- samplesPerPass = 1,
39
37
  visMode,
38
+ // Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
39
+ // on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
40
+ auxGBufferEnabled,
40
41
  } = params;
41
42
 
42
- const S = samplesPerPass | 0;
43
+ const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
43
44
 
44
45
  const computeFn = Fn( () => {
45
46
 
@@ -51,31 +52,21 @@ export function buildFinalWriteKernel( params ) {
51
52
  const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
52
53
  const rayID = uint( pixelIndex );
53
54
 
54
- // Average the S sub-samples; MRT (normal/depth/albedo) from sub-sample 0.
55
- const sampleColor = ( () => {
56
-
57
- if ( S <= 1 ) return readRayRadiance( rayBufferRO, rayID );
58
- const acc = readRayRadiance( rayBufferRO, rayID ).toVar();
59
- const mrps = uint( resolution.x ).mul( uint( resolution.y ) ).toVar(); // w*h from the resolution uniform, not baked
60
- for ( let k = 1; k < S; k ++ ) {
61
-
62
- acc.addAssign( readRayRadiance( rayBufferRO, rayID.add( uint( k ).mul( mrps ) ) ) );
63
-
64
- }
55
+ const sampleColor = readRayRadiance( rayBufferRO, rayID );
56
+ const finalColor = sampleColor.xyz.toVar();
57
+ const outputAlpha = select( transparentBackground, sampleColor.w, float( 1.0 ) ).toVar();
65
58
 
66
- acc.assign( acc.div( float( S ) ) );
67
- return acc;
59
+ // MRT comes from the per-pixel G-buffer (rayID == pixelIndex). Half-packed: decode.
60
+ // auxOn gates the decode + stores so a no-denoiser frame does no G-buffer read and no aux writes.
61
+ const finalNormalDepth = vec4( 0.0 ).toVar();
62
+ const finalAlbedo = vec4( 0.0 ).xyz.toVar();
63
+ If( auxOn, () => {
68
64
 
69
- } )();
70
- // MRT comes from the per-pixel G-buffer (rayID == pixelIndex here, i.e. sub-sample 0). Half-packed: decode.
71
- const gbuf = readGBuffer( gBufferRO, rayID );
72
- const normalDepth = gbDecodeNormalDepth( gbuf );
73
- const albedoID = vec4( gbDecodeAlbedo( gbuf ), 0.0 );
65
+ const gbuf = readGBuffer( gBufferRO, rayID );
66
+ finalNormalDepth.assign( gbDecodeNormalDepth( gbuf ) );
67
+ finalAlbedo.assign( vec4( gbDecodeAlbedo( gbuf ), 0.0 ).xyz );
74
68
 
75
- const finalColor = sampleColor.xyz.toVar();
76
- const finalNormalDepth = normalDepth.toVar();
77
- const finalAlbedo = albedoID.xyz.toVar();
78
- const outputAlpha = select( transparentBackground, sampleColor.w, float( 1.0 ) ).toVar();
69
+ } );
79
70
 
80
71
  const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
81
72
  const prevUV = pixelCoord.div( resolution );
@@ -87,8 +78,12 @@ export function buildFinalWriteKernel( params ) {
87
78
  const prevAccumSample = texture( prevAccumTexture, prevUV, 0 ).toVar();
88
79
 
89
80
  finalColor.assign( mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) );
90
- finalNormalDepth.assign( mix( texture( prevNormalDepthTexture, prevUV, 0 ), finalNormalDepth, accumulationAlpha ) );
91
- finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
81
+ If( auxOn, () => {
82
+
83
+ finalNormalDepth.assign( mix( texture( prevNormalDepthTexture, prevUV, 0 ), finalNormalDepth, accumulationAlpha ) );
84
+ finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
85
+
86
+ } );
92
87
 
93
88
  If( transparentBackground, () => {
94
89
 
@@ -107,8 +102,12 @@ export function buildFinalWriteKernel( params ) {
107
102
 
108
103
  const uintCoord = uvec2( uint( gx ), uint( gy ) );
109
104
  textureStore( writeColorTex, uintCoord, vec4( finalColor, outputAlpha ) ).toWriteOnly();
110
- textureStore( writeNDTex, uintCoord, finalNormalDepth ).toWriteOnly();
111
- textureStore( writeAlbedoTex, uintCoord, vec4( finalAlbedo, 1.0 ) ).toWriteOnly();
105
+ If( auxOn, () => {
106
+
107
+ textureStore( writeNDTex, uintCoord, finalNormalDepth ).toWriteOnly();
108
+ textureStore( writeAlbedoTex, uintCoord, vec4( finalAlbedo, 1.0 ) ).toWriteOnly();
109
+
110
+ } );
112
111
 
113
112
  } );
114
113
 
@@ -19,7 +19,7 @@ import { Ray } from './Struct.js';
19
19
  import { RAY_FLAG } from '../Processor/QueueManager.js';
20
20
  import {
21
21
  writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
22
- writeRayRadiance, writeGBuffer, writeGBufferSurfaceID,
22
+ writeRayRadiance, writeGBuffer,
23
23
  writeMediumStack,
24
24
  } from '../Processor/PackedRayBuffer.js';
25
25
 
@@ -33,39 +33,32 @@ export function buildGenerateKernel( params ) {
33
33
  cameraWorldMatrix, cameraProjectionMatrixInverse,
34
34
  enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
35
35
  renderWidth, renderHeight,
36
- // Multi-sample: S primary rays/pixel/frame; S>1 dispatch covers h*S rows, ray lands in slot subSample*(w*h) + pixelIndex.
37
- samplesPerPass = 1,
38
36
  transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
39
37
  transparentBackground, // alpha inits to 1 here (megakernel parity: PathTracerCore.js:554) — env-escape-without-opaque zeroes it in Shade
38
+ auxGBufferEnabled, // live uniform: 1 = init the per-pixel G-buffer (denoiser on), 0 = skip it
40
39
  } = params;
41
40
 
42
- const S = samplesPerPass | 0;
41
+ const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
43
42
 
44
43
  const computeFn = Fn( () => {
45
44
 
46
45
  const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
47
- const gyRaw = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
46
+ const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
48
47
 
49
- const subSample = S > 1 ? gyRaw.div( renderHeight ).toVar() : int( 0 );
50
- const gy = S > 1 ? gyRaw.sub( subSample.mul( renderHeight ) ).toVar() : gyRaw;
51
- const yBound = S > 1 ? renderHeight.mul( int( S ) ) : renderHeight;
52
-
53
- If( gx.lessThan( renderWidth ).and( gyRaw.lessThan( yBound ) ), () => {
48
+ If( gx.lessThan( renderWidth ).and( gy.lessThan( renderHeight ) ), () => {
54
49
 
55
50
  const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
56
51
  const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
57
- // maxRaysPerSample = w*h, derived from the resolution uniform (NOT baked) so resize never changes the WGSL.
58
- const rayID = S > 1
59
- ? uint( pixelIndex ).add( uint( subSample ).mul( uint( resolution.x ).mul( uint( resolution.y ) ) ) )
60
- : uint( pixelIndex );
52
+ // One ray per pixel: rayID is the pixel index.
53
+ const rayID = uint( pixelIndex );
61
54
 
62
55
  const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
63
56
  screenPosition.y.assign( screenPosition.y.negate() );
64
57
 
65
- const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: subSample, frame } ).toVar();
58
+ const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
66
59
  const seed = pcgHash( { state: baseSeed } ).toVar();
67
60
 
68
- const stratifiedJitter = getStratifiedSample( pixelCoord, subSample, int( S ), seed, resolution, frame ).toVar();
61
+ const stratifiedJitter = getStratifiedSample( pixelCoord, int( 0 ), int( 1 ), seed, resolution, frame ).toVar();
69
62
 
70
63
  const jitterScale = vec2( 2.0 ).div( resolution );
71
64
  const jitter = stratifiedJitter.sub( 0.5 ).mul( jitterScale );
@@ -78,6 +71,8 @@ export function buildGenerateKernel( params ) {
78
71
  ) );
79
72
 
80
73
  writeRayOriginMeta( rayBufferRW, rayID, ray.origin, int( 0 ), int( 0 ) );
74
+ // A fresh camera ray is NOT redirected — REDIRECTED stays clear so it sees the direct backdrop;
75
+ // ShadeKernel sets REDIRECTED on the first direction-changing interaction (see RAY_FLAG.REDIRECTED).
81
76
  writeRayDirFlags( rayBufferRW, rayID, ray.direction, uint( RAY_FLAG.ACTIVE ) );
82
77
  // pdf inits to 0 = prevBouncePdf (megakernel parity PathTracerCore.js:556). The bounce>0 env/emissive
83
78
  // MIS gate skips until an opaque scatter writes a real combinedPdf; free bounces preserve it.
@@ -87,12 +82,10 @@ export function buildGenerateKernel( params ) {
87
82
  // keeps alpha 1 → solid. Non-transparent mode is inert (FinalWrite forces alpha 1).
88
83
  writeRayRadiance( rayBufferRW, rayID, vec4( vec3( 0.0 ), select( transparentBackground, float( 1.0 ), float( 0.0 ) ) ) );
89
84
 
90
- If( subSample.equal( int( 0 ) ), () => {
85
+ If( auxOn, () => {
91
86
 
92
87
  // default: normal +Z, depth 1 (far), black albedo (background/miss)
93
88
  writeGBuffer( gBufferRW, uint( pixelIndex ), vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
94
- // surface-ID lane defaults to invalid (valid=0); Shade overwrites it at the bounce-0 hit.
95
- writeGBufferSurfaceID( gBufferRW, uint( pixelIndex ), uint( 0 ), uint( 0 ), float( 0.0 ), float( 0.0 ), uint( 0 ) );
96
89
 
97
90
  } );
98
91