rayzee 7.3.0 → 7.4.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rayzee",
3
- "version": "7.3.0",
3
+ "version": "7.4.0",
4
4
  "type": "module",
5
5
  "description": "Real-time WebGPU path tracing engine built on Three.js",
6
6
  "main": "dist/rayzee.umd.js",
@@ -34,7 +34,7 @@
34
34
  "prepublishOnly": "npm run build"
35
35
  },
36
36
  "peerDependencies": {
37
- "three": ">=0.184.0"
37
+ "three": ">=0.185.0"
38
38
  },
39
39
  "optionalDependencies": {
40
40
  "oidn-web": ">=0.3.0",
@@ -19,6 +19,13 @@ export const ENGINE_DEFAULTS = {
19
19
  useImportanceSampledEnvironment: true,
20
20
  environmentIntensity: 1,
21
21
  backgroundIntensity: 1,
22
+ // Solid backdrop color shown on camera-ray misses in 'color' background mode
23
+ // (showBackground=false, transparentBackground=false). Black = legacy hidden-backdrop look.
24
+ backgroundColor: '#000000',
25
+ // Backdrop blur (env background only). 0 = sharp/off (no cost). Cone-jitter blur of the
26
+ // primary-ray env lookup; lighting/reflections stay sharp. Samples = taps/frame (noise vs cost).
27
+ backgroundBlurriness: 0,
28
+ backgroundBlurSamples: 8,
22
29
  environmentRotation: 270.0,
23
30
  groundProjectionEnabled: false,
24
31
  groundProjectionRadius: 100,
@@ -64,16 +71,11 @@ export const ENGINE_DEFAULTS = {
64
71
  afScreenPoint: { x: 0.5, y: 0.5 },
65
72
  afSmoothingFactor: 0.15,
66
73
 
67
- // Multi-sample pool: S=samplesPerPixel rays/pixel/frame, FinalWrite averages them; interactive-only (renderMode 0, ≤ cap), else S=1.
68
- // Pixel cap (768²) bounds pool memory; covers the 512² default, excludes ≥768².
69
- wavefrontMultiSampleMaxPixels: 589824,
70
-
71
74
  enablePathTracer: true,
72
75
  enableAccumulation: true,
73
76
  pauseRendering: false,
74
77
  maxSamples: 60,
75
78
  bounces: 3,
76
- samplesPerPixel: 1,
77
79
  transmissiveBounces: 5,
78
80
  maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
79
81
  samplingTechnique: 3,
@@ -487,7 +489,7 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
487
489
  // 'interactive' — low-sample, bounded bounces, no offline denoising, controls enabled.
488
490
  // 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
489
491
  export const PRODUCTION_RENDER_CONFIG = {
490
- maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64, samplesPerPixel: 1,
492
+ maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
491
493
  renderMode: 1, enableAlphaShadows: true,
492
494
  enableOIDN: true, oidnQuality: 'balance',
493
495
  interactionModeEnabled: false,
@@ -495,7 +497,7 @@ export const PRODUCTION_RENDER_CONFIG = {
495
497
 
496
498
  export const INTERACTIVE_RENDER_CONFIG = {
497
499
  maxSamples: ENGINE_DEFAULTS.maxSamples, bounces: ENGINE_DEFAULTS.bounces,
498
- samplesPerPixel: ENGINE_DEFAULTS.samplesPerPixel, renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
500
+ renderMode: ENGINE_DEFAULTS.renderMode, enableAlphaShadows: ENGINE_DEFAULTS.enableAlphaShadows,
499
501
  transmissiveBounces: ENGINE_DEFAULTS.transmissiveBounces,
500
502
  maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
501
503
  enableOIDN: false, oidnQuality: 'fast',
@@ -959,7 +959,6 @@ export class PathTracerApp extends EventDispatcher {
959
959
  this.settings.setMany( {
960
960
  maxSamples: config.maxSamples,
961
961
  maxBounces: config.bounces,
962
- samplesPerPixel: config.samplesPerPixel,
963
962
  transmissiveBounces: config.transmissiveBounces,
964
963
  maxSubsurfaceSteps: config.maxSubsurfaceSteps,
965
964
  }, { silent: true } );
@@ -978,6 +977,10 @@ export class PathTracerApp extends EventDispatcher {
978
977
 
979
978
  }
980
979
 
980
+ // OIDN toggled directly above (bypassing setOIDNEnabled) — re-sync so the wavefront produces the
981
+ // aux MRT when OIDN is on and skips it otherwise. Runs before the reset below so kernels rebuild once.
982
+ this.denoisingManager?._syncGBufferStages?.();
983
+
981
984
  this.denoisingManager?.upscaler?.abort();
982
985
 
983
986
  if ( options.canvasWidth && options.canvasHeight ) {
@@ -24,7 +24,6 @@ export class CameraOptimizer {
24
24
  // Enhanced interaction mode settings for reduced quality during interaction
25
25
  this.interactionQualitySettings = {
26
26
  maxBounceCount: 1,
27
- numRaysPerPixel: 1,
28
27
  useEnvMapIS: false,
29
28
  // pixelRatio: 0.25,
30
29
  enableAccumulation: false,
@@ -304,28 +303,24 @@ export class CameraOptimizer {
304
303
  const presets = {
305
304
  'ultra-low': {
306
305
  maxBounceCount: 1,
307
- numRaysPerPixel: 1,
308
306
  useEnvMapIS: false,
309
307
  pixelRatio: 0.125,
310
308
  enableAccumulation: false
311
309
  },
312
310
  'low': {
313
311
  maxBounceCount: 1,
314
- numRaysPerPixel: 1,
315
312
  useEnvMapIS: false,
316
313
  pixelRatio: 0.25,
317
314
  enableAccumulation: false
318
315
  },
319
316
  'medium': {
320
317
  maxBounceCount: 2,
321
- numRaysPerPixel: 1,
322
318
  useEnvMapIS: true,
323
319
  pixelRatio: 0.5,
324
320
  enableAccumulation: false
325
321
  },
326
322
  'high': {
327
323
  maxBounceCount: 3,
328
- numRaysPerPixel: 1,
329
324
  useEnvMapIS: true,
330
325
  pixelRatio: 0.75,
331
326
  enableAccumulation: true
@@ -443,8 +443,9 @@ export class EmissiveTriangleBuilder {
443
443
  // If not emissive before and not now, nothing to do
444
444
  if ( ! isNowEmissive ) return false;
445
445
 
446
- // Fast path: just update power + CDF for affected entries
447
- const avgEmissive = ( emissive.r + emissive.g + emissive.b ) / 3.0;
446
+ // Fast path: just update power + CDF for affected entries.
447
+ // Rec.709 luma must match the build path + shader MIS weighting.
448
+ const luma = 0.2126 * emissive.r + 0.7152 * emissive.g + 0.0722 * emissive.b;
448
449
 
449
450
  this.totalEmissivePower = 0;
450
451
  for ( let i = 0; i < this.emissiveCount; i ++ ) {
@@ -452,7 +453,7 @@ export class EmissiveTriangleBuilder {
452
453
  const tri = this.emissiveTriangles[ i ];
453
454
  if ( tri.materialIndex === materialIndex ) {
454
455
 
455
- tri.power = avgEmissive * emissiveIntensity * tri.area;
456
+ tri.power = luma * emissiveIntensity * tri.area;
456
457
  tri.emissive = { r: emissive.r, g: emissive.g, b: emissive.b };
457
458
  tri.emissiveIntensity = emissiveIntensity;
458
459
  this.emissivePowerArray[ i ] = tri.power;
@@ -20,7 +20,7 @@ export const HIT_STRIDE = 2;
20
20
  export const GBUFFER_STRIDE = 2;
21
21
 
22
22
  export const RAY = {
23
- ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex+sampleIndex derived from rayID
23
+ ORIGIN_META: 0, // vec4(origin.xyz, uintBitsToFloat(perRayBounces | sssSteps<<8)); pixelIndex == rayID
24
24
  DIR_FLAGS: 1, // vec4(direction.xyz, uintBitsToFloat(bounceFlags))
25
25
  THROUGHPUT_PDF: 2, // vec4(throughput.xyz, pdf)
26
26
  RADIANCE_ALPHA: 3, // vec4(radiance.xyz, alpha)
@@ -195,8 +195,8 @@ export const gbDecodeNormalDepth = ( packed ) => {
195
195
  export const gbDecodeAlbedo = ( packed ) =>
196
196
  vec3( unpackUnorm2x16( packed.z ), unpackUnorm2x16( packed.w ).x );
197
197
 
198
- // .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15). pixelIndex +
199
- // sampleIndex are NOT stored — derived from rayID (= subSample*w*h + pixelIndex) in-kernel.
198
+ // .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15). pixelIndex is
199
+ // NOT stored — it equals rayID (one ray per pixel).
200
200
  export const writeRayOriginMeta = ( buf, id, origin, bounces, sssSteps ) =>
201
201
  buf.element( soa( id, RAY.ORIGIN_META ) )
202
202
  .assign( vec4( origin, uintBitsToFloat(
@@ -273,7 +273,6 @@ export const writeMediumSigmaA = ( buf, id, sigmaA ) =>
273
273
  // Per-ray bounce state packed into ORIGIN_META.w (written by writeRayOriginMeta alongside the origin):
274
274
  // perRayBounces = bits 0-7 (camera-bounce depth; the loop index can't track it once free bounces decouple it)
275
275
  // sssSteps = bits 8-15 (SSS random-walk step counter)
276
- // sampleIndex (the multi-sample sub-sample 0..S-1) is derived in-kernel from rayID, not stored.
277
276
  export const readPathBounces = ( buf, id ) =>
278
277
  int( floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).bitAnd( 0xFF ) );
279
278
  export const readSssSteps = ( buf, id ) =>
@@ -25,6 +25,7 @@ export const RAY_FLAG = {
25
25
  // bits 16-31: spare per-ray state carried across bounces
26
26
  HAS_HIT_OPAQUE: 1 << 16, // bit 16: ray chain has hit non-transmissive geometry (transparent-bg alpha; megakernel hasHitOpaqueSurface)
27
27
  AUX_LOCKED: 1 << 17, // bit 17: OIDN aux (normal/albedo) locked onto first non-specular hit (megakernel auxLocked)
28
+ REDIRECTED: 1 << 18, // bit 18: ray has been redirected (refraction/reflection/SSS/opaque scatter) since the camera, so env it reaches is transported light (sharp), NOT the direct backdrop. NOT set by pure alpha/transparent passthrough (direction unchanged) → env through cutout holes is still the backdrop (blur/intensity/show/color/ground-projection). Set via bitOr only (positive mask) so it never disturbs ACTIVE/bounce bits.
28
29
  };
29
30
 
30
31
  export class QueueManager {
@@ -1,4 +1,4 @@
1
- import { EventDispatcher } from 'three';
1
+ import { EventDispatcher, Color } from 'three';
2
2
  import { ENGINE_DEFAULTS } from './EngineDefaults.js';
3
3
  import { EngineEvents } from './EngineEvents.js';
4
4
 
@@ -16,11 +16,12 @@ const SETTING_ROUTES = {
16
16
  // ── Simple PathTracer uniforms ──────────────────────────
17
17
 
18
18
  maxBounces: { uniform: 'maxBounces', reset: true },
19
- samplesPerPixel: { uniform: 'samplesPerPixel', reset: true },
20
19
  transmissiveBounces: { uniform: 'transmissiveBounces', reset: true },
21
20
  maxSubsurfaceSteps: { uniform: 'maxSubsurfaceSteps', reset: true },
22
21
  environmentIntensity: { uniform: 'environmentIntensity', reset: true },
23
22
  backgroundIntensity: { uniform: 'backgroundIntensity', reset: true },
23
+ backgroundBlurriness: { uniform: 'backgroundBlurriness', reset: true },
24
+ backgroundBlurSamples: { uniform: 'backgroundBlurSamples', reset: true },
24
25
  showBackground: { uniform: 'showBackground', reset: true },
25
26
  enableEnvironment: { uniform: 'enableEnvironment', reset: true },
26
27
  groundProjectionEnabled: { uniform: 'groundProjectionEnabled', reset: true },
@@ -49,6 +50,7 @@ const SETTING_ROUTES = {
49
50
  interactionModeEnabled: { handler: 'handleInteractionModeEnabled', reset: false },
50
51
  maxSamples: { handler: 'handleMaxSamples', reset: false },
51
52
  transparentBackground: { handler: 'handleTransparentBackground' },
53
+ backgroundColor: { handler: 'handleBackgroundColor', reset: true },
52
54
  exposure: { handler: 'handleExposure' },
53
55
  saturation: { handler: 'handleSaturation' },
54
56
  renderLimitMode: { handler: 'handleRenderLimitMode' },
@@ -136,6 +138,15 @@ export class RenderSettings extends EventDispatcher {
136
138
 
137
139
  },
138
140
 
141
+ handleBackgroundColor: ( value ) => {
142
+
143
+ // Accept a hex string ('#rrggbb') or a Color; THREE.Color converts sRGB → linear working
144
+ // space, which is what the shader adds to radiance.
145
+ const c = value?.isColor ? value : new Color( value );
146
+ stages.pathTracer?.setUniform( 'backgroundColor', c );
147
+
148
+ },
149
+
139
150
  handleExposure: ( value ) => {
140
151
 
141
152
  // Three.js applies toneMappingExposure inside the tone-mapping branch,
@@ -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;
@@ -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;
@@ -336,7 +332,6 @@ export class PathTracer extends PathTracerStage {
336
332
  if ( ! this.cameraOptimizer?.isInInteractionMode() ) this.frameCount ++;
337
333
 
338
334
  if ( originalMaxBounces !== null ) this.maxBounces.value = originalMaxBounces;
339
- if ( originalSamplesPerPixel !== null ) this.samplesPerPixel.value = originalSamplesPerPixel;
340
335
 
341
336
  this.performanceMonitor?.end();
342
337
 
@@ -354,27 +349,16 @@ export class PathTracer extends PathTracerStage {
354
349
 
355
350
  }
356
351
 
357
- // S=samplesPerPixel for interactive within the pixel cap; production/tiled and high-res get S=1.
358
- _resolveSamplesPerPass( w, h ) {
352
+ // Aux MRT (normalDepth/albedo) is needed only by the denoiser/OIDN; DenoisingManager calls this to
353
+ // turn the wavefront's aux writes on/off. It's a live uniform, so toggling is just a value flip +
354
+ // accumulation reset — no kernel rebuild, no UI freeze.
355
+ setAuxGBufferEnabled( enabled ) {
359
356
 
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
- }
357
+ enabled = !! enabled;
358
+ if ( this._auxGBufferEnabled === enabled ) return;
359
+ this._auxGBufferEnabled = enabled;
360
+ this._auxGBufferUniform.value = enabled ? 1 : 0;
361
+ this.reset();
378
362
 
379
363
  }
380
364
 
@@ -464,12 +448,10 @@ export class PathTracer extends PathTracerStage {
464
448
  this._readbackFrameCounter = 0;
465
449
  this._readbackGeneration ++;
466
450
 
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 );
451
+ // Recompile only when buffers reallocate (capacity grows); otherwise resize uniforms in place.
452
+ const neededCap = PackedRayBuffer.requiredCapacity( newW * newH );
470
453
  const mustRebuild = ! this._packedBuffers
471
- || neededCap > this._packedBuffers.capacity
472
- || newS !== this._samplesPerPass;
454
+ || neededCap > this._packedBuffers.capacity;
473
455
 
474
456
  if ( mustRebuild ) {
475
457
 
@@ -485,10 +467,10 @@ export class PathTracer extends PathTracerStage {
485
467
 
486
468
  }
487
469
 
488
- // Same-capacity, same-S resize: update render-size uniforms + early-exit threshold, no recompile.
470
+ // Same-capacity resize: update render-size uniforms + early-exit threshold, no recompile.
489
471
  _resizeWavefrontInPlace( w, h ) {
490
472
 
491
- const maxRays = w * h * this._samplesPerPass;
473
+ const maxRays = w * h;
492
474
  this._wfRenderWidth.value = w;
493
475
  this._wfRenderHeight.value = h;
494
476
  this._wfMaxRayCount.value = maxRays;
@@ -509,11 +491,7 @@ export class PathTracer extends PathTracerStage {
509
491
 
510
492
  const w = this.storageTextures.renderWidth;
511
493
  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;
494
+ const maxRays = w * h;
517
495
 
518
496
  if ( this._bounceEarlyExitThreshold !== - 1 ) {
519
497
 
@@ -534,9 +512,9 @@ export class PathTracer extends PathTracerStage {
534
512
  // Per-pixel G-buffer (first-hit MRT: ND + albedo), 1 uvec4/pixel — half-precision packed (pack2x16).
535
513
  // uint (not f32) buffer: packed lanes can hit the NaN exponent range (e.g. snorm 1.0 → 0x7FFF), which a
536
514
  // 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.
515
+ // per-pixel, written by Generate/Shade bounce-0 and read only by FinalWrite.
538
516
  // 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;
517
+ const gBufferVec4s = PackedRayBuffer.requiredCapacity( maxRays ) * GBUFFER_STRIDE;
540
518
  this._gBufferAttr = new StorageInstancedBufferAttribute( new Uint32Array( gBufferVec4s * 4 ), 4 );
541
519
  const gBufferRW = storage( this._gBufferAttr, 'uvec4' );
542
520
  const gBufferRO = storage( this._gBufferAttr, 'uvec4' ).toReadOnly();
@@ -639,14 +617,13 @@ export class PathTracer extends PathTracerStage {
639
617
  anamorphicRatio: this.anamorphicRatio,
640
618
  renderWidth: this._wfRenderWidth,
641
619
  renderHeight: this._wfRenderHeight,
642
- samplesPerPass: S,
643
620
  transmissiveBounces: this.transmissiveBounces,
644
621
  transparentBackground: this.transparentBackground,
622
+ auxGBufferEnabled: this._auxGBufferUniform,
645
623
  } );
646
624
  this._kernelManager.register( 'generate',
647
625
  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 ],
626
+ [ Math.ceil( w / GENERATE_WG_SIZE ), Math.ceil( h / GENERATE_WG_SIZE ), 1 ],
650
627
  [ GENERATE_WG_SIZE, GENERATE_WG_SIZE, 1 ]
651
628
  )
652
629
  );
@@ -743,6 +720,9 @@ export class PathTracer extends PathTracerStage {
743
720
  maxSubsurfaceSteps: this.maxSubsurfaceSteps,
744
721
  transparentBackground: this.transparentBackground,
745
722
  backgroundIntensity: this.backgroundIntensity,
723
+ backgroundColor: this.backgroundColor,
724
+ backgroundBlurriness: this.backgroundBlurriness,
725
+ backgroundBlurSamples: this.backgroundBlurSamples,
746
726
  showBackground: this.showBackground,
747
727
  globalIlluminationIntensity: this.globalIlluminationIntensity,
748
728
  cameraProjectionMatrix: this.cameraProjectionMatrix,
@@ -760,6 +740,7 @@ export class PathTracer extends PathTracerStage {
760
740
  reverseMapVec4Offset: this.reverseMapVec4Offset,
761
741
  currentBounce: this._wfCurrentBounce,
762
742
  maxRayCount: this._wfMaxRayCount,
743
+ auxGBufferEnabled: this._auxGBufferUniform,
763
744
  } );
764
745
  this._kernelManager.register( 'shade',
765
746
  shadeFn().compute(
@@ -833,8 +814,8 @@ export class PathTracer extends PathTracerStage {
833
814
  prevAlbedoTexture: prevAlbedo,
834
815
  renderWidth: this._wfRenderWidth,
835
816
  renderHeight: this._wfRenderHeight,
836
- samplesPerPass: S,
837
817
  visMode: this.visMode,
818
+ auxGBufferEnabled: this._auxGBufferUniform,
838
819
  } );
839
820
  this._kernelManager.register( 'finalWrite',
840
821
  // Per-pixel (w×h) — kernel averages the S sample-slots internally.
@@ -873,7 +854,6 @@ export class PathTracer extends PathTracerStage {
873
854
  enableEnvironmentLight: this.enableEnvironment,
874
855
  visMode: this.visMode,
875
856
  debugVisScale: this.debugVisScale,
876
- samplesPerPass: this._samplesPerPass,
877
857
  albedoMaps: freshAlbedoMaps,
878
858
  normalMaps: freshNormalMaps,
879
859
  bumpMaps: freshBumpMaps,
@@ -898,11 +878,10 @@ export class PathTracer extends PathTracerStage {
898
878
 
899
879
  const w = this._wfRenderWidth.value;
900
880
  const h = this._wfRenderHeight.value;
901
- const S = this._samplesPerPass | 0;
902
881
 
903
882
  this._kernelManager.setDispatchCount( 'generate', [
904
883
  Math.ceil( w / GENERATE_WG_SIZE ),
905
- Math.ceil( ( h * S ) / GENERATE_WG_SIZE ), 1
884
+ Math.ceil( h / GENERATE_WG_SIZE ), 1
906
885
  ] );
907
886
  this._kernelManager.setDispatchCount( 'finalWrite', [
908
887
  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,
@@ -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
 
@@ -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,7 +82,7 @@ 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 ) );