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/README.md +1 -1
- package/dist/rayzee.es.js +1866 -1657
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +18 -18
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +24 -1
- package/src/PathTracerApp.js +14 -0
- package/src/Pipeline/CompletionTracker.js +4 -1
- package/src/Processor/KernelManager.js +0 -1
- package/src/Processor/QueueManager.js +8 -1
- package/src/Processor/ShaderBuilder.js +3 -0
- package/src/RenderSettings.js +9 -0
- package/src/Stages/PathTracer.js +306 -34
- package/src/TSL/BVHTraversal.js +10 -3
- package/src/TSL/Debugger.js +1 -1
- package/src/TSL/FinalWriteKernel.js +110 -8
- package/src/TSL/GenerateKernel.js +74 -46
- package/src/TSL/ShadeKernel.js +80 -3
- package/src/managers/DenoisingManager.js +6 -0
- package/src/managers/UniformManager.js +15 -0
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
Fn, wgslFn, float, vec2, vec4, int, uint, uvec2,
|
|
7
|
-
If, mix, select, texture, textureStore,
|
|
7
|
+
If, mix, select, texture, textureStore, length, atomicAdd,
|
|
8
8
|
localId, workgroupId,
|
|
9
9
|
} from 'three/tsl';
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
readRayRadiance, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
|
|
13
13
|
} from '../Processor/PackedRayBuffer.js';
|
|
14
|
+
import { luminance } from './Common.js';
|
|
15
|
+
import { COUNTER } from '../Processor/QueueManager.js';
|
|
14
16
|
|
|
15
17
|
const WG_SIZE = 16;
|
|
16
18
|
|
|
@@ -32,15 +34,27 @@ export function buildFinalWriteKernel( params ) {
|
|
|
32
34
|
resolution, frame,
|
|
33
35
|
enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
|
|
34
36
|
transparentBackground,
|
|
35
|
-
prevAccumTexture, prevAlbedoTexture,
|
|
37
|
+
prevAccumTexture, prevAlbedoTexture, prevNormalDepthTexture,
|
|
36
38
|
renderWidth, renderHeight,
|
|
37
39
|
visMode,
|
|
38
40
|
// Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
|
|
39
41
|
// on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
|
|
40
42
|
auxGBufferEnabled,
|
|
43
|
+
// Clean-aux normal (1 = temporally accumulate + renormalize the aux normal). On only for clean-aux
|
|
44
|
+
// OIDN models (calb_cnrm/high, alb_nrm/balanced); off for fast/ASVGF which want the bump normal.
|
|
45
|
+
cleanAuxNormalEnabled,
|
|
46
|
+
// Tier-1 convergence early-stop: per-pixel Welford luminance second moment + converged-pixel counter.
|
|
47
|
+
counters, m2BufferRW, useAdaptiveSampling, noiseThreshold, darkNoiseFloor, adaptiveMinSamples,
|
|
48
|
+
// Tier-2 per-pixel freeze: streakBufferRW = per-pixel freeze-candidate streak (stamped here);
|
|
49
|
+
// frozenMaskRO = dilated frozen mask from buildActivePixels (pass-through gates on it, not streak).
|
|
50
|
+
usePixelFreeze, pixelFreezeThreshold, pixelFreezeStability, streakBufferRW, frozenMaskRO,
|
|
41
51
|
} = params;
|
|
42
52
|
|
|
43
53
|
const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
|
|
54
|
+
const cleanAuxNormalOn = cleanAuxNormalEnabled.greaterThan( uint( 0 ) );
|
|
55
|
+
// useAdaptiveSampling is registered via UniformManager.ub() → an int 0/1 uniform, so compare against int(0).
|
|
56
|
+
const convOn = useAdaptiveSampling.greaterThan( int( 0 ) );
|
|
57
|
+
const adaptiveOn = usePixelFreeze.greaterThan( int( 0 ) );
|
|
44
58
|
|
|
45
59
|
const computeFn = Fn( () => {
|
|
46
60
|
|
|
@@ -56,6 +70,11 @@ export function buildFinalWriteKernel( params ) {
|
|
|
56
70
|
const finalColor = sampleColor.xyz.toVar();
|
|
57
71
|
const outputAlpha = select( transparentBackground, sampleColor.w, float( 1.0 ) ).toVar();
|
|
58
72
|
|
|
73
|
+
// A pixel frozen in a prior frame was skipped by Generate, so its rayBuffer sample is stale — pass
|
|
74
|
+
// the accumulated colour through unchanged (below) instead of mixing it. Inert unless adaptiveOn.
|
|
75
|
+
const wasFrozen = adaptiveOn.and( frame.greaterThan( uint( 0 ) ) )
|
|
76
|
+
.and( frozenMaskRO.element( rayID ).equal( uint( 1 ) ) ).toVar();
|
|
77
|
+
|
|
59
78
|
// MRT comes from the per-pixel G-buffer (rayID == pixelIndex). Half-packed: decode.
|
|
60
79
|
// auxOn gates the decode + stores so a no-denoiser frame does no G-buffer read and no aux writes.
|
|
61
80
|
const finalNormalDepth = vec4( 0.0 ).toVar();
|
|
@@ -77,20 +96,103 @@ export function buildFinalWriteKernel( params ) {
|
|
|
77
96
|
|
|
78
97
|
const prevAccumSample = texture( prevAccumTexture, prevUV, 0 ).toVar();
|
|
79
98
|
|
|
80
|
-
|
|
99
|
+
// Frozen pixels pass prev colour through unchanged (stale sample); active pixels accumulate.
|
|
100
|
+
finalColor.assign( select( wasFrozen, prevAccumSample.xyz, mix( prevAccumSample.xyz, sampleColor.xyz, accumulationAlpha ) ) );
|
|
81
101
|
If( auxOn, () => {
|
|
82
102
|
|
|
83
|
-
// Albedo averages cleanly (it's a colour).
|
|
84
|
-
// unit normals collapses toward the flat geometric mean (worse at distance), which made
|
|
85
|
-
// OIDN smooth bump detail away. Keep this frame's point-sampled normal — it varies with
|
|
86
|
-
// the bump, which is exactly what OIDN's edge-stop needs to preserve it.
|
|
103
|
+
// Albedo averages cleanly (it's a colour).
|
|
87
104
|
finalAlbedo.assign( mix( texture( prevAlbedoTexture, prevUV, 0 ).xyz, finalAlbedo, accumulationAlpha ) );
|
|
88
105
|
|
|
106
|
+
// NORMAL: by default keep this frame's POINT-SAMPLED normal — it varies with the bump,
|
|
107
|
+
// which fast/ASVGF want to preserve edge detail. But a CLEAN-AUX OIDN model (calb_cnrm/high,
|
|
108
|
+
// alb_nrm/balanced) trusts the aux and is fed per-frame point-sampled NOISE → leaked noise
|
|
109
|
+
// (high) / over-smoothing (balanced). When cleanAuxNormalOn, temporally accumulate it like
|
|
110
|
+
// the colour. Average the RAW unit normals (decode 0.5+0.5 → [-1,1]) and RENORMALIZE — a
|
|
111
|
+
// plain encoded-space mix would bias toward the flat (0.5,0.5,1) mean and collapse detail.
|
|
112
|
+
// Depth (.w) stays this frame's value; guard the degenerate near-zero mean (opposing normals).
|
|
113
|
+
If( cleanAuxNormalOn, () => {
|
|
114
|
+
|
|
115
|
+
const prevN = texture( prevNormalDepthTexture, prevUV, 0 ).xyz.mul( 2.0 ).sub( 1.0 );
|
|
116
|
+
const curN = finalNormalDepth.xyz.mul( 2.0 ).sub( 1.0 ).toVar();
|
|
117
|
+
const mixedN = mix( prevN, curN, accumulationAlpha ).toVar();
|
|
118
|
+
const len = length( mixedN );
|
|
119
|
+
const avgN = select( len.greaterThan( 1e-4 ), mixedN.div( len ), curN );
|
|
120
|
+
finalNormalDepth.assign( vec4( avgN.mul( 0.5 ).add( 0.5 ), finalNormalDepth.w ) );
|
|
121
|
+
|
|
122
|
+
} );
|
|
123
|
+
|
|
89
124
|
} );
|
|
90
125
|
|
|
91
126
|
If( transparentBackground, () => {
|
|
92
127
|
|
|
93
|
-
outputAlpha.assign( mix( prevAccumSample.w, sampleColor.w, accumulationAlpha ) );
|
|
128
|
+
outputAlpha.assign( select( wasFrozen, prevAccumSample.w, mix( prevAccumSample.w, sampleColor.w, accumulationAlpha ) ) );
|
|
129
|
+
|
|
130
|
+
} );
|
|
131
|
+
|
|
132
|
+
} );
|
|
133
|
+
|
|
134
|
+
// Tier-1 convergence: per-pixel running second moment of LUMINANCE (Welford). luminance() is linear,
|
|
135
|
+
// so luminance(running-mean color) == running-mean luminance == E[L]; m2 tracks E[L²] under the SAME
|
|
136
|
+
// global 1/(frame+1) alpha (NO per-pixel alpha). sampleVar = E[L²]-E[L]²; varOfMean = sampleVar/(N);
|
|
137
|
+
// relErr = SE(mean)/mean. A pixel counts as converged once frame>=minSamples AND relErr<threshold.
|
|
138
|
+
// The m2 write runs every frame (incl. frame 0, where alpha==1 self-inits it → no explicit clear).
|
|
139
|
+
// Sits AFTER the accumulation mix (finalColor is the mean) and BEFORE the visMode-11 mutation.
|
|
140
|
+
If( convOn, () => {
|
|
141
|
+
|
|
142
|
+
const sampleLum = luminance( sampleColor.xyz );
|
|
143
|
+
const prevM2 = m2BufferRW.element( rayID ).toVar();
|
|
144
|
+
const m2 = mix( prevM2, sampleLum.mul( sampleLum ), accumulationAlpha ).toVar();
|
|
145
|
+
m2BufferRW.element( rayID ).assign( m2 );
|
|
146
|
+
|
|
147
|
+
const meanLum = luminance( finalColor ).toVar();
|
|
148
|
+
const sampleVar = m2.sub( meanLum.mul( meanLum ) ).max( float( 0 ) );
|
|
149
|
+
const varOfMean = sampleVar.div( float( frame ).add( 1.0 ) );
|
|
150
|
+
// absSE = absolute standard error of the mean luminance; relErr = its ratio to the mean.
|
|
151
|
+
// Combined criterion: bright pixels converge on relErr<threshold; dark/dim pixels (where relErr
|
|
152
|
+
// stays high forever) converge on absSE<absFloor, since their absolute noise is imperceptible.
|
|
153
|
+
const absSE = varOfMean.sqrt().toVar();
|
|
154
|
+
const relErr = absSE.div( meanLum.add( float( 1e-4 ) ) );
|
|
155
|
+
|
|
156
|
+
If( frame.greaterThanEqual( uint( adaptiveMinSamples ) ).and(
|
|
157
|
+
relErr.lessThan( noiseThreshold ).or( absSE.lessThan( darkNoiseFloor ) )
|
|
158
|
+
), () => {
|
|
159
|
+
|
|
160
|
+
atomicAdd( counters.element( uint( COUNTER.CONVERGED_COUNT ) ), uint( 1 ) );
|
|
161
|
+
|
|
162
|
+
} );
|
|
163
|
+
|
|
164
|
+
// Maintain the freeze streak on the relErr-only predicate (NO absFloor — it bakes dim regions dark
|
|
165
|
+
// when it permanently freezes a pixel). Monotonic: streak>=K freezes for the run, so global alpha stays exact.
|
|
166
|
+
If( adaptiveOn, () => {
|
|
167
|
+
|
|
168
|
+
If( frame.equal( uint( 0 ) ), () => {
|
|
169
|
+
|
|
170
|
+
// Frame 0 (post-reset/camera-move) clears the frozen state so each run re-freezes fresh.
|
|
171
|
+
streakBufferRW.element( rayID ).assign( uint( 0 ) );
|
|
172
|
+
|
|
173
|
+
} );
|
|
174
|
+
|
|
175
|
+
If( wasFrozen, () => {
|
|
176
|
+
|
|
177
|
+
// Already frozen (passed through above) — count it.
|
|
178
|
+
atomicAdd( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 1 ) );
|
|
179
|
+
|
|
180
|
+
} );
|
|
181
|
+
|
|
182
|
+
If( wasFrozen.not().and( frame.greaterThan( uint( 0 ) ) ), () => {
|
|
183
|
+
|
|
184
|
+
const freezeCandidate = frame.greaterThanEqual( uint( adaptiveMinSamples ) )
|
|
185
|
+
.and( relErr.lessThan( pixelFreezeThreshold ) );
|
|
186
|
+
const newStreak = select( freezeCandidate, streakBufferRW.element( rayID ).add( uint( 1 ) ), uint( 0 ) ).toVar();
|
|
187
|
+
streakBufferRW.element( rayID ).assign( newStreak );
|
|
188
|
+
// Just reached K → frozen next frame; count it now so the frozen population is current.
|
|
189
|
+
If( newStreak.greaterThanEqual( uint( pixelFreezeStability ) ), () => {
|
|
190
|
+
|
|
191
|
+
atomicAdd( counters.element( uint( COUNTER.FROZEN_COUNT ) ), uint( 1 ) );
|
|
192
|
+
|
|
193
|
+
} );
|
|
194
|
+
|
|
195
|
+
} );
|
|
94
196
|
|
|
95
197
|
} );
|
|
96
198
|
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* GenerateKernel.js — wavefront primary ray generation
|
|
2
|
+
* GenerateKernel.js — wavefront primary ray generation.
|
|
3
|
+
* Two dispatch modes from one builder: 2D screen-space (default) and 1D list-driven over the active-pixel
|
|
4
|
+
* list (Tier-2 per-pixel freeze).
|
|
3
5
|
*/
|
|
4
6
|
|
|
5
7
|
import {
|
|
6
8
|
Fn, float, vec2, vec3, vec4, int, uint,
|
|
7
|
-
If, select,
|
|
9
|
+
If, select, instanceIndex, atomicLoad,
|
|
8
10
|
localId, workgroupId,
|
|
9
11
|
} from 'three/tsl';
|
|
10
12
|
|
|
@@ -16,7 +18,7 @@ import {
|
|
|
16
18
|
|
|
17
19
|
import { generateRayFromCamera } from './BVHTraversal.js';
|
|
18
20
|
import { Ray } from './Struct.js';
|
|
19
|
-
import { RAY_FLAG } from '../Processor/QueueManager.js';
|
|
21
|
+
import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
|
|
20
22
|
import {
|
|
21
23
|
writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
|
|
22
24
|
writeRayRadiance, writeGBuffer,
|
|
@@ -36,68 +38,94 @@ export function buildGenerateKernel( params ) {
|
|
|
36
38
|
transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
|
|
37
39
|
transparentBackground, // alpha inits to 1 here (megakernel parity: PathTracerCore.js:554) — env-escape-without-opaque zeroes it in Shade
|
|
38
40
|
auxGBufferEnabled, // live uniform: 1 = init the per-pixel G-buffer (denoiser on), 0 = skip it
|
|
41
|
+
// listDriven: 1D dispatch over the active-pixel list (activeIndicesRO[tid] = pixelID) instead of 2D.
|
|
42
|
+
listDriven = false, activeIndicesRO = null, counters = null,
|
|
39
43
|
} = params;
|
|
40
44
|
|
|
41
45
|
const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
|
|
42
46
|
|
|
43
|
-
|
|
47
|
+
// Shared ray-gen body for a pixel (gx, gy) — identical in both dispatch modes.
|
|
48
|
+
const emitRay = ( gx, gy ) => {
|
|
49
|
+
|
|
50
|
+
const pixelCoord = vec2( float( gx ).add( 0.5 ), float( gy ).add( 0.5 ) );
|
|
51
|
+
const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
|
|
52
|
+
// One ray per pixel: rayID is the pixel index.
|
|
53
|
+
const rayID = uint( pixelIndex );
|
|
54
|
+
|
|
55
|
+
const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
|
|
56
|
+
screenPosition.y.assign( screenPosition.y.negate() );
|
|
57
|
+
|
|
58
|
+
const baseSeed = getDecorrelatedSeed( { pixelCoord, rayIndex: int( 0 ), frame } ).toVar();
|
|
59
|
+
const seed = pcgHash( { state: baseSeed } ).toVar();
|
|
60
|
+
|
|
61
|
+
// Sample index 1 (not 0) so the AA sub-pixel jitter draws a DIFFERENT STBN cell
|
|
62
|
+
// than the first-bounce BSDF sample (ShadeKernel uses sampleIndex 0). Every bounce
|
|
63
|
+
// samples at index 0, so index 1 is collision-free — this decorrelates the sub-pixel
|
|
64
|
+
// position from the first scatter direction (they were reading the identical cell).
|
|
65
|
+
const stratifiedJitter = getStratifiedSample( pixelCoord, int( 1 ), int( 1 ), seed, resolution, frame ).toVar();
|
|
66
|
+
|
|
67
|
+
const jitterScale = vec2( 2.0 ).div( resolution );
|
|
68
|
+
const jitter = stratifiedJitter.sub( 0.5 ).mul( jitterScale );
|
|
69
|
+
const jitteredScreenPosition = screenPosition.add( jitter );
|
|
70
|
+
|
|
71
|
+
const ray = Ray.wrap( generateRayFromCamera(
|
|
72
|
+
jitteredScreenPosition, seed,
|
|
73
|
+
cameraWorldMatrix, cameraProjectionMatrixInverse,
|
|
74
|
+
enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
|
|
75
|
+
) );
|
|
76
|
+
|
|
77
|
+
writeRayOriginMeta( rayBufferRW, rayID, ray.origin, int( 0 ), int( 0 ) );
|
|
78
|
+
// A fresh camera ray is NOT redirected — REDIRECTED stays clear so it sees the direct backdrop;
|
|
79
|
+
// ShadeKernel sets REDIRECTED on the first direction-changing interaction (see RAY_FLAG.REDIRECTED).
|
|
80
|
+
writeRayDirFlags( rayBufferRW, rayID, ray.direction, uint( RAY_FLAG.ACTIVE ) );
|
|
81
|
+
// pdf inits to 0 = prevBouncePdf (megakernel parity PathTracerCore.js:556). The bounce>0 env/emissive
|
|
82
|
+
// MIS gate skips until an opaque scatter writes a real combinedPdf; free bounces preserve it.
|
|
83
|
+
writeRayThroughputPdf( rayBufferRW, rayID, vec4( 1.0, 1.0, 1.0, 0.0 ).xyz, float( 0.0 ) );
|
|
84
|
+
// Alpha inits to 1 in transparent-bg mode (megakernel parity: PathTracerCore.js:554). Shade zeroes
|
|
85
|
+
// it only on env-escape-without-opaque; a ray that dies inside geometry (e.g. SSS walk termination)
|
|
86
|
+
// keeps alpha 1 → solid. Non-transparent mode is inert (FinalWrite forces alpha 1).
|
|
87
|
+
writeRayRadiance( rayBufferRW, rayID, vec4( vec3( 0.0 ), select( transparentBackground, float( 1.0 ), float( 0.0 ) ) ) );
|
|
88
|
+
|
|
89
|
+
If( auxOn, () => {
|
|
90
|
+
|
|
91
|
+
// default: normal +Z, depth 1 (far), black albedo (background/miss)
|
|
92
|
+
writeGBuffer( gBufferRW, uint( pixelIndex ), vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
|
|
44
93
|
|
|
45
|
-
|
|
46
|
-
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
94
|
+
} );
|
|
47
95
|
|
|
48
|
-
|
|
96
|
+
writeMediumStack( rayBufferRW, rayID, uint( 0 ), uint( transmissiveBounces ), float( 1.0 ), float( 1.0 ), float( 1.0 ) );
|
|
49
97
|
|
|
50
|
-
|
|
51
|
-
const pixelIndex = gy.mul( int( resolution.x ) ).add( gx );
|
|
52
|
-
// One ray per pixel: rayID is the pixel index.
|
|
53
|
-
const rayID = uint( pixelIndex );
|
|
98
|
+
rngBufferRW.element( rayID ).assign( seed );
|
|
54
99
|
|
|
55
|
-
|
|
56
|
-
screenPosition.y.assign( screenPosition.y.negate() );
|
|
100
|
+
};
|
|
57
101
|
|
|
58
|
-
|
|
59
|
-
const seed = pcgHash( { state: baseSeed } ).toVar();
|
|
102
|
+
const computeFn = Fn( () => {
|
|
60
103
|
|
|
61
|
-
|
|
62
|
-
// than the first-bounce BSDF sample (ShadeKernel uses sampleIndex 0). Every bounce
|
|
63
|
-
// samples at index 0, so index 1 is collision-free — this decorrelates the sub-pixel
|
|
64
|
-
// position from the first scatter direction (they were reading the identical cell).
|
|
65
|
-
const stratifiedJitter = getStratifiedSample( pixelCoord, int( 1 ), int( 1 ), seed, resolution, frame ).toVar();
|
|
104
|
+
if ( listDriven ) {
|
|
66
105
|
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
106
|
+
const tid = instanceIndex;
|
|
107
|
+
// Grid is over-sized from a stale readback; bound on the live ENTERING_COUNT so extra threads no-op.
|
|
108
|
+
If( tid.lessThan( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
|
|
70
109
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
) );
|
|
110
|
+
const pixelIndex = int( activeIndicesRO.element( tid ) );
|
|
111
|
+
const gx = pixelIndex.mod( int( resolution.x ) );
|
|
112
|
+
const gy = pixelIndex.div( int( resolution.x ) );
|
|
113
|
+
emitRay( gx, gy );
|
|
76
114
|
|
|
77
|
-
|
|
78
|
-
// A fresh camera ray is NOT redirected — REDIRECTED stays clear so it sees the direct backdrop;
|
|
79
|
-
// ShadeKernel sets REDIRECTED on the first direction-changing interaction (see RAY_FLAG.REDIRECTED).
|
|
80
|
-
writeRayDirFlags( rayBufferRW, rayID, ray.direction, uint( RAY_FLAG.ACTIVE ) );
|
|
81
|
-
// pdf inits to 0 = prevBouncePdf (megakernel parity PathTracerCore.js:556). The bounce>0 env/emissive
|
|
82
|
-
// MIS gate skips until an opaque scatter writes a real combinedPdf; free bounces preserve it.
|
|
83
|
-
writeRayThroughputPdf( rayBufferRW, rayID, vec4( 1.0, 1.0, 1.0, 0.0 ).xyz, float( 0.0 ) );
|
|
84
|
-
// Alpha inits to 1 in transparent-bg mode (megakernel parity: PathTracerCore.js:554). Shade zeroes
|
|
85
|
-
// it only on env-escape-without-opaque; a ray that dies inside geometry (e.g. SSS walk termination)
|
|
86
|
-
// keeps alpha 1 → solid. Non-transparent mode is inert (FinalWrite forces alpha 1).
|
|
87
|
-
writeRayRadiance( rayBufferRW, rayID, vec4( vec3( 0.0 ), select( transparentBackground, float( 1.0 ), float( 0.0 ) ) ) );
|
|
115
|
+
} );
|
|
88
116
|
|
|
89
|
-
|
|
117
|
+
} else {
|
|
90
118
|
|
|
91
|
-
|
|
92
|
-
|
|
119
|
+
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
120
|
+
const gy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
93
121
|
|
|
94
|
-
|
|
122
|
+
If( gx.lessThan( renderWidth ).and( gy.lessThan( renderHeight ) ), () => {
|
|
95
123
|
|
|
96
|
-
|
|
124
|
+
emitRay( gx, gy );
|
|
97
125
|
|
|
98
|
-
|
|
126
|
+
} );
|
|
99
127
|
|
|
100
|
-
}
|
|
128
|
+
}
|
|
101
129
|
|
|
102
130
|
} );
|
|
103
131
|
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
If, Loop, normalize, max, exp, log, clamp, dot, length, select,
|
|
11
11
|
instanceIndex,
|
|
12
12
|
sampler,
|
|
13
|
-
atomicAdd, atomicLoad, uintBitsToFloat,
|
|
13
|
+
atomicAdd, atomicLoad, atomicStore, uintBitsToFloat,
|
|
14
14
|
Return,
|
|
15
15
|
} from 'three/tsl';
|
|
16
16
|
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
readHitDistance, readHitBarycentrics, readHitNormal,
|
|
55
55
|
readHitMaterialIndex, readHitTriangleIndex,
|
|
56
56
|
writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
|
|
57
|
-
writeGBuffer, readGBuffer, gbDecodeNormalDepth,
|
|
57
|
+
writeGBuffer, readGBuffer, gbDecodeNormalDepth, gbDecodeAlbedo,
|
|
58
58
|
readRayRadiance,
|
|
59
59
|
} from '../Processor/PackedRayBuffer.js';
|
|
60
60
|
|
|
@@ -137,6 +137,19 @@ export function buildShadeKernel( params ) {
|
|
|
137
137
|
|
|
138
138
|
const threadIdx = instanceIndex;
|
|
139
139
|
|
|
140
|
+
// Folds the former resetActiveCounter 1-thread kernel: thread 0 zeroes the survivor counter
|
|
141
|
+
// before compact re-counts it. Kept ABOVE the ENTERING_COUNT guard so it fires even at bound 0.
|
|
142
|
+
// Safe: shade never touches ACTIVE_RAY_COUNT, and the shade→compact dispatch boundary publishes it.
|
|
143
|
+
if ( counters ) {
|
|
144
|
+
|
|
145
|
+
If( threadIdx.equal( uint( 0 ) ), () => {
|
|
146
|
+
|
|
147
|
+
atomicStore( counters.element( uint( COUNTER.ACTIVE_RAY_COUNT ) ), uint( 0 ) );
|
|
148
|
+
|
|
149
|
+
} );
|
|
150
|
+
|
|
151
|
+
}
|
|
152
|
+
|
|
140
153
|
// bound on ENTERING_COUNT so an over-sized margin dispatch is safe
|
|
141
154
|
const bound = counters ? atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) : maxRayCount;
|
|
142
155
|
If( threadIdx.greaterThanEqual( bound ), () => {
|
|
@@ -320,6 +333,11 @@ export function buildShadeKernel( params ) {
|
|
|
320
333
|
const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
|
|
321
334
|
const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
|
|
322
335
|
|
|
336
|
+
// Fix ①: OIDN clean-aux for a redirected ray that escapes to the environment (e.g. smooth
|
|
337
|
+
// glass over sky). Holds the clamped env colour this ray sees; written into the aux G-buffer
|
|
338
|
+
// before the miss Return below, but only when the pixel still carries the black bounce-0 default.
|
|
339
|
+
const escapedAuxAlbedo = vec3( 0.0 ).toVar();
|
|
340
|
+
|
|
323
341
|
If( wantBackdrop.or( wantEnvLight ), () => {
|
|
324
342
|
|
|
325
343
|
// Ground projection bends the backdrop-view env lookup onto a projected sphere+disk so the lower
|
|
@@ -358,6 +376,10 @@ export function buildShadeKernel( params ) {
|
|
|
358
376
|
|
|
359
377
|
} );
|
|
360
378
|
|
|
379
|
+
// Fix ①: remember the (clamped) env colour this escaped ray sees, so a redirected ray
|
|
380
|
+
// that never captured a surface aux can feed OIDN a meaningful albedo below.
|
|
381
|
+
escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
382
|
+
|
|
361
383
|
// MIS weight for implicit env hit — prevents double-counting with NEE
|
|
362
384
|
const envMisWeight = float( 1.0 ).toVar();
|
|
363
385
|
If( isBackdropView.not().and( useEnvMapIS ), () => {
|
|
@@ -425,6 +447,25 @@ export function buildShadeKernel( params ) {
|
|
|
425
447
|
|
|
426
448
|
} );
|
|
427
449
|
|
|
450
|
+
// Fix ①: fill the black bounce-0 default aux for a redirected ray that reached the
|
|
451
|
+
// environment without ever locking a surface guide (smooth glass / specular transmission
|
|
452
|
+
// over sky). Only when the stored albedo is still the black default (dot < 1e-4), so a real
|
|
453
|
+
// specular-surface aux written upstream is never clobbered. Normal = the escaped ray
|
|
454
|
+
// direction (varies per pixel → OIDN sees structure that tracks the refracted view);
|
|
455
|
+
// albedo = the clamped env colour it sees; depth kept at the primary hit.
|
|
456
|
+
If( auxOn
|
|
457
|
+
.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
|
|
458
|
+
.and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
|
|
459
|
+
|
|
460
|
+
const gPrev = readGBuffer( gBufferRW, pixelIndex );
|
|
461
|
+
If( dot( gbDecodeAlbedo( gPrev ), vec3( 1.0 ) ).lessThan( float( 1e-4 ) ), () => {
|
|
462
|
+
|
|
463
|
+
writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, escapedAuxAlbedo );
|
|
464
|
+
|
|
465
|
+
} );
|
|
466
|
+
|
|
467
|
+
} );
|
|
468
|
+
|
|
428
469
|
writeRayRadiance( rayBufferRW, rayID, currentRadiance );
|
|
429
470
|
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
430
471
|
Return();
|
|
@@ -607,7 +648,23 @@ export function buildShadeKernel( params ) {
|
|
|
607
648
|
// (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
|
|
608
649
|
// block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
|
|
609
650
|
// (objectNormal/objectColor stay at their init for transmissive-then-miss).
|
|
610
|
-
|
|
651
|
+
//
|
|
652
|
+
// Fix ③: BLEND (semi-transparent) glass instead LOCKS its OWN normal + albedo here. Its per-frame
|
|
653
|
+
// stochastic alpha-skip-vs-opaque-shade outcome (MaterialTransmission.js:522-540) otherwise writes a
|
|
654
|
+
// DIFFERENT surface into the aux each frame (wall-behind on skip vs glass on opaque); with the normal
|
|
655
|
+
// not temporally averaged that becomes the speckle OIDN smears on — the visible artefact on
|
|
656
|
+
// alpha-transparent glass. A deterministic glass-surface guide every frame removes it. MASK / opaque
|
|
657
|
+
// materials keep the default + rely on the downstream aux-extend.
|
|
658
|
+
If( material.alphaMode.equal( int( 2 ) ), () => {
|
|
659
|
+
|
|
660
|
+
writeGBuffer( gBufferRW, pixelIndex, N, linearDepth, albedo.xyz );
|
|
661
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
662
|
+
|
|
663
|
+
} ).Else( () => {
|
|
664
|
+
|
|
665
|
+
writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
|
|
666
|
+
|
|
667
|
+
} );
|
|
611
668
|
|
|
612
669
|
} );
|
|
613
670
|
|
|
@@ -744,6 +801,26 @@ export function buildShadeKernel( params ) {
|
|
|
744
801
|
|
|
745
802
|
throughput.mulAssign( interaction.throughput );
|
|
746
803
|
|
|
804
|
+
// Fix ②: OIDN clean-aux for STOCHASTIC glass (rough / frosted / dispersive), whose refracted
|
|
805
|
+
// ray hits a different behind-surface every frame. Extending aux through it (the aux-extend on
|
|
806
|
+
// the opaque-hit path) captures that per-frame surface — albedo then temporally averaged, normal
|
|
807
|
+
// point-sampled — an inconsistent guide OIDN smears on. Instead lock the glass surface's OWN
|
|
808
|
+
// normal + tint here (deterministic, spatially coherent). Smooth non-dispersive glass is left
|
|
809
|
+
// alone: its refraction is a per-pixel delta, so the behind-surface aux captured downstream is
|
|
810
|
+
// already stable. Gated to a genuine dielectric event (not alpha-cutout passthrough or SSS).
|
|
811
|
+
If( auxOn
|
|
812
|
+
.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
|
|
813
|
+
.and( interaction.isTransmissive )
|
|
814
|
+
.and( interaction.isAlphaSkip.not() )
|
|
815
|
+
.and( interaction.isSubsurface.not() )
|
|
816
|
+
.and( material.roughness.greaterThan( 0.05 ).or( material.dispersion.greaterThan( 0.0 ) ) ), () => {
|
|
817
|
+
|
|
818
|
+
const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
|
|
819
|
+
writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, albedo.xyz );
|
|
820
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
821
|
+
|
|
822
|
+
} );
|
|
823
|
+
|
|
747
824
|
// reflection stays on same side, transmission pushes through
|
|
748
825
|
const reflectOffsetDir = select( interaction.entering, N, N.negate() );
|
|
749
826
|
const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
|
|
@@ -368,6 +368,10 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
368
368
|
// MRT read-targets directly). When none are active the wavefront skips those writes entirely.
|
|
369
369
|
s.pathTracer?.setAuxGBufferEnabled?.( normalNeeded || !! this.denoiser?.enabled );
|
|
370
370
|
|
|
371
|
+
// Clean-aux normal: temporally accumulate the aux normal only for OIDN clean-aux models
|
|
372
|
+
// (balanced/high). 'fast' and the real-time denoisers keep the point-sampled bump normal.
|
|
373
|
+
s.pathTracer?.setCleanAuxNormal?.( !! this.denoiser?.enabled && this.denoiser?.quality !== 'fast' );
|
|
374
|
+
|
|
371
375
|
// Reclaim VRAM: free the big 2048² StorageTextures of any denoiser/G-buffer stage that ended up
|
|
372
376
|
// disabled (lazily re-created on the next dispatch after re-enable). Every strategy/denoiser
|
|
373
377
|
// toggle funnels through here after the enabled flags above are settled, so this is the one
|
|
@@ -610,6 +614,8 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
610
614
|
setOIDNQuality( quality ) {
|
|
611
615
|
|
|
612
616
|
this.denoiser?.updateQuality( quality );
|
|
617
|
+
// Clean-aux normal follows the model: balanced/high (clean-aux models) → accumulate; fast → keep bump.
|
|
618
|
+
this._stages.pathTracer?.setCleanAuxNormal?.( !! this.denoiser?.enabled && quality !== 'fast' );
|
|
613
619
|
|
|
614
620
|
}
|
|
615
621
|
|
|
@@ -167,6 +167,21 @@ export class UniformManager {
|
|
|
167
167
|
u( 'visMode', DEFAULT_STATE.debugMode, 'int' );
|
|
168
168
|
u( 'debugVisScale', DEFAULT_STATE.debugVisScale, 'float' );
|
|
169
169
|
|
|
170
|
+
// Tier-1 convergence early-stop (FinalWrite reads these; live-toggled, no shader rebuild)
|
|
171
|
+
ub( 'useAdaptiveSampling', DEFAULT_STATE.useAdaptiveSampling );
|
|
172
|
+
u( 'noiseThreshold', DEFAULT_STATE.noiseThreshold, 'float' );
|
|
173
|
+
u( 'darkNoiseFloor', DEFAULT_STATE.darkNoiseFloor, 'float' );
|
|
174
|
+
u( 'adaptiveMinSamples', DEFAULT_STATE.adaptiveMinSamples, 'int' );
|
|
175
|
+
// CPU-only (read in PathTracer._isConvergedComplete, not bound to any kernel); registered here for
|
|
176
|
+
// the settings/configureForMode plumbing + the _defineUniformGetters accessor.
|
|
177
|
+
u( 'adaptiveStopFraction', DEFAULT_STATE.adaptiveStopFraction, 'float' );
|
|
178
|
+
|
|
179
|
+
// Tier-2 per-pixel freeze. usePixelFreeze gates both FinalWrite (stamp + pass-through) and render()'s
|
|
180
|
+
// frozen-compaction dispatch path.
|
|
181
|
+
ub( 'usePixelFreeze', DEFAULT_STATE.usePixelFreeze );
|
|
182
|
+
u( 'pixelFreezeThreshold', DEFAULT_STATE.pixelFreezeThreshold, 'float' );
|
|
183
|
+
u( 'pixelFreezeStability', DEFAULT_STATE.pixelFreezeStability, 'int' );
|
|
184
|
+
|
|
170
185
|
// Accumulation
|
|
171
186
|
ub( 'enableAccumulation', true );
|
|
172
187
|
u( 'accumulationAlpha', 0.0, 'float' );
|