rayzee 7.20.3 → 7.22.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 +75 -4
- package/dist/rayzee.es.js +2821 -2854
- package/dist/rayzee.umd.js +60 -58
- package/package.json +1 -1
- package/src/EngineDefaults.js +9 -2
- package/src/PathTracerApp.js +83 -0
- package/src/Pipeline/BindingAudit.js +178 -0
- package/src/Pipeline/RenderPipeline.js +5 -2
- package/src/Processor/BVHBuilder.js +1 -4
- package/src/Processor/CameraOptimizer.js +0 -5
- package/src/Processor/KernelManager.js +17 -70
- package/src/Processor/PackedRayBuffer.js +33 -8
- package/src/Processor/QueueManager.js +4 -3
- package/src/Processor/SceneProcessor.js +10 -4
- package/src/Processor/TreeletOptimizer.js +0 -7
- package/src/RenderSettings.js +41 -6
- package/src/Stages/ASVGF.js +10 -10
- package/src/Stages/BilateralFilter.js +70 -11
- package/src/Stages/EdgeFilter.js +44 -6
- package/src/Stages/NormalDepth.js +26 -15
- package/src/Stages/PathTracer.js +36 -34
- package/src/Stages/PathTracerStage.js +15 -46
- package/src/TSL/BVHTraversal.js +1 -67
- package/src/TSL/CameraRay.js +141 -0
- package/src/TSL/Common.js +1 -47
- package/src/TSL/DebugKernel.js +4 -5
- package/src/TSL/EmissiveSampling.js +40 -44
- package/src/TSL/ExtendKernel.js +11 -3
- package/src/TSL/FinalWriteKernel.js +1 -14
- package/src/TSL/GenerateKernel.js +11 -8
- package/src/TSL/LightsCore.js +3 -0
- package/src/TSL/LightsDirect.js +4 -4
- package/src/TSL/LightsSampling.js +138 -327
- package/src/TSL/PathTracerCore.js +2 -2
- package/src/TSL/Random.js +2 -2
- package/src/TSL/ShadeKernel.js +39 -17
- package/src/TSL/SortGlobalKernels.js +52 -7
- package/src/TSL/Struct.js +0 -10
- package/src/index.js +5 -0
- package/src/managers/DenoisingManager.js +13 -0
- package/src/managers/EnvironmentManager.js +6 -8
- package/src/managers/UniformManager.js +12 -2
package/package.json
CHANGED
package/src/EngineDefaults.js
CHANGED
|
@@ -21,7 +21,6 @@ export const ENGINE_DEFAULTS = {
|
|
|
21
21
|
enableEnvironment: true,
|
|
22
22
|
showBackground: true,
|
|
23
23
|
transparentBackground: false,
|
|
24
|
-
useImportanceSampledEnvironment: true,
|
|
25
24
|
environmentIntensity: 1,
|
|
26
25
|
backgroundIntensity: 1,
|
|
27
26
|
// Solid backdrop color shown on camera-ray misses in 'color' background mode
|
|
@@ -63,6 +62,12 @@ export const ENGINE_DEFAULTS = {
|
|
|
63
62
|
skyMieAnisotropy: 0.76,
|
|
64
63
|
skyPreset: 'clearMorning',
|
|
65
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
|
+
|
|
66
71
|
enableDOF: false,
|
|
67
72
|
fov: 55,
|
|
68
73
|
focusDistance: 0.8,
|
|
@@ -164,7 +169,9 @@ export const ENGINE_DEFAULTS = {
|
|
|
164
169
|
asvgfPhiDepth: 1.0,
|
|
165
170
|
asvgfVarianceBoost: 1.0,
|
|
166
171
|
asvgfMaxAccumFrames: 32,
|
|
167
|
-
|
|
172
|
+
// Must be > 0: the gradient also rejects fireflies before the EMA smears them across
|
|
173
|
+
// ~1/alpha frames. At 0 the denoiser measures ~3x worse than no denoiser at 1 spp.
|
|
174
|
+
asvgfGradientStrength: 1.0,
|
|
168
175
|
asvgfGradientSigmaScale: 2.0,
|
|
169
176
|
asvgfGradientNoiseFloor: 0.0,
|
|
170
177
|
asvgfDebugMode: 0,
|
package/src/PathTracerApp.js
CHANGED
|
@@ -1562,6 +1562,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1562
1562
|
this.cameraManager?.setAutoFocusMode( 'manual' );
|
|
1563
1563
|
if ( this.stages.autoExposure ) this.stages.autoExposure.enabled = false;
|
|
1564
1564
|
|
|
1565
|
+
// The seed axis free-runs across accumulation resets so a camera drag gets fresh
|
|
1566
|
+
// sequences; offline rendering needs the opposite. Pinning it makes seedFrame track
|
|
1567
|
+
// frameCount, so N samples reproduce bit-for-bit. reset() below zeroes the tick.
|
|
1568
|
+
stage._pinSeedToFrame = true;
|
|
1569
|
+
|
|
1565
1570
|
this._deterministic = true;
|
|
1566
1571
|
this._dispatchPinned = pinDispatch;
|
|
1567
1572
|
|
|
@@ -1580,6 +1585,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1580
1585
|
|
|
1581
1586
|
}
|
|
1582
1587
|
|
|
1588
|
+
stage._pinSeedToFrame = false;
|
|
1589
|
+
|
|
1583
1590
|
this._deterministicRestore = null;
|
|
1584
1591
|
this._deterministic = false;
|
|
1585
1592
|
this._dispatchPinned = false;
|
|
@@ -1715,6 +1722,80 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1715
1722
|
|
|
1716
1723
|
}
|
|
1717
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
|
+
|
|
1718
1799
|
// ═══════════════════════════════════════════════════════════════
|
|
1719
1800
|
// Output (absorbed from OutputAPI)
|
|
1720
1801
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -2291,6 +2372,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2291
2372
|
renderer: this.renderer,
|
|
2292
2373
|
resetCallback: () => this.reset(),
|
|
2293
2374
|
reconcileCompletion: () => this._reconcileCompletion(),
|
|
2375
|
+
denoisingManager: this.denoisingManager,
|
|
2376
|
+
cameraManager: this.cameraManager,
|
|
2294
2377
|
} );
|
|
2295
2378
|
|
|
2296
2379
|
this.renderer.toneMappingExposure = this.settings.get( 'exposure' ) ?? 1.0;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev-only guard against texture-binding aliasing.
|
|
3
|
+
*
|
|
4
|
+
* Two TextureNodes still holding the default EmptyTexture when a stage's kernel is first
|
|
5
|
+
* compiled can share one GPU binding, which then resolves to whichever is assigned last.
|
|
6
|
+
* Nothing throws — the aliased node reads a real texture, just not its own. BilateralFilter
|
|
7
|
+
* shipped this way, reading ASVGF's history counter as its variance.
|
|
8
|
+
*
|
|
9
|
+
* Rule enforced: every TextureNode a kernel reads must hold its real texture before that
|
|
10
|
+
* kernel is first dispatched. StorageTexture-typed nodes are exempt — `textureLoad` codegen
|
|
11
|
+
* only emits the required `level` parameter while the node still holds EmptyTexture, so those
|
|
12
|
+
* are deliberately bound after compile (see ASVGF.render).
|
|
13
|
+
*
|
|
14
|
+
* Off by default; the bench harness turns it on.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
let enabled = false;
|
|
18
|
+
const findings = [];
|
|
19
|
+
|
|
20
|
+
/** Per-stage audit state. WeakMap so a disposed stage does not pin its entry. */
|
|
21
|
+
const stageState = new WeakMap();
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* @param {boolean} on
|
|
25
|
+
*/
|
|
26
|
+
export function setBindingAudit( on ) {
|
|
27
|
+
|
|
28
|
+
enabled = !! on;
|
|
29
|
+
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isBindingAuditEnabled() {
|
|
33
|
+
|
|
34
|
+
return enabled;
|
|
35
|
+
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** @returns {Array<{stage: string, nodes: string[], unboundAtCompile: string[], message: string}>} */
|
|
39
|
+
export function getBindingAuditFindings() {
|
|
40
|
+
|
|
41
|
+
return findings.slice();
|
|
42
|
+
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function clearBindingAuditFindings() {
|
|
46
|
+
|
|
47
|
+
findings.length = 0;
|
|
48
|
+
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Own enumerable properties holding a TSL TextureNode. */
|
|
52
|
+
function textureNodeKeys( stage ) {
|
|
53
|
+
|
|
54
|
+
const keys = [];
|
|
55
|
+
|
|
56
|
+
for ( const key of Object.keys( stage ) ) {
|
|
57
|
+
|
|
58
|
+
if ( stage[ key ]?.isTextureNode === true ) keys.push( key );
|
|
59
|
+
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return keys;
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// EmptyTexture (the TextureNode default) is a bare `new Texture()`, so its image is null.
|
|
67
|
+
// Every real texture the stages bind — RenderTarget textures and StorageTextures alike —
|
|
68
|
+
// carries an image with a nonzero width.
|
|
69
|
+
const isUnbound = ( texture ) => ! texture || ! texture.image || ! ( texture.image.width > 0 );
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Runs a stage's render with the audit wrapped around it.
|
|
73
|
+
*
|
|
74
|
+
* The snapshot must be taken when the stage first calls `renderer.compute`, not before or
|
|
75
|
+
* after render() — the binding assignments and the compile are interleaved inside that one
|
|
76
|
+
* call, so by the time it returns everything is bound. Sampling after render() was the first
|
|
77
|
+
* version of this guard and it silently caught nothing.
|
|
78
|
+
*
|
|
79
|
+
* A later frame then resolves: a candidate now holding a StorageTexture is the sanctioned
|
|
80
|
+
* pattern; anything else was bound too late.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} stage
|
|
83
|
+
* @param {Object} renderer
|
|
84
|
+
* @param {function(): void} renderFn
|
|
85
|
+
*/
|
|
86
|
+
export function auditStageRender( stage, renderer, renderFn ) {
|
|
87
|
+
|
|
88
|
+
if ( ! enabled ) {
|
|
89
|
+
|
|
90
|
+
renderFn();
|
|
91
|
+
return;
|
|
92
|
+
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
let state = stageState.get( stage );
|
|
96
|
+
|
|
97
|
+
if ( ! state ) {
|
|
98
|
+
|
|
99
|
+
state = { phase: 'pending', unboundAtCompile: [] };
|
|
100
|
+
stageState.set( stage, state );
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if ( state.phase !== 'pending' ) {
|
|
105
|
+
|
|
106
|
+
renderFn();
|
|
107
|
+
if ( state.phase === 'watch' ) resolve( stage, state );
|
|
108
|
+
return;
|
|
109
|
+
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Shadow the prototype method for the duration of this stage's render only.
|
|
113
|
+
const hadOwn = Object.prototype.hasOwnProperty.call( renderer, 'compute' );
|
|
114
|
+
const original = renderer.compute;
|
|
115
|
+
|
|
116
|
+
renderer.compute = function ( ...args ) {
|
|
117
|
+
|
|
118
|
+
if ( state.phase === 'pending' ) {
|
|
119
|
+
|
|
120
|
+
state.unboundAtCompile = textureNodeKeys( stage )
|
|
121
|
+
.filter( ( key ) => isUnbound( stage[ key ].value ) );
|
|
122
|
+
|
|
123
|
+
// A single unbound node has nothing to alias with, so it cannot produce this bug.
|
|
124
|
+
state.phase = state.unboundAtCompile.length < 2 ? 'done' : 'watch';
|
|
125
|
+
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return original.apply( this, args );
|
|
129
|
+
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
|
|
134
|
+
renderFn();
|
|
135
|
+
|
|
136
|
+
} finally {
|
|
137
|
+
|
|
138
|
+
if ( hadOwn ) renderer.compute = original;
|
|
139
|
+
else delete renderer.compute;
|
|
140
|
+
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function resolve( stage, state ) {
|
|
146
|
+
|
|
147
|
+
const lateBound = [];
|
|
148
|
+
|
|
149
|
+
for ( const key of state.unboundAtCompile ) {
|
|
150
|
+
|
|
151
|
+
const texture = stage[ key ]?.value;
|
|
152
|
+
|
|
153
|
+
// Still unassigned — the stage may bind it on a later frame (or never, which is also
|
|
154
|
+
// not this bug). Leave the audit open rather than guessing.
|
|
155
|
+
if ( isUnbound( texture ) ) return;
|
|
156
|
+
|
|
157
|
+
if ( texture.isStorageTexture !== true ) lateBound.push( key );
|
|
158
|
+
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
state.phase = 'done';
|
|
162
|
+
|
|
163
|
+
if ( lateBound.length === 0 ) return;
|
|
164
|
+
|
|
165
|
+
findings.push( {
|
|
166
|
+
stage: stage.name,
|
|
167
|
+
nodes: lateBound,
|
|
168
|
+
unboundAtCompile: state.unboundAtCompile.slice(),
|
|
169
|
+
message:
|
|
170
|
+
`${stage.name}: ${lateBound.join( ', ' )} ` +
|
|
171
|
+
`${lateBound.length === 1 ? 'was' : 'were'} still holding EmptyTexture when the stage ` +
|
|
172
|
+
`first dispatched, alongside ${state.unboundAtCompile.length - lateBound.length} ` +
|
|
173
|
+
'other unbound node(s), and later received a non-storage texture. Two nodes unbound at ' +
|
|
174
|
+
'compile time can share one GPU binding and then both resolve to whichever is assigned ' +
|
|
175
|
+
'last. Assign these before the first dispatch.',
|
|
176
|
+
} );
|
|
177
|
+
|
|
178
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { PipelineContext } from './PipelineContext.js';
|
|
2
2
|
import { EventDispatcher } from './EventDispatcher.js';
|
|
3
|
+
import { auditStageRender } from './BindingAudit.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* RenderPipeline - Orchestrates execution of pipeline stages
|
|
@@ -183,8 +184,10 @@ export class RenderPipeline {
|
|
|
183
184
|
|
|
184
185
|
const stageStartTime = this.stats.enabled ? performance.now() : 0;
|
|
185
186
|
|
|
186
|
-
//
|
|
187
|
-
|
|
187
|
+
// Pass-through unless setBindingAudit(true) — see BindingAudit.js.
|
|
188
|
+
auditStageRender(
|
|
189
|
+
stage, this.renderer, () => stage.render( this.context, writeBuffer )
|
|
190
|
+
);
|
|
188
191
|
|
|
189
192
|
// Track timing
|
|
190
193
|
if ( this.stats.enabled ) {
|
|
@@ -101,8 +101,6 @@ export class BVHBuilder {
|
|
|
101
101
|
this.treeletSize = 5;
|
|
102
102
|
this.treeletOptimizationPasses = 1;
|
|
103
103
|
this.treeletMinImprovement = 0.02;
|
|
104
|
-
this.maxTreeletDepth = 3;
|
|
105
|
-
this.maxTreeletsPerScene = 20;
|
|
106
104
|
this.treeletComplexityThreshold = 50000;
|
|
107
105
|
|
|
108
106
|
// Reinsertion optimization configuration
|
|
@@ -191,6 +189,7 @@ export class BVHBuilder {
|
|
|
191
189
|
if ( config.size !== undefined ) this.treeletSize = Math.max( 3, Math.min( 12, config.size ) );
|
|
192
190
|
if ( config.passes !== undefined ) this.treeletOptimizationPasses = Math.max( 1, Math.min( 3, config.passes ) );
|
|
193
191
|
if ( config.minImprovement !== undefined ) this.treeletMinImprovement = Math.max( 0.001, config.minImprovement );
|
|
192
|
+
if ( config.complexityThreshold !== undefined ) this.treeletComplexityThreshold = config.complexityThreshold;
|
|
194
193
|
|
|
195
194
|
}
|
|
196
195
|
|
|
@@ -595,12 +594,10 @@ export class BVHBuilder {
|
|
|
595
594
|
|
|
596
595
|
const isLargeScene = this.totalTriangles > this.treeletComplexityThreshold;
|
|
597
596
|
const adaptiveTreeletSize = isLargeScene ? 3 : this.treeletSize;
|
|
598
|
-
const adaptiveMaxTreelets = isLargeScene ? 10 : this.maxTreeletsPerScene;
|
|
599
597
|
|
|
600
598
|
const optimizer = new TreeletOptimizer( this.traversalCost, this.intersectionCost );
|
|
601
599
|
optimizer.setTreeletSize( adaptiveTreeletSize );
|
|
602
600
|
optimizer.setMinImprovement( this.treeletMinImprovement );
|
|
603
|
-
optimizer.setMaxTreelets( adaptiveMaxTreelets );
|
|
604
601
|
|
|
605
602
|
const optimizationStartTime = performance.now();
|
|
606
603
|
|
|
@@ -24,7 +24,6 @@ export class CameraOptimizer {
|
|
|
24
24
|
// Enhanced interaction mode settings for reduced quality during interaction
|
|
25
25
|
this.interactionQualitySettings = {
|
|
26
26
|
maxBounceCount: 1,
|
|
27
|
-
useEnvMapIS: false,
|
|
28
27
|
// pixelRatio: 0.25,
|
|
29
28
|
enableAccumulation: false,
|
|
30
29
|
...settings.qualitySettings
|
|
@@ -303,25 +302,21 @@ export class CameraOptimizer {
|
|
|
303
302
|
const presets = {
|
|
304
303
|
'ultra-low': {
|
|
305
304
|
maxBounceCount: 1,
|
|
306
|
-
useEnvMapIS: false,
|
|
307
305
|
pixelRatio: 0.125,
|
|
308
306
|
enableAccumulation: false
|
|
309
307
|
},
|
|
310
308
|
'low': {
|
|
311
309
|
maxBounceCount: 1,
|
|
312
|
-
useEnvMapIS: false,
|
|
313
310
|
pixelRatio: 0.25,
|
|
314
311
|
enableAccumulation: false
|
|
315
312
|
},
|
|
316
313
|
'medium': {
|
|
317
314
|
maxBounceCount: 2,
|
|
318
|
-
useEnvMapIS: true,
|
|
319
315
|
pixelRatio: 0.5,
|
|
320
316
|
enableAccumulation: false
|
|
321
317
|
},
|
|
322
318
|
'high': {
|
|
323
319
|
maxBounceCount: 3,
|
|
324
|
-
useEnvMapIS: true,
|
|
325
320
|
pixelRatio: 0.75,
|
|
326
321
|
enableAccumulation: true
|
|
327
322
|
}
|
|
@@ -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
|
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import {
|
|
7
7
|
storage, uintBitsToFloat, floatBitsToUint, vec2, vec3, vec4, uvec4, uint, int, float, clamp,
|
|
8
|
-
packSnorm2x16, packUnorm2x16, unpackSnorm2x16, unpackUnorm2x16,
|
|
8
|
+
packSnorm2x16, packUnorm2x16, unpackSnorm2x16, unpackUnorm2x16, select, floor, log2, exp2, max,
|
|
9
9
|
} from 'three/tsl';
|
|
10
10
|
import { StorageInstancedBufferAttribute } from 'three/webgpu';
|
|
11
11
|
import { createLogger, fmt } from '../utils/Logger.js';
|
|
@@ -186,13 +186,38 @@ export const gbDecodeAlbedo = ( packed ) =>
|
|
|
186
186
|
// .w packs per-ray bounce state: perRayBounces (bits 0-7) | sssSteps (bits 8-15) | transparentCount
|
|
187
187
|
// (bits 16-21), each masked so an overrun can't bleed into its neighbour. pixelIndex is NOT stored —
|
|
188
188
|
// it equals rayID (one ray per pixel).
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
189
|
+
// Bits 22-31 of ORIGIN_META.w: alpha-passthrough distance since the last real scatter (Cycles'
|
|
190
|
+
// mis_ray_t), as a 10-bit float (4e6m, bias 8, ~0.004-500 range, ≤1.6% rel err). The emissive-hit
|
|
191
|
+
// MIS partner pdf must be evaluated at the scatter point that produced prevBouncePdf, not at the
|
|
192
|
+
// last skip surface — feeding it the skip origin collapses dist² and lets full-Le BSDF hits
|
|
193
|
+
// through nearly unweighted (fireflies on emitters inside alpha-blend shades).
|
|
194
|
+
const encodeMisRayT = ( t ) => {
|
|
195
|
+
|
|
196
|
+
const e = clamp( floor( log2( max( t, 1e-6 ) ) ), - 8.0, 7.0 );
|
|
197
|
+
const m = clamp( float( t ).mul( exp2( e.negate() ) ).sub( 1.0 ).mul( 64.0 ), 0.0, 63.0 );
|
|
198
|
+
return select( float( t ).lessThan( 0.004 ), uint( 0 ), uint( e.add( 8.0 ) ).shiftLeft( 6 ).bitOr( uint( m ) ) );
|
|
199
|
+
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
export const readMisRayT = ( buf, id ) => {
|
|
203
|
+
|
|
204
|
+
const bits = floatBitsToUint( buf.element( soa( id, RAY.ORIGIN_META ) ).w ).shiftRight( 22 );
|
|
205
|
+
const e = float( bits.shiftRight( 6 ).bitAnd( uint( 15 ) ) ).sub( 8.0 );
|
|
206
|
+
const m = float( bits.bitAnd( uint( 63 ) ) ).div( 64.0 ).add( 1.0 );
|
|
207
|
+
return select( bits.equal( uint( 0 ) ), float( 0.0 ), m.mul( exp2( e ) ) );
|
|
208
|
+
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
export const writeRayOriginMeta = ( buf, id, origin, bounces, sssSteps, transparentCount = uint( 0 ), misRayT = null ) => {
|
|
212
|
+
|
|
213
|
+
let packed = uint( bounces ).bitAnd( uint( 0xFF ) )
|
|
214
|
+
.bitOr( uint( sssSteps ).bitAnd( uint( 0xFF ) ).shiftLeft( 8 ) )
|
|
215
|
+
.bitOr( uint( transparentCount ).bitAnd( uint( 0x3F ) ).shiftLeft( 16 ) );
|
|
216
|
+
if ( misRayT !== null ) packed = packed.bitOr( encodeMisRayT( misRayT ).shiftLeft( 22 ) );
|
|
217
|
+
return buf.element( soa( id, RAY.ORIGIN_META ) )
|
|
218
|
+
.assign( vec4( origin, uintBitsToFloat( packed ) ) );
|
|
219
|
+
|
|
220
|
+
};
|
|
196
221
|
|
|
197
222
|
export const writeRayDirFlags = ( buf, id, direction, bounceFlags ) =>
|
|
198
223
|
buf.element( soa( id, RAY.DIR_FLAGS ) )
|
|
@@ -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
|
};
|
|
@@ -51,6 +51,8 @@ export class SceneProcessor {
|
|
|
51
51
|
treeletSize: 7, // 7 nodes gives 315 topologies for optimal enumeration
|
|
52
52
|
treeletOptimizationPasses: 1,
|
|
53
53
|
treeletMinImprovement: 0.01, // Minimum SAH improvement threshold
|
|
54
|
+
// Above this triangle count the builder drops treelets to size 3.
|
|
55
|
+
treeletComplexityThreshold: 50000,
|
|
54
56
|
...options
|
|
55
57
|
};
|
|
56
58
|
|
|
@@ -151,7 +153,8 @@ export class SceneProcessor {
|
|
|
151
153
|
enabled: this.config.enableTreeletOptimization,
|
|
152
154
|
size: this.config.treeletSize,
|
|
153
155
|
passes: this.config.treeletOptimizationPasses,
|
|
154
|
-
minImprovement: this.config.treeletMinImprovement
|
|
156
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
157
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
155
158
|
} );
|
|
156
159
|
|
|
157
160
|
// Create and configure texture creator
|
|
@@ -436,7 +439,8 @@ export class SceneProcessor {
|
|
|
436
439
|
enabled: originalTreeletEnabled !== false,
|
|
437
440
|
size: this.config.treeletSize,
|
|
438
441
|
passes: this.config.treeletOptimizationPasses,
|
|
439
|
-
minImprovement: this.config.treeletMinImprovement
|
|
442
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
443
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
440
444
|
},
|
|
441
445
|
reinsertionOptimization: {
|
|
442
446
|
enabled: this.bvhBuilder.enableReinsertionOptimization,
|
|
@@ -1267,7 +1271,8 @@ export class SceneProcessor {
|
|
|
1267
1271
|
enabled: this.config.enableTreeletOptimization,
|
|
1268
1272
|
size: this.config.treeletSize,
|
|
1269
1273
|
passes: this.config.treeletOptimizationPasses,
|
|
1270
|
-
minImprovement: this.config.treeletMinImprovement
|
|
1274
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
1275
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
1271
1276
|
} );
|
|
1272
1277
|
|
|
1273
1278
|
}
|
|
@@ -1953,7 +1958,8 @@ export class SceneProcessor {
|
|
|
1953
1958
|
enabled: treeletEnabled,
|
|
1954
1959
|
size: this.config.treeletSize,
|
|
1955
1960
|
passes: this.config.treeletOptimizationPasses,
|
|
1956
|
-
minImprovement: this.config.treeletMinImprovement
|
|
1961
|
+
minImprovement: this.config.treeletMinImprovement,
|
|
1962
|
+
complexityThreshold: this.config.treeletComplexityThreshold
|
|
1957
1963
|
},
|
|
1958
1964
|
reinsertionOptimization: {
|
|
1959
1965
|
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 };
|