rayzee 8.7.0 → 8.8.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 +35 -6
- package/dist/rayzee.es.js +3370 -2796
- package/dist/rayzee.umd.js +248 -10
- package/package.json +1 -1
- package/src/EngineDefaults.js +2 -0
- package/src/Passes/OIDNDenoiser.js +36 -10
- package/src/Passes/OIDNTemporalHistory.js +569 -0
- package/src/PathTracerApp.js +41 -6
- package/src/Stages/NormalDepth.js +94 -3
- package/src/Stages/PathTracer.js +1 -0
- package/src/Stages/PathTracerStage.js +4 -0
- package/src/managers/DenoisingManager.js +254 -5
- package/src/managers/LightManager.js +5 -0
- package/src/managers/TransformManager.js +38 -20
package/src/PathTracerApp.js
CHANGED
|
@@ -447,6 +447,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
447
447
|
}
|
|
448
448
|
|
|
449
449
|
this.pipeline.render();
|
|
450
|
+
this.denoisingManager?.afterTrace();
|
|
450
451
|
|
|
451
452
|
if ( ! this.stages.pathTracer.isComplete ) {
|
|
452
453
|
|
|
@@ -567,8 +568,15 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
567
568
|
/**
|
|
568
569
|
* Resets the accumulation buffer.
|
|
569
570
|
* @param {boolean} soft - When true, preserves ASVGF temporal history
|
|
571
|
+
* @param {Object} [options]
|
|
572
|
+
* @param {boolean} [options.motion] - only object placements or geometry moved; keeps the OIDN
|
|
573
|
+
* motion history, which rejects what moved per pixel
|
|
570
574
|
*/
|
|
571
|
-
reset( soft = false ) {
|
|
575
|
+
reset( soft = false, { motion = false } = {} ) {
|
|
576
|
+
|
|
577
|
+
// Objects moving is the view changing, as far as the live denoiser's refresh cadence goes.
|
|
578
|
+
if ( motion ) this.stages.pathTracer?.noteViewChanged();
|
|
579
|
+
this.denoisingManager?.beforeReset( { keepHistory: soft || motion } );
|
|
572
580
|
|
|
573
581
|
if ( this.pipeline ) {
|
|
574
582
|
|
|
@@ -582,6 +590,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
582
590
|
|
|
583
591
|
}
|
|
584
592
|
|
|
593
|
+
this.denoisingManager?.afterReset();
|
|
594
|
+
|
|
585
595
|
// Whatever is on screen stays until its replacement is ready, including while the camera
|
|
586
596
|
// moves: the denoising manager decides, since only it knows something is coming.
|
|
587
597
|
this._abortPostProcess( { keepDisplay: true } );
|
|
@@ -1617,7 +1627,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1617
1627
|
|
|
1618
1628
|
this.stages.pathTracer.updateTriangleData( this._sdf.triangles );
|
|
1619
1629
|
this.stages.pathTracer.updateBVHData( this._sdf.bvh );
|
|
1620
|
-
this.reset();
|
|
1630
|
+
this.reset( false, { motion: true } );
|
|
1621
1631
|
|
|
1622
1632
|
return result;
|
|
1623
1633
|
|
|
@@ -1639,7 +1649,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1639
1649
|
|
|
1640
1650
|
const { triRanges, bvhRanges } = this._sdf.computeBLASDirtyRanges( affectedMeshIndices );
|
|
1641
1651
|
this.stages.pathTracer.updateBufferRanges( triRanges, bvhRanges );
|
|
1642
|
-
this.reset();
|
|
1652
|
+
this.reset( false, { motion: true } );
|
|
1643
1653
|
|
|
1644
1654
|
// Kick off background rebuild for optimal SAH quality
|
|
1645
1655
|
this._sdf.scheduleBackgroundRebuild( affectedMeshIndices, ( meshIndex ) => {
|
|
@@ -1648,7 +1658,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1648
1658
|
// re-upload here is gigabytes on a large scene, for one mesh's worth of change.
|
|
1649
1659
|
const dirty = this._sdf.computeBLASDirtyRanges( [ meshIndex ] );
|
|
1650
1660
|
this.stages.pathTracer.updateBufferRanges( dirty.triRanges, dirty.bvhRanges );
|
|
1651
|
-
this.reset();
|
|
1661
|
+
this.reset( false, { motion: true } );
|
|
1652
1662
|
|
|
1653
1663
|
} );
|
|
1654
1664
|
|
|
@@ -1664,6 +1674,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1664
1674
|
|
|
1665
1675
|
if ( meshIndices.length > 0 ) {
|
|
1666
1676
|
|
|
1677
|
+
this._notePlacementsMoving( meshIndices );
|
|
1667
1678
|
this._sdf.updateMeshTransforms( meshIndices );
|
|
1668
1679
|
this.stages.pathTracer?.updateBufferRanges( [], [ this._sdf.computeTLASDirtyRange() ] );
|
|
1669
1680
|
if ( this._sdf.movesEmitters( meshIndices ) ) {
|
|
@@ -1689,7 +1700,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1689
1700
|
|
|
1690
1701
|
if ( cameras.length > 0 && this._followAnimatedCamera( cameras ) ) changed = true;
|
|
1691
1702
|
|
|
1692
|
-
if ( changed ) this.reset();
|
|
1703
|
+
if ( changed ) this.reset( false, { motion: true } );
|
|
1704
|
+
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
/** @private */
|
|
1708
|
+
_notePlacementsMoving( meshIndices ) {
|
|
1709
|
+
|
|
1710
|
+
const dm = this.denoisingManager;
|
|
1711
|
+
const table = this._sdf?.instanceTable;
|
|
1712
|
+
if ( ! dm?.historyActive || ! table ) return;
|
|
1713
|
+
|
|
1714
|
+
for ( const meshIndex of meshIndices ) {
|
|
1715
|
+
|
|
1716
|
+
const run = table.placementRunOf( meshIndex );
|
|
1717
|
+
if ( ! run ) continue;
|
|
1718
|
+
for ( let p = run.start; p < run.start + run.count; p ++ ) dm.notePlacementMoving( table.tlasLeafIndex[ p ], table.world, p * 16 );
|
|
1719
|
+
|
|
1720
|
+
}
|
|
1693
1721
|
|
|
1694
1722
|
}
|
|
1695
1723
|
|
|
@@ -1758,10 +1786,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1758
1786
|
*/
|
|
1759
1787
|
updateMeshTransforms( meshIndices ) {
|
|
1760
1788
|
|
|
1789
|
+
this._notePlacementsMoving( meshIndices );
|
|
1761
1790
|
const result = this._sdf.updateMeshTransforms( meshIndices );
|
|
1762
1791
|
|
|
1763
1792
|
this.stages.pathTracer.updateBufferRanges( [], [ this._sdf.computeTLASDirtyRange() ] );
|
|
1764
|
-
this.reset();
|
|
1793
|
+
this.reset( false, { motion: true } );
|
|
1765
1794
|
|
|
1766
1795
|
return result;
|
|
1767
1796
|
|
|
@@ -3444,6 +3473,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
3444
3473
|
this.cameraManager.setInteractionManager( this.interactionManager );
|
|
3445
3474
|
this.lightManager = new LightManager( this.scene, this._sceneHelpers, this.stages.pathTracer, {
|
|
3446
3475
|
onReset: () => this.reset(),
|
|
3476
|
+
// The gizmo stays attached to a removed light and warns every frame it draws.
|
|
3477
|
+
onLightRemoved: light => {
|
|
3478
|
+
|
|
3479
|
+
if ( this.interactionManager?.selectedObject === light ) this.interactionManager.deselect();
|
|
3480
|
+
|
|
3481
|
+
},
|
|
3447
3482
|
} );
|
|
3448
3483
|
this.goboManager = new GoboManager( this.stages.pathTracer, {
|
|
3449
3484
|
onReset: () => this.reset(),
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { Fn, vec2, vec3, vec4, float, int, uint, uvec2, uniform, min, storage, If,
|
|
1
|
+
import { Fn, vec2, vec3, vec4, float, int, uint, uvec2, uvec4, uniform, min, storage, If,
|
|
2
2
|
textureStore, workgroupId, localId } from 'three/tsl';
|
|
3
3
|
import { RenderTarget, StorageTexture } from 'three/webgpu';
|
|
4
|
-
import { HalfFloatType, RGBAFormat, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
|
|
4
|
+
import { HalfFloatType, RGBAFormat, RedIntegerFormat, UnsignedIntType, NearestFilter, Matrix4, Box2, Vector2 } from 'three';
|
|
5
5
|
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';
|
|
@@ -32,7 +32,8 @@ import { computeUVCache, processNormal, processBump, processMetalnessRoughness,
|
|
|
32
32
|
* Its .w is the sampled material roughness (NRD's normal+roughness guide).
|
|
33
33
|
*
|
|
34
34
|
* Publishes: pathtracer:normalDepth, pathtracer:prevNormalDepth,
|
|
35
|
-
* pathtracer:shadingNormal (rgb = shading normal·0.5+0.5, a = roughness)
|
|
35
|
+
* pathtracer:shadingNormal (rgb = shading normal·0.5+0.5, a = roughness),
|
|
36
|
+
* pathtracer:instanceLeaf (r32uint: the hit's transformed TLAS leaf + 1, 0 = none)
|
|
36
37
|
*/
|
|
37
38
|
export class NormalDepth extends RenderStage {
|
|
38
39
|
|
|
@@ -85,6 +86,10 @@ export class NormalDepth extends RenderStage {
|
|
|
85
86
|
stencilBuffer: false
|
|
86
87
|
} );
|
|
87
88
|
|
|
89
|
+
// Created only while a consumer asks for it (setInstanceLeafOutput).
|
|
90
|
+
this._leafStorageTex = null;
|
|
91
|
+
this._leafRT = null;
|
|
92
|
+
|
|
88
93
|
this._srcRegion = new Box2( new Vector2( 0, 0 ), new Vector2( 0, 0 ) );
|
|
89
94
|
|
|
90
95
|
// Ping-pong RTs share format with the StorageTexture so copyTextureToTexture works.
|
|
@@ -215,6 +220,7 @@ export class NormalDepth extends RenderStage {
|
|
|
215
220
|
const resH = this.resolutionHeight;
|
|
216
221
|
const outputTex = this._outputStorageTex;
|
|
217
222
|
const shadingTex = this._shadingStorageTex;
|
|
223
|
+
const leafTex = this._leafStorageTex;
|
|
218
224
|
|
|
219
225
|
const WG_SIZE = 8;
|
|
220
226
|
|
|
@@ -256,6 +262,16 @@ export class NormalDepth extends RenderStage {
|
|
|
256
262
|
result
|
|
257
263
|
).toWriteOnly();
|
|
258
264
|
|
|
265
|
+
if ( leafTex ) {
|
|
266
|
+
|
|
267
|
+
textureStore(
|
|
268
|
+
leafTex,
|
|
269
|
+
uvec2( uint( gx ), uint( gy ) ),
|
|
270
|
+
uvec4( uint( hit.instanceLeaf.add( 1 ) ), 0, 0, 0 )
|
|
271
|
+
).toWriteOnly();
|
|
272
|
+
|
|
273
|
+
}
|
|
274
|
+
|
|
259
275
|
// Shading normal: perturb the geometric normal by the normal/bump map
|
|
260
276
|
// from the SAME hit (deterministic UV → jitter-free). Miss → geo default.
|
|
261
277
|
const shadingNormal = hit.normal.toVar();
|
|
@@ -339,6 +355,7 @@ export class NormalDepth extends RenderStage {
|
|
|
339
355
|
context.setTexture( 'pathtracer:normalDepth', currentRT.texture );
|
|
340
356
|
context.setTexture( 'pathtracer:prevNormalDepth', currentRT.texture );
|
|
341
357
|
context.setTexture( 'pathtracer:shadingNormal', this._shadingRT.texture );
|
|
358
|
+
this._publishLeaf( context );
|
|
342
359
|
return;
|
|
343
360
|
|
|
344
361
|
}
|
|
@@ -367,6 +384,7 @@ export class NormalDepth extends RenderStage {
|
|
|
367
384
|
this._srcRegion.max.set( writeRT.width, writeRT.height );
|
|
368
385
|
this.renderer.copyTextureToTexture( this._outputStorageTex, writeRT.texture, this._srcRegion );
|
|
369
386
|
this.renderer.copyTextureToTexture( this._shadingStorageTex, this._shadingRT.texture, this._srcRegion );
|
|
387
|
+
if ( this._leafRT ) this.renderer.copyTextureToTexture( this._leafStorageTex, this._leafRT.texture, this._srcRegion );
|
|
370
388
|
|
|
371
389
|
// First dispatch: seed prev from current so ASVGF doesn't see false
|
|
372
390
|
// disocclusion on frame 1.
|
|
@@ -380,6 +398,7 @@ export class NormalDepth extends RenderStage {
|
|
|
380
398
|
context.setTexture( 'pathtracer:normalDepth', writeRT.texture );
|
|
381
399
|
context.setTexture( 'pathtracer:prevNormalDepth', prevRT.texture );
|
|
382
400
|
context.setTexture( 'pathtracer:shadingNormal', this._shadingRT.texture );
|
|
401
|
+
this._publishLeaf( context );
|
|
383
402
|
|
|
384
403
|
this._dirty = false;
|
|
385
404
|
|
|
@@ -391,6 +410,7 @@ export class NormalDepth extends RenderStage {
|
|
|
391
410
|
|
|
392
411
|
this._outputStorageTex?.dispose();
|
|
393
412
|
this._shadingStorageTex?.dispose();
|
|
413
|
+
this._leafStorageTex?.dispose();
|
|
394
414
|
this.reset();
|
|
395
415
|
// The textures backing the ping-pong are gone, so the prev-frame G-buffer is too.
|
|
396
416
|
this._hasHistory = false;
|
|
@@ -417,6 +437,13 @@ export class NormalDepth extends RenderStage {
|
|
|
417
437
|
this._rtB.texture.needsUpdate = true;
|
|
418
438
|
this._shadingRT.setSize( width, height );
|
|
419
439
|
this._shadingRT.texture.needsUpdate = true;
|
|
440
|
+
if ( this._leafRT ) {
|
|
441
|
+
|
|
442
|
+
this._leafRT.setSize( width, height );
|
|
443
|
+
this._leafRT.texture.needsUpdate = true;
|
|
444
|
+
|
|
445
|
+
}
|
|
446
|
+
|
|
420
447
|
this._hasHistory = false;
|
|
421
448
|
this.resolutionWidth.value = width;
|
|
422
449
|
this.resolutionHeight.value = height;
|
|
@@ -451,7 +478,69 @@ export class NormalDepth extends RenderStage {
|
|
|
451
478
|
|
|
452
479
|
this._outputStorageTex = mk();
|
|
453
480
|
this._shadingStorageTex = mk();
|
|
481
|
+
if ( this._leafStorageTex ) {
|
|
482
|
+
|
|
483
|
+
this._leafStorageTex.dispose();
|
|
484
|
+
this._leafStorageTex = this._makeLeafStorage();
|
|
485
|
+
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
this._computeBuilt = false;
|
|
489
|
+
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Whether to write `pathtracer:instanceLeaf` (the hit's transformed TLAS leaf + 1, 0 = none).
|
|
494
|
+
* Off by default: it costs a full-reserve texture, and changing it rebuilds the kernel.
|
|
495
|
+
*/
|
|
496
|
+
setInstanceLeafOutput( enabled ) {
|
|
497
|
+
|
|
498
|
+
enabled = !! enabled;
|
|
499
|
+
if ( enabled === !! this._leafStorageTex ) return;
|
|
500
|
+
|
|
501
|
+
this._computeNode?.dispose();
|
|
502
|
+
this._computeNode = null;
|
|
454
503
|
this._computeBuilt = false;
|
|
504
|
+
this._dirty = true;
|
|
505
|
+
|
|
506
|
+
if ( enabled ) {
|
|
507
|
+
|
|
508
|
+
this._leafStorageTex = this._makeLeafStorage();
|
|
509
|
+
this._leafRT = new RenderTarget( this._rtA.width, this._rtA.height, {
|
|
510
|
+
type: UnsignedIntType,
|
|
511
|
+
format: RedIntegerFormat,
|
|
512
|
+
minFilter: NearestFilter,
|
|
513
|
+
magFilter: NearestFilter,
|
|
514
|
+
depthBuffer: false,
|
|
515
|
+
stencilBuffer: false
|
|
516
|
+
} );
|
|
517
|
+
|
|
518
|
+
} else {
|
|
519
|
+
|
|
520
|
+
this._leafStorageTex.dispose();
|
|
521
|
+
this._leafRT.dispose();
|
|
522
|
+
this._leafStorageTex = null;
|
|
523
|
+
this._leafRT = null;
|
|
524
|
+
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
_publishLeaf( context ) {
|
|
530
|
+
|
|
531
|
+
if ( this._leafRT ) context.setTexture( 'pathtracer:instanceLeaf', this._leafRT.texture );
|
|
532
|
+
else if ( context.getTexture( 'pathtracer:instanceLeaf' ) ) context.removeTexture( 'pathtracer:instanceLeaf' );
|
|
533
|
+
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
_makeLeafStorage() {
|
|
537
|
+
|
|
538
|
+
const t = new StorageTexture( MAX_STORAGE_TEXTURE_SIZE, MAX_STORAGE_TEXTURE_SIZE );
|
|
539
|
+
t.type = UnsignedIntType;
|
|
540
|
+
t.format = RedIntegerFormat;
|
|
541
|
+
t.minFilter = NearestFilter;
|
|
542
|
+
t.magFilter = NearestFilter;
|
|
543
|
+
return t;
|
|
455
544
|
|
|
456
545
|
}
|
|
457
546
|
|
|
@@ -461,6 +550,8 @@ export class NormalDepth extends RenderStage {
|
|
|
461
550
|
this._outputStorageTex?.dispose();
|
|
462
551
|
this._shadingStorageTex?.dispose();
|
|
463
552
|
this._shadingRT?.dispose();
|
|
553
|
+
this._leafStorageTex?.dispose();
|
|
554
|
+
this._leafRT?.dispose();
|
|
464
555
|
this._rtA?.dispose();
|
|
465
556
|
this._rtB?.dispose();
|
|
466
557
|
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -303,6 +303,7 @@ export class PathTracer extends PathTracerStage {
|
|
|
303
303
|
const auxMixes = this._updateAuxAccumulationUniforms( frameValue );
|
|
304
304
|
this.frame.value = frameValue;
|
|
305
305
|
this.seedFrame.value = this._seedTick ++;
|
|
306
|
+
this.tracedFrames ++;
|
|
306
307
|
|
|
307
308
|
this._setWfDispatch();
|
|
308
309
|
|
|
@@ -155,6 +155,9 @@ export class PathTracerStage extends RenderStage {
|
|
|
155
155
|
// Pinned back to the accumulation index in deterministic mode.
|
|
156
156
|
this._seedTick = 0;
|
|
157
157
|
this._pinSeedToFrame = false;
|
|
158
|
+
// Monotonic, unlike the seed axis; let observers tell a traced frame and a reset apart.
|
|
159
|
+
this.tracedFrames = 0;
|
|
160
|
+
this.resetCount = 0;
|
|
158
161
|
|
|
159
162
|
// Denoising management state
|
|
160
163
|
this.lastRenderMode = - 1;
|
|
@@ -635,6 +638,7 @@ export class PathTracerStage extends RenderStage {
|
|
|
635
638
|
*/
|
|
636
639
|
reset() {
|
|
637
640
|
|
|
641
|
+
this.resetCount ++;
|
|
638
642
|
this.frameCount = 0;
|
|
639
643
|
this.frame.value = 0;
|
|
640
644
|
this.hasPreviousAccumulated.value = 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { EventDispatcher } from 'three';
|
|
1
|
+
import { EventDispatcher, Matrix4 } from 'three';
|
|
2
2
|
import { OIDNDenoiser } from '../Passes/OIDNDenoiser.js';
|
|
3
|
+
import { OIDNTemporalHistory } from '../Passes/OIDNTemporalHistory.js';
|
|
3
4
|
import { AIUpscaler } from '../Passes/AIUpscaler.js';
|
|
4
5
|
import { EngineEvents } from '../EngineEvents.js';
|
|
5
6
|
import { createLogger } from '../utils/Logger.js';
|
|
@@ -73,6 +74,11 @@ const INTERACTION_HOPELESS_FACTOR = 3;
|
|
|
73
74
|
// screen, so any clock would read a perfectly good frame as abandoned the moment it woke.
|
|
74
75
|
const HELD_FRAME_MAX_FAILURES = 3;
|
|
75
76
|
|
|
77
|
+
// Still samples after which the motion history no longer helps; it fades out linearly until then.
|
|
78
|
+
const HISTORY_HANDOFF_SAMPLES = 16;
|
|
79
|
+
|
|
80
|
+
const historyFade = ( samples ) => Math.max( 0, 1 - samples / HISTORY_HANDOFF_SAMPLES );
|
|
81
|
+
|
|
76
82
|
/**
|
|
77
83
|
* Orchestrates all denoising, post-processing, and AI upscaling:
|
|
78
84
|
* - Real-time denoiser strategy switching (ASVGF / NRD / EdgeAware / None)
|
|
@@ -151,6 +157,20 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
151
157
|
this._onPostProcessRefresh = null;
|
|
152
158
|
this._onDisplayRefresh = null;
|
|
153
159
|
|
|
160
|
+
this.temporalHistory = DEFAULT_STATE.oidnTemporalHistory;
|
|
161
|
+
this._history = null;
|
|
162
|
+
// Stale after a scene-changing reset, or one the app did not announce (material/env edits).
|
|
163
|
+
this._historyDirty = true;
|
|
164
|
+
this._knownResetCount = 0;
|
|
165
|
+
this._seenTracedFrames = 0;
|
|
166
|
+
this._viewProj = new Matrix4();
|
|
167
|
+
this._historyCamera = { world: null, projInv: null, viewProj: null };
|
|
168
|
+
// TLAS leaf → { world, offset, prev }: placements moved since the last traced frame.
|
|
169
|
+
this._movedPlacements = new Map();
|
|
170
|
+
this._movedUpload = { count: 0, leaves: new Uint32Array( 0 ), toPrev: new Float32Array( 0 ) };
|
|
171
|
+
this._matA = new Matrix4();
|
|
172
|
+
this._matB = new Matrix4();
|
|
173
|
+
|
|
154
174
|
// Resolution tracking — used for canvas restoration on reset
|
|
155
175
|
this._lastRenderWidth = 0;
|
|
156
176
|
this._lastRenderHeight = 0;
|
|
@@ -211,6 +231,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
211
231
|
this._holdWhileMoving = null;
|
|
212
232
|
this._movingDenoiseMs.length = 0;
|
|
213
233
|
this._movingHopeless = false;
|
|
234
|
+
this._historyDirty = true;
|
|
214
235
|
// A run in flight was sized for the old resolution and setSize rebuilds the network under
|
|
215
236
|
// it. Resets no longer cancel a run while its frame is on screen, so this has to.
|
|
216
237
|
this.denoiser?.abort();
|
|
@@ -229,6 +250,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
this.upscaler?.setBaseSize( width, height );
|
|
253
|
+
this._syncGBufferStages();
|
|
232
254
|
|
|
233
255
|
}
|
|
234
256
|
|
|
@@ -273,9 +295,11 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
273
295
|
adapterInfo: null
|
|
274
296
|
} ),
|
|
275
297
|
|
|
276
|
-
getGPUTextures: () => {
|
|
298
|
+
getGPUTextures: ( { continuous = false } = {} ) => {
|
|
277
299
|
|
|
278
300
|
if ( ! pt?.storageTextures?.readTarget ) return null;
|
|
301
|
+
const history = continuous ? this._historyTextures() : null;
|
|
302
|
+
if ( history ) return history;
|
|
279
303
|
const readTextures = pt.storageTextures.getReadTextures();
|
|
280
304
|
const { backend } = this.renderer;
|
|
281
305
|
return {
|
|
@@ -534,8 +558,9 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
534
558
|
const mv = s.motionVector;
|
|
535
559
|
|
|
536
560
|
const motionNeeded = this.requiresMotionVectors;
|
|
537
|
-
// pathtracer:normalDepth consumed by ASVGF, NRD, EdgeFilter, BilateralFilter
|
|
538
|
-
const normalNeeded = motionNeeded || !! ( s.edgeFilter?.enabled || s.bilateralFilter?.enabled );
|
|
561
|
+
// pathtracer:normalDepth consumed by ASVGF, NRD, EdgeFilter, BilateralFilter, the OIDN history
|
|
562
|
+
const normalNeeded = motionNeeded || this.historyWanted || !! ( s.edgeFilter?.enabled || s.bilateralFilter?.enabled );
|
|
563
|
+
if ( ! this.historyWanted ) this._releaseHistory();
|
|
539
564
|
|
|
540
565
|
if ( nd ) {
|
|
541
566
|
|
|
@@ -543,6 +568,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
543
568
|
// (not the stale static fast-path) and seeds prev = current.
|
|
544
569
|
if ( normalNeeded && ! nd.enabled ) nd.reset();
|
|
545
570
|
nd.enabled = normalNeeded;
|
|
571
|
+
nd.setInstanceLeafOutput?.( this.historyWanted );
|
|
546
572
|
|
|
547
573
|
}
|
|
548
574
|
|
|
@@ -630,7 +656,12 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
630
656
|
const best = this.movingCostMs;
|
|
631
657
|
const seen = this._movingDenoiseMs.length;
|
|
632
658
|
|
|
633
|
-
if ( best > INTERACTION_HOLD_BUDGET_MS * INTERACTION_HOPELESS_FACTOR )
|
|
659
|
+
if ( best > INTERACTION_HOLD_BUDGET_MS * INTERACTION_HOPELESS_FACTOR ) {
|
|
660
|
+
|
|
661
|
+
this._movingHopeless = true;
|
|
662
|
+
this._syncGBufferStages();
|
|
663
|
+
|
|
664
|
+
}
|
|
634
665
|
|
|
635
666
|
// One reading is a warm-up; two that are both over budget are the answer.
|
|
636
667
|
const affordable = ! this._movingHopeless && ( seen < 2 || best <= INTERACTION_HOLD_BUDGET_MS );
|
|
@@ -781,6 +812,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
781
812
|
setCadenceSuspended( suspended ) {
|
|
782
813
|
|
|
783
814
|
this._cadenceSuspended = !! suspended;
|
|
815
|
+
this._historyDirty = true;
|
|
784
816
|
if ( ! this._cadenceSuspended ) return;
|
|
785
817
|
|
|
786
818
|
// The accumulation is what a final render shows; a held preview frame would otherwise sit
|
|
@@ -817,6 +849,221 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
817
849
|
|
|
818
850
|
}
|
|
819
851
|
|
|
852
|
+
// ── Motion history (OIDN live view) ──────────────────────────
|
|
853
|
+
|
|
854
|
+
/** Feeds OIDN the reprojected history of restarted frames while the view moves. */
|
|
855
|
+
setTemporalHistory( enabled ) {
|
|
856
|
+
|
|
857
|
+
this.temporalHistory = !! enabled;
|
|
858
|
+
this._syncGBufferStages();
|
|
859
|
+
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
// Not at a size proven too slow to denoise while moving: the raw render owns the view there.
|
|
863
|
+
get historyWanted() {
|
|
864
|
+
|
|
865
|
+
return this.temporalHistory && this.continuousDenoise && !! this.denoiser?.enabled && ! this._movingHopeless;
|
|
866
|
+
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
get historyActive() {
|
|
870
|
+
|
|
871
|
+
const pt = this._stages.pathTracer;
|
|
872
|
+
return this.historyWanted && ! this._cadenceSuspended && !! this._stages.normalDepth?.enabled
|
|
873
|
+
&& ( pt?.uniforms?.get( 'cameraProjection' )?.value ?? 0 ) === 0
|
|
874
|
+
&& ! ( pt?.visMode?.value > 0 );
|
|
875
|
+
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Called by the app before each of its resets, while the accumulation still exists.
|
|
880
|
+
* @param {Object} [options]
|
|
881
|
+
* @param {boolean} [options.keepHistory] - only the view or object placements changed
|
|
882
|
+
*/
|
|
883
|
+
beforeReset( { keepHistory = false } = {} ) {
|
|
884
|
+
|
|
885
|
+
const pt = this._stages.pathTracer;
|
|
886
|
+
if ( ! pt || ! this.historyActive ) return;
|
|
887
|
+
|
|
888
|
+
const unannounced = pt.resetCount !== this._knownResetCount;
|
|
889
|
+
if ( ! keepHistory || ( unannounced && pt.frameCount < 2 ) ) {
|
|
890
|
+
|
|
891
|
+
this._historyDirty = true;
|
|
892
|
+
return;
|
|
893
|
+
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// A move that starts from a still image starts from that image, not from one sample.
|
|
897
|
+
if ( pt.frameCount < 2 ) return;
|
|
898
|
+
const src = this._historyInputs();
|
|
899
|
+
if ( ! src ) return;
|
|
900
|
+
this._ensureHistory().merge( src, pt.frameCount, this._historyCameraNow(), {
|
|
901
|
+
commit: true,
|
|
902
|
+
keepHistory: ! this._historyDirty && ! unannounced,
|
|
903
|
+
historyScale: historyFade( pt.frameCount ),
|
|
904
|
+
} );
|
|
905
|
+
this._historyDirty = false;
|
|
906
|
+
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** Called by the app right after its own resets, so they are not taken for unannounced ones. */
|
|
910
|
+
afterReset() {
|
|
911
|
+
|
|
912
|
+
this._knownResetCount = this._stages.pathTracer?.resetCount ?? 0;
|
|
913
|
+
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/** Called after every render-loop tick that ran the pipeline. */
|
|
917
|
+
afterTrace() {
|
|
918
|
+
|
|
919
|
+
const pt = this._stages.pathTracer;
|
|
920
|
+
if ( ! pt || pt.tracedFrames === this._seenTracedFrames ) return;
|
|
921
|
+
this._seenTracedFrames = pt.tracedFrames;
|
|
922
|
+
|
|
923
|
+
if ( pt.resetCount !== this._knownResetCount ) {
|
|
924
|
+
|
|
925
|
+
this._historyDirty = true;
|
|
926
|
+
this._knownResetCount = pt.resetCount;
|
|
927
|
+
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Accumulating: the history holds still and is merged in when a refresh starts.
|
|
931
|
+
if ( ! this.historyActive || pt.frame.value !== 0 ) {
|
|
932
|
+
|
|
933
|
+
this._movedPlacements.clear();
|
|
934
|
+
return;
|
|
935
|
+
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const src = this._historyInputs();
|
|
939
|
+
if ( ! src ) {
|
|
940
|
+
|
|
941
|
+
this._movedPlacements.clear();
|
|
942
|
+
this._historyDirty = true;
|
|
943
|
+
return;
|
|
944
|
+
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const history = this._ensureHistory();
|
|
948
|
+
if ( this._historyDirty ) history.invalidate();
|
|
949
|
+
history.accumulate( src, this._historyCameraNow(), this._takeMovedPlacements() );
|
|
950
|
+
this._historyDirty = false;
|
|
951
|
+
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Records a placement's transform before it changes, so the history can follow the object.
|
|
956
|
+
* @param {number} leaf - its TLAS leaf node
|
|
957
|
+
* @param {Float32Array} world - the placement matrix pool
|
|
958
|
+
* @param {number} offset - where this placement's matrix starts in it
|
|
959
|
+
*/
|
|
960
|
+
notePlacementMoving( leaf, world, offset ) {
|
|
961
|
+
|
|
962
|
+
if ( leaf < 0 || ! this.historyActive || this._movedPlacements.has( leaf ) ) return;
|
|
963
|
+
this._movedPlacements.set( leaf, { world, offset, prev: world.slice( offset, offset + 16 ) } );
|
|
964
|
+
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Leaves in ascending order, each with its current→previous world matrix.
|
|
968
|
+
_takeMovedPlacements() {
|
|
969
|
+
|
|
970
|
+
const moved = this._movedPlacements;
|
|
971
|
+
const upload = this._movedUpload;
|
|
972
|
+
upload.count = moved.size;
|
|
973
|
+
if ( ! moved.size ) return upload;
|
|
974
|
+
|
|
975
|
+
if ( upload.leaves.length < moved.size ) {
|
|
976
|
+
|
|
977
|
+
upload.leaves = new Uint32Array( moved.size * 2 );
|
|
978
|
+
upload.toPrev = new Float32Array( moved.size * 32 );
|
|
979
|
+
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const leaves = [ ...moved.keys() ].sort( ( a, b ) => a - b );
|
|
983
|
+
for ( let i = 0; i < leaves.length; i ++ ) {
|
|
984
|
+
|
|
985
|
+
const { world, offset, prev } = moved.get( leaves[ i ] );
|
|
986
|
+
this._matB.fromArray( world, offset ).invert();
|
|
987
|
+
this._matA.fromArray( prev ).multiply( this._matB ).toArray( upload.toPrev, i * 16 );
|
|
988
|
+
upload.leaves[ i ] = leaves[ i ];
|
|
989
|
+
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
moved.clear();
|
|
993
|
+
return upload;
|
|
994
|
+
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// What a live refresh denoises, or null for the plain accumulation.
|
|
998
|
+
_historyTextures() {
|
|
999
|
+
|
|
1000
|
+
const pt = this._stages.pathTracer;
|
|
1001
|
+
const history = this._history;
|
|
1002
|
+
if ( ! history?.valid || this._historyDirty || ! this.historyActive || pt.isComplete ) return null;
|
|
1003
|
+
if ( pt.resetCount !== this._knownResetCount ) return null;
|
|
1004
|
+
if ( history.width !== this._lastRenderWidth || history.height !== this._lastRenderHeight ) return null;
|
|
1005
|
+
if ( pt.frameCount <= 1 ) return history.textures;
|
|
1006
|
+
if ( pt.frameCount >= HISTORY_HANDOFF_SAMPLES ) return null;
|
|
1007
|
+
|
|
1008
|
+
const src = this._historyInputs();
|
|
1009
|
+
return src ? history.merge( src, pt.frameCount, this._historyCameraNow(), { historyScale: historyFade( pt.frameCount ) } ) : null;
|
|
1010
|
+
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
_historyInputs() {
|
|
1014
|
+
|
|
1015
|
+
const pt = this._stages.pathTracer;
|
|
1016
|
+
const ctx = this.pipeline?.context;
|
|
1017
|
+
const backend = this.renderer?.backend;
|
|
1018
|
+
if ( ! pt?.storageTextures?.readTarget || ! ctx || ! backend ) return null;
|
|
1019
|
+
|
|
1020
|
+
const read = pt.storageTextures.getReadTextures();
|
|
1021
|
+
const gpu = ( texture ) => ( texture ? backend.get( texture )?.texture : null );
|
|
1022
|
+
const src = {
|
|
1023
|
+
color: gpu( read.color ),
|
|
1024
|
+
albedo: gpu( read.albedo ),
|
|
1025
|
+
normal: gpu( read.normalDepth ),
|
|
1026
|
+
geo: gpu( ctx.getTexture( 'pathtracer:normalDepth' ) ),
|
|
1027
|
+
geoPrev: gpu( ctx.getTexture( 'pathtracer:prevNormalDepth' ) ),
|
|
1028
|
+
shading: gpu( ctx.getTexture( 'pathtracer:shadingNormal' ) ),
|
|
1029
|
+
leaf: gpu( ctx.getTexture( 'pathtracer:instanceLeaf' ) ),
|
|
1030
|
+
width: pt.width,
|
|
1031
|
+
height: pt.height,
|
|
1032
|
+
};
|
|
1033
|
+
return src.color && src.albedo && src.normal && src.geo && src.geoPrev && src.shading && src.leaf ? src : null;
|
|
1034
|
+
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
// The camera of the frame last traced: the path tracer's uniforms, not the live camera.
|
|
1038
|
+
_historyCameraNow() {
|
|
1039
|
+
|
|
1040
|
+
const pt = this._stages.pathTracer;
|
|
1041
|
+
this._viewProj.multiplyMatrices( pt.cameraProjectionMatrix.value, pt.cameraViewMatrix.value );
|
|
1042
|
+
const camera = this._historyCamera;
|
|
1043
|
+
camera.world = pt.cameraWorldMatrix.value.elements;
|
|
1044
|
+
camera.projInv = pt.cameraProjectionMatrixInverse.value.elements;
|
|
1045
|
+
camera.viewProj = this._viewProj.elements;
|
|
1046
|
+
return camera;
|
|
1047
|
+
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
_ensureHistory() {
|
|
1051
|
+
|
|
1052
|
+
this._history ??= new OIDNTemporalHistory( this.renderer.backend.device );
|
|
1053
|
+
this._history.settings.cleanAux = !! this.denoiser?.expectsCleanAux( this._finalQuality );
|
|
1054
|
+
return this._history;
|
|
1055
|
+
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
_releaseHistory() {
|
|
1059
|
+
|
|
1060
|
+
this._history?.dispose();
|
|
1061
|
+
this._history = null;
|
|
1062
|
+
this._historyDirty = true;
|
|
1063
|
+
this._movedPlacements.clear();
|
|
1064
|
+
|
|
1065
|
+
}
|
|
1066
|
+
|
|
820
1067
|
/**
|
|
821
1068
|
* Selects the AI upscaler's model. 'neural' is fixed at 2x and reads the denoised picture, so it
|
|
822
1069
|
* is a no-op without a denoiser — feeding it raw Monte-Carlo noise measurably loses to bilinear.
|
|
@@ -1190,6 +1437,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1190
1437
|
|
|
1191
1438
|
this._unpublishOutput();
|
|
1192
1439
|
this._restoreRenderDisplay();
|
|
1440
|
+
this._historyDirty = true;
|
|
1193
1441
|
|
|
1194
1442
|
}
|
|
1195
1443
|
|
|
@@ -1203,6 +1451,7 @@ export class DenoisingManager extends EventDispatcher {
|
|
|
1203
1451
|
|
|
1204
1452
|
// Before the denoiser destroys the picture the Compositor would otherwise still sample.
|
|
1205
1453
|
this._unpublishOutput();
|
|
1454
|
+
this._releaseHistory();
|
|
1206
1455
|
|
|
1207
1456
|
if ( this.denoiser ) {
|
|
1208
1457
|
|