rayzee 7.15.2 → 7.17.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 +2400 -2215
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +55 -55
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +20 -5
- package/src/PathTracerApp.js +108 -6
- package/src/Processor/PackedRayBuffer.js +49 -23
- package/src/RenderSettings.js +4 -2
- package/src/Stages/ASVGF.js +38 -0
- package/src/Stages/BilateralFilter.js +23 -0
- package/src/Stages/EdgeFilter.js +18 -0
- package/src/Stages/MotionVector.js +23 -0
- package/src/Stages/NormalDepth.js +22 -0
- package/src/Stages/PathTracer.js +310 -166
- package/src/Stages/Variance.js +23 -0
- package/src/TSL/FinalWriteKernel.js +19 -12
- package/src/TSL/GenerateKernel.js +28 -17
- package/src/TSL/ShadeKernel.js +155 -78
- package/src/managers/CameraManager.js +160 -9
package/src/Stages/Variance.js
CHANGED
|
@@ -404,6 +404,29 @@ export class Variance extends RenderStage {
|
|
|
404
404
|
|
|
405
405
|
}
|
|
406
406
|
|
|
407
|
+
// Reserved-storage change (e.g. 4K toggle): recreate the MAX-preallocated StorageTextures at the new live
|
|
408
|
+
// reserved size + rebuild the compute nodes in place. Same stage object, so refs/wiring stay valid.
|
|
409
|
+
reallocateReservedStorage() {
|
|
410
|
+
|
|
411
|
+
this._computeNodeA?.dispose();
|
|
412
|
+
this._computeNodeB?.dispose();
|
|
413
|
+
this._storageTexA?.dispose();
|
|
414
|
+
this._storageTexB?.dispose();
|
|
415
|
+
const mk = () => {
|
|
416
|
+
|
|
417
|
+
const t = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
|
|
418
|
+
t.type = FloatType; t.format = RGBAFormat; t.minFilter = LinearFilter; t.magFilter = LinearFilter;
|
|
419
|
+
return t;
|
|
420
|
+
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
this._storageTexA = mk();
|
|
424
|
+
this._storageTexB = mk();
|
|
425
|
+
this._buildCompute();
|
|
426
|
+
this._compiled = false;
|
|
427
|
+
|
|
428
|
+
}
|
|
429
|
+
|
|
407
430
|
dispose() {
|
|
408
431
|
|
|
409
432
|
this._computeNodeA?.dispose();
|
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* FinalWriteKernel.js — wavefront final output: temporal accumulation + MRT StorageTexture writes (16×16, 2D).
|
|
3
|
+
*
|
|
4
|
+
* Chunked path pool (docs/internal/specs/wavefront-chunked-pool.md): runs 2D over one row band
|
|
5
|
+
* (renderWidth × chunkRows). A thread's band-local row `localGy` maps to global row `gy = chunkRowBase + localGy`.
|
|
6
|
+
* Path state + the per-chunk G-buffer are read at the LOCAL slot `rayID = localGy·W + gx`; the persistent
|
|
7
|
+
* per-pixel buffers (m2/streak/frozenMask) and the output texture use the GLOBAL pixel `p = gy·W + gx`.
|
|
3
8
|
*/
|
|
4
9
|
|
|
5
10
|
import {
|
|
@@ -35,7 +40,8 @@ export function buildFinalWriteKernel( params ) {
|
|
|
35
40
|
enableAccumulation, hasPreviousAccumulated, accumulationAlpha, cameraIsMoving,
|
|
36
41
|
transparentBackground,
|
|
37
42
|
prevAccumTexture, prevAlbedoTexture, prevNormalDepthTexture,
|
|
38
|
-
renderWidth,
|
|
43
|
+
renderWidth,
|
|
44
|
+
chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
|
|
39
45
|
visMode,
|
|
40
46
|
// Aux MRT (normalDepth + albedo) feeds only the denoiser/OIDN. Gated by a live uniform (1 = denoiser
|
|
41
47
|
// on): when off, skip the G-buffer decode, the prev-frame aux mix, and the two aux stores.
|
|
@@ -59,12 +65,13 @@ export function buildFinalWriteKernel( params ) {
|
|
|
59
65
|
const computeFn = Fn( () => {
|
|
60
66
|
|
|
61
67
|
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
62
|
-
const
|
|
68
|
+
const localGy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
63
69
|
|
|
64
|
-
If( gx.lessThan( renderWidth ).and(
|
|
70
|
+
If( gx.lessThan( renderWidth ).and( localGy.lessThan( chunkRows ) ), () => {
|
|
65
71
|
|
|
66
|
-
const
|
|
67
|
-
const rayID = uint(
|
|
72
|
+
const gy = localGy.add( chunkRowBase ); // GLOBAL row
|
|
73
|
+
const rayID = uint( localGy.mul( renderWidth ).add( gx ) ); // LOCAL path/gBuffer slot
|
|
74
|
+
const pixelId = uint( gy.mul( renderWidth ).add( gx ) ); // GLOBAL pixel (persistent buffers + texture)
|
|
68
75
|
|
|
69
76
|
const sampleColor = readRayRadiance( rayBufferRO, rayID );
|
|
70
77
|
const finalColor = sampleColor.xyz.toVar();
|
|
@@ -73,9 +80,9 @@ export function buildFinalWriteKernel( params ) {
|
|
|
73
80
|
// A pixel frozen in a prior frame was skipped by Generate, so its rayBuffer sample is stale — pass
|
|
74
81
|
// the accumulated colour through unchanged (below) instead of mixing it. Inert unless adaptiveOn.
|
|
75
82
|
const wasFrozen = adaptiveOn.and( frame.greaterThan( uint( 0 ) ) )
|
|
76
|
-
.and( frozenMaskRO.element(
|
|
83
|
+
.and( frozenMaskRO.element( pixelId ).equal( uint( 1 ) ) ).toVar();
|
|
77
84
|
|
|
78
|
-
// MRT comes from the per-
|
|
85
|
+
// MRT comes from the per-chunk G-buffer (LOCAL slot). Half-packed: decode.
|
|
79
86
|
// auxOn gates the decode + stores so a no-denoiser frame does no G-buffer read and no aux writes.
|
|
80
87
|
const finalNormalDepth = vec4( 0.0 ).toVar();
|
|
81
88
|
const finalAlbedo = vec4( 0.0 ).xyz.toVar();
|
|
@@ -140,9 +147,9 @@ export function buildFinalWriteKernel( params ) {
|
|
|
140
147
|
If( convOn, () => {
|
|
141
148
|
|
|
142
149
|
const sampleLum = luminance( sampleColor.xyz );
|
|
143
|
-
const prevM2 = m2BufferRW.element(
|
|
150
|
+
const prevM2 = m2BufferRW.element( pixelId ).toVar();
|
|
144
151
|
const m2 = mix( prevM2, sampleLum.mul( sampleLum ), accumulationAlpha ).toVar();
|
|
145
|
-
m2BufferRW.element(
|
|
152
|
+
m2BufferRW.element( pixelId ).assign( m2 );
|
|
146
153
|
|
|
147
154
|
const meanLum = luminance( finalColor ).toVar();
|
|
148
155
|
const sampleVar = m2.sub( meanLum.mul( meanLum ) ).max( float( 0 ) );
|
|
@@ -168,7 +175,7 @@ export function buildFinalWriteKernel( params ) {
|
|
|
168
175
|
If( frame.equal( uint( 0 ) ), () => {
|
|
169
176
|
|
|
170
177
|
// Frame 0 (post-reset/camera-move) clears the frozen state so each run re-freezes fresh.
|
|
171
|
-
streakBufferRW.element(
|
|
178
|
+
streakBufferRW.element( pixelId ).assign( uint( 0 ) );
|
|
172
179
|
|
|
173
180
|
} );
|
|
174
181
|
|
|
@@ -183,8 +190,8 @@ export function buildFinalWriteKernel( params ) {
|
|
|
183
190
|
|
|
184
191
|
const freezeCandidate = frame.greaterThanEqual( uint( adaptiveMinSamples ) )
|
|
185
192
|
.and( relErr.lessThan( pixelFreezeThreshold ) );
|
|
186
|
-
const newStreak = select( freezeCandidate, streakBufferRW.element(
|
|
187
|
-
streakBufferRW.element(
|
|
193
|
+
const newStreak = select( freezeCandidate, streakBufferRW.element( pixelId ).add( uint( 1 ) ), uint( 0 ) ).toVar();
|
|
194
|
+
streakBufferRW.element( pixelId ).assign( newStreak );
|
|
188
195
|
// Just reached K → frozen next frame; count it now so the frozen population is current.
|
|
189
196
|
If( newStreak.greaterThanEqual( uint( pixelFreezeStability ) ), () => {
|
|
190
197
|
|
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
* GenerateKernel.js — wavefront primary ray generation.
|
|
3
3
|
* Two dispatch modes from one builder: 2D screen-space (default) and 1D list-driven over the active-pixel
|
|
4
4
|
* list (Tier-2 per-pixel freeze).
|
|
5
|
+
*
|
|
6
|
+
* Chunked path pool (docs/internal/specs/wavefront-chunked-pool.md): the image is streamed through a fixed
|
|
7
|
+
* device-budget pool in row bands. A kernel thread maps to a LOCAL path slot `rayID` (∈ [0,B)); the global
|
|
8
|
+
* pixel is `pixelBase + rayID` where `pixelBase = chunkRowBase · renderWidth`. Path state / gBuffer are
|
|
9
|
+
* written at the LOCAL slot; camera ray + RNG seed use the GLOBAL pixel coord (so a pixel's sample sequence
|
|
10
|
+
* is stable across chunks and frames).
|
|
5
11
|
*/
|
|
6
12
|
|
|
7
13
|
import {
|
|
@@ -22,7 +28,7 @@ import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
|
|
|
22
28
|
import {
|
|
23
29
|
writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf,
|
|
24
30
|
writeRayRadiance, writeGBuffer,
|
|
25
|
-
writeMediumStack,
|
|
31
|
+
writeMediumStack, writeFeatureThroughput,
|
|
26
32
|
} from '../Processor/PackedRayBuffer.js';
|
|
27
33
|
|
|
28
34
|
const WG_SIZE = 16;
|
|
@@ -34,23 +40,21 @@ export function buildGenerateKernel( params ) {
|
|
|
34
40
|
resolution, frame,
|
|
35
41
|
cameraWorldMatrix, cameraProjectionMatrixInverse,
|
|
36
42
|
enableDOF, focalLength, aperture, focusDistance, sceneScale, apertureScale, anamorphicRatio,
|
|
37
|
-
renderWidth,
|
|
43
|
+
renderWidth,
|
|
44
|
+
chunkRowBase, chunkRows, // row band offset (global first row) + row count for this chunk
|
|
38
45
|
transmissiveBounces, // per-ray refraction budget (megakernel parity: PathTracerCore.js:606)
|
|
39
46
|
transparentBackground, // alpha inits to 1 here (megakernel parity: PathTracerCore.js:554) — env-escape-without-opaque zeroes it in Shade
|
|
40
47
|
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] =
|
|
48
|
+
// listDriven: 1D dispatch over the active-pixel list (activeIndicesRO[tid] = LOCAL slot) instead of 2D.
|
|
42
49
|
listDriven = false, activeIndicesRO = null, counters = null,
|
|
43
50
|
} = params;
|
|
44
51
|
|
|
45
52
|
const auxOn = auxGBufferEnabled.greaterThan( uint( 0 ) );
|
|
46
53
|
|
|
47
|
-
// Shared ray-gen body
|
|
48
|
-
const emitRay = ( gx, gy ) => {
|
|
54
|
+
// Shared ray-gen body. (gx, gy) is the GLOBAL pixel; rayID is the LOCAL path-slot (both dispatch modes).
|
|
55
|
+
const emitRay = ( gx, gy, rayID ) => {
|
|
49
56
|
|
|
50
57
|
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
58
|
|
|
55
59
|
const screenPosition = pixelCoord.div( resolution ).mul( 2.0 ).sub( 1.0 ).toVar();
|
|
56
60
|
screenPosition.y.assign( screenPosition.y.negate() );
|
|
@@ -88,8 +92,10 @@ export function buildGenerateKernel( params ) {
|
|
|
88
92
|
|
|
89
93
|
If( auxOn, () => {
|
|
90
94
|
|
|
91
|
-
// default: normal +Z, depth 1 (far), black albedo (background/miss)
|
|
92
|
-
writeGBuffer( gBufferRW,
|
|
95
|
+
// default: normal +Z, depth 1 (far), black albedo (background/miss). Per-CHUNK G-buffer → LOCAL slot.
|
|
96
|
+
writeGBuffer( gBufferRW, rayID, vec3( 0.0, 0.0, 1.0 ), float( 1.0 ), vec3( 0.0 ) );
|
|
97
|
+
// DDFA: seed the see-through aux tint to white (no tint yet). RMW preserves slot-5 xyz (sigmaA).
|
|
98
|
+
writeFeatureThroughput( rayBufferRW, rayID, vec3( 1.0 ) );
|
|
93
99
|
|
|
94
100
|
} );
|
|
95
101
|
|
|
@@ -107,21 +113,26 @@ export function buildGenerateKernel( params ) {
|
|
|
107
113
|
// Grid is over-sized from a stale readback; bound on the live ENTERING_COUNT so extra threads no-op.
|
|
108
114
|
If( tid.lessThan( atomicLoad( counters.element( uint( COUNTER.ENTERING_COUNT ) ) ) ), () => {
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
116
|
+
// active list holds LOCAL slots; global pixel = pixelBase + local slot.
|
|
117
|
+
const localSlot = int( activeIndicesRO.element( tid ) );
|
|
118
|
+
const globalPixel = localSlot.add( chunkRowBase.mul( renderWidth ) );
|
|
119
|
+
const gx = globalPixel.mod( renderWidth );
|
|
120
|
+
const gy = globalPixel.div( renderWidth );
|
|
121
|
+
emitRay( gx, gy, uint( localSlot ) );
|
|
114
122
|
|
|
115
123
|
} );
|
|
116
124
|
|
|
117
125
|
} else {
|
|
118
126
|
|
|
127
|
+
// 2D grid over (renderWidth × chunkRows). local_gy is the band-local row; global gy adds chunkRowBase.
|
|
119
128
|
const gx = int( workgroupId.x ).mul( WG_SIZE ).add( int( localId.x ) );
|
|
120
|
-
const
|
|
129
|
+
const localGy = int( workgroupId.y ).mul( WG_SIZE ).add( int( localId.y ) );
|
|
121
130
|
|
|
122
|
-
If( gx.lessThan( renderWidth ).and(
|
|
131
|
+
If( gx.lessThan( renderWidth ).and( localGy.lessThan( chunkRows ) ), () => {
|
|
123
132
|
|
|
124
|
-
|
|
133
|
+
const gy = localGy.add( chunkRowBase );
|
|
134
|
+
const localRayID = uint( localGy.mul( renderWidth ).add( gx ) );
|
|
135
|
+
emitRay( gx, gy, localRayID );
|
|
125
136
|
|
|
126
137
|
} );
|
|
127
138
|
|
package/src/TSL/ShadeKernel.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import {
|
|
8
8
|
Fn, float, vec2, vec3, vec4, int, uint,
|
|
9
9
|
bool as tslBool,
|
|
10
|
-
If, Loop, normalize, max, exp, log, clamp, dot, length, select,
|
|
10
|
+
If, Loop, normalize, max, exp, log, clamp, dot, length, select, smoothstep, mix,
|
|
11
11
|
instanceIndex,
|
|
12
12
|
sampler,
|
|
13
13
|
atomicAdd, atomicLoad, atomicStore, uintBitsToFloat,
|
|
@@ -27,7 +27,8 @@ import { sampleChromaticCollision, sampleHenyeyGreenstein, subsurfaceCoefficient
|
|
|
27
27
|
import { calculateIndirectLighting } from './LightsIndirect.js';
|
|
28
28
|
import { IndirectLightingResult, sampleCone } from './LightsCore.js';
|
|
29
29
|
import { regularizePathContribution, generateSampledDirection, computeNDCDepth, handleRussianRoulette } from './PathTracerCore.js';
|
|
30
|
-
import { getImportanceSamplingInfo } from './MaterialProperties.js';
|
|
30
|
+
import { getImportanceSamplingInfo, evaluateDFG } from './MaterialProperties.js';
|
|
31
|
+
import { dielectricF0 } from './Fresnel.js';
|
|
31
32
|
import { sampleClearcoat, ClearcoatResult } from './Clearcoat.js';
|
|
32
33
|
import { refineDisplacedIntersection, DisplacementResult } from './Displacement.js';
|
|
33
34
|
import { calculateEmissiveTriangleContribution, calculateEmissiveLightPdf, EmissiveSample } from './EmissiveSampling.js';
|
|
@@ -44,6 +45,7 @@ import {
|
|
|
44
45
|
BRDFWeights,
|
|
45
46
|
MaterialCache,
|
|
46
47
|
DirectLightingDual,
|
|
48
|
+
DFGResult,
|
|
47
49
|
} from './Struct.js';
|
|
48
50
|
import { RandomValue, getRandomSample } from './Random.js';
|
|
49
51
|
import { RAY_FLAG, COUNTER } from '../Processor/QueueManager.js';
|
|
@@ -54,8 +56,9 @@ import {
|
|
|
54
56
|
readHitDistance, readHitBarycentrics, readHitNormal,
|
|
55
57
|
readHitMaterialIndex, readHitTriangleIndex,
|
|
56
58
|
writeRayOriginMeta, writeRayDirFlags, writeRayThroughputPdf, writeRayRadiance,
|
|
57
|
-
writeGBuffer, readGBuffer, gbDecodeNormalDepth,
|
|
59
|
+
writeGBuffer, readGBuffer, gbDecodeNormalDepth,
|
|
58
60
|
readRayRadiance,
|
|
61
|
+
readFeatureThroughput, writeFeatureThroughput,
|
|
59
62
|
} from '../Processor/PackedRayBuffer.js';
|
|
60
63
|
|
|
61
64
|
const WG_SIZE = 256;
|
|
@@ -90,6 +93,7 @@ export function buildShadeKernel( params ) {
|
|
|
90
93
|
globalIlluminationIntensity,
|
|
91
94
|
cameraProjectionMatrix, cameraViewMatrix,
|
|
92
95
|
fireflyThreshold, frame, resolution,
|
|
96
|
+
chunkRowBase, // chunked path pool: global pixel = chunkRowBase·W + localSlot (rayID). See spec.
|
|
93
97
|
emissiveTriangleCount, emissiveVec4Offset, emissiveTotalPower,
|
|
94
98
|
emissiveBoost, totalTriangleCount, enableEmissiveTriangleSampling,
|
|
95
99
|
lightBVHNodeCount, reverseMapVec4Offset,
|
|
@@ -183,6 +187,11 @@ export function buildShadeKernel( params ) {
|
|
|
183
187
|
const pixelIndex = rayID;
|
|
184
188
|
const rngState = rngBufferRW.element( rayID ).toVar();
|
|
185
189
|
|
|
190
|
+
// DDFA see-through aux tint carried across smooth glass/mirror; committed into the OIDN/ASVGF
|
|
191
|
+
// albedo guide at the first diffuse-enough surface (or env). Mutated locally, persisted on every
|
|
192
|
+
// deferring continue (a missed persist = stale tint). 1 = untinted (direct hit → today's albedo).
|
|
193
|
+
const featCarry = readFeatureThroughput( rayBufferRW, rayID ).toVar();
|
|
194
|
+
|
|
186
195
|
const hitDist = readHitDistance( hitBufferRO, rayID ).toVar();
|
|
187
196
|
const hitNormal = readHitNormal( hitBufferRO, rayID ).toVar();
|
|
188
197
|
// hitInfo.uv is the interpolated texture UV (not barycentrics)
|
|
@@ -309,7 +318,8 @@ export function buildShadeKernel( params ) {
|
|
|
309
318
|
} );
|
|
310
319
|
|
|
311
320
|
writeRayRadiance( rayBufferRW, rayID, vec4( outRgb, outAlpha ) );
|
|
312
|
-
|
|
321
|
+
// DDFA: the plane's aux (written above) is a committed surface — lock it (cosmetic; ray dies here).
|
|
322
|
+
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ).bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
313
323
|
rngBufferRW.element( rayID ).assign( rngState );
|
|
314
324
|
Return();
|
|
315
325
|
|
|
@@ -333,9 +343,8 @@ export function buildShadeKernel( params ) {
|
|
|
333
343
|
const wantBackdrop = isBackdropView.and( showBackground ); // draw env image as backdrop
|
|
334
344
|
const wantEnvLight = isBackdropView.not().and( enableEnvironmentLight ); // env as light on redirected bounces
|
|
335
345
|
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
// before the miss Return below, but only when the pixel still carries the black bounce-0 default.
|
|
346
|
+
// DDFA: env colour a redirected ray escaping to the environment sees (e.g. smooth glass over
|
|
347
|
+
// sky). Committed (tinted by featCarry) into the aux G-buffer before the miss Return below.
|
|
339
348
|
const escapedAuxAlbedo = vec3( 0.0 ).toVar();
|
|
340
349
|
|
|
341
350
|
If( wantBackdrop.or( wantEnvLight ), () => {
|
|
@@ -376,8 +385,8 @@ export function buildShadeKernel( params ) {
|
|
|
376
385
|
|
|
377
386
|
} );
|
|
378
387
|
|
|
379
|
-
//
|
|
380
|
-
//
|
|
388
|
+
// DDFA: remember the (clamped) env colour this escaped ray sees, for the redirected-escape
|
|
389
|
+
// aux commit below (tinted by the accumulated see-through throughput).
|
|
381
390
|
escapedAuxAlbedo.assign( clamp( envColor, vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
382
391
|
|
|
383
392
|
// MIS weight for implicit env hit — prevents double-counting with NEE
|
|
@@ -447,22 +456,18 @@ export function buildShadeKernel( params ) {
|
|
|
447
456
|
|
|
448
457
|
} );
|
|
449
458
|
|
|
450
|
-
//
|
|
451
|
-
//
|
|
452
|
-
//
|
|
453
|
-
//
|
|
454
|
-
//
|
|
455
|
-
//
|
|
459
|
+
// DDFA: a redirected ray (specular chain — glass/mirror) that escaped to the environment without
|
|
460
|
+
// ever committing a surface guide commits the env colour it sees, tinted by the accumulated
|
|
461
|
+
// see-through throughput (red glass over sky → sky×red). Normal = the escaped direction (varies
|
|
462
|
+
// per pixel → OIDN sees structure tracking the refracted view); depth kept at the primary hit.
|
|
463
|
+
// AUX_LOCKED-clear alone means "never committed" — the old black-probe is redundant. A direct
|
|
464
|
+
// backdrop (REDIRECTED clear) keeps Generate's black/far default (regression-safe sky guide).
|
|
456
465
|
If( auxOn
|
|
457
466
|
.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) )
|
|
458
467
|
.and( flags.bitAnd( uint( RAY_FLAG.REDIRECTED ) ).notEqual( uint( 0 ) ) ), () => {
|
|
459
468
|
|
|
460
469
|
const gPrev = readGBuffer( gBufferRW, pixelIndex );
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, escapedAuxAlbedo );
|
|
464
|
-
|
|
465
|
-
} );
|
|
470
|
+
writeGBuffer( gBufferRW, pixelIndex, normalize( direction ), gbDecodeNormalDepth( gPrev ).w, clamp( escapedAuxAlbedo.mul( featCarry ), vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
466
471
|
|
|
467
472
|
} );
|
|
468
473
|
|
|
@@ -496,7 +501,14 @@ export function buildShadeKernel( params ) {
|
|
|
496
501
|
If( max( max( mSigmaS.x, mSigmaS.y ), mSigmaS.z ).lessThanEqual( 0.0 ), () => {
|
|
497
502
|
|
|
498
503
|
// glass: Beer-Lambert absorption
|
|
499
|
-
|
|
504
|
+
const beer = exp( mSigmaA.mul( hitDist ).negate() ).toVar();
|
|
505
|
+
throughput.mulAssign( beer );
|
|
506
|
+
// DDFA: colored-glass volume tints the deferred aux guide by the same absorption.
|
|
507
|
+
If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
|
|
508
|
+
|
|
509
|
+
featCarry.mulAssign( beer );
|
|
510
|
+
|
|
511
|
+
} );
|
|
500
512
|
|
|
501
513
|
} ).Else( () => {
|
|
502
514
|
|
|
@@ -585,6 +597,10 @@ export function buildShadeKernel( params ) {
|
|
|
585
597
|
material, samplingUV, N,
|
|
586
598
|
) ).toVar();
|
|
587
599
|
|
|
600
|
+
// DDFA: snapshot the UNCLAMPED roughness (before the min-0.05 clamp below) for the specular↔diffuse
|
|
601
|
+
// classification, so a perfect mirror reads 0 (fully specular → defers).
|
|
602
|
+
const rawRough = matSamples.roughness.toVar();
|
|
603
|
+
|
|
588
604
|
// BRDF functions read material.color/metalness/roughness, so apply samples here
|
|
589
605
|
material.color.assign( matSamples.albedo );
|
|
590
606
|
material.metalness.assign( matSamples.metalness.clamp( 0.0, 1.0 ) );
|
|
@@ -635,7 +651,24 @@ export function buildShadeKernel( params ) {
|
|
|
635
651
|
|
|
636
652
|
} );
|
|
637
653
|
|
|
638
|
-
//
|
|
654
|
+
// ─── DDFA classification (Cycles smoothstep-by-roughness, not a hard binary) ───
|
|
655
|
+
// nonspec fraction over a 2-pseudo-closure model: a Lambert closure (weight 1-metal, fully diffuse)
|
|
656
|
+
// + a glossy/glass closure (weight metal·(1-trans)+trans, classified by roughness). Commit the aux at
|
|
657
|
+
// the first surface with a meaningful diffuse response (nonspec≥0.25); smooth glass/mirror defers.
|
|
658
|
+
// featPrefix = the tint accumulated BEFORE this surface (read-only for this bounce's commits).
|
|
659
|
+
const metal = material.metalness.toVar();
|
|
660
|
+
const trans = material.transmission.toVar();
|
|
661
|
+
const roughFrac = smoothstep( float( 0.0 ), float( 0.15 ), rawRough ).toVar();
|
|
662
|
+
const auxCommit = clamp(
|
|
663
|
+
float( 1.0 ).sub( metal ).mul( float( 1.0 ).sub( trans ) )
|
|
664
|
+
.add( roughFrac.mul( metal.mul( float( 1.0 ).sub( trans ) ).add( trans ) ) ),
|
|
665
|
+
0.0, 1.0,
|
|
666
|
+
).greaterThanEqual( float( 0.25 ) ).toVar();
|
|
667
|
+
const featPrefix = featCarry.toVar();
|
|
668
|
+
|
|
669
|
+
// first-hit MRT data (bounce 0 only): write the primary DEPTH now with the default normal/albedo.
|
|
670
|
+
// The real normal/albedo are committed by the DDFA decision blocks below (which re-pack this depth);
|
|
671
|
+
// they may defer through smooth glass/mirror and commit at the first diffuse-enough surface or the env.
|
|
639
672
|
If( bounceIndex.equal( 0 ).and( auxOn ), () => {
|
|
640
673
|
|
|
641
674
|
const linearDepth = computeNDCDepth( {
|
|
@@ -643,28 +676,7 @@ export function buildShadeKernel( params ) {
|
|
|
643
676
|
cameraProjectionMatrix,
|
|
644
677
|
cameraViewMatrix,
|
|
645
678
|
} );
|
|
646
|
-
|
|
647
|
-
// Write the primary DEPTH now with the miss-default aux; the real normal/albedo are captured below
|
|
648
|
-
// (aux-extend) and may extend through specular surfaces (gap #9). Glass rays Return at the transparency
|
|
649
|
-
// block before that capture, so a glass-then-escape pixel keeps this default aux — megakernel parity
|
|
650
|
-
// (objectNormal/objectColor stay at their init for transmissive-then-miss).
|
|
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
|
-
} );
|
|
679
|
+
writeGBuffer( gBufferRW, pixelIndex, vec3( 0.0, 0.0, 1.0 ), linearDepth, vec3( 0.0 ) );
|
|
668
680
|
|
|
669
681
|
} );
|
|
670
682
|
|
|
@@ -698,6 +710,53 @@ export function buildShadeKernel( params ) {
|
|
|
698
710
|
// persist any wavelength locked on a fresh dispersive transmission; identity write otherwise
|
|
699
711
|
pathWavelength.assign( interaction.pathWavelength );
|
|
700
712
|
|
|
713
|
+
// ─── DDFA aux decision at a transmissive / alpha / SSS interaction. Runs for BOTH the continuing and
|
|
714
|
+
// the BLEND fall-through paths, so it sits before If(continueRay). If/ElseIf chaining so BLEND wins
|
|
715
|
+
// over the transmission branch (fixes the BLEND-glass per-frame flicker + the BLEND+transmission case). ───
|
|
716
|
+
If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
|
|
717
|
+
|
|
718
|
+
const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
|
|
719
|
+
// N here is the mapped normal — faceforward it (a ray exiting glass sees a back-facing N).
|
|
720
|
+
const Nff = select( dot( N, direction.negate() ).lessThan( 0.0 ), N.negate(), N ).toVar();
|
|
721
|
+
const auxAlbedo = clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ).toVar();
|
|
722
|
+
|
|
723
|
+
If( material.alphaMode.equal( int( 2 ) ), () => {
|
|
724
|
+
|
|
725
|
+
// BLEND: deterministic own-surface commit on BOTH stochastic skip + shade frames (no flicker).
|
|
726
|
+
writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
|
|
727
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
728
|
+
|
|
729
|
+
} ).ElseIf( interaction.isAlphaSkip, () => {
|
|
730
|
+
|
|
731
|
+
// MASK cutout passthrough: keep deferring, tint unchanged (backdrop through the hole commits later).
|
|
732
|
+
|
|
733
|
+
} ).ElseIf( interaction.isSubsurface, () => {
|
|
734
|
+
|
|
735
|
+
// SSS boundary: force-commit the skin surface as diffuse.
|
|
736
|
+
writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
|
|
737
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
738
|
+
|
|
739
|
+
} ).ElseIf( interaction.isTransmissive, () => {
|
|
740
|
+
|
|
741
|
+
If( auxCommit.or( material.dispersion.greaterThan( 0.0 ) ), () => {
|
|
742
|
+
|
|
743
|
+
// frosted / rough / dispersive glass: commit its own surface (deterministic per frame).
|
|
744
|
+
writeGBuffer( gBufferRW, pixelIndex, Nff, primaryDepth, auxAlbedo );
|
|
745
|
+
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
746
|
+
|
|
747
|
+
} ).Else( () => {
|
|
748
|
+
|
|
749
|
+
// smooth glass: DEFER — tint by the glass colour (matches the transmission throughput,
|
|
750
|
+
// which multiplies material.color at both reflect and refract; achromatic eta²/Fr excluded).
|
|
751
|
+
featCarry.mulAssign( clamp( albedo, vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
752
|
+
|
|
753
|
+
} );
|
|
754
|
+
|
|
755
|
+
} );
|
|
756
|
+
// else (opaque non-skip): the opaque decision block below handles it (needs the viewer-facing N).
|
|
757
|
+
|
|
758
|
+
} );
|
|
759
|
+
|
|
701
760
|
If( interaction.continueRay, () => {
|
|
702
761
|
|
|
703
762
|
// update medium stack for transmission (not reflection/TIR)
|
|
@@ -801,26 +860,6 @@ export function buildShadeKernel( params ) {
|
|
|
801
860
|
|
|
802
861
|
throughput.mulAssign( interaction.throughput );
|
|
803
862
|
|
|
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
|
-
|
|
824
863
|
// reflection stays on same side, transmission pushes through
|
|
825
864
|
const reflectOffsetDir = select( interaction.entering, N, N.negate() );
|
|
826
865
|
const offsetDir = select( interaction.didReflect, reflectOffsetDir, direction );
|
|
@@ -844,6 +883,13 @@ export function buildShadeKernel( params ) {
|
|
|
844
883
|
writeRayThroughputPdf( rayBufferRW, rayID, throughput, readRayPdf( rayBufferRW, rayID ) );
|
|
845
884
|
writeRayRadiance( rayBufferRW, rayID, currentRadiance );
|
|
846
885
|
writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
|
|
886
|
+
// DDFA: persist the (possibly tinted) see-through throughput for the next bounce. MUST run after
|
|
887
|
+
// the writeMediumSigmaA above — both RMW slot 5 (sigmaA.xyz / featTP.w). Gated AUX_LOCKED-clear.
|
|
888
|
+
If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
|
|
889
|
+
|
|
890
|
+
writeFeatureThroughput( rayBufferRW, rayID, featCarry );
|
|
891
|
+
|
|
892
|
+
} );
|
|
847
893
|
rngBufferRW.element( rayID ).assign( rngState );
|
|
848
894
|
Return();
|
|
849
895
|
|
|
@@ -930,22 +976,29 @@ export function buildShadeKernel( params ) {
|
|
|
930
976
|
|
|
931
977
|
} );
|
|
932
978
|
|
|
933
|
-
//
|
|
934
|
-
//
|
|
935
|
-
//
|
|
936
|
-
|
|
937
|
-
// stays at the primary hit (read back + re-packed; the snorm depth re-pack is idempotent — no drift).
|
|
938
|
-
If( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ).and( auxOn ), () => {
|
|
979
|
+
// ─── DDFA opaque aux decision: commit at the first diffuse-enough surface, defer through smooth
|
|
980
|
+
// mirror/metal so the guide describes what the mirror reflects, not the mirror itself. N is already
|
|
981
|
+
// viewer-facing (two-sided flip above); depth read back + re-packed (idempotent snorm — no drift). ───
|
|
982
|
+
If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
|
|
939
983
|
|
|
940
984
|
const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const auxIsMirror = material.metalness.greaterThan( 0.7 ).and( material.roughness.lessThan( 0.3 ) );
|
|
944
|
-
const auxIsTransmissive = material.transmission.greaterThan( 0.5 );
|
|
945
|
-
If( auxIsMirror.or( auxIsTransmissive ).not(), () => {
|
|
985
|
+
If( auxCommit, () => {
|
|
946
986
|
|
|
987
|
+
// diffuse / glossy / rough-metal: commit the surface guide, tinted by the see-through prefix.
|
|
988
|
+
writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
947
989
|
flags.assign( flags.bitOr( uint( RAY_FLAG.AUX_LOCKED ) ) );
|
|
948
990
|
|
|
991
|
+
} ).Else( () => {
|
|
992
|
+
|
|
993
|
+
// smooth mirror / metal: DEFER — tint by the specular directional albedo (metal colour),
|
|
994
|
+
// matching the opaque throughput (≈ F at the near-delta mirror lobe). Achromatic energy excluded.
|
|
995
|
+
const NoV = max( dot( N, V ), float( 1e-3 ) );
|
|
996
|
+
const F0 = clamp(
|
|
997
|
+
mix( dielectricF0( material.ior ).mul( material.specularColor ), albedo, material.metalness ).mul( material.specularIntensity ),
|
|
998
|
+
vec3( 0.0 ), vec3( 1.0 ),
|
|
999
|
+
);
|
|
1000
|
+
featCarry.mulAssign( DFGResult.wrap( evaluateDFG( F0, NoV, max( rawRough, float( 0.02 ) ) ) ).E_total );
|
|
1001
|
+
|
|
949
1002
|
} );
|
|
950
1003
|
|
|
951
1004
|
} );
|
|
@@ -955,11 +1008,14 @@ export function buildShadeKernel( params ) {
|
|
|
955
1008
|
material.clearcoat, material.emissive, material.subsurface,
|
|
956
1009
|
) ).toVar();
|
|
957
1010
|
|
|
958
|
-
// STBN keyed on (pixel, bounceIndex, frame).
|
|
1011
|
+
// STBN keyed on (GLOBAL pixel, bounceIndex, frame). pixelIndex is the LOCAL path slot; the global pixel
|
|
1012
|
+
// = chunkRowBase·W + localSlot, so the blue-noise pattern stays spatially aligned across row-band chunks
|
|
1013
|
+
// (and matches Generate's per-pixel RNG seed). chunkRowBase is 0 in the single-chunk case.
|
|
959
1014
|
const _resX = int( resolution.x ).toVar();
|
|
1015
|
+
const _globalPixel = int( pixelIndex ).add( chunkRowBase.mul( _resX ) );
|
|
960
1016
|
const _pixelCoord = vec2(
|
|
961
|
-
float(
|
|
962
|
-
float(
|
|
1017
|
+
float( _globalPixel.mod( _resX ) ).add( 0.5 ),
|
|
1018
|
+
float( _globalPixel.div( _resX ) ).add( 0.5 ),
|
|
963
1019
|
);
|
|
964
1020
|
const xi = getRandomSample( _pixelCoord, int( 0 ), bounceIndex, rngState, int( - 1 ), resolution, frame ).toVar();
|
|
965
1021
|
const emptyWeights = BRDFWeights( {
|
|
@@ -1174,6 +1230,14 @@ export function buildShadeKernel( params ) {
|
|
|
1174
1230
|
).toVar();
|
|
1175
1231
|
If( rrSurvival.lessThanEqual( 0.0 ), () => {
|
|
1176
1232
|
|
|
1233
|
+
// DDFA terminal fallback: a ray dying while still deferring (e.g. a mirror maze) commits its last
|
|
1234
|
+
// surface instead of leaving a black guide OIDN would demod-amplify. N is viewer-facing here.
|
|
1235
|
+
If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
|
|
1236
|
+
|
|
1237
|
+
const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
|
|
1238
|
+
writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
1239
|
+
|
|
1240
|
+
} );
|
|
1177
1241
|
writeRayRadiance( rayBufferRW, rayID, currentRadiance );
|
|
1178
1242
|
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
1179
1243
|
rngBufferRW.element( rayID ).assign( rngState );
|
|
@@ -1185,6 +1249,13 @@ export function buildShadeKernel( params ) {
|
|
|
1185
1249
|
// Terminate on CAMERA depth (opaque scatter count), not path length — glass/SSS free bounces no longer burn the maxBounces budget (gap #4).
|
|
1186
1250
|
If( cameraDepth.greaterThanEqual( maxBounceCount ), () => {
|
|
1187
1251
|
|
|
1252
|
+
// DDFA terminal fallback (see RR-kill above): commit the last surface if still deferring.
|
|
1253
|
+
If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
|
|
1254
|
+
|
|
1255
|
+
const primaryDepth = gbDecodeNormalDepth( readGBuffer( gBufferRW, pixelIndex ) ).w;
|
|
1256
|
+
writeGBuffer( gBufferRW, pixelIndex, N, primaryDepth, clamp( albedo.mul( featPrefix ), vec3( 0.0 ), vec3( 1.0 ) ) );
|
|
1257
|
+
|
|
1258
|
+
} );
|
|
1188
1259
|
writeRayRadiance( rayBufferRW, rayID, currentRadiance );
|
|
1189
1260
|
writeRayDirFlags( rayBufferRW, rayID, direction, flags.bitAnd( uint( ~ RAY_FLAG.ACTIVE ) ) );
|
|
1190
1261
|
rngBufferRW.element( rayID ).assign( rngState );
|
|
@@ -1200,6 +1271,12 @@ export function buildShadeKernel( params ) {
|
|
|
1200
1271
|
writeRayThroughputPdf( rayBufferRW, rayID, throughput, bouncePdf );
|
|
1201
1272
|
writeRayRadiance( rayBufferRW, rayID, currentRadiance );
|
|
1202
1273
|
writeMediumStack( rayBufferRW, rayID, uint( mediumStackDepth ), uint( transTraversals ), mediumStack_ior_1, mediumStack_ior_2, mediumStack_ior_3, uint( pathWavelength.add( 0.5 ) ) );
|
|
1274
|
+
// DDFA: persist the (possibly mirror-tinted) see-through throughput for the next bounce.
|
|
1275
|
+
If( auxOn.and( flags.bitAnd( uint( RAY_FLAG.AUX_LOCKED ) ).equal( uint( 0 ) ) ), () => {
|
|
1276
|
+
|
|
1277
|
+
writeFeatureThroughput( rayBufferRW, rayID, featCarry );
|
|
1278
|
+
|
|
1279
|
+
} );
|
|
1203
1280
|
rngBufferRW.element( rayID ).assign( rngState );
|
|
1204
1281
|
|
|
1205
1282
|
} );
|