rayzee 7.11.0 → 7.12.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.11.0",
3
+ "version": "7.12.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",
@@ -83,6 +83,19 @@ export const ENGINE_DEFAULTS = {
83
83
  bounces: 3,
84
84
  transmissiveBounces: 5,
85
85
  maxSubsurfaceSteps: 8, // interactive default: low cap (bounded random-walk SSS)
86
+
87
+ // Adaptive sampling (Blender-style): stop the frame once enough pixels drop below the noise threshold.
88
+ useAdaptiveSampling: true,
89
+ noiseThreshold: 0.02, // per-pixel noise below which a pixel is converged
90
+ darkNoiseFloor: 0.003, // extra absolute-noise floor so dark pixels can converge too
91
+ adaptiveMinSamples: 8, // min samples before adaptive sampling can trigger
92
+ adaptiveStopFraction: 0.95, // retire the frame once this fraction of pixels has converged
93
+ // Per-pixel freeze: skip tracing pixels that individually converged (noise threshold only — no dark floor,
94
+ // which would bake dim regions too dark). Naturally engages only on static/idle views. See docs/specs/tier2-adaptive-sampling-plan.md.
95
+ usePixelFreeze: false,
96
+ pixelFreezeThreshold: 0.02, // per-pixel noise below which a pixel becomes a freeze candidate
97
+ pixelFreezeStability: 8, // consecutive candidate frames before a pixel freezes
98
+
86
99
  samplingTechnique: 3,
87
100
  enableEmissiveTriangleSampling: false,
88
101
  emissiveBoost: 1.0,
@@ -556,10 +569,18 @@ export const DEFAULT_TEXTURE_MATRIX = [ 0, 0, 1, 1, 0, 0, 0, 1 ];
556
569
  // 'interactive' — low-sample, bounded bounces, no offline denoising, controls enabled.
557
570
  // 'production' — high-sample, deep bounces, OIDN enabled, controls disabled.
558
571
  export const PRODUCTION_RENDER_CONFIG = {
559
- maxSamples: 30, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
572
+ // maxSamples is a CEILING: adaptive sampling retires the frame once adaptiveStopFraction of pixels converge,
573
+ // so easy scenes finish well under it while hard GI scenes use the full budget.
574
+ maxSamples: 150, bounces: 20, transmissiveBounces: 8, maxSubsurfaceSteps: 64,
560
575
  renderMode: 1, enableAlphaShadows: true,
561
576
  enableOIDN: true, oidnQuality: 'balance',
562
577
  interactionModeEnabled: false,
578
+ // Looser thresholds + stop at 90%, leaning on OIDN to clean the residual tail.
579
+ useAdaptiveSampling: true,
580
+ noiseThreshold: 0.1,
581
+ darkNoiseFloor: 0.01,
582
+ adaptiveStopFraction: 0.9,
583
+ usePixelFreeze: true,
563
584
  };
564
585
 
565
586
  export const INTERACTIVE_RENDER_CONFIG = {
@@ -569,6 +590,8 @@ export const INTERACTIVE_RENDER_CONFIG = {
569
590
  maxSubsurfaceSteps: ENGINE_DEFAULTS.maxSubsurfaceSteps,
570
591
  enableOIDN: false, oidnQuality: 'fast',
571
592
  interactionModeEnabled: true,
593
+ useAdaptiveSampling: true, // idle refine stops early when converged; frozen during motion
594
+ usePixelFreeze: true, // speeds up idle refinement on heavy/high-res views; inert while moving (freeze resets)
572
595
  };
573
596
 
574
597
  // Memory management constants
@@ -323,6 +323,7 @@ export class PathTracerApp extends EventDispatcher {
323
323
  const tracker = this.stages.pathTracer?.vramTracker;
324
324
  const frame = this.stages.pathTracer?.frameCount ?? 0;
325
325
  if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
326
+
326
327
  updateStats( {
327
328
  timeElapsed: this.completion.timeElapsed,
328
329
  samples: getDisplaySamples( this.stages.pathTracer ),
@@ -1236,6 +1237,19 @@ export class PathTracerApp extends EventDispatcher {
1236
1237
  this.stages.pathTracer?.setUniform( 'renderMode', parseInt( config.renderMode ) );
1237
1238
  this.stages.pathTracer?.setUniform( 'enableAlphaShadows', config.enableAlphaShadows ?? false );
1238
1239
 
1240
+ // Tier-1 convergence early-stop (live uniforms, no kernel rebuild). Threshold/min-samples fall back to
1241
+ // engine defaults when a config doesn't override them.
1242
+ this.stages.pathTracer?.setUniform( 'useAdaptiveSampling', config.useAdaptiveSampling ?? false );
1243
+ this.stages.pathTracer?.setUniform( 'noiseThreshold', config.noiseThreshold ?? DEFAULT_STATE.noiseThreshold );
1244
+ this.stages.pathTracer?.setUniform( 'darkNoiseFloor', config.darkNoiseFloor ?? DEFAULT_STATE.darkNoiseFloor );
1245
+ this.stages.pathTracer?.setUniform( 'adaptiveStopFraction', config.adaptiveStopFraction ?? DEFAULT_STATE.adaptiveStopFraction );
1246
+ this.stages.pathTracer?.setUniform( 'adaptiveMinSamples', config.adaptiveMinSamples ?? DEFAULT_STATE.adaptiveMinSamples );
1247
+
1248
+ // Tier-2 per-pixel freeze (per-mode; falls back off when a config doesn't set it).
1249
+ this.stages.pathTracer?.setUniform( 'usePixelFreeze', config.usePixelFreeze ?? false );
1250
+ this.stages.pathTracer?.setUniform( 'pixelFreezeThreshold', config.pixelFreezeThreshold ?? DEFAULT_STATE.pixelFreezeThreshold );
1251
+ this.stages.pathTracer?.setUniform( 'pixelFreezeStability', config.pixelFreezeStability ?? DEFAULT_STATE.pixelFreezeStability );
1252
+
1239
1253
  this.stages.pathTracer?.updateCompletionThreshold?.();
1240
1254
 
1241
1255
  const denoiser = this.denoisingManager?.denoiser;
@@ -48,7 +48,10 @@ export class CompletionTracker {
48
48
 
49
49
  if ( this.isTimeLimitReached( renderLimitMode, renderTimeLimit ) ) return true;
50
50
 
51
- return pathTracer.frameCount >= pathTracer.completionThreshold;
51
+ // Tier-1 convergence early-stop must agree with PathTracer.render()'s own check, else the app-level
52
+ // reconcile would flip isComplete back off and keep dispatching after the frame converged.
53
+ return pathTracer.frameCount >= pathTracer.completionThreshold
54
+ || ( pathTracer._isConvergedComplete?.() ?? false );
52
55
 
53
56
  }
54
57
 
@@ -16,7 +16,6 @@ const WORKGROUP_SIZES = {
16
16
  connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
17
17
  accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
18
18
  compact: [ 256, 1, 1 ], // 1D ray-parallel
19
- resetCounters: [ 1, 1, 1 ], // Single thread
20
19
  finalWrite: [ 16, 16, 1 ], // 2D screen-space
21
20
  };
22
21
 
@@ -11,7 +11,14 @@ export const COUNTER = {
11
11
  ACTIVE_RAY_COUNT: 0,
12
12
  // rays entering current bounce; snapshotted before ACTIVE_RAY_COUNT reset so over-sized dispatch is safe.
13
13
  ENTERING_COUNT: 1,
14
- COUNT: 2,
14
+ // per-frame count of pixels whose Tier-1 relative-error dropped below threshold; zeroed at frame start by
15
+ // initActiveIndices, incremented in FinalWrite, read back async to drive the whole-frame convergence early-stop.
16
+ CONVERGED_COUNT: 2,
17
+ // Tier-2 per-pixel freeze: FROZEN_COUNT = pixels skipped this frame; ACTIVE_PIXEL_COUNT = bounce-0 active
18
+ // count (maxRays − frozen), read back to size next frame's grid.
19
+ FROZEN_COUNT: 3,
20
+ ACTIVE_PIXEL_COUNT: 4,
21
+ COUNT: 5,
15
22
  };
16
23
 
17
24
  /** Ray flag bits packed into rayBounceFlags (uint) */
@@ -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
  }
@@ -40,6 +40,15 @@ const SETTING_ROUTES = {
40
40
  samplingTechnique: { uniform: 'samplingTechnique', reset: true },
41
41
  fireflyThreshold: { uniform: 'fireflyThreshold', reset: true },
42
42
  enableAlphaShadows: { uniform: 'enableAlphaShadows', reset: true },
43
+ // Adaptive sampling — whole-frame early-stop (useAdaptiveSampling) + per-pixel freeze (usePixelFreeze)
44
+ useAdaptiveSampling: { uniform: 'useAdaptiveSampling', reset: true },
45
+ noiseThreshold: { uniform: 'noiseThreshold', reset: true },
46
+ darkNoiseFloor: { uniform: 'darkNoiseFloor', reset: true },
47
+ adaptiveMinSamples: { uniform: 'adaptiveMinSamples', reset: true },
48
+ adaptiveStopFraction: { uniform: 'adaptiveStopFraction', reset: true },
49
+ usePixelFreeze: { uniform: 'usePixelFreeze', reset: true },
50
+ pixelFreezeThreshold: { uniform: 'pixelFreezeThreshold', reset: true },
51
+ pixelFreezeStability: { uniform: 'pixelFreezeStability', reset: true },
43
52
  enableEmissiveTriangleSampling: { uniform: 'enableEmissiveTriangleSampling', reset: true },
44
53
  emissiveBoost: { uniform: 'emissiveBoost', reset: true },
45
54
  visMode: { uniform: 'visMode', reset: true },