rayzee 5.3.7 → 5.4.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 +85 -2
- package/dist/rayzee.es.js +1727 -1679
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +7 -7
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +2 -2
- package/src/Passes/AIUpscaler.js +30 -6
- package/src/Passes/OIDNDenoiser.js +57 -15
- package/src/PathTracerApp.js +153 -19
- package/src/Pipeline/RenderPipeline.js +10 -1
- package/src/Processor/ShaderBuilder.js +53 -5
- package/src/Stages/ASVGF.js +4 -4
- package/src/Stages/AdaptiveSampling.js +2 -2
- package/src/Stages/AutoExposure.js +42 -32
- package/src/Stages/BilateralFilter.js +2 -2
- package/src/Stages/Display.js +2 -1
- package/src/Stages/EdgeFilter.js +6 -3
- package/src/Stages/MotionVector.js +2 -2
- package/src/Stages/NormalDepth.js +1 -1
- package/src/Stages/PathTracer.js +1 -3
- package/src/Stages/SSRC.js +4 -4
- package/src/Stages/Variance.js +2 -2
- package/src/TSL/wgslGlobalVarsPatch.js +60 -0
- package/src/index.js +1 -1
- package/src/managers/LightManager.js +20 -0
- package/src/managers/UniformManager.js +19 -0
- package/src/managers/helpers/OutlineHelper.js +3 -1
- package/src/README.md +0 -284
- package/src/TSL/storageTexturePatch.js +0 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rayzee",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Real-time WebGPU path tracing engine built on Three.js",
|
|
6
6
|
"main": "dist/rayzee.umd.js",
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
"prepublishOnly": "npm run build"
|
|
35
35
|
},
|
|
36
36
|
"peerDependencies": {
|
|
37
|
-
"three": ">=0.
|
|
37
|
+
"three": ">=0.184.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
40
|
"stats-gl": "^4.0.2"
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -114,6 +114,14 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
114
114
|
this._baseWidth = output.width;
|
|
115
115
|
this._baseHeight = output.height;
|
|
116
116
|
|
|
117
|
+
// Pooled HDR readback staging buffer — reused across _captureSourceHDR calls.
|
|
118
|
+
// Rebuilt only when the source texture dimensions change (same spirit as
|
|
119
|
+
// r184's ReadbackBuffer; we can't use renderer.getArrayBufferAsync directly
|
|
120
|
+
// because our source is a raw GPUTexture, not a Three.js BufferAttribute).
|
|
121
|
+
this._hdrStagingBuffer = null;
|
|
122
|
+
this._hdrStagingWidth = 0;
|
|
123
|
+
this._hdrStagingHeight = 0;
|
|
124
|
+
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
// ─── Model Management ─────────────────────────────────────────────────────
|
|
@@ -477,14 +485,26 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
477
485
|
const width = colorTexture.width;
|
|
478
486
|
const height = colorTexture.height;
|
|
479
487
|
|
|
480
|
-
// GPU texture → staging buffer → CPU readback
|
|
488
|
+
// GPU texture → pooled staging buffer → CPU readback.
|
|
489
|
+
// The staging buffer is kept alive between calls (unmap, don't destroy)
|
|
490
|
+
// and only re-created when texture dimensions change.
|
|
481
491
|
const bytesPerRow = Math.ceil( width * 16 / 256 ) * 256; // rgba32float=16 bytes, aligned to 256
|
|
482
492
|
const bufferSize = bytesPerRow * height;
|
|
483
493
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
494
|
+
if ( this._hdrStagingWidth !== width || this._hdrStagingHeight !== height ) {
|
|
495
|
+
|
|
496
|
+
this._hdrStagingBuffer?.destroy();
|
|
497
|
+
this._hdrStagingBuffer = device.createBuffer( {
|
|
498
|
+
label: 'aiupscaler-hdr-readback',
|
|
499
|
+
size: bufferSize,
|
|
500
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
501
|
+
} );
|
|
502
|
+
this._hdrStagingWidth = width;
|
|
503
|
+
this._hdrStagingHeight = height;
|
|
504
|
+
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
const stagingBuffer = this._hdrStagingBuffer;
|
|
488
508
|
|
|
489
509
|
const encoder = device.createCommandEncoder();
|
|
490
510
|
encoder.copyTextureToBuffer(
|
|
@@ -511,7 +531,6 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
511
531
|
}
|
|
512
532
|
|
|
513
533
|
stagingBuffer.unmap();
|
|
514
|
-
stagingBuffer.destroy();
|
|
515
534
|
|
|
516
535
|
// Mark as HDR so _extractTile and _tensorToImageData handle it correctly
|
|
517
536
|
return { data, width, height, isHDR: true };
|
|
@@ -856,6 +875,11 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
856
875
|
this._upscaledAlpha = null;
|
|
857
876
|
this.state.abortController = null;
|
|
858
877
|
|
|
878
|
+
this._hdrStagingBuffer?.destroy();
|
|
879
|
+
this._hdrStagingBuffer = null;
|
|
880
|
+
this._hdrStagingWidth = 0;
|
|
881
|
+
this._hdrStagingHeight = 0;
|
|
882
|
+
|
|
859
883
|
console.log( 'AIUpscaler disposed' );
|
|
860
884
|
|
|
861
885
|
}
|
|
@@ -76,6 +76,17 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
76
76
|
// Cached GPU storage buffers for texture→buffer copies (reused across denoise calls)
|
|
77
77
|
this._gpuInputBuffers = { color: null, albedo: null, normal: null };
|
|
78
78
|
this._gpuInputBufferSize = { width: 0, height: 0 };
|
|
79
|
+
// Shared pad-strip buffer for non-256-aligned widths. Reused across
|
|
80
|
+
// color/albedo/normal copies within the same encoder (WebGPU command
|
|
81
|
+
// order guarantees the overwrites are serialized).
|
|
82
|
+
this._gpuInputPadBuffer = null;
|
|
83
|
+
this._gpuInputPaddedRowBytes = 0;
|
|
84
|
+
// Pooled MAP_READ staging buffer for _cacheInputAlpha. Only allocated
|
|
85
|
+
// when transparent-background readback is used, destroyed on resolution
|
|
86
|
+
// change or dispose. Same spirit as r184's ReadbackBuffer — we can't use
|
|
87
|
+
// renderer.getArrayBufferAsync because the source is a raw GPUBuffer,
|
|
88
|
+
// not a Three.js BufferAttribute.
|
|
89
|
+
this._alphaReadbackBuffer = null;
|
|
79
90
|
|
|
80
91
|
// Cached alpha channel from the input color buffer (OIDN discards alpha)
|
|
81
92
|
this._cachedAlpha = null;
|
|
@@ -385,13 +396,15 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
385
396
|
|
|
386
397
|
// Copy render target textures → tightly packed GPU storage buffers for oidn-web.
|
|
387
398
|
// copyTextureToBuffer requires bytesPerRow to be a multiple of 256. When the tight
|
|
388
|
-
// row size (width * 16) isn't aligned, copy via a padded
|
|
389
|
-
// then strip padding row-by-row.
|
|
399
|
+
// row size (width * 16) isn't aligned, copy via a shared pre-allocated padded buffer
|
|
400
|
+
// (see _ensureGPUInputBuffers) then strip padding row-by-row. The pad buffer is
|
|
401
|
+
// reused across color/albedo/normal — safe because WebGPU serializes commands
|
|
402
|
+
// within a single encoder.
|
|
390
403
|
const encoder = device.createCommandEncoder( { label: 'oidn-tex-to-buf' } );
|
|
391
404
|
const tightRowBytes = width * 16; // rgba32float
|
|
392
|
-
const paddedRowBytes =
|
|
393
|
-
const needsPadStrip = paddedRowBytes
|
|
394
|
-
const
|
|
405
|
+
const paddedRowBytes = this._gpuInputPaddedRowBytes;
|
|
406
|
+
const needsPadStrip = paddedRowBytes > tightRowBytes;
|
|
407
|
+
const padBuf = this._gpuInputPadBuffer;
|
|
395
408
|
|
|
396
409
|
const copyTex = ( tex, tightBuf ) => {
|
|
397
410
|
|
|
@@ -405,9 +418,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
405
418
|
|
|
406
419
|
} else {
|
|
407
420
|
|
|
408
|
-
const padBuf = device.createBuffer( { size: paddedRowBytes * height, usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC } );
|
|
409
|
-
stagingBufs.push( padBuf );
|
|
410
|
-
|
|
411
421
|
encoder.copyTextureToBuffer(
|
|
412
422
|
{ texture: tex, mipLevel: 0 },
|
|
413
423
|
{ buffer: padBuf, offset: 0, bytesPerRow: paddedRowBytes, rowsPerImage: height },
|
|
@@ -429,7 +439,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
429
439
|
copyTex( textures.normal, this._gpuInputBuffers.normal );
|
|
430
440
|
|
|
431
441
|
device.queue.submit( [ encoder.finish() ] );
|
|
432
|
-
for ( const buf of stagingBufs ) buf.destroy();
|
|
433
442
|
|
|
434
443
|
// Cache alpha channel from input color buffer when transparent background is enabled.
|
|
435
444
|
// OIDN only processes RGB — the alpha channel is lost, so we read it before denoising.
|
|
@@ -478,6 +487,25 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
478
487
|
this._gpuInputBuffers.normal = device.createBuffer( { label: 'oidn-in-normal', size: byteSize, usage } );
|
|
479
488
|
this._gpuInputBufferSize = { width, height };
|
|
480
489
|
|
|
490
|
+
// Pre-allocate the row-pad staging buffer when width * 16 isn't 256-aligned.
|
|
491
|
+
// Shared across the three texture copies; recreated only on resolution change.
|
|
492
|
+
const tightRowBytes = width * 16;
|
|
493
|
+
const paddedRowBytes = Math.ceil( tightRowBytes / 256 ) * 256;
|
|
494
|
+
if ( paddedRowBytes !== tightRowBytes ) {
|
|
495
|
+
|
|
496
|
+
this._gpuInputPadBuffer = device.createBuffer( {
|
|
497
|
+
label: 'oidn-in-pad',
|
|
498
|
+
size: paddedRowBytes * height,
|
|
499
|
+
usage: GPUBufferUsage.COPY_DST | GPUBufferUsage.COPY_SRC
|
|
500
|
+
} );
|
|
501
|
+
this._gpuInputPaddedRowBytes = paddedRowBytes;
|
|
502
|
+
|
|
503
|
+
} else {
|
|
504
|
+
|
|
505
|
+
this._gpuInputPaddedRowBytes = tightRowBytes;
|
|
506
|
+
|
|
507
|
+
}
|
|
508
|
+
|
|
481
509
|
}
|
|
482
510
|
|
|
483
511
|
_destroyGPUInputBuffers() {
|
|
@@ -485,7 +513,12 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
485
513
|
this._gpuInputBuffers.color?.destroy();
|
|
486
514
|
this._gpuInputBuffers.albedo?.destroy();
|
|
487
515
|
this._gpuInputBuffers.normal?.destroy();
|
|
516
|
+
this._gpuInputPadBuffer?.destroy();
|
|
517
|
+
this._alphaReadbackBuffer?.destroy();
|
|
488
518
|
this._gpuInputBuffers = { color: null, albedo: null, normal: null };
|
|
519
|
+
this._gpuInputPadBuffer = null;
|
|
520
|
+
this._gpuInputPaddedRowBytes = 0;
|
|
521
|
+
this._alphaReadbackBuffer = null;
|
|
489
522
|
this._gpuInputBufferSize = { width: 0, height: 0 };
|
|
490
523
|
|
|
491
524
|
}
|
|
@@ -497,11 +530,21 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
497
530
|
async _cacheInputAlpha( device, width, height ) {
|
|
498
531
|
|
|
499
532
|
const byteSize = width * height * 16; // rgba32float, tightly packed
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
533
|
+
|
|
534
|
+
// Lazy-allocate the pooled staging buffer on first call at this resolution.
|
|
535
|
+
// _destroyGPUInputBuffers clears it on resolution change or dispose, so if
|
|
536
|
+
// it is non-null here, it already matches the current resolution.
|
|
537
|
+
if ( this._alphaReadbackBuffer === null ) {
|
|
538
|
+
|
|
539
|
+
this._alphaReadbackBuffer = device.createBuffer( {
|
|
540
|
+
label: 'oidn-alpha-readback',
|
|
541
|
+
size: byteSize,
|
|
542
|
+
usage: GPUBufferUsage.MAP_READ | GPUBufferUsage.COPY_DST
|
|
543
|
+
} );
|
|
544
|
+
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
const staging = this._alphaReadbackBuffer;
|
|
505
548
|
|
|
506
549
|
const enc = device.createCommandEncoder();
|
|
507
550
|
enc.copyBufferToBuffer( this._gpuInputBuffers.color, 0, staging, 0, byteSize );
|
|
@@ -520,7 +563,6 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
520
563
|
}
|
|
521
564
|
|
|
522
565
|
staging.unmap();
|
|
523
|
-
staging.destroy();
|
|
524
566
|
|
|
525
567
|
this._cachedAlpha = alpha;
|
|
526
568
|
this._cachedAlphaWidth = width;
|
package/src/PathTracerApp.js
CHANGED
|
@@ -19,7 +19,7 @@ import { Display } from './Stages/Display.js';
|
|
|
19
19
|
import { RenderPipeline } from './Pipeline/RenderPipeline.js';
|
|
20
20
|
import { CompletionTracker } from './Pipeline/CompletionTracker.js';
|
|
21
21
|
import { ENGINE_DEFAULTS as DEFAULT_STATE, FINAL_RENDER_CONFIG, PREVIEW_RENDER_CONFIG } from './EngineDefaults.js';
|
|
22
|
-
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples } from './Processor/utils.js';
|
|
22
|
+
import { updateStats, updateLoading, resetLoading, setStatusCallback, getDisplaySamples, disposeObjectFromMemory } from './Processor/utils.js';
|
|
23
23
|
import { BuildTimer } from './Processor/BuildTimer.js';
|
|
24
24
|
import { InteractionManager } from './managers/InteractionManager.js';
|
|
25
25
|
import { EngineEvents } from './EngineEvents.js';
|
|
@@ -129,6 +129,45 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
129
129
|
// Resolution state
|
|
130
130
|
this._resizeDebounceTimer = null;
|
|
131
131
|
|
|
132
|
+
// Tracked listeners for clean dispose()
|
|
133
|
+
this._trackedListeners = [];
|
|
134
|
+
this._disposed = false;
|
|
135
|
+
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Registers an event listener and tracks it for automatic cleanup on dispose().
|
|
140
|
+
* @param {EventTarget|{addEventListener:Function, removeEventListener:Function}} target
|
|
141
|
+
* @param {string} type
|
|
142
|
+
* @param {Function} handler
|
|
143
|
+
*/
|
|
144
|
+
_addTrackedListener( target, type, handler ) {
|
|
145
|
+
|
|
146
|
+
if ( ! target ) return;
|
|
147
|
+
target.addEventListener( type, handler );
|
|
148
|
+
this._trackedListeners.push( { target, type, handler } );
|
|
149
|
+
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Removes all listeners registered via _addTrackedListener. */
|
|
153
|
+
_removeTrackedListeners() {
|
|
154
|
+
|
|
155
|
+
for ( const { target, type, handler } of this._trackedListeners ) {
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
|
|
159
|
+
target.removeEventListener( type, handler );
|
|
160
|
+
|
|
161
|
+
} catch ( err ) {
|
|
162
|
+
|
|
163
|
+
console.warn( 'PathTracerApp: failed to remove listener', type, err );
|
|
164
|
+
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
this._trackedListeners.length = 0;
|
|
170
|
+
|
|
132
171
|
}
|
|
133
172
|
|
|
134
173
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -356,19 +395,22 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
356
395
|
*/
|
|
357
396
|
dispose() {
|
|
358
397
|
|
|
359
|
-
this.
|
|
360
|
-
this.
|
|
361
|
-
setStatusCallback( null );
|
|
398
|
+
if ( this._disposed ) return;
|
|
399
|
+
this._disposed = true;
|
|
362
400
|
|
|
363
|
-
|
|
401
|
+
this.stopAnimation();
|
|
402
|
+
clearTimeout( this._resizeDebounceTimer );
|
|
364
403
|
|
|
365
|
-
|
|
404
|
+
// Remove all tracked listeners (app-owned subscriptions across managers/DOM)
|
|
405
|
+
this._removeTrackedListeners();
|
|
366
406
|
|
|
367
|
-
|
|
407
|
+
setStatusCallback( null );
|
|
368
408
|
|
|
409
|
+
this.animationManager?.dispose();
|
|
369
410
|
this.transformManager?.dispose();
|
|
370
411
|
this.overlayManager?.dispose();
|
|
371
412
|
this._sceneHelpers?.clear();
|
|
413
|
+
this.lightManager?.dispose();
|
|
372
414
|
this.denoisingManager?.dispose();
|
|
373
415
|
this.pipeline?.dispose();
|
|
374
416
|
this.interactionManager?.dispose();
|
|
@@ -382,9 +424,6 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
382
424
|
|
|
383
425
|
}
|
|
384
426
|
|
|
385
|
-
clearTimeout( this._resizeDebounceTimer );
|
|
386
|
-
window.removeEventListener( 'resize', this.resizeHandler );
|
|
387
|
-
|
|
388
427
|
this.isInitialized = false;
|
|
389
428
|
|
|
390
429
|
}
|
|
@@ -393,6 +432,61 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
393
432
|
// Asset Loading
|
|
394
433
|
// ═══════════════════════════════════════════════════════════════
|
|
395
434
|
|
|
435
|
+
/**
|
|
436
|
+
* Tears down the current scene: stops animation, deselects, disposes
|
|
437
|
+
* the loaded model + its GPU resources, clears lights, and seeds the
|
|
438
|
+
* path tracer with an empty scene. Leaves the renderer, pipeline, and
|
|
439
|
+
* managers intact so a subsequent loadModel() can reuse them.
|
|
440
|
+
*
|
|
441
|
+
* Safe to call at any point after init() (including while idle).
|
|
442
|
+
* Throws if called concurrently with a load.
|
|
443
|
+
*/
|
|
444
|
+
unloadScene() {
|
|
445
|
+
|
|
446
|
+
if ( ! this.isInitialized ) return;
|
|
447
|
+
if ( this._loadingInProgress ) {
|
|
448
|
+
|
|
449
|
+
throw new Error( 'PathTracerApp.unloadScene: cannot unload while a load is in progress' );
|
|
450
|
+
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
if ( this._disposed ) return;
|
|
454
|
+
|
|
455
|
+
// Stop animation + refit
|
|
456
|
+
this.animationManager?.dispose();
|
|
457
|
+
this._animRefitInFlight = false;
|
|
458
|
+
|
|
459
|
+
// Drop selection + transform gizmo attachment
|
|
460
|
+
this.interactionManager?.deselect();
|
|
461
|
+
this.transformManager?.detach?.();
|
|
462
|
+
|
|
463
|
+
// Dispose the loaded model (geometries, materials, textures)
|
|
464
|
+
if ( this.assetLoader?.targetModel ) {
|
|
465
|
+
|
|
466
|
+
disposeObjectFromMemory( this.assetLoader.targetModel );
|
|
467
|
+
this.assetLoader.targetModel = null;
|
|
468
|
+
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Clear lights in the WebGPU light scene
|
|
472
|
+
this.lightManager?.clearLights?.();
|
|
473
|
+
|
|
474
|
+
// Seed path tracer with empty data (matches the init-time seed)
|
|
475
|
+
if ( this.stages.pathTracer ) {
|
|
476
|
+
|
|
477
|
+
this.stages.pathTracer.setTriangleData( new Float32Array( 32 ), 0 );
|
|
478
|
+
this.stages.pathTracer.setBVHData( new Float32Array( 16 ) );
|
|
479
|
+
this.stages.pathTracer.materialData.setMaterialData( new Float32Array( 16 ) );
|
|
480
|
+
this.stages.pathTracer.setEmissiveTriangleData?.( new Float32Array( 0 ), 0, 0 );
|
|
481
|
+
this.stages.pathTracer.setupMaterial();
|
|
482
|
+
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
this.reset();
|
|
486
|
+
this.dispatchEvent( { type: 'SceneUnloaded' } );
|
|
487
|
+
|
|
488
|
+
}
|
|
489
|
+
|
|
396
490
|
/**
|
|
397
491
|
* Loads a model, builds BVH, and uploads scene data.
|
|
398
492
|
* @param {string} url - Model URL
|
|
@@ -427,6 +521,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
427
521
|
*/
|
|
428
522
|
async loadEnvironment( url ) {
|
|
429
523
|
|
|
524
|
+
if ( this._loadingInProgress ) {
|
|
525
|
+
|
|
526
|
+
throw new Error( 'PathTracerApp.loadEnvironment: another load is already in progress' );
|
|
527
|
+
|
|
528
|
+
}
|
|
529
|
+
|
|
430
530
|
this._loadingInProgress = true;
|
|
431
531
|
|
|
432
532
|
try {
|
|
@@ -469,6 +569,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
469
569
|
/** Shared pipeline: load asset → sync controls → build BVH → reset → dispatch events */
|
|
470
570
|
async _loadWithSceneRebuild( loadFn, eventPayload ) {
|
|
471
571
|
|
|
572
|
+
if ( this._loadingInProgress ) {
|
|
573
|
+
|
|
574
|
+
throw new Error( 'PathTracerApp: another load is already in progress' );
|
|
575
|
+
|
|
576
|
+
}
|
|
577
|
+
|
|
472
578
|
this._loadingInProgress = true;
|
|
473
579
|
|
|
474
580
|
try {
|
|
@@ -547,6 +653,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
547
653
|
this.stages.pathTracer.setupMaterial();
|
|
548
654
|
timer.end( 'Material setup (TSL compile)' );
|
|
549
655
|
|
|
656
|
+
// Front-load GPU pipeline creation so the first animate frame is snappy:
|
|
657
|
+
// - compute: Three.js has no async compute compile — one dispatch at
|
|
658
|
+
// build time moves the stall to this loading moment.
|
|
659
|
+
// - raster fallback: compileAsync yields to main thread (r184+).
|
|
660
|
+
timer.start( 'Pipeline precompile' );
|
|
661
|
+
this.stages.pathTracer.shaderBuilder.forceCompile( this.renderer );
|
|
662
|
+
try {
|
|
663
|
+
|
|
664
|
+
await this.renderer.compileAsync( this.meshScene, this.cameraManager.camera );
|
|
665
|
+
|
|
666
|
+
} catch ( err ) {
|
|
667
|
+
|
|
668
|
+
console.warn( 'PathTracerApp: raster fallback precompile failed', err );
|
|
669
|
+
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
timer.end( 'Pipeline precompile' );
|
|
673
|
+
|
|
550
674
|
// Wait for CDF
|
|
551
675
|
if ( cdfPromise ) {
|
|
552
676
|
|
|
@@ -843,6 +967,16 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
843
967
|
|
|
844
968
|
}
|
|
845
969
|
|
|
970
|
+
/**
|
|
971
|
+
* Whether a model/environment load is currently in progress.
|
|
972
|
+
* @returns {boolean}
|
|
973
|
+
*/
|
|
974
|
+
get isLoading() {
|
|
975
|
+
|
|
976
|
+
return this._loadingInProgress;
|
|
977
|
+
|
|
978
|
+
}
|
|
979
|
+
|
|
846
980
|
/**
|
|
847
981
|
* Whether the path tracer has finished converging.
|
|
848
982
|
* @returns {boolean}
|
|
@@ -1011,7 +1145,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1011
1145
|
this.assetLoader.setRenderer( this.renderer );
|
|
1012
1146
|
this.assetLoader.createFloorPlane();
|
|
1013
1147
|
|
|
1014
|
-
this.cameraManager.controls
|
|
1148
|
+
this._addTrackedListener( this.cameraManager.controls, 'change', () => {
|
|
1015
1149
|
|
|
1016
1150
|
this.needsReset = true;
|
|
1017
1151
|
this.wake();
|
|
@@ -1093,8 +1227,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1093
1227
|
_wireEvents() {
|
|
1094
1228
|
|
|
1095
1229
|
// Forward manager events → app events
|
|
1096
|
-
this.
|
|
1097
|
-
this.
|
|
1230
|
+
this._addTrackedListener( this.cameraManager, 'CameraSwitched', ( e ) => this.dispatchEvent( e ) );
|
|
1231
|
+
this._addTrackedListener( this.cameraManager, EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
|
|
1098
1232
|
|
|
1099
1233
|
this._forwardEvents( this.denoisingManager, [
|
|
1100
1234
|
EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
|
|
@@ -1111,12 +1245,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1111
1245
|
EngineEvents.ANIMATION_PAUSED,
|
|
1112
1246
|
EngineEvents.ANIMATION_STOPPED,
|
|
1113
1247
|
] );
|
|
1114
|
-
this.
|
|
1248
|
+
this._addTrackedListener( this.animationManager, EngineEvents.ANIMATION_PAUSED, () => {
|
|
1115
1249
|
|
|
1116
1250
|
this._animRefitInFlight = false;
|
|
1117
1251
|
|
|
1118
1252
|
} );
|
|
1119
|
-
this.
|
|
1253
|
+
this._addTrackedListener( this.animationManager, EngineEvents.ANIMATION_STOPPED, () => {
|
|
1120
1254
|
|
|
1121
1255
|
this._animRefitInFlight = false;
|
|
1122
1256
|
|
|
@@ -1152,7 +1286,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1152
1286
|
this.resizeHandler = () => this.onResize();
|
|
1153
1287
|
if ( this._autoResize ) {
|
|
1154
1288
|
|
|
1155
|
-
|
|
1289
|
+
this._addTrackedListener( window, 'resize', this.resizeHandler );
|
|
1156
1290
|
|
|
1157
1291
|
}
|
|
1158
1292
|
|
|
@@ -1183,9 +1317,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1183
1317
|
|
|
1184
1318
|
};
|
|
1185
1319
|
|
|
1186
|
-
this.
|
|
1320
|
+
this._addTrackedListener( this.assetLoader, 'load', this._onAssetLoaded );
|
|
1187
1321
|
|
|
1188
|
-
this.
|
|
1322
|
+
this._addTrackedListener( this.assetLoader, 'modelProcessed', ( event ) => {
|
|
1189
1323
|
|
|
1190
1324
|
const cameras = [ this.cameraManager.camera, ...( event.cameras || [] ) ];
|
|
1191
1325
|
this.cameraManager.setCameras( cameras );
|
|
@@ -1382,7 +1516,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1382
1516
|
if ( ! source ) return;
|
|
1383
1517
|
for ( const type of eventTypes ) {
|
|
1384
1518
|
|
|
1385
|
-
|
|
1519
|
+
this._addTrackedListener( source, type, ( e ) => this.dispatchEvent( e ) );
|
|
1386
1520
|
|
|
1387
1521
|
}
|
|
1388
1522
|
|
|
@@ -449,7 +449,16 @@ export class RenderPipeline {
|
|
|
449
449
|
|
|
450
450
|
console.group( '[Pipeline] Performance Stats' );
|
|
451
451
|
console.log( `Frames: ${stats.frameCount}` );
|
|
452
|
-
|
|
452
|
+
|
|
453
|
+
if ( stats.totalMin !== undefined ) {
|
|
454
|
+
|
|
455
|
+
console.log( `Total: ${stats.total.toFixed( 2 )}ms (min: ${stats.totalMin.toFixed( 2 )}ms, max: ${stats.totalMax.toFixed( 2 )}ms)` );
|
|
456
|
+
|
|
457
|
+
} else {
|
|
458
|
+
|
|
459
|
+
console.log( 'Total: no frames recorded yet (pipeline.render() has not run since stats were enabled — the path tracer may have converged and stopped its animation loop)' );
|
|
460
|
+
|
|
461
|
+
}
|
|
453
462
|
|
|
454
463
|
for ( const [ name, timing ] of Object.entries( stats.stages ) ) {
|
|
455
464
|
|
|
@@ -50,9 +50,18 @@ export class ShaderBuilder {
|
|
|
50
50
|
this._dispatchX = 0;
|
|
51
51
|
this._dispatchY = 0;
|
|
52
52
|
|
|
53
|
+
// Reused per-frame dispatchSize array — avoids GC pressure from
|
|
54
|
+
// allocating [x,y,z] on every setFullScreenDispatch/setTileDispatch call.
|
|
55
|
+
// WebGPUBackend only reads indices 0..2 of this array during compute dispatch.
|
|
56
|
+
this._dispatchSize = [ 0, 0, 1 ];
|
|
57
|
+
|
|
53
58
|
// Scene texture nodes cache (for in-place updates on model change)
|
|
54
59
|
this._sceneTextureNodes = null;
|
|
55
60
|
|
|
61
|
+
// Whether the GPU compute pipeline has been compiled (via a real dispatch).
|
|
62
|
+
// Reset on setupCompute() rebuilds and on dispose().
|
|
63
|
+
this._compiled = false;
|
|
64
|
+
|
|
56
65
|
}
|
|
57
66
|
|
|
58
67
|
/**
|
|
@@ -89,6 +98,9 @@ export class ShaderBuilder {
|
|
|
89
98
|
writeTex.color, writeTex.normalDepth, writeTex.albedo
|
|
90
99
|
);
|
|
91
100
|
|
|
101
|
+
// New compute node → needs a fresh GPU pipeline compile
|
|
102
|
+
this._compiled = false;
|
|
103
|
+
|
|
92
104
|
timer.end( 'Build compute node (TSL)' );
|
|
93
105
|
|
|
94
106
|
timer.print();
|
|
@@ -131,7 +143,13 @@ export class ShaderBuilder {
|
|
|
131
143
|
this._dispatchX = Math.ceil( width / WG_SIZE );
|
|
132
144
|
this._dispatchY = Math.ceil( height / WG_SIZE );
|
|
133
145
|
|
|
134
|
-
if ( this.computeNode )
|
|
146
|
+
if ( this.computeNode ) {
|
|
147
|
+
|
|
148
|
+
this._dispatchSize[ 0 ] = this._dispatchX;
|
|
149
|
+
this._dispatchSize[ 1 ] = this._dispatchY;
|
|
150
|
+
this.computeNode.dispatchSize = this._dispatchSize;
|
|
151
|
+
|
|
152
|
+
}
|
|
135
153
|
|
|
136
154
|
this.renderWidth.value = width;
|
|
137
155
|
this.renderHeight.value = height;
|
|
@@ -158,7 +176,13 @@ export class ShaderBuilder {
|
|
|
158
176
|
const dispatchX = Math.ceil( tileWidth / WG_SIZE );
|
|
159
177
|
const dispatchY = Math.ceil( tileHeight / WG_SIZE );
|
|
160
178
|
|
|
161
|
-
if ( this.computeNode )
|
|
179
|
+
if ( this.computeNode ) {
|
|
180
|
+
|
|
181
|
+
this._dispatchSize[ 0 ] = dispatchX;
|
|
182
|
+
this._dispatchSize[ 1 ] = dispatchY;
|
|
183
|
+
this.computeNode.dispatchSize = this._dispatchSize;
|
|
184
|
+
|
|
185
|
+
}
|
|
162
186
|
|
|
163
187
|
}
|
|
164
188
|
|
|
@@ -170,13 +194,36 @@ export class ShaderBuilder {
|
|
|
170
194
|
this.tileOffsetX.value = 0;
|
|
171
195
|
this.tileOffsetY.value = 0;
|
|
172
196
|
|
|
173
|
-
if ( this.computeNode )
|
|
197
|
+
if ( this.computeNode ) {
|
|
198
|
+
|
|
199
|
+
this._dispatchSize[ 0 ] = this._dispatchX;
|
|
200
|
+
this._dispatchSize[ 1 ] = this._dispatchY;
|
|
201
|
+
this.computeNode.dispatchSize = this._dispatchSize;
|
|
202
|
+
|
|
203
|
+
}
|
|
174
204
|
|
|
175
205
|
}
|
|
176
206
|
|
|
177
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Front-load GPU compute pipeline creation via a single dispatch.
|
|
209
|
+
*
|
|
210
|
+
* Three.js WebGPU has no `createComputePipelineAsync` path — compute
|
|
211
|
+
* pipelines always compile synchronously on first `renderer.compute(node)`.
|
|
212
|
+
* Calling this at build time (while a "Compiling shaders…" status is
|
|
213
|
+
* already visible) moves the stall off the first animate frame.
|
|
214
|
+
*
|
|
215
|
+
* The dispatch writes to ping-pong storage textures whose contents are
|
|
216
|
+
* discarded by the subsequent `reset()` (frame counter back to 0 →
|
|
217
|
+
* `hasPreviousAccumulated = 0` → prev textures are not read).
|
|
218
|
+
*
|
|
219
|
+
* @param {object} renderer - WebGPURenderer
|
|
220
|
+
*/
|
|
221
|
+
forceCompile( renderer ) {
|
|
222
|
+
|
|
223
|
+
if ( this._compiled || ! this.computeNode || ! renderer ) return;
|
|
178
224
|
|
|
179
|
-
|
|
225
|
+
this._compiled = true;
|
|
226
|
+
renderer.compute( this.computeNode );
|
|
180
227
|
|
|
181
228
|
}
|
|
182
229
|
|
|
@@ -379,6 +426,7 @@ export class ShaderBuilder {
|
|
|
379
426
|
this.prevAlbedoTexNode = null;
|
|
380
427
|
this.adaptiveSamplingTexNode = null;
|
|
381
428
|
this._sceneTextureNodes = null;
|
|
429
|
+
this._compiled = false;
|
|
382
430
|
|
|
383
431
|
}
|
|
384
432
|
|
package/src/Stages/ASVGF.js
CHANGED
|
@@ -803,10 +803,10 @@ export class ASVGF extends RenderStage {
|
|
|
803
803
|
// Update dispatch dimensions
|
|
804
804
|
this._dispatchX = Math.ceil( width / 8 );
|
|
805
805
|
this._dispatchY = Math.ceil( height / 8 );
|
|
806
|
-
this._gradientNode.
|
|
807
|
-
this._temporalNodeA.
|
|
808
|
-
this._temporalNodeB.
|
|
809
|
-
this._heatmapComputeNode.
|
|
806
|
+
this._gradientNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
807
|
+
this._temporalNodeA.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
808
|
+
this._temporalNodeB.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
809
|
+
this._heatmapComputeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
810
810
|
|
|
811
811
|
}
|
|
812
812
|
|
|
@@ -422,8 +422,8 @@ export class AdaptiveSampling extends RenderStage {
|
|
|
422
422
|
// Update dispatch dimensions
|
|
423
423
|
this._dispatchX = Math.ceil( width / 16 );
|
|
424
424
|
this._dispatchY = Math.ceil( height / 16 );
|
|
425
|
-
this._computeNode.
|
|
426
|
-
this._heatmapComputeNode.
|
|
425
|
+
this._computeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
426
|
+
this._heatmapComputeNode.dispatchSize = [ this._dispatchX, this._dispatchY, 1 ];
|
|
427
427
|
|
|
428
428
|
}
|
|
429
429
|
|