rayzee 8.6.1 → 8.8.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 +35 -6
- package/dist/rayzee.es.js +4114 -2994
- package/dist/rayzee.umd.js +256 -18
- 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 +131 -7
- package/src/Processor/AssetLoader.js +14 -8
- package/src/Processor/EmissiveTriangleBuilder.js +70 -0
- package/src/Processor/PBRT/PBRTAnimation.js +646 -0
- package/src/Processor/PBRT/PBRTParser.js +179 -52
- package/src/Processor/PBRT/PBRTSceneBuilder.js +181 -25
- package/src/Processor/PBRT/index.js +73 -14
- package/src/Processor/SceneProcessor.js +22 -0
- package/src/Stages/NormalDepth.js +94 -3
- package/src/Stages/PathTracer.js +1 -0
- package/src/Stages/PathTracerStage.js +20 -1
- package/src/managers/AnimationManager.js +202 -8
- package/src/managers/DenoisingManager.js +254 -5
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AnimationManager — Drives
|
|
2
|
+
* AnimationManager — Drives animation clips for the path tracer.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* a
|
|
4
|
+
* Deforming clips (a SkinnedMesh or morph track) return a per-mesh position reader for
|
|
5
|
+
* PathTracerApp.refitBVH(). Rigid clips return nothing and hand the moved meshes to
|
|
6
|
+
* `applyPoseCallback`, which updates placements only — a refit would bake world positions into
|
|
7
|
+
* triangles that other placements share.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
|
-
import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce } from 'three';
|
|
10
|
+
import { AnimationMixer, EventDispatcher, Timer, Vector3, LoopRepeat, LoopOnce, PropertyBinding } from 'three';
|
|
10
11
|
import { EngineEvents } from '../EngineEvents.js';
|
|
11
12
|
|
|
12
13
|
export class AnimationManager extends EventDispatcher {
|
|
@@ -31,9 +32,20 @@ export class AnimationManager extends EventDispatcher {
|
|
|
31
32
|
this._savedTimeScale = 1;
|
|
32
33
|
this.onFinished = null; // callback when a non-looping clip ends
|
|
33
34
|
|
|
35
|
+
this._rigid = false;
|
|
36
|
+
this._plans = null; // per clip: { meshes: Int32Array, visibility: boolean, cameras: Camera[] }
|
|
37
|
+
this._allPlan = null;
|
|
38
|
+
this._activeClip = 0;
|
|
39
|
+
this._slotOf = null; // meshIndex -> row in the two caches below, or -1
|
|
40
|
+
this._lastWorld = null; // world matrix last handed to applyPoseCallback, 16 per row
|
|
41
|
+
this._lastVisible = null; // 0 / 1, or 2 before the first pose
|
|
42
|
+
|
|
34
43
|
/** Injected by PathTracerApp — wakes the render loop after play/resume. */
|
|
35
44
|
this.wakeCallback = null;
|
|
36
45
|
|
|
46
|
+
/** Injected by PathTracerApp — receives `{ meshIndices, visibilityChanged, cameras }`. */
|
|
47
|
+
this.applyPoseCallback = null;
|
|
48
|
+
|
|
37
49
|
}
|
|
38
50
|
|
|
39
51
|
/**
|
|
@@ -88,6 +100,10 @@ export class AnimationManager extends EventDispatcher {
|
|
|
88
100
|
|
|
89
101
|
this.actions = actions;
|
|
90
102
|
|
|
103
|
+
this._rigid = ! meshes.some( m => m?.isSkinnedMesh ) &&
|
|
104
|
+
! animations.some( clip => clip.tracks.some( t => t.name.includes( 'morphTargetInfluences' ) ) );
|
|
105
|
+
this._planClips( animations );
|
|
106
|
+
|
|
91
107
|
// Listen for non-looping clip completion
|
|
92
108
|
this.mixer.addEventListener( 'finished', () => {
|
|
93
109
|
|
|
@@ -121,7 +137,154 @@ export class AnimationManager extends EventDispatcher {
|
|
|
121
137
|
this._meshPositions = [];
|
|
122
138
|
|
|
123
139
|
const skinnedCount = meshes.filter( m => m.isSkinnedMesh ).length;
|
|
124
|
-
console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles` );
|
|
140
|
+
console.debug( `[AnimationManager] Init: ${animations.length} clips, ${meshes.length} meshes (${skinnedCount} skinned), ${offset} triangles, ${this._rigid ? 'rigid' : 'deforming'}` );
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Which meshes and cameras each clip moves, directly or through an ancestor. @private */
|
|
145
|
+
_planClips( animations ) {
|
|
146
|
+
|
|
147
|
+
const root = this._mixerRoot;
|
|
148
|
+
const byName = new Map();
|
|
149
|
+
// First name in pre-order wins, as in PropertyBinding.findNode.
|
|
150
|
+
root.traverse?.( node => {
|
|
151
|
+
|
|
152
|
+
if ( ! byName.has( node.name ) ) byName.set( node.name, node );
|
|
153
|
+
byName.set( node.uuid, node );
|
|
154
|
+
|
|
155
|
+
} );
|
|
156
|
+
|
|
157
|
+
this._plans = animations.map( clip => {
|
|
158
|
+
|
|
159
|
+
const animated = new Set();
|
|
160
|
+
let visibility = false;
|
|
161
|
+
for ( const track of clip.tracks ) {
|
|
162
|
+
|
|
163
|
+
const { nodeName, propertyName } = PropertyBinding.parseTrackName( track.name );
|
|
164
|
+
const node = byName.get( nodeName );
|
|
165
|
+
if ( ! node ) continue;
|
|
166
|
+
animated.add( node );
|
|
167
|
+
if ( propertyName === 'visible' ) visibility = true;
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const meshes = [];
|
|
172
|
+
for ( let m = 0; m < this._meshes.length; m ++ ) {
|
|
173
|
+
|
|
174
|
+
for ( let o = this._meshes[ m ]; o; o = o.parent ) {
|
|
175
|
+
|
|
176
|
+
if ( animated.has( o ) ) {
|
|
177
|
+
|
|
178
|
+
meshes.push( m );
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const cameras = new Set();
|
|
188
|
+
for ( const node of animated ) node.traverse( o => o.isCamera && cameras.add( o ) );
|
|
189
|
+
|
|
190
|
+
return { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
|
|
191
|
+
|
|
192
|
+
} );
|
|
193
|
+
|
|
194
|
+
this._allPlan = null;
|
|
195
|
+
if ( ! this._rigid ) return;
|
|
196
|
+
|
|
197
|
+
this._slotOf = new Int32Array( this._meshes.length ).fill( - 1 );
|
|
198
|
+
let rows = 0;
|
|
199
|
+
for ( const plan of this._plans ) for ( const m of plan.meshes ) if ( this._slotOf[ m ] < 0 ) this._slotOf[ m ] = rows ++;
|
|
200
|
+
|
|
201
|
+
// Unknown until the first pose, which therefore sends every animated mesh.
|
|
202
|
+
this._lastWorld = new Float64Array( rows * 16 ).fill( NaN );
|
|
203
|
+
this._lastVisible = new Uint8Array( rows ).fill( 2 );
|
|
204
|
+
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** The plan for a clip index, or the union of every clip for -1. @private */
|
|
208
|
+
_planFor( clipIndex ) {
|
|
209
|
+
|
|
210
|
+
if ( ! this._plans ) return null;
|
|
211
|
+
if ( clipIndex >= 0 ) return this._plans[ clipIndex ] ?? null;
|
|
212
|
+
|
|
213
|
+
if ( ! this._allPlan ) {
|
|
214
|
+
|
|
215
|
+
const meshes = new Set(), cameras = new Set();
|
|
216
|
+
let visibility = false;
|
|
217
|
+
for ( const plan of this._plans ) {
|
|
218
|
+
|
|
219
|
+
plan.meshes.forEach( m => meshes.add( m ) );
|
|
220
|
+
plan.cameras.forEach( c => cameras.add( c ) );
|
|
221
|
+
visibility ||= plan.visibility;
|
|
222
|
+
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this._allPlan = { meshes: Int32Array.from( meshes ), visibility, cameras: [ ...cameras ] };
|
|
226
|
+
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return this._allPlan;
|
|
230
|
+
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Send meshes whose world matrix or visibility changed since the last pose. @private */
|
|
234
|
+
_applyRigidPose( clipIndex ) {
|
|
235
|
+
|
|
236
|
+
const plan = this._planFor( clipIndex );
|
|
237
|
+
if ( ! plan ) return;
|
|
238
|
+
|
|
239
|
+
const moved = [];
|
|
240
|
+
let visibilityChanged = false;
|
|
241
|
+
const last = this._lastWorld, lastVisible = this._lastVisible;
|
|
242
|
+
|
|
243
|
+
for ( const m of plan.meshes ) {
|
|
244
|
+
|
|
245
|
+
const mesh = this._meshes[ m ];
|
|
246
|
+
const row = this._slotOf[ m ];
|
|
247
|
+
const e = mesh.matrixWorld.elements;
|
|
248
|
+
const o = row * 16;
|
|
249
|
+
|
|
250
|
+
for ( let i = 0; i < 16; i ++ ) {
|
|
251
|
+
|
|
252
|
+
if ( last[ o + i ] !== e[ i ] ) {
|
|
253
|
+
|
|
254
|
+
for ( let j = 0; j < 16; j ++ ) last[ o + j ] = e[ j ];
|
|
255
|
+
moved.push( m );
|
|
256
|
+
break;
|
|
257
|
+
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if ( plan.visibility || lastVisible[ row ] === 2 ) {
|
|
263
|
+
|
|
264
|
+
let visible = 1;
|
|
265
|
+
for ( let n = mesh; n; n = n.parent ) if ( ! n.visible ) {
|
|
266
|
+
|
|
267
|
+
visible = 0;
|
|
268
|
+
break;
|
|
269
|
+
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if ( lastVisible[ row ] !== visible ) {
|
|
273
|
+
|
|
274
|
+
lastVisible[ row ] = visible;
|
|
275
|
+
visibilityChanged = true;
|
|
276
|
+
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if ( moved.length || visibilityChanged || plan.cameras.length ) {
|
|
284
|
+
|
|
285
|
+
this.applyPoseCallback?.( { meshIndices: moved, visibilityChanged, cameras: plan.cameras } );
|
|
286
|
+
|
|
287
|
+
}
|
|
125
288
|
|
|
126
289
|
}
|
|
127
290
|
|
|
@@ -145,6 +308,7 @@ export class AnimationManager extends EventDispatcher {
|
|
|
145
308
|
|
|
146
309
|
}
|
|
147
310
|
|
|
311
|
+
this._activeClip = clipIndex;
|
|
148
312
|
this.timer.reset();
|
|
149
313
|
this.isPlaying = true;
|
|
150
314
|
this.wakeCallback?.();
|
|
@@ -188,10 +352,19 @@ export class AnimationManager extends EventDispatcher {
|
|
|
188
352
|
|
|
189
353
|
if ( ! this.mixer ) return;
|
|
190
354
|
|
|
355
|
+
// Restores every animated property to its loaded value.
|
|
191
356
|
this.mixer.stopAllAction();
|
|
192
357
|
this.mixer.timeScale = this._savedTimeScale || 1;
|
|
193
358
|
this.timer.reset();
|
|
194
359
|
this.isPlaying = false;
|
|
360
|
+
|
|
361
|
+
if ( this._rigid ) {
|
|
362
|
+
|
|
363
|
+
this._mixerRoot.updateMatrixWorld( true );
|
|
364
|
+
this._applyRigidPose( - 1 );
|
|
365
|
+
|
|
366
|
+
}
|
|
367
|
+
|
|
195
368
|
this.dispatchEvent( { type: EngineEvents.ANIMATION_STOPPED } );
|
|
196
369
|
|
|
197
370
|
}
|
|
@@ -229,12 +402,15 @@ export class AnimationManager extends EventDispatcher {
|
|
|
229
402
|
*
|
|
230
403
|
* @param {number} time - Absolute time in seconds
|
|
231
404
|
* @param {number} [clipIndex=0] - Clip to evaluate, or -1 for all active
|
|
232
|
-
* @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if no
|
|
405
|
+
* @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if no
|
|
406
|
+
* mixer or the clips are rigid
|
|
233
407
|
*/
|
|
234
408
|
seekTo( time, clipIndex = 0 ) {
|
|
235
409
|
|
|
236
410
|
if ( ! this.mixer || this.actions.length === 0 ) return null;
|
|
237
411
|
|
|
412
|
+
this._activeClip = clipIndex;
|
|
413
|
+
|
|
238
414
|
// Ensure the target action(s) are active so setTime evaluates them.
|
|
239
415
|
// Actions must NOT be paused — setTime() calls update() internally,
|
|
240
416
|
// which skips paused actions entirely.
|
|
@@ -279,7 +455,8 @@ export class AnimationManager extends EventDispatcher {
|
|
|
279
455
|
* Advance animation and prepare deformed positions.
|
|
280
456
|
* Call once per frame from the animate loop.
|
|
281
457
|
*
|
|
282
|
-
* @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if not
|
|
458
|
+
* @returns {function(number): Float32Array|null} Per-mesh reader for refitBVH, or null if not
|
|
459
|
+
* playing or the clips are rigid
|
|
283
460
|
*/
|
|
284
461
|
update() {
|
|
285
462
|
|
|
@@ -312,6 +489,16 @@ export class AnimationManager extends EventDispatcher {
|
|
|
312
489
|
// once here, on mixerRoot rather than the scene, keeps unrelated static objects out of it.
|
|
313
490
|
this._mixerRoot.updateMatrixWorld( true );
|
|
314
491
|
|
|
492
|
+
if ( this._rigid ) {
|
|
493
|
+
|
|
494
|
+
this._applyRigidPose( this._activeClip );
|
|
495
|
+
return null;
|
|
496
|
+
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
const cameras = this._planFor( this._activeClip )?.cameras;
|
|
500
|
+
if ( cameras?.length ) this.applyPoseCallback?.( { meshIndices: [], visibilityChanged: false, cameras } );
|
|
501
|
+
|
|
315
502
|
return m => this._computeMeshPositions( m );
|
|
316
503
|
|
|
317
504
|
}
|
|
@@ -446,6 +633,13 @@ export class AnimationManager extends EventDispatcher {
|
|
|
446
633
|
this._meshPositions = null;
|
|
447
634
|
this._skinnedCache = null;
|
|
448
635
|
this._clipsCache = null;
|
|
636
|
+
this._rigid = false;
|
|
637
|
+
this._plans = null;
|
|
638
|
+
this._allPlan = null;
|
|
639
|
+
this._activeClip = 0;
|
|
640
|
+
this._slotOf = null;
|
|
641
|
+
this._lastWorld = null;
|
|
642
|
+
this._lastVisible = null;
|
|
449
643
|
|
|
450
644
|
}
|
|
451
645
|
|
|
@@ -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
|
|