rayzee 7.1.0 → 7.2.1
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 +1441 -1293
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +45 -45
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/EngineDefaults.js +13 -16
- package/src/PathTracerApp.js +16 -4
- package/src/Processor/EmissiveTriangleBuilder.js +39 -5
- package/src/Processor/LightBVHBuilder.js +113 -27
- package/src/Processor/SceneProcessor.js +19 -1
- package/src/Stages/ASVGF.js +40 -14
- package/src/Stages/BilateralFilter.js +23 -4
- package/src/Stages/NormalDepth.js +101 -7
- package/src/Stages/PathTracer.js +1 -0
- package/src/Stages/PathTracerStage.js +14 -3
- package/src/Stages/Variance.js +7 -3
- package/src/TSL/EmissiveSampling.js +3 -2
- package/src/TSL/LightBVHSampling.js +216 -25
- package/src/TSL/ShadeKernel.js +21 -6
- package/src/managers/CameraManager.js +19 -0
- package/src/managers/UniformManager.js +4 -0
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -161,11 +161,15 @@ export const MAX_STORAGE_TEXTURE_SIZE = 2048;
|
|
|
161
161
|
|
|
162
162
|
export const ASVGF_QUALITY_PRESETS = {
|
|
163
163
|
// phiColor / phiDepth are RELATIVE tolerances (fractions). Bigger = more
|
|
164
|
-
// permissive.
|
|
165
|
-
//
|
|
164
|
+
// permissive. The adaptive temporal gradient (gradientStrength > 0) is always
|
|
165
|
+
// on: it measures real change in units of noise σ (gradientSigmaScale), so a
|
|
166
|
+
// static scene reads ~0 (no convergence penalty) and only moving lights / anim
|
|
167
|
+
// / disocclusion drop history. See ASVGF._buildGradientCompute.
|
|
166
168
|
low: {
|
|
167
169
|
temporalAlpha: 0.1,
|
|
168
|
-
gradientStrength: 0.
|
|
170
|
+
gradientStrength: 0.8,
|
|
171
|
+
gradientSigmaScale: 2.5,
|
|
172
|
+
gradientNoiseFloor: 0.05,
|
|
169
173
|
atrousIterations: 3,
|
|
170
174
|
phiColor: 1.0,
|
|
171
175
|
phiNormal: 64.0,
|
|
@@ -176,7 +180,9 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
176
180
|
},
|
|
177
181
|
medium: {
|
|
178
182
|
temporalAlpha: 0.03,
|
|
179
|
-
gradientStrength:
|
|
183
|
+
gradientStrength: 1.0,
|
|
184
|
+
gradientSigmaScale: 2.5,
|
|
185
|
+
gradientNoiseFloor: 0.05,
|
|
180
186
|
atrousIterations: 4,
|
|
181
187
|
phiColor: 0.5,
|
|
182
188
|
phiNormal: 128.0,
|
|
@@ -187,7 +193,9 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
187
193
|
},
|
|
188
194
|
high: {
|
|
189
195
|
temporalAlpha: 0.0,
|
|
190
|
-
gradientStrength:
|
|
196
|
+
gradientStrength: 1.0,
|
|
197
|
+
gradientSigmaScale: 2.5,
|
|
198
|
+
gradientNoiseFloor: 0.05,
|
|
191
199
|
atrousIterations: 6,
|
|
192
200
|
phiColor: 0.3,
|
|
193
201
|
phiNormal: 256.0,
|
|
@@ -198,17 +206,6 @@ export const ASVGF_QUALITY_PRESETS = {
|
|
|
198
206
|
}
|
|
199
207
|
};
|
|
200
208
|
|
|
201
|
-
// Adaptive variants — same SVGF quality as the base preset plus the temporal-
|
|
202
|
-
// gradient anti-lag enabled (gradientStrength > 0). The gradient measures real
|
|
203
|
-
// change in units of noise σ (gradientSigmaScale), so a static scene reads ~0
|
|
204
|
-
// and convergence is unaffected; only moving lights/anim/disocclusion drop
|
|
205
|
-
// history. Mutating the exported object is fine (const binding, mutable object);
|
|
206
|
-
// spreading lets each inherit its base. See ASVGF._buildGradientCompute.
|
|
207
|
-
const ADAPTIVE_GRADIENT = { gradientSigmaScale: 2.5, gradientNoiseFloor: 0.05 };
|
|
208
|
-
ASVGF_QUALITY_PRESETS.low_adaptive = { ...ASVGF_QUALITY_PRESETS.low, gradientStrength: 0.8, ...ADAPTIVE_GRADIENT };
|
|
209
|
-
ASVGF_QUALITY_PRESETS.medium_adaptive = { ...ASVGF_QUALITY_PRESETS.medium, gradientStrength: 1.0, ...ADAPTIVE_GRADIENT };
|
|
210
|
-
ASVGF_QUALITY_PRESETS.high_adaptive = { ...ASVGF_QUALITY_PRESETS.high, gradientStrength: 1.0, ...ADAPTIVE_GRADIENT };
|
|
211
|
-
|
|
212
209
|
export const CAMERA_RANGES = {
|
|
213
210
|
fov: {
|
|
214
211
|
min: 10,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -310,12 +310,19 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
310
310
|
}
|
|
311
311
|
|
|
312
312
|
this._ensureVRAMWiring();
|
|
313
|
-
|
|
313
|
+
// VRAM is monotonic and only changes on allocation events (scene/env
|
|
314
|
+
// load, resize — each re-measures via _ensureVRAMWiring). Within an
|
|
315
|
+
// accumulation burst nothing reallocates, so re-walking every stage's
|
|
316
|
+
// textures each frame is wasted. Measure at burst start (catches any
|
|
317
|
+
// reset-triggered allocation) + a periodic backstop; read cached otherwise.
|
|
318
|
+
const tracker = this.stages.pathTracer?.vramTracker;
|
|
319
|
+
const frame = this.stages.pathTracer?.frameCount ?? 0;
|
|
320
|
+
if ( tracker && ( frame <= 1 || frame % 30 === 0 ) ) tracker.measure();
|
|
314
321
|
updateStats( {
|
|
315
322
|
timeElapsed: this.completion.timeElapsed,
|
|
316
323
|
samples: getDisplaySamples( this.stages.pathTracer ),
|
|
317
|
-
memoryUsed:
|
|
318
|
-
memoryPeak:
|
|
324
|
+
memoryUsed: tracker?.current ?? 0,
|
|
325
|
+
memoryPeak: tracker?.peak ?? 0,
|
|
319
326
|
} );
|
|
320
327
|
|
|
321
328
|
// Check time limit
|
|
@@ -1183,8 +1190,13 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1183
1190
|
if ( result ) {
|
|
1184
1191
|
|
|
1185
1192
|
this.stages.pathTracer.setEmissiveTriangleData(
|
|
1186
|
-
result.rawData, result.emissiveCount, result.totalPower,
|
|
1193
|
+
result.rawData, result.emissiveCount, result.totalPower, result.bitTrailMap,
|
|
1187
1194
|
);
|
|
1195
|
+
if ( result.lightBVHNodeData ) {
|
|
1196
|
+
|
|
1197
|
+
this.stages.pathTracer.setLightBVHData( result.lightBVHNodeData, result.lightBVHNodeCount );
|
|
1198
|
+
|
|
1199
|
+
}
|
|
1188
1200
|
|
|
1189
1201
|
}
|
|
1190
1202
|
|
|
@@ -22,6 +22,10 @@ export class EmissiveTriangleBuilder {
|
|
|
22
22
|
this.cdfArray = null;
|
|
23
23
|
this.lightBVHNodeData = null;
|
|
24
24
|
this.lightBVHNodeCount = 0;
|
|
25
|
+
// Per-triangle bit-trail (root→leaf path through the Light BVH), indexed by absolute
|
|
26
|
+
// triangleIndex, -1 for non-emissive. Lets the GPU re-walk the descent pdf for MIS.
|
|
27
|
+
this.emissiveBitTrailMap = null;
|
|
28
|
+
this._totalTriangleCount = 0;
|
|
25
29
|
|
|
26
30
|
}
|
|
27
31
|
|
|
@@ -37,6 +41,7 @@ export class EmissiveTriangleBuilder {
|
|
|
37
41
|
|
|
38
42
|
this.emissiveTriangles = [];
|
|
39
43
|
this.totalEmissivePower = 0;
|
|
44
|
+
this._totalTriangleCount = triangleCount;
|
|
40
45
|
|
|
41
46
|
const FLOATS_PER_TRIANGLE = TRIANGLE_DATA_LAYOUT.FLOATS_PER_TRIANGLE;
|
|
42
47
|
const MATERIAL_INDEX_OFFSET = TRIANGLE_DATA_LAYOUT.UV_C_MAT_OFFSET + 2; // materialIndex within vec4
|
|
@@ -74,9 +79,21 @@ export class EmissiveTriangleBuilder {
|
|
|
74
79
|
|
|
75
80
|
const area = this._calculateTriangleArea( v0x, v0y, v0z, v1x, v1y, v1z, v2x, v2y, v2z );
|
|
76
81
|
|
|
77
|
-
// Calculate emissive power (luminance * intensity * area)
|
|
78
|
-
|
|
79
|
-
const
|
|
82
|
+
// Calculate emissive power (Rec.709 luminance * intensity * area) — must match the
|
|
83
|
+
// shader's calculateEmissiveLightPdf luma weighting for MIS consistency.
|
|
84
|
+
const luma = 0.2126 * emissive.r + 0.7152 * emissive.g + 0.0722 * emissive.b;
|
|
85
|
+
const power = luma * emissiveIntensity * area;
|
|
86
|
+
|
|
87
|
+
// Geometric normal (emission cone axis). BackSide flips it; DoubleSide → two-sided.
|
|
88
|
+
let nx = ( v1y - v0y ) * ( v2z - v0z ) - ( v1z - v0z ) * ( v2y - v0y );
|
|
89
|
+
let ny = ( v1z - v0z ) * ( v2x - v0x ) - ( v1x - v0x ) * ( v2z - v0z );
|
|
90
|
+
let nz = ( v1x - v0x ) * ( v2y - v0y ) - ( v1y - v0y ) * ( v2x - v0x );
|
|
91
|
+
const nl = Math.sqrt( nx * nx + ny * ny + nz * nz ) || 1;
|
|
92
|
+
const sideSign = material.side === 1 ? - 1 : 1; // THREE.BackSide flips emission normal
|
|
93
|
+
nx = nx / nl * sideSign;
|
|
94
|
+
ny = ny / nl * sideSign;
|
|
95
|
+
nz = nz / nl * sideSign;
|
|
96
|
+
const twoSided = material.side === 2; // THREE.DoubleSide
|
|
80
97
|
|
|
81
98
|
// Centroid for BVH split decisions
|
|
82
99
|
const cx = ( v0x + v1x + v2x ) / 3;
|
|
@@ -100,6 +117,7 @@ export class EmissiveTriangleBuilder {
|
|
|
100
117
|
emissiveIntensity: emissiveIntensity,
|
|
101
118
|
cx, cy, cz,
|
|
102
119
|
bMinX, bMinY, bMinZ, bMaxX, bMaxY, bMaxZ,
|
|
120
|
+
nx, ny, nz, twoSided,
|
|
103
121
|
} );
|
|
104
122
|
|
|
105
123
|
this.totalEmissivePower += power;
|
|
@@ -459,22 +477,34 @@ export class EmissiveTriangleBuilder {
|
|
|
459
477
|
|
|
460
478
|
if ( this.emissiveCount === 0 ) {
|
|
461
479
|
|
|
462
|
-
// Dummy single leaf node
|
|
480
|
+
// Dummy single leaf node (whole-sphere cone)
|
|
463
481
|
this.lightBVHNodeData = new Float32Array( 16 );
|
|
464
482
|
this.lightBVHNodeData[ 7 ] = 1.0; // isLeaf
|
|
483
|
+
this.lightBVHNodeData[ 14 ] = 1.0; // cone axis z
|
|
484
|
+
this.lightBVHNodeData[ 15 ] = - 1.0; // cosThetaO = whole sphere
|
|
465
485
|
this.lightBVHNodeCount = 1;
|
|
486
|
+
this.emissiveBitTrailMap = new Float32Array( Math.max( this._totalTriangleCount, 1 ) ).fill( - 1 );
|
|
466
487
|
return 1;
|
|
467
488
|
|
|
468
489
|
}
|
|
469
490
|
|
|
470
491
|
const builder = new LightBVHBuilder();
|
|
471
|
-
const { nodeData, nodeCount, sortedPerm } = builder.build( this.emissiveTriangles );
|
|
492
|
+
const { nodeData, nodeCount, sortedPerm, bitTrails } = builder.build( this.emissiveTriangles );
|
|
472
493
|
this.lightBVHNodeData = nodeData;
|
|
473
494
|
this.lightBVHNodeCount = nodeCount;
|
|
474
495
|
|
|
475
496
|
// Rebuild emissive raw data in sorted leaf order so node start/count refs are valid
|
|
476
497
|
this._rebuildSortedEmissiveData( sortedPerm );
|
|
477
498
|
|
|
499
|
+
// Build the per-triangle bit-trail map (indexed by absolute triangleIndex; -1 = non-emissive)
|
|
500
|
+
this.emissiveBitTrailMap = new Float32Array( Math.max( this._totalTriangleCount, 1 ) ).fill( - 1 );
|
|
501
|
+
for ( let i = 0; i < sortedPerm.length; i ++ ) {
|
|
502
|
+
|
|
503
|
+
const triIndex = this.emissiveTriangles[ sortedPerm[ i ] ].triangleIndex;
|
|
504
|
+
this.emissiveBitTrailMap[ triIndex ] = bitTrails[ i ];
|
|
505
|
+
|
|
506
|
+
}
|
|
507
|
+
|
|
478
508
|
return nodeCount;
|
|
479
509
|
|
|
480
510
|
}
|
|
@@ -544,6 +574,10 @@ export class EmissiveTriangleBuilder {
|
|
|
544
574
|
this.cdfArray = null;
|
|
545
575
|
this.lightBVHNodeData = null;
|
|
546
576
|
this.lightBVHNodeCount = 0;
|
|
577
|
+
// Per-triangle bit-trail (root→leaf path through the Light BVH), indexed by absolute
|
|
578
|
+
// triangleIndex, -1 for non-emissive. Lets the GPU re-walk the descent pdf for MIS.
|
|
579
|
+
this.emissiveBitTrailMap = null;
|
|
580
|
+
this._totalTriangleCount = 0;
|
|
547
581
|
|
|
548
582
|
}
|
|
549
583
|
|
|
@@ -4,14 +4,20 @@
|
|
|
4
4
|
* CPU-side BVH builder over emissive triangles for spatially-aware light sampling.
|
|
5
5
|
* Each node occupies 4 vec4s (16 floats, BVH_STRIDE=4):
|
|
6
6
|
*
|
|
7
|
-
* vec4[0]: [aabb.minX, aabb.minY, aabb.minZ, totalPower]
|
|
8
|
-
* vec4[1]: [aabb.maxX, aabb.maxY, aabb.maxZ, isLeaf]
|
|
7
|
+
* vec4[0]: [aabb.minX, aabb.minY, aabb.minZ, totalPower] // power = Rec.709 luma-weighted
|
|
8
|
+
* vec4[1]: [aabb.maxX, aabb.maxY, aabb.maxZ, isLeaf] // isLeaf: 0.0=inner, 1.0=leaf
|
|
9
9
|
* vec4[2]: inner → [leftChildIdx, rightChildIdx, 0, 0]
|
|
10
10
|
* leaf → [emissiveStart, emissiveCount, 0, 0]
|
|
11
|
-
* vec4[3]: [
|
|
11
|
+
* vec4[3]: [coneAxisX, coneAxisY, coneAxisZ, cosThetaO] // emission orientation cone (Conty-Kulla)
|
|
12
|
+
*
|
|
13
|
+
* The orientation cone bounds the emission directions of the cluster (θ_o = spread of emitter
|
|
14
|
+
* normals; θ_e = π/2 assumed for diffuse emitters, applied at sample time). cosThetaO = -1 means
|
|
15
|
+
* "whole sphere" (mixed/two-sided cluster → never culled by orientation).
|
|
12
16
|
*
|
|
13
17
|
* Build algorithm: median split on longest centroid AABB axis, maxLeafSize=8.
|
|
14
|
-
* Output: pre-order flattened array with
|
|
18
|
+
* Output: pre-order flattened array with left child immediately after parent.
|
|
19
|
+
* Also emits a per-(sorted)-triangle bit-trail: the sequence of left(0)/right(1) child choices
|
|
20
|
+
* from the root to that triangle's leaf, so the GPU can re-walk the exact descent for MIS.
|
|
15
21
|
*/
|
|
16
22
|
export class LightBVHBuilder {
|
|
17
23
|
|
|
@@ -26,19 +32,22 @@ export class LightBVHBuilder {
|
|
|
26
32
|
*
|
|
27
33
|
* @param {Array} emissiveTriangles - Array of objects:
|
|
28
34
|
* { triangleIndex, power, area, emissive, emissiveIntensity, cx, cy, cz,
|
|
29
|
-
* bMinX, bMinY, bMinZ, bMaxX, bMaxY, bMaxZ }
|
|
30
|
-
* @returns {{ nodeData: Float32Array, nodeCount: number, sortedPerm: Int32Array }}
|
|
35
|
+
* bMinX, bMinY, bMinZ, bMaxX, bMaxY, bMaxZ, nx, ny, nz, twoSided }
|
|
36
|
+
* @returns {{ nodeData: Float32Array, nodeCount: number, sortedPerm: Int32Array, bitTrails: Float32Array }}
|
|
31
37
|
* sortedPerm[i] = original index in emissiveTriangles for position i in sorted leaf order
|
|
38
|
+
* bitTrails[i] = root→leaf bit-trail for the triangle now at sorted position i (as a float-encoded int)
|
|
32
39
|
*/
|
|
33
40
|
build( emissiveTriangles ) {
|
|
34
41
|
|
|
35
42
|
const n = emissiveTriangles.length;
|
|
36
43
|
if ( n === 0 ) {
|
|
37
44
|
|
|
38
|
-
// Dummy leaf node
|
|
45
|
+
// Dummy leaf node — whole-sphere cone so importance never culls it
|
|
39
46
|
const nodeData = new Float32Array( 16 );
|
|
40
47
|
nodeData[ 7 ] = 1.0; // isLeaf
|
|
41
|
-
|
|
48
|
+
nodeData[ 14 ] = 1.0; // cone axis z
|
|
49
|
+
nodeData[ 15 ] = - 1.0; // cosThetaO = whole sphere
|
|
50
|
+
return { nodeData, nodeCount: 1, sortedPerm: new Int32Array( 0 ), bitTrails: new Float32Array( 0 ) };
|
|
42
51
|
|
|
43
52
|
}
|
|
44
53
|
|
|
@@ -52,8 +61,11 @@ export class LightBVHBuilder {
|
|
|
52
61
|
const nodeData = new Float32Array( maxNodes * 16 );
|
|
53
62
|
let nodeCount = 0;
|
|
54
63
|
|
|
55
|
-
//
|
|
56
|
-
const
|
|
64
|
+
// bit-trail per sorted position (filled at leaves). 24-bit safe → tree depth < 24 (≈16M leaf clusters).
|
|
65
|
+
const bitTrails = new Float32Array( n );
|
|
66
|
+
|
|
67
|
+
// Recursively build; returns { nodeIndex, cone }. `trail`/`depth` accumulate the root→leaf path.
|
|
68
|
+
const buildRecursive = ( start, end, trail, depth ) => {
|
|
57
69
|
|
|
58
70
|
const nodeIndex = nodeCount ++;
|
|
59
71
|
const nodeOffset = nodeIndex * 16;
|
|
@@ -95,16 +107,11 @@ export class LightBVHBuilder {
|
|
|
95
107
|
nodeData[ nodeOffset + 4 ] = maxX;
|
|
96
108
|
nodeData[ nodeOffset + 5 ] = maxY;
|
|
97
109
|
nodeData[ nodeOffset + 6 ] = maxZ;
|
|
98
|
-
// nodeData[nodeOffset + 7] = isLeaf — set below
|
|
99
|
-
|
|
100
|
-
// vec4[3]: zeros (reserved)
|
|
101
|
-
nodeData[ nodeOffset + 12 ] = 0;
|
|
102
|
-
nodeData[ nodeOffset + 13 ] = 0;
|
|
103
|
-
nodeData[ nodeOffset + 14 ] = 0;
|
|
104
|
-
nodeData[ nodeOffset + 15 ] = 0;
|
|
105
110
|
|
|
106
111
|
const count = end - start;
|
|
107
112
|
|
|
113
|
+
let cone;
|
|
114
|
+
|
|
108
115
|
if ( count <= this.maxLeafSize ) {
|
|
109
116
|
|
|
110
117
|
// LEAF NODE
|
|
@@ -114,6 +121,18 @@ export class LightBVHBuilder {
|
|
|
114
121
|
nodeData[ nodeOffset + 10 ] = 0;
|
|
115
122
|
nodeData[ nodeOffset + 11 ] = 0;
|
|
116
123
|
|
|
124
|
+
// Cone = union of this leaf's triangle emission cones; write this leaf's bit-trail
|
|
125
|
+
cone = null;
|
|
126
|
+
for ( let i = start; i < end; i ++ ) {
|
|
127
|
+
|
|
128
|
+
const tri = emissiveTriangles[ indices[ i ] ];
|
|
129
|
+
cone = coneUnion( cone, triangleCone( tri ) );
|
|
130
|
+
bitTrails[ i ] = trail;
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if ( ! cone ) cone = { ax: 0, ay: 0, az: 1, cosO: - 1 };
|
|
135
|
+
|
|
117
136
|
} else {
|
|
118
137
|
|
|
119
138
|
// INNER NODE — find longest centroid axis and split at median
|
|
@@ -135,24 +154,31 @@ export class LightBVHBuilder {
|
|
|
135
154
|
|
|
136
155
|
nodeData[ nodeOffset + 7 ] = 0.0; // isLeaf = false (inner)
|
|
137
156
|
|
|
138
|
-
// Build
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const rightChildIdx = buildRecursive( mid, end );
|
|
157
|
+
// Build left child immediately after this node (pre-order), then right.
|
|
158
|
+
// Trail bit at `depth`: 0 for left, 1 for right.
|
|
159
|
+
const left = buildRecursive( start, mid, trail, depth + 1 );
|
|
160
|
+
const right = buildRecursive( mid, end, trail + Math.pow( 2, depth ), depth + 1 );
|
|
143
161
|
|
|
144
|
-
nodeData[ nodeOffset + 8 ] =
|
|
145
|
-
nodeData[ nodeOffset + 9 ] =
|
|
162
|
+
nodeData[ nodeOffset + 8 ] = left.nodeIndex;
|
|
163
|
+
nodeData[ nodeOffset + 9 ] = right.nodeIndex;
|
|
146
164
|
nodeData[ nodeOffset + 10 ] = 0;
|
|
147
165
|
nodeData[ nodeOffset + 11 ] = 0;
|
|
148
166
|
|
|
167
|
+
cone = coneUnion( left.cone, right.cone );
|
|
168
|
+
|
|
149
169
|
}
|
|
150
170
|
|
|
151
|
-
|
|
171
|
+
// vec4[3]: [coneAxisX, coneAxisY, coneAxisZ, cosThetaO]
|
|
172
|
+
nodeData[ nodeOffset + 12 ] = cone.ax;
|
|
173
|
+
nodeData[ nodeOffset + 13 ] = cone.ay;
|
|
174
|
+
nodeData[ nodeOffset + 14 ] = cone.az;
|
|
175
|
+
nodeData[ nodeOffset + 15 ] = cone.cosO;
|
|
176
|
+
|
|
177
|
+
return { nodeIndex, cone };
|
|
152
178
|
|
|
153
179
|
};
|
|
154
180
|
|
|
155
|
-
buildRecursive( 0, n );
|
|
181
|
+
buildRecursive( 0, n, 0, 0 );
|
|
156
182
|
|
|
157
183
|
// sortedPerm: the rearranged indices array (leaf order)
|
|
158
184
|
const sortedPerm = new Int32Array( n );
|
|
@@ -164,7 +190,7 @@ export class LightBVHBuilder {
|
|
|
164
190
|
|
|
165
191
|
console.log( `[LightBVHBuilder] Built BVH: ${nodeCount} nodes for ${n} emissive triangles` );
|
|
166
192
|
|
|
167
|
-
return { nodeData: trimmedData, nodeCount, sortedPerm };
|
|
193
|
+
return { nodeData: trimmedData, nodeCount, sortedPerm, bitTrails };
|
|
168
194
|
|
|
169
195
|
}
|
|
170
196
|
|
|
@@ -217,3 +243,63 @@ export class LightBVHBuilder {
|
|
|
217
243
|
}
|
|
218
244
|
|
|
219
245
|
}
|
|
246
|
+
|
|
247
|
+
// ================================================================================
|
|
248
|
+
// ORIENTATION CONE HELPERS (Conty-Estevez & Kulla 2018 / PBRT-v4 DirectionCone)
|
|
249
|
+
// ================================================================================
|
|
250
|
+
|
|
251
|
+
// Per-triangle emission cone: axis = geometric normal, θ_o = 0 (single direction).
|
|
252
|
+
// Two-sided emitters emit into both hemispheres → whole sphere (cosO = -1).
|
|
253
|
+
function triangleCone( tri ) {
|
|
254
|
+
|
|
255
|
+
if ( tri.twoSided ) return { ax: tri.nx, ay: tri.ny, az: tri.nz, cosO: - 1 };
|
|
256
|
+
return { ax: tri.nx, ay: tri.ny, az: tri.nz, cosO: 1 };
|
|
257
|
+
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Union of two direction cones (PBRT-v4 DirectionCone::Union). cosO === -1 ⇒ whole sphere.
|
|
261
|
+
function coneUnion( a, b ) {
|
|
262
|
+
|
|
263
|
+
if ( ! a ) return b;
|
|
264
|
+
if ( ! b ) return a;
|
|
265
|
+
if ( a.cosO <= - 1 ) return a; // a is already whole sphere
|
|
266
|
+
if ( b.cosO <= - 1 ) return b;
|
|
267
|
+
|
|
268
|
+
const cosA = Math.min( Math.max( a.cosO, - 1 ), 1 );
|
|
269
|
+
const cosB = Math.min( Math.max( b.cosO, - 1 ), 1 );
|
|
270
|
+
const thetaA = Math.acos( cosA );
|
|
271
|
+
const thetaB = Math.acos( cosB );
|
|
272
|
+
const dotAB = Math.min( Math.max( a.ax * b.ax + a.ay * b.ay + a.az * b.az, - 1 ), 1 );
|
|
273
|
+
const thetaD = Math.acos( dotAB );
|
|
274
|
+
|
|
275
|
+
// One cone already contains the other
|
|
276
|
+
if ( Math.min( thetaD + thetaB, Math.PI ) <= thetaA ) return a;
|
|
277
|
+
if ( Math.min( thetaD + thetaA, Math.PI ) <= thetaB ) return b;
|
|
278
|
+
|
|
279
|
+
const thetaO = ( thetaA + thetaB + thetaD ) * 0.5;
|
|
280
|
+
if ( thetaO >= Math.PI ) return { ax: a.ax, ay: a.ay, az: a.az, cosO: - 1 };
|
|
281
|
+
|
|
282
|
+
const thetaR = thetaO - thetaA;
|
|
283
|
+
|
|
284
|
+
// Rotation axis = normalize(cross(a, b))
|
|
285
|
+
let wx = a.ay * b.az - a.az * b.ay;
|
|
286
|
+
let wy = a.az * b.ax - a.ax * b.az;
|
|
287
|
+
let wz = a.ax * b.ay - a.ay * b.ax;
|
|
288
|
+
const wl = Math.sqrt( wx * wx + wy * wy + wz * wz );
|
|
289
|
+
if ( wl < 1e-8 ) return { ax: a.ax, ay: a.ay, az: a.az, cosO: - 1 }; // (anti)parallel → whole sphere
|
|
290
|
+
wx /= wl; wy /= wl; wz /= wl;
|
|
291
|
+
|
|
292
|
+
// Rodrigues: rotate a.axis around (wx,wy,wz) by thetaR
|
|
293
|
+
const c = Math.cos( thetaR ), s = Math.sin( thetaR );
|
|
294
|
+
const kdotv = wx * a.ax + wy * a.ay + wz * a.az;
|
|
295
|
+
const cx = wy * a.az - wz * a.ay;
|
|
296
|
+
const cy = wz * a.ax - wx * a.az;
|
|
297
|
+
const cz = wx * a.ay - wy * a.ax;
|
|
298
|
+
const ax = a.ax * c + cx * s + wx * kdotv * ( 1 - c );
|
|
299
|
+
const ay = a.ay * c + cy * s + wy * kdotv * ( 1 - c );
|
|
300
|
+
const az = a.az * c + cz * s + wz * kdotv * ( 1 - c );
|
|
301
|
+
const al = Math.sqrt( ax * ax + ay * ay + az * az ) || 1;
|
|
302
|
+
|
|
303
|
+
return { ax: ax / al, ay: ay / al, az: az / al, cosO: Math.cos( thetaO ) };
|
|
304
|
+
|
|
305
|
+
}
|
|
@@ -90,6 +90,7 @@ export class SceneProcessor {
|
|
|
90
90
|
this.emissiveTriangleCount = 0;
|
|
91
91
|
this.lightBVHNodeData = null;
|
|
92
92
|
this.lightBVHNodeCount = 0;
|
|
93
|
+
this.emissiveBitTrailMap = null;
|
|
93
94
|
|
|
94
95
|
// Initialize processing components
|
|
95
96
|
this._initProcessors();
|
|
@@ -818,6 +819,8 @@ export class SceneProcessor {
|
|
|
818
819
|
this.lightBVHNodeCount = this.emissiveTriangleBuilder.lightBVHNodeCount;
|
|
819
820
|
// Replace emissiveTriangleData with sorted version (LBVH reorders it)
|
|
820
821
|
this.emissiveTriangleData = this.emissiveTriangleBuilder.emissiveTriangleData || this.emissiveTriangleData;
|
|
822
|
+
// Per-triangle bit-trail map for the bounce-hit MIS re-walk
|
|
823
|
+
this.emissiveBitTrailMap = this.emissiveTriangleBuilder.emissiveBitTrailMap;
|
|
821
824
|
|
|
822
825
|
}
|
|
823
826
|
|
|
@@ -872,6 +875,7 @@ export class SceneProcessor {
|
|
|
872
875
|
this.instanceTable = null;
|
|
873
876
|
this.lightBVHNodeData = null;
|
|
874
877
|
this.lightBVHNodeCount = 0;
|
|
878
|
+
this.emissiveBitTrailMap = null;
|
|
875
879
|
|
|
876
880
|
// Reset performance metrics
|
|
877
881
|
this.performanceMetrics = {
|
|
@@ -1406,6 +1410,7 @@ export class SceneProcessor {
|
|
|
1406
1410
|
this.emissiveTriangleData,
|
|
1407
1411
|
this.emissiveTriangleCount,
|
|
1408
1412
|
this.emissiveTotalPower,
|
|
1413
|
+
this.emissiveBitTrailMap,
|
|
1409
1414
|
);
|
|
1410
1415
|
|
|
1411
1416
|
}
|
|
@@ -1450,10 +1455,23 @@ export class SceneProcessor {
|
|
|
1450
1455
|
|
|
1451
1456
|
if ( ! changed ) return null;
|
|
1452
1457
|
|
|
1458
|
+
// Rebuild the Light BVH + sorted emissive data + bit-trail map so the stochastic descent and
|
|
1459
|
+
// the bounce-hit MIS re-walk stay consistent after powers / the emissive set change.
|
|
1460
|
+
this.emissiveTriangleBuilder.buildLightBVH();
|
|
1461
|
+
this.lightBVHNodeData = this.emissiveTriangleBuilder.lightBVHNodeData;
|
|
1462
|
+
this.lightBVHNodeCount = this.emissiveTriangleBuilder.lightBVHNodeCount;
|
|
1463
|
+
this.emissiveTriangleData = this.emissiveTriangleBuilder.emissiveTriangleData;
|
|
1464
|
+
this.emissiveBitTrailMap = this.emissiveTriangleBuilder.emissiveBitTrailMap;
|
|
1465
|
+
this.emissiveTriangleCount = this.emissiveTriangleBuilder.emissiveCount;
|
|
1466
|
+
this.emissiveTotalPower = this.emissiveTriangleBuilder.totalEmissivePower;
|
|
1467
|
+
|
|
1453
1468
|
return {
|
|
1454
|
-
rawData: this.emissiveTriangleBuilder.
|
|
1469
|
+
rawData: this.emissiveTriangleBuilder.emissiveTriangleData,
|
|
1455
1470
|
emissiveCount: this.emissiveTriangleBuilder.emissiveCount,
|
|
1456
1471
|
totalPower: this.emissiveTriangleBuilder.totalEmissivePower,
|
|
1472
|
+
bitTrailMap: this.emissiveBitTrailMap,
|
|
1473
|
+
lightBVHNodeData: this.lightBVHNodeData,
|
|
1474
|
+
lightBVHNodeCount: this.lightBVHNodeCount,
|
|
1457
1475
|
};
|
|
1458
1476
|
|
|
1459
1477
|
}
|
package/src/Stages/ASVGF.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Fn, vec3, vec4, float, int, uint, ivec2, uvec2, uniform,
|
|
2
|
-
If, dot, max, min, abs, mix, pow, exp, sqrt,
|
|
2
|
+
If, dot, max, min, abs, mix, pow, exp, sqrt, select,
|
|
3
3
|
textureLoad, textureStore, workgroupArray, workgroupBarrier, localId, workgroupId } from 'three/tsl';
|
|
4
4
|
import { RenderTarget, TextureNode, StorageTexture } from 'three/webgpu';
|
|
5
5
|
import { HalfFloatType, FloatType, RGBAFormat, NearestFilter, LinearFilter, Box2, Vector2 } from 'three';
|
|
@@ -7,6 +7,11 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
7
7
|
import { luminance } from '../TSL/Common.js';
|
|
8
8
|
import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
9
9
|
|
|
10
|
+
// Replace NaN/±Inf with a bounded value so one firefly can't permanently poison the
|
|
11
|
+
// temporal EMA (mix() propagates NaN forever). Per-channel: NaN (x!=x) → 0, ±Inf → [0,1e7].
|
|
12
|
+
const sanitize1 = ( x ) => select( x.equal( x ), x, float( 0.0 ) ).clamp( 0.0, 1e7 );
|
|
13
|
+
const sanitizeRGB = ( c ) => vec3( sanitize1( c.x ), sanitize1( c.y ), sanitize1( c.z ) );
|
|
14
|
+
|
|
10
15
|
/**
|
|
11
16
|
* ASVGF — SVGF temporal denoising with albedo demodulation + adaptive-α.
|
|
12
17
|
*
|
|
@@ -15,8 +20,8 @@ import { ALBEDO_EPS, MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
|
15
20
|
* floored by a per-pixel σ (max(Δ − k·σ, 0)), max-normalised and squared
|
|
16
21
|
* (Q2RTX get_gradient). gradientStrength scales it into effectiveAlpha =
|
|
17
22
|
* mix(baseAlpha, 1, gradient·gradientStrength) — high change ⇒ drop history
|
|
18
|
-
* (anti-lag).
|
|
19
|
-
*
|
|
23
|
+
* (anti-lag). All quality presets enable it (gradientStrength > 0); a static
|
|
24
|
+
* scene reads gradient ≈ 0, so convergence is unaffected.
|
|
20
25
|
*
|
|
21
26
|
* Reads: pathtracer:color, pathtracer:albedo, pathtracer:normalDepth,
|
|
22
27
|
* pathtracer:prevNormalDepth, motionVector:screenSpace
|
|
@@ -46,6 +51,9 @@ export class ASVGF extends RenderStage {
|
|
|
46
51
|
this.resH = uniform( options.height || 1 );
|
|
47
52
|
|
|
48
53
|
this.temporalEnabledU = uniform( 1.0 );
|
|
54
|
+
// 1.0 for one frame after asvgf:reset → forces a fresh sample so stale pre-reset
|
|
55
|
+
// (wrong-scene) history isn't blended in (mirrors Variance's _needsWarmReset).
|
|
56
|
+
this.forceResetU = uniform( 0.0 );
|
|
49
57
|
|
|
50
58
|
this._colorTexNode = new TextureNode();
|
|
51
59
|
this._albedoTexNode = new TextureNode();
|
|
@@ -118,6 +126,7 @@ export class ASVGF extends RenderStage {
|
|
|
118
126
|
|
|
119
127
|
this.currentMoments = 0; // 0 = write A, read B; 1 = write B, read A
|
|
120
128
|
this._compiled = false;
|
|
129
|
+
this._needsWarmReset = false;
|
|
121
130
|
|
|
122
131
|
this._dispatchX = Math.ceil( w / 8 );
|
|
123
132
|
this._dispatchY = Math.ceil( h / 8 );
|
|
@@ -207,7 +216,7 @@ export class ASVGF extends RenderStage {
|
|
|
207
216
|
// safeAlbedo = max(albedo, ALBEDO_EPS) keeps sky/dark-material round-trip).
|
|
208
217
|
const curColor = textureLoad( colorTex, ivec2( gxL, gyL ) ).xyz;
|
|
209
218
|
const curAlbedo = textureLoad( albedoTex, ivec2( gxL, gyL ) ).xyz;
|
|
210
|
-
const curLighting = curColor.div( max( curAlbedo, vec3( ALBEDO_EPS ) ) );
|
|
219
|
+
const curLighting = sanitizeRGB( curColor.div( max( curAlbedo, vec3( ALBEDO_EPS ) ) ) );
|
|
211
220
|
const curLum = luminance( curLighting ).toVar();
|
|
212
221
|
sharedCurLum.element( k ).assign( curLum );
|
|
213
222
|
|
|
@@ -217,8 +226,10 @@ export class ASVGF extends RenderStage {
|
|
|
217
226
|
const motion = textureLoad( motionTex, ivec2( gxL, gyL ) );
|
|
218
227
|
const prevXf = float( gxL ).sub( motion.x.mul( resW ) );
|
|
219
228
|
const prevYf = float( gyL ).sub( motion.y.mul( resH ) );
|
|
220
|
-
|
|
221
|
-
|
|
229
|
+
// Round-to-nearest (not truncate) — the gradient does a nearest-neighbour
|
|
230
|
+
// history lookup; +0.5 removes the half-pixel floor bias vs the temporal tap.
|
|
231
|
+
const prevX = int( prevXf.add( 0.5 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
232
|
+
const prevY = int( prevYf.add( 0.5 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
222
233
|
const motionValid = motion.w.greaterThan( 0.5 );
|
|
223
234
|
const histLighting = textureLoad( histTex, ivec2( prevX, prevY ) ).xyz;
|
|
224
235
|
const histLum = motionValid.select( luminance( histLighting ), curLum );
|
|
@@ -303,8 +314,8 @@ export class ASVGF extends RenderStage {
|
|
|
303
314
|
// Trust the gradient only once enough history has accumulated at the
|
|
304
315
|
// reprojected centre — early frames have noisy history → false fires.
|
|
305
316
|
const cMotion = textureLoad( motionTex, ivec2( gx, gy ) );
|
|
306
|
-
const cPrevX = int( float( gx ).sub( cMotion.x.mul( resW ) ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
307
|
-
const cPrevY = int( float( gy ).sub( cMotion.y.mul( resH ) ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
317
|
+
const cPrevX = int( float( gx ).sub( cMotion.x.mul( resW ) ).add( 0.5 ) ).clamp( int( 0 ), int( resW ).sub( 1 ) );
|
|
318
|
+
const cPrevY = int( float( gy ).sub( cMotion.y.mul( resH ) ).add( 0.5 ) ).clamp( int( 0 ), int( resH ).sub( 1 ) );
|
|
308
319
|
const histLen = cMotion.w.greaterThan( 0.5 )
|
|
309
320
|
.select( textureLoad( histTex, ivec2( cPrevX, cPrevY ) ).w, float( 0.0 ) );
|
|
310
321
|
const confidence = histLen.div( 4.0 ).clamp( 0.0, 1.0 );
|
|
@@ -355,6 +366,7 @@ export class ASVGF extends RenderStage {
|
|
|
355
366
|
const temporalAlphaMin = this.temporalAlpha;
|
|
356
367
|
const gradientStrength = this.gradientStrength;
|
|
357
368
|
const temporalEnabledU = this.temporalEnabledU;
|
|
369
|
+
const forceResetU = this.forceResetU;
|
|
358
370
|
const resW = this.resW;
|
|
359
371
|
const resH = this.resH;
|
|
360
372
|
|
|
@@ -374,23 +386,28 @@ export class ASVGF extends RenderStage {
|
|
|
374
386
|
// Same safeAlbedo on both demod and re-mod sides → exact
|
|
375
387
|
// round-trip for sky/miss rays where albedo=0.
|
|
376
388
|
const safeAlbedo = max( currentAlbedo, vec3( ALBEDO_EPS ) );
|
|
377
|
-
const currentLighting = currentColor.div( safeAlbedo );
|
|
389
|
+
const currentLighting = sanitizeRGB( currentColor.div( safeAlbedo ) );
|
|
378
390
|
|
|
379
391
|
// Defaults = fresh sample (no temporal blend).
|
|
380
392
|
const demodResult = vec4( currentLighting, 1.0 ).toVar();
|
|
381
393
|
const modulatedResult = vec4( currentColor, 1.0 ).toVar();
|
|
382
394
|
|
|
383
|
-
|
|
395
|
+
// forceResetU skips the blend for one frame after asvgf:reset → re-anchors
|
|
396
|
+
// history to the current (post-reset) scene instead of the stale ping-pong.
|
|
397
|
+
If( temporalEnabledU.greaterThan( 0.5 ).and( forceResetU.lessThan( 0.5 ) ), () => {
|
|
384
398
|
|
|
385
399
|
const motion = textureLoad( motionTex, coord );
|
|
386
400
|
const motionValid = motion.w.greaterThan( 0.5 );
|
|
387
401
|
|
|
388
402
|
const prevXf = float( gx ).sub( motion.x.mul( resW ) );
|
|
389
403
|
const prevYf = float( gy ).sub( motion.y.mul( resH ) );
|
|
404
|
+
// Upper bound is the inclusive last pixel (< res, not < res-1): the 2×2 taps
|
|
405
|
+
// already clamp to [0,res-1] and wSum gates bad taps, so res-1 wrongly
|
|
406
|
+
// rejected the trailing column/row. Matches MotionVector's inclusive UV.
|
|
390
407
|
const prevOnScreen = prevXf.greaterThanEqual( 0.0 )
|
|
391
|
-
.and( prevXf.lessThan( float( resW )
|
|
408
|
+
.and( prevXf.lessThan( float( resW ) ) )
|
|
392
409
|
.and( prevYf.greaterThanEqual( 0.0 ) )
|
|
393
|
-
.and( prevYf.lessThan( float( resH )
|
|
410
|
+
.and( prevYf.lessThan( float( resH ) ) );
|
|
394
411
|
|
|
395
412
|
If( motionValid.and( prevOnScreen ), () => {
|
|
396
413
|
|
|
@@ -458,7 +475,7 @@ export class ASVGF extends RenderStage {
|
|
|
458
475
|
.add( p11.w.mul( v11 ) )
|
|
459
476
|
.mul( invWSum );
|
|
460
477
|
|
|
461
|
-
// adaptive α —
|
|
478
|
+
// adaptive α — gradient·gradientStrength boosts toward 1 on change.
|
|
462
479
|
const gradient = textureLoad( gradientTex, coord ).x;
|
|
463
480
|
const adaptiveBoost = gradient.mul( gradientStrength ).clamp( 0.0, 1.0 );
|
|
464
481
|
|
|
@@ -468,7 +485,9 @@ export class ASVGF extends RenderStage {
|
|
|
468
485
|
);
|
|
469
486
|
const effectiveAlpha = mix( baseAlpha, float( 1.0 ), adaptiveBoost );
|
|
470
487
|
|
|
471
|
-
|
|
488
|
+
// Sanitize the blend too: a NaN already baked into prevLighting
|
|
489
|
+
// would otherwise survive mix() and re-poison the write target.
|
|
490
|
+
const blendedLighting = sanitizeRGB( mix( prevLighting, currentLighting, effectiveAlpha ) );
|
|
472
491
|
const newHistory = min( prevHistory.add( 1.0 ), maxAccumFrames );
|
|
473
492
|
|
|
474
493
|
demodResult.assign( vec4( blendedLighting, newHistory ) );
|
|
@@ -701,7 +720,11 @@ export class ASVGF extends RenderStage {
|
|
|
701
720
|
|
|
702
721
|
}
|
|
703
722
|
|
|
723
|
+
// One-shot fresh re-anchor after asvgf:reset, threaded through the SINGLE
|
|
724
|
+
// writeNode dispatch (a dual-node warmup would alias the read target — see setSize).
|
|
725
|
+
this.forceResetU.value = this._needsWarmReset ? 1.0 : 0.0;
|
|
704
726
|
this.renderer.compute( writeNode );
|
|
727
|
+
this._needsWarmReset = false;
|
|
705
728
|
|
|
706
729
|
// Copy active region out of the over-allocated StorageTextures into
|
|
707
730
|
// right-sized RTs; downstream stages UV-sample these.
|
|
@@ -767,6 +790,9 @@ export class ASVGF extends RenderStage {
|
|
|
767
790
|
resetTemporalData() {
|
|
768
791
|
|
|
769
792
|
this.currentMoments = 0;
|
|
793
|
+
// Re-anchor history on the next frame — the ping-pong textures still hold
|
|
794
|
+
// pre-reset (wrong-scene) lighting + history count, which would otherwise blend in.
|
|
795
|
+
this._needsWarmReset = true;
|
|
770
796
|
|
|
771
797
|
}
|
|
772
798
|
|