rayzee 7.21.0 → 7.22.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 +75 -4
- package/dist/rayzee.es.js +634 -604
- package/dist/rayzee.umd.js +52 -52
- package/package.json +1 -1
- package/src/EngineDefaults.js +6 -0
- package/src/PathTracerApp.js +76 -0
- package/src/Processor/BVHBuilder.js +4 -6
- package/src/Processor/KernelManager.js +17 -70
- package/src/Processor/QueueManager.js +4 -3
- package/src/Processor/SceneProcessor.js +12 -4
- package/src/Processor/TreeletOptimizer.js +0 -7
- package/src/RenderSettings.js +41 -6
- package/src/Stages/NormalDepth.js +16 -13
- package/src/Stages/PathTracer.js +29 -31
- package/src/TSL/BVHTraversal.js +1 -67
- package/src/TSL/CameraRay.js +141 -0
- package/src/TSL/DebugKernel.js +4 -5
- package/src/TSL/ExtendKernel.js +11 -3
- package/src/TSL/FinalWriteKernel.js +1 -14
- package/src/TSL/GenerateKernel.js +11 -8
- package/src/TSL/SortGlobalKernels.js +52 -7
- package/src/managers/DenoisingManager.js +13 -0
- package/src/managers/EnvironmentManager.js +6 -3
- package/src/managers/UniformManager.js +8 -1
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -62,6 +62,12 @@ export const ENGINE_DEFAULTS = {
|
|
|
62
62
|
skyMieAnisotropy: 0.76,
|
|
63
63
|
skyPreset: 'clearMorning',
|
|
64
64
|
|
|
65
|
+
// Camera projection — 'perspective' | 'equirectangular'. Panorama ranges are UI-facing degrees.
|
|
66
|
+
cameraProjection: 'perspective',
|
|
67
|
+
panoramaLonRange: [ - 180, 180 ],
|
|
68
|
+
panoramaLatRange: [ - 90, 90 ],
|
|
69
|
+
panoramaLevelHorizon: true,
|
|
70
|
+
|
|
65
71
|
enableDOF: false,
|
|
66
72
|
fov: 55,
|
|
67
73
|
focusDistance: 0.8,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -1722,6 +1722,80 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1722
1722
|
|
|
1723
1723
|
}
|
|
1724
1724
|
|
|
1725
|
+
/**
|
|
1726
|
+
* Per-kernel GPU milliseconds for the last resolved frame.
|
|
1727
|
+
*
|
|
1728
|
+
* {@link PathTracerApp#getGPUTimings} only reports the frame aggregate, which buries a change to
|
|
1729
|
+
* one kernel under everything else. The backend's timestamp query pool already retains one
|
|
1730
|
+
* duration per compute pass, keyed `c:<frameCalls>:<nodeId>:f<frame>` (three.js
|
|
1731
|
+
* `WebGPUTimestampQueryPool.resolveQueriesAsync` → `timestamps`); this attributes those back to
|
|
1732
|
+
* kernel names through `KernelManager`'s registry.
|
|
1733
|
+
*
|
|
1734
|
+
* Durations are SUMMED per kernel across the frame, so `extend` reports its whole per-frame cost
|
|
1735
|
+
* over every bounce iteration rather than one bounce. `unattributed` collects passes belonging to
|
|
1736
|
+
* no registered kernel (other stages, denoisers) so `sum(kernels) + unattributed` reconciles with
|
|
1737
|
+
* `total` — a gap there means a pass was missed, not that a kernel is free.
|
|
1738
|
+
*
|
|
1739
|
+
* The pool's map accumulates across frames and is never pruned, hence the newest-frame filter.
|
|
1740
|
+
*
|
|
1741
|
+
* Requires {@link PathTracerApp#enableGPUTiming}. Returns null when timestamps are unavailable.
|
|
1742
|
+
*
|
|
1743
|
+
* @returns {Promise<{kernels: Object<string, number>, total: number, unattributed: number, frame: number}|null>}
|
|
1744
|
+
*/
|
|
1745
|
+
async getKernelGPUTimings() {
|
|
1746
|
+
|
|
1747
|
+
const renderer = this.renderer;
|
|
1748
|
+
if ( ! renderer?.backend?.trackTimestamp ) return null;
|
|
1749
|
+
|
|
1750
|
+
await renderer.resolveTimestampsAsync( 'compute' );
|
|
1751
|
+
|
|
1752
|
+
const timestamps = renderer.backend.timestampQueryPool?.compute?.timestamps;
|
|
1753
|
+
if ( ! timestamps || timestamps.size === 0 ) return null;
|
|
1754
|
+
|
|
1755
|
+
const nameByNodeId = new Map();
|
|
1756
|
+
const kernelMap = this.stages?.pathTracer?._kernelManager?.kernels;
|
|
1757
|
+
if ( kernelMap ) {
|
|
1758
|
+
|
|
1759
|
+
for ( const [ name, node ] of kernelMap ) nameByNodeId.set( node.id, name );
|
|
1760
|
+
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
const parsed = [];
|
|
1764
|
+
let frame = - 1;
|
|
1765
|
+
|
|
1766
|
+
for ( const [ uid, ms ] of timestamps ) {
|
|
1767
|
+
|
|
1768
|
+
// 'c:<frameCalls>:<nodeId>:f<frame>'
|
|
1769
|
+
const parts = uid.split( ':' );
|
|
1770
|
+
if ( parts.length !== 4 ) continue;
|
|
1771
|
+
|
|
1772
|
+
const f = Number( parts[ 3 ].slice( 1 ) );
|
|
1773
|
+
if ( ! Number.isFinite( f ) ) continue;
|
|
1774
|
+
|
|
1775
|
+
parsed.push( { nodeId: Number( parts[ 2 ] ), f, ms } );
|
|
1776
|
+
if ( f > frame ) frame = f;
|
|
1777
|
+
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const kernels = {};
|
|
1781
|
+
let total = 0;
|
|
1782
|
+
let unattributed = 0;
|
|
1783
|
+
|
|
1784
|
+
for ( const entry of parsed ) {
|
|
1785
|
+
|
|
1786
|
+
if ( entry.f !== frame ) continue;
|
|
1787
|
+
|
|
1788
|
+
total += entry.ms;
|
|
1789
|
+
const name = nameByNodeId.get( entry.nodeId );
|
|
1790
|
+
if ( name === undefined ) unattributed += entry.ms;
|
|
1791
|
+
else kernels[ name ] = ( kernels[ name ] ?? 0 ) + entry.ms;
|
|
1792
|
+
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
return { kernels, total, unattributed, frame };
|
|
1796
|
+
|
|
1797
|
+
}
|
|
1798
|
+
|
|
1725
1799
|
// ═══════════════════════════════════════════════════════════════
|
|
1726
1800
|
// Output (absorbed from OutputAPI)
|
|
1727
1801
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2298,6 +2372,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2298
2372
|
renderer: this.renderer,
|
|
2299
2373
|
resetCallback: () => this.reset(),
|
|
2300
2374
|
reconcileCompletion: () => this._reconcileCompletion(),
|
|
2375
|
+
denoisingManager: this.denoisingManager,
|
|
2376
|
+
cameraManager: this.cameraManager,
|
|
2301
2377
|
} );
|
|
2302
2378
|
|
|
2303
2379
|
this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
|
|
@@ -96,13 +96,12 @@ export class BVHBuilder {
|
|
|
96
96
|
reinsertionIterations: 0
|
|
97
97
|
};
|
|
98
98
|
|
|
99
|
-
//
|
|
100
|
-
|
|
99
|
+
// Off: repairs LBVH trees, so on this binned-SAH build it buys 0.005% tree SAH for 72%
|
|
100
|
+
// of build time.
|
|
101
|
+
this.enableTreeletOptimization = false;
|
|
101
102
|
this.treeletSize = 5;
|
|
102
103
|
this.treeletOptimizationPasses = 1;
|
|
103
104
|
this.treeletMinImprovement = 0.02;
|
|
104
|
-
this.maxTreeletDepth = 3;
|
|
105
|
-
this.maxTreeletsPerScene = 20;
|
|
106
105
|
this.treeletComplexityThreshold = 50000;
|
|
107
106
|
|
|
108
107
|
// Reinsertion optimization configuration
|
|
@@ -191,6 +190,7 @@ export class BVHBuilder {
|
|
|
191
190
|
if ( config.size !== undefined ) this.treeletSize = Math.max( 3, Math.min( 12, config.size ) );
|
|
192
191
|
if ( config.passes !== undefined ) this.treeletOptimizationPasses = Math.max( 1, Math.min( 3, config.passes ) );
|
|
193
192
|
if ( config.minImprovement !== undefined ) this.treeletMinImprovement = Math.max( 0.001, config.minImprovement );
|
|
193
|
+
if ( config.complexityThreshold !== undefined ) this.treeletComplexityThreshold = config.complexityThreshold;
|
|
194
194
|
|
|
195
195
|
}
|
|
196
196
|
|
|
@@ -595,12 +595,10 @@ export class BVHBuilder {
|
|
|
595
595
|
|
|
596
596
|
const isLargeScene = this.totalTriangles > this.treeletComplexityThreshold;
|
|
597
597
|
const adaptiveTreeletSize = isLargeScene ? 3 : this.treeletSize;
|
|
598
|
-
const adaptiveMaxTreelets = isLargeScene ? 10 : this.maxTreeletsPerScene;
|
|
599
598
|
|
|
600
599
|
const optimizer = new TreeletOptimizer( this.traversalCost, this.intersectionCost );
|
|
601
600
|
optimizer.setTreeletSize( adaptiveTreeletSize );
|
|
602
601
|
optimizer.setMinImprovement( this.treeletMinImprovement );
|
|
603
|
-
optimizer.setMaxTreelets( adaptiveMaxTreelets );
|
|
604
602
|
|
|
605
603
|
const optimizationStartTime = performance.now();
|
|
606
604
|
|
|
@@ -4,25 +4,14 @@
|
|
|
4
4
|
* Builds, caches, and dispatches individual compute nodes for the wavefront
|
|
5
5
|
* path tracing pipeline. Each kernel is a separate `Fn().compute()` node.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* Dispatch grids are derived from each node's own `workgroupSize`, so the divisor can never
|
|
8
|
+
* drift from the size the kernel was compiled with — see `setDispatchForCount`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
12
12
|
|
|
13
13
|
const log = createLogger( 'wavefront' );
|
|
14
14
|
|
|
15
|
-
/** Default workgroup sizes per kernel type */
|
|
16
|
-
const WORKGROUP_SIZES = {
|
|
17
|
-
generate: [ 16, 16, 1 ], // 2D screen-space
|
|
18
|
-
extend: [ 256, 1, 1 ], // 1D ray-parallel
|
|
19
|
-
shade: [ 256, 1, 1 ], // 1D ray-parallel (material-sorted when sort enabled)
|
|
20
|
-
connect: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
21
|
-
accumulate: [ 256, 1, 1 ], // 1D shadow-ray-parallel
|
|
22
|
-
compact: [ 256, 1, 1 ], // 1D ray-parallel
|
|
23
|
-
finalWrite: [ 16, 16, 1 ], // 2D screen-space
|
|
24
|
-
};
|
|
25
|
-
|
|
26
15
|
export class KernelManager {
|
|
27
16
|
|
|
28
17
|
/**
|
|
@@ -41,12 +30,6 @@ export class KernelManager {
|
|
|
41
30
|
*/
|
|
42
31
|
this.kernels = new Map();
|
|
43
32
|
|
|
44
|
-
/**
|
|
45
|
-
* Map of kernel name → workgroup size [x, y, z].
|
|
46
|
-
* @type {Map<string, number[]>}
|
|
47
|
-
*/
|
|
48
|
-
this.workgroupSizes = new Map();
|
|
49
|
-
|
|
50
33
|
/**
|
|
51
34
|
* Timing data for performance profiling.
|
|
52
35
|
* @type {Map<string, {compiledOnce: boolean, lastDispatchMs: number}>}
|
|
@@ -74,13 +57,6 @@ export class KernelManager {
|
|
|
74
57
|
this._firstDispatches = [];
|
|
75
58
|
this._firstDispatchFlush = null;
|
|
76
59
|
|
|
77
|
-
// Initialize workgroup sizes from defaults
|
|
78
|
-
for ( const [ name, wgSize ] of Object.entries( WORKGROUP_SIZES ) ) {
|
|
79
|
-
|
|
80
|
-
this.workgroupSizes.set( name, wgSize );
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
|
|
84
60
|
}
|
|
85
61
|
|
|
86
62
|
/**
|
|
@@ -177,61 +153,32 @@ export class KernelManager {
|
|
|
177
153
|
}
|
|
178
154
|
|
|
179
155
|
/**
|
|
180
|
-
*
|
|
156
|
+
* Size a 1D kernel's grid to cover `count` items, deriving the divisor from the workgroup size the
|
|
157
|
+
* kernel was actually registered with. Extend/Shade bound on ENTERING_COUNT rather than on the grid,
|
|
158
|
+
* so an under-sized grid silently drops the tail of the active list — never hardcode the divisor.
|
|
181
159
|
* @param {string} name - Kernel name
|
|
182
|
-
* @param {number
|
|
160
|
+
* @param {number} count - Number of items to cover
|
|
183
161
|
*/
|
|
184
|
-
|
|
162
|
+
setDispatchForCount( name, count ) {
|
|
185
163
|
|
|
186
164
|
const node = this.kernels.get( name );
|
|
187
165
|
if ( ! node ) return;
|
|
188
|
-
node.dispatchSize = count;
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Calculate 2D dispatch dimensions for a screen-space kernel.
|
|
194
|
-
* @param {number} width - Render width in pixels
|
|
195
|
-
* @param {number} height - Render height in pixels
|
|
196
|
-
* @param {string} kernelName - Kernel name for WG size lookup
|
|
197
|
-
* @returns {number[]} [dispatchX, dispatchY, 1]
|
|
198
|
-
*/
|
|
199
|
-
calcScreenDispatch( width, height, kernelName ) {
|
|
200
|
-
|
|
201
|
-
const wg = this.workgroupSizes.get( kernelName ) || [ 16, 16, 1 ];
|
|
202
|
-
return [
|
|
203
|
-
Math.ceil( width / wg[ 0 ] ),
|
|
204
|
-
Math.ceil( height / wg[ 1 ] ),
|
|
205
|
-
1
|
|
206
|
-
];
|
|
207
|
-
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Calculate 1D dispatch dimensions for a ray-parallel kernel.
|
|
212
|
-
* @param {number} rayCount - Number of rays to process
|
|
213
|
-
* @param {string} kernelName - Kernel name for WG size lookup
|
|
214
|
-
* @returns {number[]} [dispatchX, 1, 1]
|
|
215
|
-
*/
|
|
216
|
-
calcRayDispatch( rayCount, kernelName ) {
|
|
217
|
-
|
|
218
|
-
const wg = this.workgroupSizes.get( kernelName ) || [ 256, 1, 1 ];
|
|
219
|
-
return [
|
|
220
|
-
Math.ceil( rayCount / wg[ 0 ] ),
|
|
221
|
-
1,
|
|
222
|
-
1
|
|
223
|
-
];
|
|
166
|
+
node.dispatchSize = [ Math.ceil( count / node.workgroupSize[ 0 ] ), 1, 1 ];
|
|
224
167
|
|
|
225
168
|
}
|
|
226
169
|
|
|
227
170
|
/**
|
|
228
|
-
*
|
|
229
|
-
* @param {string} name
|
|
230
|
-
* @
|
|
171
|
+
* Size a 2D screen-space kernel's grid, deriving both divisors from the registered workgroup size.
|
|
172
|
+
* @param {string} name - Kernel name
|
|
173
|
+
* @param {number} width - Width in pixels
|
|
174
|
+
* @param {number} height - Height in pixels
|
|
231
175
|
*/
|
|
232
|
-
|
|
176
|
+
setDispatchForGrid( name, width, height ) {
|
|
233
177
|
|
|
234
|
-
|
|
178
|
+
const node = this.kernels.get( name );
|
|
179
|
+
if ( ! node ) return;
|
|
180
|
+
const wg = node.workgroupSize;
|
|
181
|
+
node.dispatchSize = [ Math.ceil( width / wg[ 0 ] ), Math.ceil( height / wg[ 1 ] ), 1 ];
|
|
235
182
|
|
|
236
183
|
}
|
|
237
184
|
|
|
@@ -17,9 +17,10 @@ export const COUNTER = {
|
|
|
17
17
|
// per-frame count of pixels whose Tier-1 relative-error dropped below threshold; zeroed at frame start by
|
|
18
18
|
// initActiveIndices, incremented in FinalWrite, read back async to drive the whole-frame convergence early-stop.
|
|
19
19
|
CONVERGED_COUNT: 2,
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
|
|
20
|
+
// Reserved hole — was FROZEN_COUNT, a write-only counter no reader ever consumed. Kept as a hole so
|
|
21
|
+
// slot indices stay stable across the JS enum and every kernel that stores by index.
|
|
22
|
+
RESERVED_3: 3,
|
|
23
|
+
// Tier-2 per-pixel freeze: bounce-0 active count (maxRays − frozen), read back to size next frame's grid.
|
|
23
24
|
ACTIVE_PIXEL_COUNT: 4,
|
|
24
25
|
COUNT: 5,
|
|
25
26
|
};
|
|
@@ -48,9 +48,13 @@ export class SceneProcessor {
|
|
|
48
48
|
enableTextureCache: true,
|
|
49
49
|
maxConcurrentTextureTasks: Math.min( navigator.hardwareConcurrency || 4, 6 ),
|
|
50
50
|
// Treelet optimization configuration
|
|
51
|
+
// Keep: `_buildBVH` sends `enabled: value !== false`, so undefined re-enables treelets.
|
|
52
|
+
enableTreeletOptimization: false,
|
|
51
53
|
treeletSize: 7, // 7 nodes gives 315 topologies for optimal enumeration
|
|
52
54
|
treeletOptimizationPasses: 1,
|
|
53
55
|
treeletMinImprovement: 0.01, // Minimum SAH improvement threshold
|
|
56
|
+
// Above this triangle count the builder drops treelets to size 3.
|
|
57
|
+
treeletComplexityThreshold: 50000,
|
|
54
58
|
...options
|
|
55
59
|
};
|
|
56
60
|
|
|
@@ -151,7 +155,8 @@ export class SceneProcessor {
|
|
|
151
155
|
enabled: this.config.enableTreeletOptimization,
|
|
152
156
|
size: this.config.treeletSize,
|
|
153
157
|
passes: this.config.treeletOptimizationPasses,
|
|
154
|
-
minImprovement: this.config.treeletMinImprovement
|
|
158
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
159
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
155
160
|
} );
|
|
156
161
|
|
|
157
162
|
// Create and configure texture creator
|
|
@@ -436,7 +441,8 @@ export class SceneProcessor {
|
|
|
436
441
|
enabled: originalTreeletEnabled !== false,
|
|
437
442
|
size: this.config.treeletSize,
|
|
438
443
|
passes: this.config.treeletOptimizationPasses,
|
|
439
|
-
minImprovement: this.config.treeletMinImprovement
|
|
444
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
445
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
440
446
|
},
|
|
441
447
|
reinsertionOptimization: {
|
|
442
448
|
enabled: this.bvhBuilder.enableReinsertionOptimization,
|
|
@@ -1267,7 +1273,8 @@ export class SceneProcessor {
|
|
|
1267
1273
|
enabled: this.config.enableTreeletOptimization,
|
|
1268
1274
|
size: this.config.treeletSize,
|
|
1269
1275
|
passes: this.config.treeletOptimizationPasses,
|
|
1270
|
-
minImprovement: this.config.treeletMinImprovement
|
|
1276
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
1277
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
1271
1278
|
} );
|
|
1272
1279
|
|
|
1273
1280
|
}
|
|
@@ -1953,7 +1960,8 @@ export class SceneProcessor {
|
|
|
1953
1960
|
enabled: treeletEnabled,
|
|
1954
1961
|
size: this.config.treeletSize,
|
|
1955
1962
|
passes: this.config.treeletOptimizationPasses,
|
|
1956
|
-
minImprovement: this.config.treeletMinImprovement
|
|
1963
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
1964
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
1957
1965
|
},
|
|
1958
1966
|
reinsertionOptimization: {
|
|
1959
1967
|
enabled: this.bvhBuilder.enableReinsertionOptimization,
|
|
@@ -545,13 +545,6 @@ export class TreeletOptimizer {
|
|
|
545
545
|
|
|
546
546
|
}
|
|
547
547
|
|
|
548
|
-
setMaxTreelets() {
|
|
549
|
-
|
|
550
|
-
// No-op — we process all valid treelets now.
|
|
551
|
-
// Kept for API compatibility with BVHBuilder.
|
|
552
|
-
|
|
553
|
-
}
|
|
554
|
-
|
|
555
548
|
getStatistics() {
|
|
556
549
|
|
|
557
550
|
return { ...this.stats };
|
package/src/RenderSettings.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { EventDispatcher, Color } from 'three';
|
|
1
|
+
import { EventDispatcher, Color, Vector2, MathUtils } from 'three';
|
|
2
2
|
import { ENGINE_DEFAULTS } from './EngineDefaults.js';
|
|
3
3
|
import { EngineEvents } from './EngineEvents.js';
|
|
4
4
|
|
|
@@ -32,6 +32,7 @@ const SETTING_ROUTES = {
|
|
|
32
32
|
enableGroundCatcher: { uniform: 'enableGroundCatcher', reset: true },
|
|
33
33
|
groundCatcherHeight: { uniform: 'groundCatcherHeight', reset: true },
|
|
34
34
|
globalIlluminationIntensity: { uniform: 'globalIlluminationIntensity', reset: true },
|
|
35
|
+
panoramaLevelHorizon: { uniform: 'panoLevelHorizon', reset: true },
|
|
35
36
|
enableDOF: { uniform: 'enableDOF', reset: true },
|
|
36
37
|
focusDistance: { uniform: 'focusDistance', reset: false },
|
|
37
38
|
focalLength: { uniform: 'focalLength', reset: true },
|
|
@@ -56,6 +57,9 @@ const SETTING_ROUTES = {
|
|
|
56
57
|
|
|
57
58
|
// ── Multi-stage / special handling ────────────────────────────
|
|
58
59
|
|
|
60
|
+
cameraProjection: { handler: 'handleCameraProjection', reset: true },
|
|
61
|
+
panoramaLonRange: { handler: 'handlePanoramaLonRange', reset: true },
|
|
62
|
+
panoramaLatRange: { handler: 'handlePanoramaLatRange', reset: true },
|
|
59
63
|
interactionModeEnabled: { handler: 'handleInteractionModeEnabled', reset: false },
|
|
60
64
|
maxSamples: { handler: 'handleMaxSamples', reset: false },
|
|
61
65
|
transparentBackground: { handler: 'handleTransparentBackground' },
|
|
@@ -122,13 +126,15 @@ export class RenderSettings extends EventDispatcher {
|
|
|
122
126
|
* @param {Object} params.stages - Pipeline stages { pathTracer, compositor, autoExposure, ... }
|
|
123
127
|
* @param {Function} params.resetCallback - Called to reset accumulation
|
|
124
128
|
* @param {Function} [params.reconcileCompletion] - Called when completion limits change
|
|
129
|
+
* @param {Object} [params.denoisingManager] - Needed to force ASVGF off under panorama
|
|
130
|
+
* @param {Object} [params.cameraManager] - Needed to force auto-focus manual under panorama
|
|
125
131
|
*/
|
|
126
|
-
bind(
|
|
132
|
+
bind( params ) {
|
|
127
133
|
|
|
128
|
-
this._pathTracer = stages.pathTracer;
|
|
129
|
-
this._resetCallback = resetCallback;
|
|
134
|
+
this._pathTracer = params.stages.pathTracer;
|
|
135
|
+
this._resetCallback = params.resetCallback;
|
|
130
136
|
this._delegates = {};
|
|
131
|
-
this._handlers = this._buildHandlers(
|
|
137
|
+
this._handlers = this._buildHandlers( params );
|
|
132
138
|
|
|
133
139
|
}
|
|
134
140
|
|
|
@@ -136,10 +142,39 @@ export class RenderSettings extends EventDispatcher {
|
|
|
136
142
|
* Builds handler functions for multi-stage settings that can't
|
|
137
143
|
* be routed with a simple uniform forward.
|
|
138
144
|
*/
|
|
139
|
-
_buildHandlers( stages, renderer, reconcileCompletion ) {
|
|
145
|
+
_buildHandlers( { stages, renderer, reconcileCompletion, denoisingManager, cameraManager } ) {
|
|
146
|
+
|
|
147
|
+
// UniformManager copies into the existing node, so one scratch vector serves every write.
|
|
148
|
+
const panoScratch = new Vector2();
|
|
149
|
+
const setPanoRange = ( uniform, value ) => {
|
|
150
|
+
|
|
151
|
+
panoScratch.set( value?.[ 0 ] ?? 0, value?.[ 1 ] ?? 0 ).multiplyScalar( MathUtils.DEG2RAD );
|
|
152
|
+
stages.pathTracer?.setUniform( uniform, panoScratch );
|
|
153
|
+
|
|
154
|
+
};
|
|
140
155
|
|
|
141
156
|
return {
|
|
142
157
|
|
|
158
|
+
handleCameraProjection: ( value ) => {
|
|
159
|
+
|
|
160
|
+
const isPanorama = value === 'equirectangular';
|
|
161
|
+
stages.pathTracer?.setUniform( 'cameraProjection', isPanorama ? 1 : 0 );
|
|
162
|
+
|
|
163
|
+
if ( ! isPanorama ) return;
|
|
164
|
+
|
|
165
|
+
// ASVGF is driven entirely by MotionVector, which unprojects through
|
|
166
|
+
// projectionMatrixInverse — meaningless once every pixel is its own direction.
|
|
167
|
+
// Fall back to the spatial-only denoiser rather than leaving no strategy.
|
|
168
|
+
if ( denoisingManager?.denoiserStrategy === 'asvgf' ) denoisingManager.setDenoiserStrategy( 'edgeaware' );
|
|
169
|
+
// Auto-focus raycasts via Raycaster.setFromCamera, which only knows the frustum.
|
|
170
|
+
cameraManager?.setAutoFocusMode( 'manual' );
|
|
171
|
+
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
handlePanoramaLonRange: ( value ) => setPanoRange( 'panoLonRange', value ),
|
|
175
|
+
|
|
176
|
+
handlePanoramaLatRange: ( value ) => setPanoRange( 'panoLatRange', value ),
|
|
177
|
+
|
|
143
178
|
handleTransparentBackground: ( value ) => {
|
|
144
179
|
|
|
145
180
|
stages.pathTracer?.setUniform( 'transparentBackground', value );
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Fn, vec3, vec4, float, int, uint, uvec2, uniform,
|
|
1
|
+
import { Fn, vec2, vec3, vec4, float, int, uint, uvec2, uniform, min, storage, If,
|
|
2
2
|
textureStore, workgroupId, localId } from 'three/tsl';
|
|
3
3
|
import { RenderTarget, StorageTexture } from 'three/webgpu';
|
|
4
4
|
import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
|
|
@@ -6,6 +6,7 @@ import { RenderStage, StageExecutionMode } from '../Pipeline/RenderStage.js';
|
|
|
6
6
|
import { MAX_STORAGE_TEXTURE_SIZE } from '../EngineDefaults.js';
|
|
7
7
|
import { Ray, HitInfo, RayTracingMaterial, UVCache } from '../TSL/Struct.js';
|
|
8
8
|
import { traverseBVH } from '../TSL/BVHTraversal.js';
|
|
9
|
+
import { cameraRayDirection } from '../TSL/CameraRay.js';
|
|
9
10
|
import { getMaterial } from '../TSL/Common.js';
|
|
10
11
|
import { computeUVCache, processNormal, processBump, buildBucketTextureNodes, refreshBucketTextureNodes, setMaterialBucketTextures } from '../TSL/TextureSampling.js';
|
|
11
12
|
|
|
@@ -48,6 +49,12 @@ export class NormalDepth extends RenderStage {
|
|
|
48
49
|
|
|
49
50
|
this.cameraWorldMatrix = uniform( new Matrix4(), 'mat4' );
|
|
50
51
|
this.cameraProjectionMatrixInverse = uniform( new Matrix4(), 'mat4' );
|
|
52
|
+
// Mirrored from the path tracer each frame — these rays must use the same camera model
|
|
53
|
+
// as the colour buffer or the denoiser's normal/depth edge-stops fight the image.
|
|
54
|
+
this.cameraProjection = uniform( 0, 'int' );
|
|
55
|
+
this.panoLonRange = uniform( new Vector2(), 'vec2' );
|
|
56
|
+
this.panoLatRange = uniform( new Vector2(), 'vec2' );
|
|
57
|
+
this.panoLevelHorizon = uniform( 1, 'int' );
|
|
51
58
|
this.resolutionWidth = uniform( options.width || 1 );
|
|
52
59
|
this.resolutionHeight = uniform( options.height || 1 );
|
|
53
60
|
|
|
@@ -218,20 +225,12 @@ export class NormalDepth extends RenderStage {
|
|
|
218
225
|
|
|
219
226
|
If( gx.lessThan( int( resW ) ).and( gy.lessThan( int( resH ) ) ), () => {
|
|
220
227
|
|
|
221
|
-
// Pixel center → NDC, Y negated for Three.js WebGPU.
|
|
222
|
-
const ndcX = float( gx ).add( 0.5 ).div( resW ).mul( 2.0 ).sub( 1.0 );
|
|
223
|
-
const ndcY = float( gy ).add( 0.5 ).div( resH ).mul( 2.0 ).sub( 1.0 ).negate();
|
|
224
|
-
const ndcPos = vec3( ndcX, ndcY, 1.0 );
|
|
225
|
-
|
|
226
228
|
// No jitter — deterministic per-pixel ray so the temporal gate
|
|
227
229
|
// sees stable per-pixel normals across frames.
|
|
228
|
-
const
|
|
229
|
-
const rayDirWorld =
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
camWorldMat[ 1 ].xyz,
|
|
233
|
-
camWorldMat[ 2 ].xyz
|
|
234
|
-
).mul( rayDirCS.xyz.div( rayDirCS.w ) )
|
|
230
|
+
const uv = vec2( float( gx ).add( 0.5 ).div( resW ), float( gy ).add( 0.5 ).div( resH ) );
|
|
231
|
+
const rayDirWorld = cameraRayDirection(
|
|
232
|
+
uv, camWorldMat, camProjInvMat,
|
|
233
|
+
this.cameraProjection, this.panoLonRange, this.panoLatRange, this.panoLevelHorizon
|
|
235
234
|
);
|
|
236
235
|
const rayOrigin = vec3( camWorldMat[ 3 ] );
|
|
237
236
|
|
|
@@ -307,6 +306,10 @@ export class NormalDepth extends RenderStage {
|
|
|
307
306
|
|
|
308
307
|
this.cameraWorldMatrix.value.copy( pt.uniforms.get( 'cameraWorldMatrix' ).value );
|
|
309
308
|
this.cameraProjectionMatrixInverse.value.copy( pt.uniforms.get( 'cameraProjectionMatrixInverse' ).value );
|
|
309
|
+
this.cameraProjection.value = pt.uniforms.get( 'cameraProjection' ).value;
|
|
310
|
+
this.panoLonRange.value.copy( pt.uniforms.get( 'panoLonRange' ).value );
|
|
311
|
+
this.panoLatRange.value.copy( pt.uniforms.get( 'panoLatRange' ).value );
|
|
312
|
+
this.panoLevelHorizon.value = pt.uniforms.get( 'panoLevelHorizon' ).value;
|
|
310
313
|
|
|
311
314
|
}
|
|
312
315
|
|