rayzee 7.22.3 → 7.22.5
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/dist/rayzee.es.js +1574 -1466
- package/dist/rayzee.umd.js +43 -43
- package/package.json +1 -1
- package/src/EngineDefaults.js +20 -13
- package/src/PathTracerApp.js +13 -0
- package/src/Processor/QueueManager.js +6 -1
- package/src/RenderSettings.js +13 -1
- package/src/Stages/Compositor.js +132 -2
- package/src/Stages/PathTracer.js +192 -15
- package/src/TSL/FinalWriteKernel.js +61 -34
- package/src/TSL/GenerateKernel.js +4 -4
- package/src/TSL/PathTracerCore.js +5 -1
- package/src/TSL/ShadeKernel.js +4 -1
- package/src/managers/UniformManager.js +4 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -93,24 +93,31 @@ export const ENGINE_DEFAULTS = {
|
|
|
93
93
|
|
|
94
94
|
// Adaptive sampling (Blender-style): stop the frame once enough pixels drop below the noise threshold.
|
|
95
95
|
useAdaptiveSampling: true,
|
|
96
|
-
|
|
96
|
+
// √-luminance-normalized per-pixel noise below which a pixel counts as converged. Base tracks the
|
|
97
|
+
// interactive tier — that is what a freshly booted engine renders before configureForMode runs.
|
|
98
|
+
noiseThreshold: 0.1,
|
|
97
99
|
adaptiveMinSamples: 8, // min samples before adaptive sampling can trigger
|
|
98
|
-
|
|
100
|
+
// Fraction of pixels that must pass the 3×3-eroded convergence count before the frame retires; the
|
|
101
|
+
// geometry-only fraction has to clear the same bar (PathTracer._isConvergedComplete).
|
|
102
|
+
// ENGINE-INTERNAL: a calibration constant rather than a quality dial — it only means anything against
|
|
103
|
+
// those two counts. configureForMode supplies the per-tier value.
|
|
104
|
+
adaptiveStopFraction: 0.90,
|
|
99
105
|
// Per-pixel freeze: skip tracing pixels that individually converged (noise threshold only — no dark floor,
|
|
100
106
|
// which would bake dim regions too dark). Naturally engages only on static/idle views.
|
|
107
|
+
// Set together with useAdaptiveSampling by the single UI switch — two tiers of one feature.
|
|
101
108
|
usePixelFreeze: true,
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
// ENGINE-INTERNAL, and not the sibling of noiseThreshold it looks like: freeze bars on plain relErr where
|
|
110
|
+
// the frame test uses the √-normalized error, so the same number is 2-5× stricter here. Deriving it from
|
|
111
|
+
// noiseThreshold was measured and rejected — a pixel frozen before it satisfies the frame test can never
|
|
112
|
+
// satisfy it afterwards, so a looser bar delays the early stop rather than hastening it.
|
|
113
|
+
pixelFreezeThreshold: 0.02,
|
|
114
|
+
pixelFreezeStability: 8, // ENGINE-INTERNAL: consecutive candidate frames before a pixel freezes
|
|
115
|
+
convergenceOverlay: false, // display-only Compositor overlay; never alters the render
|
|
104
116
|
|
|
105
117
|
samplingTechnique: 2,
|
|
106
118
|
enableEmissiveTriangleSampling: false,
|
|
107
119
|
emissiveBoost: 1.0,
|
|
108
120
|
|
|
109
|
-
temporalVarianceWeight: 0.6,
|
|
110
|
-
enableEarlyTermination: true,
|
|
111
|
-
earlyTerminationThreshold: 0.002,
|
|
112
|
-
performanceModeAdaptive: 'medium',
|
|
113
|
-
|
|
114
121
|
fireflyThreshold: 3.0,
|
|
115
122
|
// Wavefront material-coherence sort: global counting-sort of entering rays by material before
|
|
116
123
|
// Shade (material-pure workgroups), under dynamic dispatch. Measured −8% at 1024²/8b. Gated on
|
|
@@ -599,11 +606,10 @@ export const PRODUCTION_RENDER_CONFIG = {
|
|
|
599
606
|
renderMode: 1, enableAlphaShadows: true,
|
|
600
607
|
enableOIDN: true, oidnQuality: 'balance',
|
|
601
608
|
interactionModeEnabled: false,
|
|
602
|
-
//
|
|
603
|
-
// the unbiased residual for OIDN.
|
|
609
|
+
// 0.94 against the eroded count ≈ the old raw-count 0.98; erosion holds the fraction a few points lower.
|
|
604
610
|
useAdaptiveSampling: true,
|
|
605
|
-
noiseThreshold: 0.
|
|
606
|
-
adaptiveStopFraction: 0.
|
|
611
|
+
noiseThreshold: 0.02,
|
|
612
|
+
adaptiveStopFraction: 0.94,
|
|
607
613
|
usePixelFreeze: true,
|
|
608
614
|
};
|
|
609
615
|
|
|
@@ -615,6 +621,7 @@ export const INTERACTIVE_RENDER_CONFIG = {
|
|
|
615
621
|
enableOIDN: false, oidnQuality: 'fast',
|
|
616
622
|
interactionModeEnabled: true,
|
|
617
623
|
useAdaptiveSampling: true, // idle refine stops early when converged; frozen during motion
|
|
624
|
+
noiseThreshold: 0.1, // loose: preview wants a fast settle, not a clean one
|
|
618
625
|
usePixelFreeze: true, // speeds up idle refinement on heavy/high-res views; inert while moving (freeze resets)
|
|
619
626
|
};
|
|
620
627
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -1984,6 +1984,18 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1984
1984
|
|
|
1985
1985
|
}
|
|
1986
1986
|
|
|
1987
|
+
/**
|
|
1988
|
+
* Adaptive-sampling telemetry: `{ converged, activePixels, totalPixels, frame }`.
|
|
1989
|
+
* Counts come from an async readback taken on settled views only, so they lag a few frames
|
|
1990
|
+
* and read zero while the camera moves.
|
|
1991
|
+
* @returns {?Object}
|
|
1992
|
+
*/
|
|
1993
|
+
getConvergenceStats() {
|
|
1994
|
+
|
|
1995
|
+
return this.stages.pathTracer?.getConvergenceStats?.() ?? null;
|
|
1996
|
+
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1987
1999
|
/** The path tracer's VRAM tracker, or null before stages are built. */
|
|
1988
2000
|
get vram() {
|
|
1989
2001
|
|
|
@@ -2567,6 +2579,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2567
2579
|
|
|
2568
2580
|
this.stages.compositor = new Compositor( this.renderer, {
|
|
2569
2581
|
saturation: this.settings.get( 'saturation' ) ?? DEFAULT_STATE.saturation,
|
|
2582
|
+
pathTracer: this.stages.pathTracer,
|
|
2570
2583
|
} );
|
|
2571
2584
|
|
|
2572
2585
|
}
|
|
@@ -22,7 +22,12 @@ export const COUNTER = {
|
|
|
22
22
|
RESERVED_3: 3,
|
|
23
23
|
// Tier-2 per-pixel freeze: bounce-0 active count (maxRays − frozen), read back to size next frame's grid.
|
|
24
24
|
ACTIVE_PIXEL_COUNT: 4,
|
|
25
|
-
|
|
25
|
+
// Geometry-only convergence gate. CONVERGED_COUNT counts every pixel, so a mostly-sky frame inherits the
|
|
26
|
+
// sky's free convergence and can retire while the subject is still noisy. These count the same eroded
|
|
27
|
+
// bits restricted to pixels that hit geometry, so the stop can require both fractions.
|
|
28
|
+
GEOMETRY_COUNT: 5,
|
|
29
|
+
CONVERGED_GEOMETRY_COUNT: 6,
|
|
30
|
+
COUNT: 7,
|
|
26
31
|
};
|
|
27
32
|
|
|
28
33
|
/** Ray flag bits packed into rayBounceFlags (uint) */
|
package/src/RenderSettings.js
CHANGED
|
@@ -50,6 +50,7 @@ const SETTING_ROUTES = {
|
|
|
50
50
|
usePixelFreeze: { uniform: 'usePixelFreeze', reset: true },
|
|
51
51
|
pixelFreezeThreshold: { uniform: 'pixelFreezeThreshold', reset: true },
|
|
52
52
|
pixelFreezeStability: { uniform: 'pixelFreezeStability', reset: true },
|
|
53
|
+
convergenceOverlay: { handler: 'handleConvergenceOverlay', reset: false },
|
|
53
54
|
enableEmissiveTriangleSampling: { uniform: 'enableEmissiveTriangleSampling', reset: true },
|
|
54
55
|
emissiveBoost: { uniform: 'emissiveBoost', reset: true },
|
|
55
56
|
visMode: { uniform: 'visMode', reset: true },
|
|
@@ -142,7 +143,7 @@ export class RenderSettings extends EventDispatcher {
|
|
|
142
143
|
* Builds handler functions for multi-stage settings that can't
|
|
143
144
|
* be routed with a simple uniform forward.
|
|
144
145
|
*/
|
|
145
|
-
_buildHandlers( { stages, renderer, reconcileCompletion, denoisingManager, cameraManager } ) {
|
|
146
|
+
_buildHandlers( { stages, renderer, resetCallback, reconcileCompletion, denoisingManager, cameraManager } ) {
|
|
146
147
|
|
|
147
148
|
// UniformManager copies into the existing node, so one scratch vector serves every write.
|
|
148
149
|
const panoScratch = new Vector2();
|
|
@@ -209,6 +210,17 @@ export class RenderSettings extends EventDispatcher {
|
|
|
209
210
|
|
|
210
211
|
},
|
|
211
212
|
|
|
213
|
+
handleConvergenceOverlay: ( value ) => {
|
|
214
|
+
|
|
215
|
+
stages.pathTracer?.setUniform( 'convergenceOverlay', value );
|
|
216
|
+
stages.compositor?.setConvergenceOverlay( value );
|
|
217
|
+
|
|
218
|
+
// With adaptive sampling off nothing kept m2 current, so the estimate has no history to
|
|
219
|
+
// read — restart accumulation so it self-inits at frame 0 instead of painting stale error.
|
|
220
|
+
if ( value && ! this.get( 'useAdaptiveSampling' ) ) resetCallback?.();
|
|
221
|
+
|
|
222
|
+
},
|
|
223
|
+
|
|
212
224
|
handleRenderLimitMode: ( value ) => {
|
|
213
225
|
|
|
214
226
|
stages.pathTracer?.setRenderLimitMode?.( value );
|
package/src/Stages/Compositor.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { vec4, vec3, uv, uniform, select, dot, mix } from 'three/tsl';
|
|
1
|
+
import { vec4, vec3, uv, uniform, select, dot, mix, float, int, uint, storage, Fn, If } from 'three/tsl';
|
|
2
2
|
import { MeshBasicNodeMaterial, QuadMesh, TextureNode } from 'three/webgpu';
|
|
3
|
-
import { NoBlending } from 'three';
|
|
3
|
+
import { NoBlending, NoToneMapping } from 'three';
|
|
4
4
|
import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
5
5
|
import { REC709_LUMINANCE_COEFFICIENTS } from '../TSL/Common.js';
|
|
6
6
|
|
|
@@ -31,6 +31,17 @@ export class Compositor extends RenderStage {
|
|
|
31
31
|
|
|
32
32
|
this._transparentBackground = uniform( 0, 'int' );
|
|
33
33
|
|
|
34
|
+
// Convergence debug overlay — reads the path tracer's per-pixel convergence buffers read-only.
|
|
35
|
+
// Material is compiled on first enable, so the normal display path carries none of its bindings.
|
|
36
|
+
this._pathTracer = options.pathTracer ?? null;
|
|
37
|
+
this._convergenceOverlay = false;
|
|
38
|
+
this._debugMaterial = null;
|
|
39
|
+
this._debugQuad = null;
|
|
40
|
+
this._debugTexNode = null;
|
|
41
|
+
this._debugM2Attr = null;
|
|
42
|
+
this._debugMaskAttr = null;
|
|
43
|
+
this._debugExposureComp = uniform( 1.0 );
|
|
44
|
+
|
|
34
45
|
// TextureNode reused across frames — only `.value` mutates, shader doesn't recompile.
|
|
35
46
|
this._sourceTexNode = new TextureNode();
|
|
36
47
|
|
|
@@ -63,10 +74,117 @@ export class Compositor extends RenderStage {
|
|
|
63
74
|
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Builds (or rebuilds) the convergence-overlay material.
|
|
79
|
+
*
|
|
80
|
+
* Re-derives FinalWrite's own convergence predicate from the accumulated colour (the running mean)
|
|
81
|
+
* and the m2 buffer (its running second moment), so what is painted IS the decision the renderer
|
|
82
|
+
* makes — nothing is recomputed differently or written back.
|
|
83
|
+
*
|
|
84
|
+
* @returns {boolean} True when a material is ready to render.
|
|
85
|
+
*/
|
|
86
|
+
_ensureDebugMaterial() {
|
|
87
|
+
|
|
88
|
+
const src = this._pathTracer?.getConvergenceDebugSource?.();
|
|
89
|
+
if ( ! src ) return false;
|
|
90
|
+
|
|
91
|
+
// The buffers are only reallocated when the render reserve grows; rebind when that happens.
|
|
92
|
+
if ( this._debugMaterial && this._debugM2Attr === src.m2 && this._debugMaskAttr === src.frozenMask ) return true;
|
|
93
|
+
|
|
94
|
+
this._debugMaterial?.dispose();
|
|
95
|
+
this._debugTexNode?.dispose();
|
|
96
|
+
|
|
97
|
+
this._debugM2Attr = src.m2;
|
|
98
|
+
this._debugMaskAttr = src.frozenMask;
|
|
99
|
+
this._debugTexNode = new TextureNode();
|
|
100
|
+
|
|
101
|
+
// Must stay one Fn body: as a raw node graph TSL emits a .toVar() inside the FIRST branch that demands
|
|
102
|
+
// it, and sibling branches then read an unassigned var<private>.
|
|
103
|
+
const overlayColor = Fn( () => {
|
|
104
|
+
|
|
105
|
+
const texel = this._debugTexNode.sample( uv() ).toVar();
|
|
106
|
+
const lum = dot( texel.xyz, REC709_LUMINANCE_COEFFICIENTS ).toVar();
|
|
107
|
+
|
|
108
|
+
// The overlay draws at canvas size, the buffers are indexed at render size — go through the pixel index.
|
|
109
|
+
const w = src.renderWidth;
|
|
110
|
+
const h = src.renderHeight;
|
|
111
|
+
const ix = int( uv().x.mul( float( w ) ) ).min( w.sub( int( 1 ) ) ).max( int( 0 ) );
|
|
112
|
+
const iy = int( uv().y.mul( float( h ) ) ).min( h.sub( int( 1 ) ) ).max( int( 0 ) );
|
|
113
|
+
const pixelId = uint( iy.mul( w ).add( ix ) ).toVar();
|
|
114
|
+
|
|
115
|
+
const m2 = storage( src.m2, 'float' ).toReadOnly().element( pixelId ).toVar();
|
|
116
|
+
const frozenFlag = storage( src.frozenMask, 'uint' ).toReadOnly().element( pixelId ).toVar();
|
|
117
|
+
|
|
118
|
+
const sampleVar = m2.sub( lum.mul( lum ) ).max( float( 0 ) );
|
|
119
|
+
const absSE = sampleVar.div( float( src.frame ).add( 1.0 ) ).sqrt().toVar();
|
|
120
|
+
|
|
121
|
+
// Freeze bars on the raw relative error; the whole-frame stop bars on the √-normalized one.
|
|
122
|
+
// Show whichever is actually steering the render.
|
|
123
|
+
const freezeOn = src.usePixelFreeze.greaterThan( int( 0 ) );
|
|
124
|
+
const convNorm = select( lum.lessThan( float( 1.0 ) ), lum.sqrt(), lum ).toVar();
|
|
125
|
+
const err = select( freezeOn, absSE.div( lum.add( float( 1e-4 ) ) ), absSE.div( convNorm.add( float( 1e-4 ) ) ) ).toVar();
|
|
126
|
+
const bar = select( freezeOn, src.pixelFreezeThreshold, src.noiseThreshold ).toVar();
|
|
127
|
+
const ratio = err.div( bar.max( float( 1e-6 ) ) ).toVar();
|
|
128
|
+
|
|
129
|
+
// Dim tone-mapped grayscale so scene structure stays readable under the overlay.
|
|
130
|
+
const base = vec3( lum.div( lum.add( float( 1.0 ) ) ).sqrt().mul( 0.55 ) ).toVar();
|
|
131
|
+
const out = base.toVar();
|
|
132
|
+
|
|
133
|
+
// Below minSamples the variance is untrustworthy — at frame 0 it is identically zero, which would
|
|
134
|
+
// paint a converged image over pure noise.
|
|
135
|
+
const eligible = float( src.frame ).greaterThanEqual( float( src.adaptiveMinSamples ) );
|
|
136
|
+
|
|
137
|
+
If( ratio.greaterThanEqual( float( 1.0 ) ).or( eligible.not() ), () => {
|
|
138
|
+
|
|
139
|
+
const heat = mix( vec3( 1.0, 0.8, 0.1 ), vec3( 1.0, 0.1, 0.05 ), ratio.sub( 1.0 ).div( 3.0 ).clamp( 0.0, 1.0 ) );
|
|
140
|
+
out.assign( mix( base, heat, 0.85 ) );
|
|
141
|
+
|
|
142
|
+
} );
|
|
143
|
+
|
|
144
|
+
// Frozen wins: these are the pixels the renderer actually skipped, dilation halo included.
|
|
145
|
+
If( frozenFlag.equal( uint( 1 ) ).and( freezeOn ), () => {
|
|
146
|
+
|
|
147
|
+
out.assign( mix( base, vec3( 0.1, 0.45, 1.0 ), 0.55 ) );
|
|
148
|
+
|
|
149
|
+
} );
|
|
150
|
+
|
|
151
|
+
return out;
|
|
152
|
+
|
|
153
|
+
} );
|
|
154
|
+
|
|
155
|
+
// Undo the output pass's exposure (tone-mapped configs only) and linear→sRGB so the palette reads the
|
|
156
|
+
// same at any exposure.
|
|
157
|
+
this._debugMaterial = new MeshBasicNodeMaterial();
|
|
158
|
+
this._debugMaterial.colorNode = vec4( overlayColor().pow( 2.2 ).mul( this._debugExposureComp ), 1.0 );
|
|
159
|
+
this._debugMaterial.blending = NoBlending;
|
|
160
|
+
this._debugQuad = new QuadMesh( this._debugMaterial );
|
|
161
|
+
|
|
162
|
+
return true;
|
|
163
|
+
|
|
164
|
+
}
|
|
165
|
+
|
|
66
166
|
render( context ) {
|
|
67
167
|
|
|
68
168
|
if ( ! this.enabled ) return;
|
|
69
169
|
|
|
170
|
+
if ( this._convergenceOverlay && this._ensureDebugMaterial() ) {
|
|
171
|
+
|
|
172
|
+
// The error estimate is paired with the raw accumulated colour, not a denoised derivative.
|
|
173
|
+
const ptColor = context.getTexture( 'pathtracer:color' );
|
|
174
|
+
if ( ptColor ) {
|
|
175
|
+
|
|
176
|
+
this._debugTexNode.value = ptColor;
|
|
177
|
+
const exposed = this.renderer.toneMapping !== NoToneMapping;
|
|
178
|
+
this._debugExposureComp.value = exposed ? 1 / Math.max( this.renderer.toneMappingExposure, 1e-4 ) : 1;
|
|
179
|
+
|
|
180
|
+
this.renderer.setRenderTarget( null );
|
|
181
|
+
this._debugQuad.render( this.renderer );
|
|
182
|
+
return;
|
|
183
|
+
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
}
|
|
187
|
+
|
|
70
188
|
const sourceTexture = this._resolveSourceTexture( context );
|
|
71
189
|
if ( ! sourceTexture ) return;
|
|
72
190
|
|
|
@@ -77,6 +195,12 @@ export class Compositor extends RenderStage {
|
|
|
77
195
|
|
|
78
196
|
}
|
|
79
197
|
|
|
198
|
+
setConvergenceOverlay( enabled ) {
|
|
199
|
+
|
|
200
|
+
this._convergenceOverlay = !! enabled;
|
|
201
|
+
|
|
202
|
+
}
|
|
203
|
+
|
|
80
204
|
setSaturation( value ) {
|
|
81
205
|
|
|
82
206
|
this.saturation.value = value;
|
|
@@ -93,8 +217,14 @@ export class Compositor extends RenderStage {
|
|
|
93
217
|
|
|
94
218
|
this._sourceTexNode?.dispose();
|
|
95
219
|
this.compositorMaterial?.dispose();
|
|
220
|
+
this._debugTexNode?.dispose();
|
|
221
|
+
this._debugMaterial?.dispose();
|
|
96
222
|
// QuadMesh extends Mesh — no dispose method; material already released above.
|
|
97
223
|
this.compositorQuad = null;
|
|
224
|
+
this._debugQuad = null;
|
|
225
|
+
this._debugMaterial = null;
|
|
226
|
+
this._debugTexNode = null;
|
|
227
|
+
this._pathTracer = null;
|
|
98
228
|
|
|
99
229
|
}
|
|
100
230
|
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -54,6 +54,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
54
54
|
// Tier-2 dilation: per-pixel frozen mask (1=skip), written race-free in buildActivePixels; a frozen pixel
|
|
55
55
|
// stays active if any 8-neighbour is still active (Cycles box-filter) to avoid hard frozen/active seams.
|
|
56
56
|
this._frozenMaskAttr = null;
|
|
57
|
+
this._convDebugSource = null; // memoized handle set for the Compositor's convergence overlay
|
|
57
58
|
this._dilateFrozenUniform = uniform( 1, 'int' ); // 1 = dilate (default); 0 = plain per-pixel freeze
|
|
58
59
|
this._wavefrontReady = false;
|
|
59
60
|
|
|
@@ -99,6 +100,9 @@ export class PathTracer extends PathTracerStage {
|
|
|
99
100
|
// single-flight guard for its async counter read. Zeroed on reset/camera-move/resize; never refreshed
|
|
100
101
|
// mid-motion (the readback early-return + frozen frameCount keep the stop from firing while moving).
|
|
101
102
|
this._convergedFraction = 0;
|
|
103
|
+
// Subject-only convergence, from the same eroded bits. Starts at 0 so the stop cannot fire unmeasured.
|
|
104
|
+
this._convergedGeometryFraction = 0;
|
|
105
|
+
this._geometryPixelCount = 0;
|
|
102
106
|
this._convergedReadbackPending = false;
|
|
103
107
|
|
|
104
108
|
// Tier-2: last settled active-pixel count (maxRays − frozen), sizes next frame's bounce-0 grid.
|
|
@@ -272,6 +276,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
272
276
|
this._readbackGeneration ++;
|
|
273
277
|
// Drop the stale converged fraction so the early-stop can't fire on the pose we're leaving.
|
|
274
278
|
this._convergedFraction = 0;
|
|
279
|
+
this._convergedGeometryFraction = 0;
|
|
275
280
|
// Tier-2: drop the stale active-pixel count so bounce 0 full-sizes until the new pose re-measures.
|
|
276
281
|
this._lastActivePixelCount = 0;
|
|
277
282
|
|
|
@@ -372,6 +377,14 @@ export class PathTracer extends PathTracerStage {
|
|
|
372
377
|
|
|
373
378
|
}
|
|
374
379
|
|
|
380
|
+
// Full-frame kernel, so first chunk only, after the zeroing above. Frame 0 skips it — those bits
|
|
381
|
+
// still belong to the pre-reset view. Only on frames whose counters are actually read back.
|
|
382
|
+
if ( chunkIndex === 0 && frameValue > 0 && this.useAdaptiveSampling.value > 0 && this._willReadCountersThisFrame() ) {
|
|
383
|
+
|
|
384
|
+
km.dispatch( 'countConvergedDilated' );
|
|
385
|
+
|
|
386
|
+
}
|
|
387
|
+
|
|
375
388
|
for ( let bounce = 0; bounce <= loopBound; bounce ++ ) {
|
|
376
389
|
|
|
377
390
|
this._wfCurrentBounce.value = bounce;
|
|
@@ -487,9 +500,15 @@ export class PathTracer extends PathTracerStage {
|
|
|
487
500
|
// _convergedFraction is zeroed on camera-move and never refreshed mid-motion (readback early-returns).
|
|
488
501
|
_isConvergedComplete() {
|
|
489
502
|
|
|
503
|
+
// BOTH fractions must clear the bar. The whole-frame one counts every pixel, so it is diluted by
|
|
504
|
+
// however much easy background is in shot — the bar effectively becomes (bar−bgShare)/(1−bgShare) on
|
|
505
|
+
// the subject, which makes it framing-dependent. The geometry-only fraction gates that. Neither
|
|
506
|
+
// replaces the other: background can be genuinely noisy (DOF, sharp env under AA jitter), and then
|
|
507
|
+
// the whole-frame fraction is the binding one.
|
|
490
508
|
return this.useAdaptiveSampling.value > 0
|
|
491
509
|
&& this.frameCount >= this.adaptiveMinSamples.value
|
|
492
|
-
&& this._convergedFraction >= this.adaptiveStopFraction.value
|
|
510
|
+
&& this._convergedFraction >= this.adaptiveStopFraction.value
|
|
511
|
+
&& this._convergedGeometryFraction >= this.adaptiveStopFraction.value;
|
|
493
512
|
|
|
494
513
|
}
|
|
495
514
|
|
|
@@ -497,10 +516,62 @@ export class PathTracer extends PathTracerStage {
|
|
|
497
516
|
|
|
498
517
|
super.reset();
|
|
499
518
|
this._convergedFraction = 0;
|
|
519
|
+
this._convergedGeometryFraction = 0;
|
|
500
520
|
this._lastActivePixelCount = 0;
|
|
501
521
|
|
|
502
522
|
}
|
|
503
523
|
|
|
524
|
+
/**
|
|
525
|
+
* Per-pixel convergence state for the Compositor's debug overlay: the buffers FinalWrite maintains
|
|
526
|
+
* plus the uniforms needed to re-derive its predicate. Read-only — the overlay never writes back.
|
|
527
|
+
* Null until the wavefront kernels exist.
|
|
528
|
+
*/
|
|
529
|
+
getConvergenceDebugSource() {
|
|
530
|
+
|
|
531
|
+
if ( ! this._m2Attr || ! this._frozenMaskAttr ) return null;
|
|
532
|
+
// Polled every frame while the overlay is on — rebuild only when the buffers actually change.
|
|
533
|
+
if ( this._convDebugSource?.m2 === this._m2Attr && this._convDebugSource.frozenMask === this._frozenMaskAttr ) {
|
|
534
|
+
|
|
535
|
+
return this._convDebugSource;
|
|
536
|
+
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
this._convDebugSource = {
|
|
540
|
+
m2: this._m2Attr,
|
|
541
|
+
frozenMask: this._frozenMaskAttr,
|
|
542
|
+
frame: this.frame,
|
|
543
|
+
renderWidth: this._wfRenderWidth,
|
|
544
|
+
renderHeight: this._wfRenderHeight,
|
|
545
|
+
noiseThreshold: this.noiseThreshold,
|
|
546
|
+
pixelFreezeThreshold: this.pixelFreezeThreshold,
|
|
547
|
+
usePixelFreeze: this.usePixelFreeze,
|
|
548
|
+
adaptiveMinSamples: this.adaptiveMinSamples,
|
|
549
|
+
};
|
|
550
|
+
|
|
551
|
+
return this._convDebugSource;
|
|
552
|
+
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* Whole-frame adaptive-sampling telemetry, from the counters readback (settled views only).
|
|
557
|
+
* `converged` is the fraction that met the noise threshold; `activePixels` is the count the
|
|
558
|
+
* frozen-compaction path actually traced last settled frame.
|
|
559
|
+
*/
|
|
560
|
+
getConvergenceStats() {
|
|
561
|
+
|
|
562
|
+
const total = this._wfRenderWidth.value * this._wfRenderHeight.value;
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
converged: this._convergedFraction,
|
|
566
|
+
convergedGeometry: this._convergedGeometryFraction,
|
|
567
|
+
geometryPixels: this._geometryPixelCount,
|
|
568
|
+
activePixels: this._lastActivePixelCount,
|
|
569
|
+
totalPixels: total,
|
|
570
|
+
frame: this.frameCount,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
}
|
|
574
|
+
|
|
504
575
|
// Parent resizes storageTextures/shaderBuilder; wavefront also needs its buffers/uniforms/kernels rebuilt.
|
|
505
576
|
_handleResize() {
|
|
506
577
|
|
|
@@ -551,6 +622,22 @@ export class PathTracer extends PathTracerStage {
|
|
|
551
622
|
|
|
552
623
|
}
|
|
553
624
|
|
|
625
|
+
/**
|
|
626
|
+
* Whether THIS frame's counters will actually be read back, evaluated before the dispatches that fill
|
|
627
|
+
* them. Mirrors _maybeReadbackCounters' guards exactly; both run inside the same synchronous render()
|
|
628
|
+
* call, so no async continuation can flip a flag between them.
|
|
629
|
+
*
|
|
630
|
+
* countConvergedDilated is a full-frame pass whose only consumer is that readback, and the readback is
|
|
631
|
+
* on a 4-frame cadence — counting every frame threw ~75% of the work away.
|
|
632
|
+
*/
|
|
633
|
+
_willReadCountersThisFrame() {
|
|
634
|
+
|
|
635
|
+
if ( this.cameraChanged || this.cameraOptimizer?.isInInteractionMode() ) return false;
|
|
636
|
+
if ( this._readbackPending || this._convergedReadbackPending ) return false;
|
|
637
|
+
return this._readbackFrameCounter + 1 >= this._readbackEveryNFrames;
|
|
638
|
+
|
|
639
|
+
}
|
|
640
|
+
|
|
554
641
|
// Async readback of the per-bounce snapshot every N frames; never awaited, so the early-exit uses past-frame data.
|
|
555
642
|
_maybeReadbackCounters() {
|
|
556
643
|
|
|
@@ -620,6 +707,11 @@ export class PathTracer extends PathTracerStage {
|
|
|
620
707
|
|
|
621
708
|
const c = new Uint32Array( buf );
|
|
622
709
|
this._convergedFraction = c[ COUNTER.CONVERGED_COUNT ] / total;
|
|
710
|
+
// No geometry at all (pure environment) leaves nothing to gate on — 1 lets the
|
|
711
|
+
// whole-frame fraction decide alone.
|
|
712
|
+
const geo = c[ COUNTER.GEOMETRY_COUNT ];
|
|
713
|
+
this._geometryPixelCount = geo;
|
|
714
|
+
this._convergedGeometryFraction = geo > 0 ? c[ COUNTER.CONVERGED_GEOMETRY_COUNT ] / geo : 1;
|
|
623
715
|
// Tier-2: bounce-0 active-pixel count measured this settled frame → sizes next frame's grid.
|
|
624
716
|
this._lastActivePixelCount = c[ COUNTER.ACTIVE_PIXEL_COUNT ];
|
|
625
717
|
|
|
@@ -673,6 +765,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
673
765
|
this._readbackFrameCounter = 0;
|
|
674
766
|
this._readbackGeneration ++;
|
|
675
767
|
this._convergedFraction = 0;
|
|
768
|
+
this._convergedGeometryFraction = 0;
|
|
676
769
|
this._lastActivePixelCount = 0;
|
|
677
770
|
|
|
678
771
|
// Chunked path pool: the wavefront buffers are sized to the fixed device budget B (not the
|
|
@@ -843,20 +936,28 @@ export class PathTracer extends PathTracerStage {
|
|
|
843
936
|
// max resolution (small — 12 B/pixel) and NEVER realloc on a resize.
|
|
844
937
|
const maxPixels = MAX_STORAGE_TEXTURE_SIZE * MAX_STORAGE_TEXTURE_SIZE;
|
|
845
938
|
|
|
846
|
-
//
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
939
|
+
// Only reallocated on growth, never on a plain rebuild: the Compositor's overlay binds m2/frozenMask
|
|
940
|
+
// read-only and a fresh attribute would strand its bind group. Surviving contents are safe — every
|
|
941
|
+
// consumer re-seeds at frame 0.
|
|
942
|
+
if ( ! this._m2Attr || this._m2Attr.count < maxPixels ) {
|
|
943
|
+
|
|
944
|
+
// Tier-1 convergence: per-pixel running mean of luminance² (Welford second moment), read+written by FinalWrite.
|
|
945
|
+
freeStorageAttribute( this.renderer, this._m2Attr );
|
|
946
|
+
this._m2Attr = new StorageInstancedBufferAttribute( new Float32Array( maxPixels ), 1 );
|
|
947
|
+
|
|
948
|
+
// Tier-2: per-pixel freeze-candidate streak. FinalWrite writes (RW), buildActivePixels reads (RO).
|
|
949
|
+
freeStorageAttribute( this.renderer, this._streakAttr );
|
|
950
|
+
this._streakAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
850
951
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
952
|
+
// Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
|
|
953
|
+
freeStorageAttribute( this.renderer, this._frozenMaskAttr );
|
|
954
|
+
this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
955
|
+
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const m2RW = storage( this._m2Attr, 'float' );
|
|
854
959
|
const streakRW = storage( this._streakAttr, 'uint' );
|
|
855
960
|
const streakRO = storage( this._streakAttr, 'uint' ).toReadOnly();
|
|
856
|
-
|
|
857
|
-
// Tier-2: dilated frozen mask (1 = skip). buildActivePixels writes; active-list + FinalWrite read → race-free.
|
|
858
|
-
freeStorageAttribute( this.renderer, this._frozenMaskAttr );
|
|
859
|
-
this._frozenMaskAttr = new StorageInstancedBufferAttribute( new Uint32Array( maxPixels ), 1 );
|
|
860
961
|
const frozenMaskRW = storage( this._frozenMaskAttr, 'uint' );
|
|
861
962
|
const frozenMaskRO = storage( this._frozenMaskAttr, 'uint' ).toReadOnly();
|
|
862
963
|
|
|
@@ -929,6 +1030,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
929
1030
|
If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
|
|
930
1031
|
|
|
931
1032
|
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
1033
|
+
atomicStore( counters.element( uint( COUNTER.GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
1034
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
932
1035
|
|
|
933
1036
|
} );
|
|
934
1037
|
|
|
@@ -988,6 +1091,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
988
1091
|
If( this._wfIsFirstChunk.greaterThan( uint( 0 ) ), () => {
|
|
989
1092
|
|
|
990
1093
|
atomicStore( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 0 ) );
|
|
1094
|
+
atomicStore( counters.element( uint( COUNTER.GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
1095
|
+
atomicStore( counters.element( uint( COUNTER.CONVERGED_GEOMETRY_COUNT ) ), uint( 0 ) );
|
|
991
1096
|
|
|
992
1097
|
} );
|
|
993
1098
|
|
|
@@ -1010,7 +1115,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
1010
1115
|
// (streak<K), softening the boundary. Mask is read by FinalWrite too → race-free.
|
|
1011
1116
|
const frozen = uint( 0 ).toVar();
|
|
1012
1117
|
|
|
1013
|
-
|
|
1118
|
+
// Bits 31/30 of the streak word are the converged + geometry flags — mask them off wherever the streak is read.
|
|
1119
|
+
If( this.frame.greaterThan( uint( 0 ) ).and( streakRO.element( p ).bitAnd( uint( 0x3FFFFFFF ) ).greaterThanEqual( uint( freezeK ) ) ), () => {
|
|
1014
1120
|
|
|
1015
1121
|
frozen.assign( uint( 1 ) );
|
|
1016
1122
|
|
|
@@ -1026,7 +1132,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1026
1132
|
If( nx.greaterThanEqual( int( 0 ) ).and( nx.lessThan( this._wfRenderWidth ) )
|
|
1027
1133
|
.and( ny.greaterThanEqual( int( 0 ) ) ).and( ny.lessThan( this._wfRenderHeight ) ), () => {
|
|
1028
1134
|
|
|
1029
|
-
If( streakRO.element( uint( ny.mul( this._wfRenderWidth ).add( nx ) ) ).lessThan( uint( freezeK ) ), () => {
|
|
1135
|
+
If( streakRO.element( uint( ny.mul( this._wfRenderWidth ).add( nx ) ) ).bitAnd( uint( 0x3FFFFFFF ) ).lessThan( uint( freezeK ) ), () => {
|
|
1030
1136
|
|
|
1031
1137
|
frozen.assign( uint( 0 ) ); // a still-active neighbour keeps this pixel active
|
|
1032
1138
|
|
|
@@ -1057,6 +1163,74 @@ export class PathTracer extends PathTracerStage {
|
|
|
1057
1163
|
buildActiveFn().compute( [ Math.ceil( ( this._chunkRows * w ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
|
|
1058
1164
|
);
|
|
1059
1165
|
|
|
1166
|
+
// Whole-frame converged count, eroded 3×3 (Cycles film_adaptive_sampling_filter_x/y parity): a pixel
|
|
1167
|
+
// counts only if it AND its 8 in-bounds neighbours carry last frame's converged bit. A pointwise count
|
|
1168
|
+
// is blind to undiscovered energy — all-zero pixels have zero variance and pass any threshold, so on
|
|
1169
|
+
// indirect-dominant scenes it peaks on a black image and retires the frame at ~10 spp. Erosion lets
|
|
1170
|
+
// each discovered speckle poison its neighbourhood while energy is still being found.
|
|
1171
|
+
// Reads the PREVIOUS frame's bits, which costs nothing: the stop already lags by the readback cadence.
|
|
1172
|
+
const countConvergedFn = Fn( () => {
|
|
1173
|
+
|
|
1174
|
+
const tid = instanceIndex;
|
|
1175
|
+
const W = this._wfRenderWidth;
|
|
1176
|
+
const H = this._wfRenderHeight;
|
|
1177
|
+
If( tid.lessThan( uint( W.mul( H ) ) ), () => {
|
|
1178
|
+
|
|
1179
|
+
const px = int( tid ).mod( W );
|
|
1180
|
+
const py = int( tid ).div( W );
|
|
1181
|
+
const CONV_BIT = uint( 0x80000000 );
|
|
1182
|
+
const GEOM_BIT = uint( 0x40000000 );
|
|
1183
|
+
|
|
1184
|
+
const word = streakRO.element( tid ).toVar();
|
|
1185
|
+
const isGeometry = word.bitAnd( GEOM_BIT ).notEqual( uint( 0 ) );
|
|
1186
|
+
const allConverged = word.bitAnd( CONV_BIT ).notEqual( uint( 0 ) ).toVar();
|
|
1187
|
+
|
|
1188
|
+
If( allConverged, () => {
|
|
1189
|
+
|
|
1190
|
+
for ( const [ dx, dy ] of [[ - 1, - 1 ], [ 0, - 1 ], [ 1, - 1 ], [ - 1, 0 ], [ 1, 0 ], [ - 1, 1 ], [ 0, 1 ], [ 1, 1 ]] ) {
|
|
1191
|
+
|
|
1192
|
+
const nx = px.add( int( dx ) );
|
|
1193
|
+
const ny = py.add( int( dy ) );
|
|
1194
|
+
If( nx.greaterThanEqual( int( 0 ) ).and( nx.lessThan( W ) )
|
|
1195
|
+
.and( ny.greaterThanEqual( int( 0 ) ) ).and( ny.lessThan( H ) ), () => {
|
|
1196
|
+
|
|
1197
|
+
If( streakRO.element( uint( ny.mul( W ).add( nx ) ) ).bitAnd( CONV_BIT ).equal( uint( 0 ) ), () => {
|
|
1198
|
+
|
|
1199
|
+
allConverged.assign( false );
|
|
1200
|
+
|
|
1201
|
+
} );
|
|
1202
|
+
|
|
1203
|
+
} );
|
|
1204
|
+
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
} );
|
|
1208
|
+
|
|
1209
|
+
If( allConverged, () => {
|
|
1210
|
+
|
|
1211
|
+
atomicAdd( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 1 ) );
|
|
1212
|
+
|
|
1213
|
+
} );
|
|
1214
|
+
|
|
1215
|
+
// Same eroded verdict, restricted to subject pixels — the framing-independent view.
|
|
1216
|
+
If( isGeometry, () => {
|
|
1217
|
+
|
|
1218
|
+
atomicAdd( counters.element( uint( COUNTER.GEOMETRY_COUNT ) ), uint( 1 ) );
|
|
1219
|
+
If( allConverged, () => {
|
|
1220
|
+
|
|
1221
|
+
atomicAdd( counters.element( uint( COUNTER.CONVERGED_GEOMETRY_COUNT ) ), uint( 1 ) );
|
|
1222
|
+
|
|
1223
|
+
} );
|
|
1224
|
+
|
|
1225
|
+
} );
|
|
1226
|
+
|
|
1227
|
+
} );
|
|
1228
|
+
|
|
1229
|
+
} );
|
|
1230
|
+
this._kernelManager.register( 'countConvergedDilated',
|
|
1231
|
+
countConvergedFn().compute( [ Math.ceil( ( w * h ) / LIST_WG_SIZE ), 1, 1 ], [ LIST_WG_SIZE, 1, 1 ] )
|
|
1232
|
+
);
|
|
1233
|
+
|
|
1060
1234
|
const seedEnterFn = Fn( () => {
|
|
1061
1235
|
|
|
1062
1236
|
const active = atomicLoad( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ) );
|
|
@@ -1319,7 +1493,6 @@ export class PathTracer extends PathTracerStage {
|
|
|
1319
1493
|
visMode: this.visMode,
|
|
1320
1494
|
auxGBufferEnabled: this._auxGBufferUniform,
|
|
1321
1495
|
cleanAuxNormalEnabled: this._cleanAuxNormalUniform,
|
|
1322
|
-
counters,
|
|
1323
1496
|
m2BufferRW: m2RW,
|
|
1324
1497
|
useAdaptiveSampling: this.useAdaptiveSampling,
|
|
1325
1498
|
noiseThreshold: this.noiseThreshold,
|
|
@@ -1329,6 +1502,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1329
1502
|
pixelFreezeThreshold: this.pixelFreezeThreshold,
|
|
1330
1503
|
streakBufferRW: streakRW,
|
|
1331
1504
|
frozenMaskRO, // dilated frozen mask (read-only; matches the active-list decision)
|
|
1505
|
+
convergenceOverlay: this.convergenceOverlay,
|
|
1332
1506
|
chunkRowBase: this._wfChunkRowBase,
|
|
1333
1507
|
chunkRows: this._wfChunkRows,
|
|
1334
1508
|
} );
|
|
@@ -1403,6 +1577,8 @@ export class PathTracer extends PathTracerStage {
|
|
|
1403
1577
|
const h = this._wfRenderHeight.value;
|
|
1404
1578
|
|
|
1405
1579
|
this._kernelManager.setDispatchForGrid( 'debug', w, h );
|
|
1580
|
+
// Full-frame (not per-chunk); sized from live values so an in-place resize can't strand its grid.
|
|
1581
|
+
this._kernelManager.setDispatchForCount( 'countConvergedDilated', w * h );
|
|
1406
1582
|
|
|
1407
1583
|
}
|
|
1408
1584
|
|
|
@@ -1445,6 +1621,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
1445
1621
|
this._m2Attr = null;
|
|
1446
1622
|
this._streakAttr = null;
|
|
1447
1623
|
this._frozenMaskAttr = null;
|
|
1624
|
+
this._convDebugSource = null;
|
|
1448
1625
|
this._wavefrontReady = false;
|
|
1449
1626
|
|
|
1450
1627
|
}
|