rayzee 6.4.0 → 7.0.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 +24 -5
- package/dist/rayzee.es.js +4953 -4225
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +157 -236
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +29 -13
- package/src/PathTracerApp.js +119 -26
- package/src/Pipeline/PipelineContext.js +1 -2
- package/src/Pipeline/RenderPipeline.js +1 -1
- package/src/Pipeline/RenderStage.js +1 -1
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/GeometryExtractor.js +22 -1
- package/src/Processor/KernelManager.js +277 -0
- package/src/Processor/PackedRayBuffer.js +265 -0
- package/src/Processor/QueueManager.js +173 -0
- package/src/Processor/SceneProcessor.js +1 -0
- package/src/Processor/ShaderBuilder.js +11 -316
- package/src/Processor/StorageTexturePool.js +29 -15
- package/src/Processor/TextureCreator.js +6 -0
- package/src/Processor/VRAMTracker.js +169 -0
- package/src/Processor/utils.js +11 -110
- package/src/RenderSettings.js +1 -3
- package/src/Stages/ASVGF.js +76 -20
- package/src/Stages/BilateralFilter.js +34 -10
- package/src/Stages/EdgeFilter.js +2 -3
- package/src/Stages/MotionVector.js +16 -9
- package/src/Stages/NormalDepth.js +17 -5
- package/src/Stages/PathTracer.js +671 -1456
- package/src/Stages/PathTracerStage.js +1451 -0
- package/src/Stages/SSRC.js +32 -15
- package/src/Stages/Variance.js +35 -12
- package/src/TSL/BVHTraversal.js +7 -1
- package/src/TSL/Common.js +12 -2
- package/src/TSL/CompactKernel.js +110 -0
- package/src/TSL/DebugKernel.js +98 -0
- package/src/TSL/Environment.js +13 -11
- package/src/TSL/ExtendKernel.js +75 -0
- package/src/TSL/FinalWriteKernel.js +121 -0
- package/src/TSL/GenerateKernel.js +109 -0
- package/src/TSL/LightsSampling.js +2 -2
- package/src/TSL/MaterialTransmission.js +32 -2
- package/src/TSL/PathTracerCore.js +43 -912
- package/src/TSL/ShadeKernel.js +873 -0
- package/src/TSL/Struct.js +5 -0
- package/src/TSL/Subsurface.js +232 -0
- package/src/TSL/patches.js +81 -4
- package/src/index.js +3 -0
- package/src/managers/CameraManager.js +1 -1
- package/src/managers/DenoisingManager.js +40 -75
- package/src/managers/EnvironmentManager.js +30 -39
- package/src/managers/MaterialDataManager.js +60 -1
- package/src/managers/OverlayManager.js +7 -22
- package/src/managers/UniformManager.js +1 -3
- package/src/managers/helpers/TileHelper.js +2 -2
- package/src/Stages/AdaptiveSampling.js +0 -483
- package/src/TSL/PathTracer.js +0 -384
- package/src/managers/TileManager.js +0 -298
package/src/TSL/Struct.js
CHANGED
|
@@ -50,6 +50,11 @@ export const RayTracingMaterial = struct( {
|
|
|
50
50
|
iridescence: 'float',
|
|
51
51
|
iridescenceIOR: 'float',
|
|
52
52
|
iridescenceThicknessRange: 'vec2',
|
|
53
|
+
subsurface: 'float', // 0 = off, blends opaque BRDF → random-walk SSS
|
|
54
|
+
subsurfaceColor: 'vec3', // single-scatter albedo (tint light picks up inside)
|
|
55
|
+
subsurfaceRadius: 'vec3', // per-channel mean free path
|
|
56
|
+
subsurfaceRadiusScale: 'float', // scalar multiplier on radius
|
|
57
|
+
subsurfaceAnisotropy: 'float', // Henyey-Greenstein g (-1..1)
|
|
53
58
|
} );
|
|
54
59
|
|
|
55
60
|
// Lightweight material for shadow ray evaluation — only the fields needed
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subsurface.js - Random-walk subsurface scattering.
|
|
3
|
+
*
|
|
4
|
+
* Reuses the refraction interface + medium stack (PathTracerCore.js). The new physics
|
|
5
|
+
* is inside the medium: a ray collides mid-flight (sigma_s > 0) and scatters via a
|
|
6
|
+
* Henyey-Greenstein phase function instead of flying straight + absorbing (glass).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
Fn,
|
|
11
|
+
float,
|
|
12
|
+
vec2,
|
|
13
|
+
vec3,
|
|
14
|
+
If,
|
|
15
|
+
select,
|
|
16
|
+
abs,
|
|
17
|
+
dot,
|
|
18
|
+
clamp,
|
|
19
|
+
max,
|
|
20
|
+
exp,
|
|
21
|
+
log,
|
|
22
|
+
sqrt,
|
|
23
|
+
cos,
|
|
24
|
+
sin,
|
|
25
|
+
normalize,
|
|
26
|
+
reflect,
|
|
27
|
+
refract,
|
|
28
|
+
} from 'three/tsl';
|
|
29
|
+
|
|
30
|
+
import { struct } from './patches.js';
|
|
31
|
+
import { TWO_PI, EPSILON, constructTBN } from './Common.js';
|
|
32
|
+
import { RandomValue } from './Random.js';
|
|
33
|
+
import { iorToFresnel0, fresnelSchlickFloat } from './Fresnel.js';
|
|
34
|
+
import { ImportanceSampleGGX } from './MaterialSampling.js';
|
|
35
|
+
|
|
36
|
+
// ================================================================================
|
|
37
|
+
// STRUCTS
|
|
38
|
+
// ================================================================================
|
|
39
|
+
|
|
40
|
+
export const CollisionSample = struct( {
|
|
41
|
+
didScatter: 'bool',
|
|
42
|
+
t: 'float', // collision distance (clamped to surfaceDist when no scatter)
|
|
43
|
+
weight: 'vec3', // throughput multiplier for the segment (chromatic-MIS)
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
export const MediumCoeffs = struct( {
|
|
47
|
+
sigmaT: 'vec3',
|
|
48
|
+
sigmaS: 'vec3',
|
|
49
|
+
sigmaA: 'vec3',
|
|
50
|
+
} );
|
|
51
|
+
|
|
52
|
+
export const SubsurfaceEntryResult = struct( {
|
|
53
|
+
direction: 'vec3',
|
|
54
|
+
throughput: 'vec3',
|
|
55
|
+
didReflect: 'bool',
|
|
56
|
+
} );
|
|
57
|
+
|
|
58
|
+
// ================================================================================
|
|
59
|
+
// HENYEY-GREENSTEIN PHASE SAMPLING
|
|
60
|
+
// ================================================================================
|
|
61
|
+
|
|
62
|
+
// Returns a scattered direction (unit). cosTheta is relative to the propagation dir
|
|
63
|
+
// `wi`, so g > 0 is forward scattering. Inverse-CDF sampling is exact, so the brute-force
|
|
64
|
+
// walk needs no extra weight at the vertex (hence no pdf is returned).
|
|
65
|
+
export const sampleHenyeyGreenstein = Fn( ( [ wi, g, xi ] ) => {
|
|
66
|
+
|
|
67
|
+
const cosTheta = float( 0.0 ).toVar();
|
|
68
|
+
|
|
69
|
+
If( abs( g ).lessThan( 0.001 ), () => {
|
|
70
|
+
|
|
71
|
+
cosTheta.assign( float( 1.0 ).sub( xi.x.mul( 2.0 ) ) ); // isotropic; avoids 1/(2g)
|
|
72
|
+
|
|
73
|
+
} ).Else( () => {
|
|
74
|
+
|
|
75
|
+
const denom = max( float( 1.0 ).sub( g ).add( g.mul( 2.0 ).mul( xi.x ) ), 1e-4 );
|
|
76
|
+
const sqrTerm = float( 1.0 ).sub( g.mul( g ) ).div( denom );
|
|
77
|
+
cosTheta.assign( float( 1.0 ).add( g.mul( g ) ).sub( sqrTerm.mul( sqrTerm ) ).div( g.mul( 2.0 ) ) );
|
|
78
|
+
|
|
79
|
+
} );
|
|
80
|
+
|
|
81
|
+
cosTheta.assign( clamp( cosTheta, - 1.0, 1.0 ) );
|
|
82
|
+
const sinTheta = sqrt( max( float( 0.0 ), float( 1.0 ).sub( cosTheta.mul( cosTheta ) ) ) );
|
|
83
|
+
const phi = float( TWO_PI ).mul( xi.y );
|
|
84
|
+
|
|
85
|
+
// Basis with wi as 3rd column → result is already unit length.
|
|
86
|
+
const TBN = constructTBN( { N: wi } );
|
|
87
|
+
return TBN.mul( vec3( sinTheta.mul( cos( phi ) ), sinTheta.mul( sin( phi ) ), cosTheta ) );
|
|
88
|
+
|
|
89
|
+
} );
|
|
90
|
+
|
|
91
|
+
// ================================================================================
|
|
92
|
+
// CHROMATIC COLLISION-DISTANCE SAMPLING (hero-channel spectral MIS)
|
|
93
|
+
// ================================================================================
|
|
94
|
+
|
|
95
|
+
// Per-channel sigma_t can't be represented by one scalar distance. Pick a channel
|
|
96
|
+
// ∝ throughput, sample t against it, and weight by the balance-heuristic combined pdf
|
|
97
|
+
// p̄ = Σ pmf_c·p_c — the shared scalar p̄ is what suppresses color fireflies.
|
|
98
|
+
export const sampleChromaticCollision = Fn( ( [ sigmaT, sigmaS, beta, surfaceDist, rngState ] ) => {
|
|
99
|
+
|
|
100
|
+
const w = max( beta, vec3( 1e-4 ) ); // floor so no channel goes unsampled
|
|
101
|
+
const pmf = w.div( w.x.add( w.y ).add( w.z ) ).toVar();
|
|
102
|
+
|
|
103
|
+
// .toVar() pins the single RNG draw (else it re-executes per comparison → state drift).
|
|
104
|
+
const u = RandomValue( rngState ).toVar();
|
|
105
|
+
const cSigmaT = float( 0.0 ).toVar();
|
|
106
|
+
If( u.lessThan( pmf.x ), () => {
|
|
107
|
+
|
|
108
|
+
cSigmaT.assign( sigmaT.x );
|
|
109
|
+
|
|
110
|
+
} ).ElseIf( u.lessThan( pmf.x.add( pmf.y ) ), () => {
|
|
111
|
+
|
|
112
|
+
cSigmaT.assign( sigmaT.y );
|
|
113
|
+
|
|
114
|
+
} ).Else( () => {
|
|
115
|
+
|
|
116
|
+
cSigmaT.assign( sigmaT.z );
|
|
117
|
+
|
|
118
|
+
} );
|
|
119
|
+
|
|
120
|
+
const xi = RandomValue( rngState ).toVar();
|
|
121
|
+
const t = log( max( float( 1.0 ).sub( xi ), 1e-6 ) ).negate().div( max( cSigmaT, 1e-6 ) ).toVar();
|
|
122
|
+
|
|
123
|
+
const didScatter = t.lessThan( surfaceDist ).toVar();
|
|
124
|
+
const tOut = t.toVar();
|
|
125
|
+
const weight = vec3( 0.0 ).toVar();
|
|
126
|
+
|
|
127
|
+
If( didScatter, () => {
|
|
128
|
+
|
|
129
|
+
const Tr = exp( sigmaT.mul( t ).negate() ).toVar();
|
|
130
|
+
const pBar = dot( pmf, sigmaT.mul( Tr ) );
|
|
131
|
+
weight.assign( sigmaS.mul( Tr ).div( max( pBar, 1e-6 ) ) );
|
|
132
|
+
|
|
133
|
+
} ).Else( () => {
|
|
134
|
+
|
|
135
|
+
const Tr = exp( sigmaT.mul( surfaceDist ).negate() ).toVar();
|
|
136
|
+
const pBar = dot( pmf, Tr );
|
|
137
|
+
weight.assign( Tr.div( max( pBar, 1e-6 ) ) );
|
|
138
|
+
tOut.assign( surfaceDist );
|
|
139
|
+
|
|
140
|
+
} );
|
|
141
|
+
|
|
142
|
+
return CollisionSample( { didScatter, t: tOut, weight } );
|
|
143
|
+
|
|
144
|
+
} );
|
|
145
|
+
|
|
146
|
+
// ================================================================================
|
|
147
|
+
// PARAMETER → COEFFICIENT MAPPING (Cycles-style)
|
|
148
|
+
// ================================================================================
|
|
149
|
+
|
|
150
|
+
// sigma_t = 1/(radius·scale), sigma_s = albedo·sigma_t, sigma_a = sigma_t - sigma_s.
|
|
151
|
+
// subsurfaceColor is the single-scatter albedo, so the per-event weight carries the tint.
|
|
152
|
+
export const subsurfaceCoefficients = Fn( ( [ subsurfaceColor, subsurfaceRadius, radiusScale ] ) => {
|
|
153
|
+
|
|
154
|
+
const r = max( subsurfaceRadius.mul( radiusScale ), vec3( 1e-4 ) );
|
|
155
|
+
const sigmaT = vec3( 1.0 ).div( r );
|
|
156
|
+
const sigmaS = subsurfaceColor.mul( sigmaT );
|
|
157
|
+
const sigmaA = max( sigmaT.sub( sigmaS ), vec3( 0.0 ) );
|
|
158
|
+
|
|
159
|
+
return MediumCoeffs( { sigmaT, sigmaS, sigmaA } );
|
|
160
|
+
|
|
161
|
+
} );
|
|
162
|
+
|
|
163
|
+
// ================================================================================
|
|
164
|
+
// DIELECTRIC BOUNDARY (enter / exit the SSS medium)
|
|
165
|
+
// ================================================================================
|
|
166
|
+
|
|
167
|
+
// Dielectric interface driven by material.ior: reflect (Fresnel/TIR) or refract across
|
|
168
|
+
// the boundary. No color tint — the scattering color lives in sigma_s.
|
|
169
|
+
export const handleSubsurfaceEntry = Fn( ( [
|
|
170
|
+
rayDir, normal, material, entering, rngState, currentMediumIOR, previousMediumIOR,
|
|
171
|
+
] ) => {
|
|
172
|
+
|
|
173
|
+
const result = SubsurfaceEntryResult( {
|
|
174
|
+
direction: vec3( 0.0 ),
|
|
175
|
+
throughput: vec3( 1.0 ),
|
|
176
|
+
didReflect: false,
|
|
177
|
+
} ).toVar();
|
|
178
|
+
|
|
179
|
+
const N = select( entering, normal, normal.negate() ).toVar();
|
|
180
|
+
const n1 = select( entering, currentMediumIOR, material.ior ).toVar();
|
|
181
|
+
const n2 = select( entering, material.ior, previousMediumIOR ).toVar();
|
|
182
|
+
|
|
183
|
+
const cosThetaI = abs( dot( N, rayDir ) );
|
|
184
|
+
const sinThetaT2 = n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) ).mul( float( 1.0 ).sub( cosThetaI.mul( cosThetaI ) ) );
|
|
185
|
+
const tir = sinThetaT2.greaterThan( 1.0 ).toVar();
|
|
186
|
+
|
|
187
|
+
const F0 = iorToFresnel0( n2, n1 );
|
|
188
|
+
const Fr = select( tir, float( 1.0 ), fresnelSchlickFloat( cosThetaI, F0 ) ).toVar();
|
|
189
|
+
const reflectProb = clamp( Fr, 0.02, 0.98 ).toVar();
|
|
190
|
+
|
|
191
|
+
const doReflect = tir.or( RandomValue( rngState ).lessThan( reflectProb ) ).toVar();
|
|
192
|
+
result.didReflect.assign( doReflect );
|
|
193
|
+
|
|
194
|
+
If( doReflect, () => {
|
|
195
|
+
|
|
196
|
+
// GGX-sampled reflection: a perfect mirror here makes SSS surfaces read as polished ceramic.
|
|
197
|
+
const xiR = vec2( RandomValue( rngState ), RandomValue( rngState ) );
|
|
198
|
+
const H = ImportanceSampleGGX( { N, roughness: material.roughness, Xi: xiR } );
|
|
199
|
+
const reflDir = reflect( rayDir, H ).toVar();
|
|
200
|
+
If( dot( reflDir, N ).lessThanEqual( 0.0 ), () => {
|
|
201
|
+
|
|
202
|
+
reflDir.assign( reflect( rayDir, N ) ); // rough sample dipped below surface
|
|
203
|
+
|
|
204
|
+
} );
|
|
205
|
+
result.direction.assign( reflDir );
|
|
206
|
+
result.throughput.assign( vec3( Fr.div( max( reflectProb, 0.02 ) ) ) );
|
|
207
|
+
|
|
208
|
+
} ).Else( () => {
|
|
209
|
+
|
|
210
|
+
const refrDir = refract( rayDir, N, n1.div( max( n2, EPSILON ) ) ).toVar();
|
|
211
|
+
|
|
212
|
+
If( dot( refrDir, refrDir ).lessThan( 0.0001 ), () => {
|
|
213
|
+
|
|
214
|
+
result.direction.assign( reflect( rayDir, N ) );
|
|
215
|
+
result.didReflect.assign( true );
|
|
216
|
+
|
|
217
|
+
} ).Else( () => {
|
|
218
|
+
|
|
219
|
+
result.direction.assign( normalize( refrDir ) );
|
|
220
|
+
// (1-Fr) transmission + (n1/n2)² radiance scale (cancels round-trip).
|
|
221
|
+
const radianceScale = n1.mul( n1 ).div( max( n2.mul( n2 ), EPSILON ) );
|
|
222
|
+
result.throughput.assign( vec3(
|
|
223
|
+
float( 1.0 ).sub( Fr ).div( max( float( 1.0 ).sub( reflectProb ), 0.02 ) ).mul( radianceScale )
|
|
224
|
+
) );
|
|
225
|
+
|
|
226
|
+
} );
|
|
227
|
+
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
return result;
|
|
231
|
+
|
|
232
|
+
} );
|
package/src/TSL/patches.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rayzee patches for Three.js / TSL.
|
|
3
3
|
*
|
|
4
|
-
* Side-effect on import: installs `WebGPUBackend.
|
|
5
|
-
* (restores r183 function-scoped `var` emission for compute
|
|
6
|
-
* a register-allocation regression in the path tracer's hot
|
|
4
|
+
* Side-effect on import: installs two `WebGPUBackend.prototype` overrides —
|
|
5
|
+
* `createNodeBuilder` (restores r183 function-scoped `var` emission for compute
|
|
6
|
+
* shaders, preventing a register-allocation regression in the path tracer's hot
|
|
7
|
+
* loop) and `initTimestampQuery` (enlarges the stats-gl timestamp query pool so
|
|
8
|
+
* the wavefront tracer's high per-frame compute-pass count doesn't overflow it).
|
|
7
9
|
*
|
|
8
10
|
* Export: `struct()` — drop-in replacement for TSL's `struct()` returning
|
|
9
11
|
* a proxy factory that supports GLSL-style dot-notation field access.
|
|
@@ -64,7 +66,82 @@ WebGPUBackend.prototype.createNodeBuilder = function ( object, renderer ) {
|
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
// ---------------------------------------------------------------------------
|
|
67
|
-
// 2.
|
|
69
|
+
// 2. Larger timestamp query pool (stats-gl GPU/compute timing)
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Three.js lazily creates each timestamp query pool with a hardcoded 2048
|
|
72
|
+
// queries (= 1024 passes) — `WebGPUBackend.initTimestampQuery` / the upstream
|
|
73
|
+
// `// TODO: Variable maxQueries?`. The wavefront tracer issues hundreds of
|
|
74
|
+
// compute passes per frame (peak right after a maxBounces change: the survivor
|
|
75
|
+
// curve is invalid, so the bounce loop runs the full `loopBound` at full
|
|
76
|
+
// dispatch with no early-exit — ~560 passes / 1124 queries at production
|
|
77
|
+
// settings). stats-gl resolves once per frame, but the resolve is async and
|
|
78
|
+
// `mapAsync` lags several frames under that GPU load, so the counter isn't reset
|
|
79
|
+
// before it overflows → "Maximum number of queries exceeded" + dropped timings.
|
|
80
|
+
//
|
|
81
|
+
// Two parts:
|
|
82
|
+
// a) Grow the pool to 4096 queries on first use. 4096 is the WebGPU hard cap on
|
|
83
|
+
// a query set's count ("Query count exceeds the maximum query count (4096)")
|
|
84
|
+
// — 2× the upstream default, the most a single pool can hold (~2048 passes).
|
|
85
|
+
// That alone fully covers the interactive case (~200 passes/frame); anything
|
|
86
|
+
// larger fails CreateQuerySet validation.
|
|
87
|
+
// b) When even 4096 isn't enough (production spike), degrade gracefully: skip
|
|
88
|
+
// tracking the overflow passes silently instead of `warnOnce` + an invalid
|
|
89
|
+
// descriptor. The reported compute ms briefly undercounts during the spike;
|
|
90
|
+
// rendering is never affected (timestamps don't gate compute correctness).
|
|
91
|
+
const TIMESTAMP_POOL_MAX_QUERIES = 4096;
|
|
92
|
+
|
|
93
|
+
// Drop-in for the pool's allocateQueriesForContext minus the warnOnce on overflow
|
|
94
|
+
// — returns null silently when full so the pass is cleanly skipped (see below).
|
|
95
|
+
function _allocateQueriesSilently( uid ) {
|
|
96
|
+
|
|
97
|
+
if ( ! this.trackTimestamp || this.isDisposed ) return null;
|
|
98
|
+
if ( this.currentQueryIndex + 2 > this.maxQueries ) return null; // full: skip, no warn
|
|
99
|
+
const baseOffset = this.currentQueryIndex;
|
|
100
|
+
this.currentQueryIndex += 2;
|
|
101
|
+
this.queryOffsets.set( uid, baseOffset );
|
|
102
|
+
return baseOffset;
|
|
103
|
+
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const _origInitTimestampQuery = WebGPUBackend.prototype.initTimestampQuery;
|
|
107
|
+
|
|
108
|
+
WebGPUBackend.prototype.initTimestampQuery = function ( type, uid, descriptor ) {
|
|
109
|
+
|
|
110
|
+
const poolWasMissing = this.trackTimestamp && ! this.timestampQueryPool[ type ];
|
|
111
|
+
|
|
112
|
+
_origInitTimestampQuery.call( this, type, uid, descriptor );
|
|
113
|
+
|
|
114
|
+
// (a) First use: replace the fresh 2048 pool with a 4096 one of the same class,
|
|
115
|
+
// migrating the single allocation just made (offset 0) and re-pointing this
|
|
116
|
+
// pass's descriptor. Safe — first pass of the first tracked frame, nothing in
|
|
117
|
+
// flight. Swap in the silent allocator so future overflows don't warn.
|
|
118
|
+
if ( poolWasMissing ) {
|
|
119
|
+
|
|
120
|
+
const pool = this.timestampQueryPool[ type ];
|
|
121
|
+
if ( pool && pool.maxQueries < TIMESTAMP_POOL_MAX_QUERIES && descriptor.timestampWrites ) {
|
|
122
|
+
|
|
123
|
+
const Pool = pool.constructor;
|
|
124
|
+
const bigPool = new Pool( this.device, type, TIMESTAMP_POOL_MAX_QUERIES );
|
|
125
|
+
bigPool.allocateQueriesForContext = _allocateQueriesSilently;
|
|
126
|
+
bigPool.allocateQueriesForContext( uid ); // re-take offset 0 for this pass
|
|
127
|
+
this.timestampQueryPool[ type ] = bigPool;
|
|
128
|
+
descriptor.timestampWrites.querySet = bigPool.querySet; // offsets 0/1 unchanged
|
|
129
|
+
pool.dispose(); // nothing in flight — first pass of the first tracked frame
|
|
130
|
+
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// (b) On overflow the (silent) allocator returns null, but upstream still wrote
|
|
136
|
+
// a descriptor with a null begin index — which both collides on slot 1 and is
|
|
137
|
+
// invalid. Drop it so the pass is cleanly untimed.
|
|
138
|
+
const tw = descriptor.timestampWrites;
|
|
139
|
+
if ( tw && tw.beginningOfPassWriteIndex == null ) descriptor.timestampWrites = undefined;
|
|
140
|
+
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// 3. TSL struct proxy — enables GLSL-style dot-notation field access
|
|
68
145
|
// ---------------------------------------------------------------------------
|
|
69
146
|
// TSL structs require `.get('fieldName')` for member access, but GLSL-style
|
|
70
147
|
// dot notation (`.fieldName`) is more natural and matches ported code.
|
package/src/index.js
CHANGED
|
@@ -44,6 +44,9 @@ export { IESManager } from './managers/IESManager.js';
|
|
|
44
44
|
export { DenoisingManager } from './managers/DenoisingManager.js';
|
|
45
45
|
export { OverlayManager } from './managers/OverlayManager.js';
|
|
46
46
|
|
|
47
|
+
// VRAM accounting
|
|
48
|
+
export { VRAMTracker, bufferBytes, textureBytes } from './Processor/VRAMTracker.js';
|
|
49
|
+
|
|
47
50
|
// Pipeline infrastructure (for advanced consumers building custom stages)
|
|
48
51
|
export { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
49
52
|
export { RenderStage, StageExecutionMode } from './Pipeline/RenderStage.js';
|
|
@@ -261,7 +261,7 @@ export class CameraManager extends EventDispatcher {
|
|
|
261
261
|
* @param {Object} params.assetLoader
|
|
262
262
|
* @param {import('three').Mesh} params.floorPlane
|
|
263
263
|
* @param {number} params.currentFocusDistance
|
|
264
|
-
* @param {import('
|
|
264
|
+
* @param {import('../Stages/PathTracer.js').PathTracer} params.pathTracer
|
|
265
265
|
* @param {Function} params.setFocusDistance - Callback to update uniform + settings
|
|
266
266
|
* @param {Function} params.softReset - Callback for soft accumulation reset
|
|
267
267
|
* @param {Function} params.hardReset - Callback for hard accumulation reset
|
|
@@ -41,7 +41,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
41
41
|
this.pipeline = pipeline;
|
|
42
42
|
|
|
43
43
|
// Stage references — only used internally for orchestration
|
|
44
|
-
this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter,
|
|
44
|
+
this._stages = stages; // { pathTracer, asvgf, variance, bilateralFilter, edgeFilter, ssrc, autoExposure, compositor }
|
|
45
45
|
|
|
46
46
|
this._getExposure = getExposure;
|
|
47
47
|
this._getSaturation = getSaturation;
|
|
@@ -231,7 +231,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
231
231
|
|
|
232
232
|
// Disable all real-time denoisers first
|
|
233
233
|
if ( s.asvgf ) s.asvgf.enabled = false;
|
|
234
|
-
if ( s.variance
|
|
234
|
+
if ( s.variance ) s.variance.enabled = false;
|
|
235
235
|
if ( s.bilateralFilter ) s.bilateralFilter.enabled = false;
|
|
236
236
|
if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( false );
|
|
237
237
|
if ( s.ssrc ) s.ssrc.enabled = false;
|
|
@@ -258,6 +258,8 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
258
258
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
this._syncGBufferStages();
|
|
262
|
+
|
|
261
263
|
}
|
|
262
264
|
|
|
263
265
|
/**
|
|
@@ -282,6 +284,8 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
282
284
|
// Coordinate with EdgeAware filtering
|
|
283
285
|
if ( s.edgeFilter ) s.edgeFilter.setFilteringEnabled( ! enabled );
|
|
284
286
|
|
|
287
|
+
this._syncGBufferStages();
|
|
288
|
+
|
|
285
289
|
}
|
|
286
290
|
|
|
287
291
|
/**
|
|
@@ -315,35 +319,49 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
315
319
|
}
|
|
316
320
|
|
|
317
321
|
/**
|
|
318
|
-
*
|
|
319
|
-
*
|
|
322
|
+
* Gate the G-buffer stages (NormalDepth, MotionVector) on demand: they only
|
|
323
|
+
* need to run when a real-time denoiser consumes their output. Idling them
|
|
324
|
+
* otherwise skips MotionVector's per-frame compute + copies during preview
|
|
325
|
+
* navigation and frees their textures. Call after any consumer toggle.
|
|
326
|
+
*
|
|
327
|
+
* MotionVector requires NormalDepth (reads pathtracer:normalDepth) and its
|
|
328
|
+
* consumers (ASVGF, SSRC) are a subset of NormalDepth's, so NormalDepth is
|
|
329
|
+
* always enabled whenever MotionVector is. Adaptive sampling / Variance / OIDN
|
|
330
|
+
* do NOT read these signals, so they don't keep the G-buffer alive.
|
|
320
331
|
*/
|
|
321
|
-
|
|
332
|
+
_syncGBufferStages() {
|
|
322
333
|
|
|
323
334
|
const s = this._stages;
|
|
335
|
+
const nd = s.normalDepth;
|
|
336
|
+
const mv = s.motionVector;
|
|
324
337
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
338
|
+
// motionVector:* consumed by ASVGF + SSRC
|
|
339
|
+
const motionNeeded = !! ( s.asvgf?.enabled || s.ssrc?.enabled );
|
|
340
|
+
// pathtracer:normalDepth consumed by ASVGF, SSRC, EdgeFilter, BilateralFilter
|
|
341
|
+
const normalNeeded = motionNeeded || !! ( s.edgeFilter?.enabled || s.bilateralFilter?.enabled );
|
|
329
342
|
|
|
330
|
-
|
|
343
|
+
if ( nd ) {
|
|
331
344
|
|
|
332
|
-
|
|
333
|
-
|
|
345
|
+
// On disabled→enabled, re-arm dirty/history so the first frame recomputes
|
|
346
|
+
// (not the stale static fast-path) and seeds prev = current.
|
|
347
|
+
if ( normalNeeded && ! nd.enabled ) nd.reset();
|
|
348
|
+
nd.enabled = normalNeeded;
|
|
334
349
|
|
|
335
|
-
|
|
350
|
+
}
|
|
336
351
|
|
|
337
|
-
|
|
352
|
+
if ( mv ) {
|
|
338
353
|
|
|
339
|
-
|
|
354
|
+
// On re-enable, force a camera-history reseed (matricesInitialized survives
|
|
355
|
+
// normal resets) so the first frame reports zero motion, not a spike.
|
|
356
|
+
if ( motionNeeded && ! mv.enabled ) {
|
|
340
357
|
|
|
341
|
-
|
|
358
|
+
mv.matricesInitialized = false;
|
|
359
|
+
mv.isFirstFrame = true;
|
|
360
|
+
mv.frameCount = 0;
|
|
342
361
|
|
|
343
|
-
|
|
344
|
-
if ( ! enabled && this.pipeline?.context && ! s.asvgf?.enabled ) {
|
|
362
|
+
}
|
|
345
363
|
|
|
346
|
-
|
|
364
|
+
mv.enabled = motionNeeded;
|
|
347
365
|
|
|
348
366
|
}
|
|
349
367
|
|
|
@@ -546,6 +564,8 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
546
564
|
|
|
547
565
|
}
|
|
548
566
|
|
|
567
|
+
this._syncGBufferStages();
|
|
568
|
+
|
|
549
569
|
}
|
|
550
570
|
|
|
551
571
|
/** Updates SSRC stage parameters. */
|
|
@@ -569,29 +589,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
569
589
|
|
|
570
590
|
}
|
|
571
591
|
|
|
572
|
-
/**
|
|
573
|
-
* Updates adaptive sampling parameters (with settings bridge).
|
|
574
|
-
* @param {Object} params
|
|
575
|
-
*/
|
|
576
|
-
setAdaptiveSamplingParams( params ) {
|
|
577
|
-
|
|
578
|
-
if ( params.min !== undefined ) this._stages.pathTracer?.setUniform( 'adaptiveSamplingMin', params.min );
|
|
579
|
-
if ( params.adaptiveSamplingMax !== undefined ) this._settings?.set( 'adaptiveSamplingMax', params.adaptiveSamplingMax );
|
|
580
|
-
this._stages.adaptiveSampling?.setAdaptiveSamplingParameters( params );
|
|
581
|
-
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Toggle the AdaptiveSampling heatmap compute pass. When enabled, the
|
|
586
|
-
* stage writes the heatmap to its public `heatmapTarget` RenderTarget —
|
|
587
|
-
* the host is responsible for rendering it.
|
|
588
|
-
*/
|
|
589
|
-
toggleAdaptiveSamplingHelper( enabled ) {
|
|
590
|
-
|
|
591
|
-
this._stages.adaptiveSampling?.setHeatmapEnabled( enabled );
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
592
|
// ── OIDN ─────────────────────────────────────────────────────
|
|
596
593
|
|
|
597
594
|
/** Enables or disables Intel OIDN denoiser. */
|
|
@@ -608,27 +605,13 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
608
605
|
|
|
609
606
|
}
|
|
610
607
|
|
|
611
|
-
/** Enables or disables the
|
|
612
|
-
setOIDNTileHelper( enabled ) {
|
|
613
|
-
|
|
614
|
-
this._setTileHelper( enabled );
|
|
615
|
-
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
/** Enables or disables the tile helper overlay. */
|
|
608
|
+
/** Enables or disables the denoise/upscale progress overlay. */
|
|
619
609
|
setTileHelperEnabled( enabled ) {
|
|
620
610
|
|
|
621
611
|
this._setTileHelper( enabled );
|
|
622
612
|
|
|
623
613
|
}
|
|
624
614
|
|
|
625
|
-
/** Enables or disables tile highlight. */
|
|
626
|
-
setTileHighlightEnabled( enabled ) {
|
|
627
|
-
|
|
628
|
-
this._setTileHelper( enabled );
|
|
629
|
-
|
|
630
|
-
}
|
|
631
|
-
|
|
632
615
|
// ── AI Upscaler ──────────────────────────────────────────────
|
|
633
616
|
|
|
634
617
|
/** Enables or disables the AI upscaler. */
|
|
@@ -665,17 +648,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
665
648
|
|
|
666
649
|
}
|
|
667
650
|
|
|
668
|
-
/**
|
|
669
|
-
* Enables or disables adaptive sampling (convenience wrapper with settings bridge).
|
|
670
|
-
* @param {boolean} enabled
|
|
671
|
-
*/
|
|
672
|
-
setAdaptiveSampling( enabled ) {
|
|
673
|
-
|
|
674
|
-
this._settings?.set( 'useAdaptiveSampling', enabled );
|
|
675
|
-
this.setAdaptiveSamplingEnabled( enabled );
|
|
676
|
-
|
|
677
|
-
}
|
|
678
|
-
|
|
679
651
|
/**
|
|
680
652
|
* Switches strategy with automatic reset (convenience wrapper).
|
|
681
653
|
* @param {'none'|'asvgf'|'ssrc'|'edgeaware'} strategy
|
|
@@ -702,7 +674,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
702
674
|
|
|
703
675
|
}
|
|
704
676
|
|
|
705
|
-
|
|
706
677
|
_getEffectiveExposure() {
|
|
707
678
|
|
|
708
679
|
return this._stages.autoExposure?.enabled
|
|
@@ -717,12 +688,6 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
717
688
|
|
|
718
689
|
}
|
|
719
690
|
|
|
720
|
-
_isAdaptiveSamplingActive() {
|
|
721
|
-
|
|
722
|
-
return this._stages.adaptiveSampling?.enabled ?? false;
|
|
723
|
-
|
|
724
|
-
}
|
|
725
|
-
|
|
726
691
|
_clearDenoiserTextures() {
|
|
727
692
|
|
|
728
693
|
const ctx = this.pipeline?.context;
|