rayzee 5.3.8 → 5.4.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/dist/rayzee.es.js +2933 -2888
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +53 -53
- 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 +154 -21
- package/src/Pipeline/RenderPipeline.js +10 -1
- package/src/Processor/AssetLoader.js +40 -18
- package/src/Processor/EquirectHDRInfo.js +38 -29
- package/src/Processor/InstanceTable.js +16 -0
- package/src/Processor/SceneProcessor.js +22 -33
- package/src/Processor/ShaderBuilder.js +67 -22
- package/src/Processor/TLASBuilder.js +9 -4
- 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 +88 -46
- package/src/Stages/SSRC.js +4 -4
- package/src/Stages/Variance.js +2 -2
- package/src/TSL/BVHTraversal.js +15 -63
- package/src/TSL/Clearcoat.js +1 -1
- package/src/TSL/Displacement.js +1 -1
- package/src/TSL/EmissiveSampling.js +17 -13
- package/src/TSL/Environment.js +12 -9
- package/src/TSL/LightBVHSampling.js +3 -2
- package/src/TSL/LightsCore.js +1 -1
- package/src/TSL/LightsDirect.js +1 -1
- package/src/TSL/LightsIndirect.js +0 -1
- package/src/TSL/LightsSampling.js +2 -2
- package/src/TSL/MaterialTransmission.js +1 -1
- package/src/TSL/PathTracer.js +4 -4
- package/src/TSL/PathTracerCore.js +6 -6
- package/src/TSL/Struct.js +1 -1
- package/src/TSL/patches.js +145 -0
- package/src/index.js +1 -1
- package/src/managers/EnvironmentManager.js +32 -56
- package/src/managers/LightManager.js +20 -0
- package/src/managers/UniformManager.js +22 -0
- package/src/managers/helpers/OutlineHelper.js +3 -1
- package/src/TSL/storageTexturePatch.js +0 -31
- package/src/TSL/structProxy.js +0 -87
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rayzee",
|
|
3
|
-
"version": "5.
|
|
3
|
+
"version": "5.4.1",
|
|
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 {
|
|
@@ -533,8 +639,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
533
639
|
|
|
534
640
|
if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, environmentTexture ) ) return false;
|
|
535
641
|
|
|
536
|
-
//
|
|
537
|
-
// shader graph captures the storage node during compilation)
|
|
642
|
+
// Patch per-mesh visibility into the TLAS leaves we just uploaded
|
|
538
643
|
this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
|
|
539
644
|
this.stages.pathTracer.setMeshVisibilityData( this.stages.pathTracer._meshRefs );
|
|
540
645
|
|
|
@@ -547,6 +652,24 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
547
652
|
this.stages.pathTracer.setupMaterial();
|
|
548
653
|
timer.end( 'Material setup (TSL compile)' );
|
|
549
654
|
|
|
655
|
+
// Front-load GPU pipeline creation so the first animate frame is snappy:
|
|
656
|
+
// - compute: Three.js has no async compute compile — one dispatch at
|
|
657
|
+
// build time moves the stall to this loading moment.
|
|
658
|
+
// - raster fallback: compileAsync yields to main thread (r184+).
|
|
659
|
+
timer.start( 'Pipeline precompile' );
|
|
660
|
+
this.stages.pathTracer.shaderBuilder.forceCompile( this.renderer );
|
|
661
|
+
try {
|
|
662
|
+
|
|
663
|
+
await this.renderer.compileAsync( this.meshScene, this.cameraManager.camera );
|
|
664
|
+
|
|
665
|
+
} catch ( err ) {
|
|
666
|
+
|
|
667
|
+
console.warn( 'PathTracerApp: raster fallback precompile failed', err );
|
|
668
|
+
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
timer.end( 'Pipeline precompile' );
|
|
672
|
+
|
|
550
673
|
// Wait for CDF
|
|
551
674
|
if ( cdfPromise ) {
|
|
552
675
|
|
|
@@ -843,6 +966,16 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
843
966
|
|
|
844
967
|
}
|
|
845
968
|
|
|
969
|
+
/**
|
|
970
|
+
* Whether a model/environment load is currently in progress.
|
|
971
|
+
* @returns {boolean}
|
|
972
|
+
*/
|
|
973
|
+
get isLoading() {
|
|
974
|
+
|
|
975
|
+
return this._loadingInProgress;
|
|
976
|
+
|
|
977
|
+
}
|
|
978
|
+
|
|
846
979
|
/**
|
|
847
980
|
* Whether the path tracer has finished converging.
|
|
848
981
|
* @returns {boolean}
|
|
@@ -1011,7 +1144,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1011
1144
|
this.assetLoader.setRenderer( this.renderer );
|
|
1012
1145
|
this.assetLoader.createFloorPlane();
|
|
1013
1146
|
|
|
1014
|
-
this.cameraManager.controls
|
|
1147
|
+
this._addTrackedListener( this.cameraManager.controls, 'change', () => {
|
|
1015
1148
|
|
|
1016
1149
|
this.needsReset = true;
|
|
1017
1150
|
this.wake();
|
|
@@ -1093,8 +1226,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1093
1226
|
_wireEvents() {
|
|
1094
1227
|
|
|
1095
1228
|
// Forward manager events → app events
|
|
1096
|
-
this.
|
|
1097
|
-
this.
|
|
1229
|
+
this._addTrackedListener( this.cameraManager, 'CameraSwitched', ( e ) => this.dispatchEvent( e ) );
|
|
1230
|
+
this._addTrackedListener( this.cameraManager, EngineEvents.AUTO_FOCUS_UPDATED, ( e ) => this.dispatchEvent( e ) );
|
|
1098
1231
|
|
|
1099
1232
|
this._forwardEvents( this.denoisingManager, [
|
|
1100
1233
|
EngineEvents.DENOISING_START, EngineEvents.DENOISING_END,
|
|
@@ -1111,12 +1244,12 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1111
1244
|
EngineEvents.ANIMATION_PAUSED,
|
|
1112
1245
|
EngineEvents.ANIMATION_STOPPED,
|
|
1113
1246
|
] );
|
|
1114
|
-
this.
|
|
1247
|
+
this._addTrackedListener( this.animationManager, EngineEvents.ANIMATION_PAUSED, () => {
|
|
1115
1248
|
|
|
1116
1249
|
this._animRefitInFlight = false;
|
|
1117
1250
|
|
|
1118
1251
|
} );
|
|
1119
|
-
this.
|
|
1252
|
+
this._addTrackedListener( this.animationManager, EngineEvents.ANIMATION_STOPPED, () => {
|
|
1120
1253
|
|
|
1121
1254
|
this._animRefitInFlight = false;
|
|
1122
1255
|
|
|
@@ -1152,7 +1285,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1152
1285
|
this.resizeHandler = () => this.onResize();
|
|
1153
1286
|
if ( this._autoResize ) {
|
|
1154
1287
|
|
|
1155
|
-
|
|
1288
|
+
this._addTrackedListener( window, 'resize', this.resizeHandler );
|
|
1156
1289
|
|
|
1157
1290
|
}
|
|
1158
1291
|
|
|
@@ -1183,9 +1316,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1183
1316
|
|
|
1184
1317
|
};
|
|
1185
1318
|
|
|
1186
|
-
this.
|
|
1319
|
+
this._addTrackedListener( this.assetLoader, 'load', this._onAssetLoaded );
|
|
1187
1320
|
|
|
1188
|
-
this.
|
|
1321
|
+
this._addTrackedListener( this.assetLoader, 'modelProcessed', ( event ) => {
|
|
1189
1322
|
|
|
1190
1323
|
const cameras = [ this.cameraManager.camera, ...( event.cameras || [] ) ];
|
|
1191
1324
|
this.cameraManager.setCameras( cameras );
|
|
@@ -1382,7 +1515,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1382
1515
|
if ( ! source ) return;
|
|
1383
1516
|
for ( const type of eventTypes ) {
|
|
1384
1517
|
|
|
1385
|
-
|
|
1518
|
+
this._addTrackedListener( source, type, ( e ) => this.dispatchEvent( e ) );
|
|
1386
1519
|
|
|
1387
1520
|
}
|
|
1388
1521
|
|
|
@@ -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
|
|
|
@@ -472,20 +472,28 @@ export class AssetLoader extends EventDispatcher {
|
|
|
472
472
|
const loader = await this.createGLTFLoader();
|
|
473
473
|
loader.manager = manager;
|
|
474
474
|
|
|
475
|
-
|
|
475
|
+
try {
|
|
476
476
|
|
|
477
|
-
|
|
478
|
-
gltf => {
|
|
477
|
+
return await new Promise( ( resolve, reject ) => {
|
|
479
478
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
|
|
479
|
+
loader.parse( gltfContent, '',
|
|
480
|
+
gltf => {
|
|
483
481
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
482
|
+
if ( this.targetModel ) disposeObjectFromMemory( this.targetModel );
|
|
483
|
+
this.targetModel = gltf.scene;
|
|
484
|
+
this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
|
|
487
485
|
|
|
488
|
-
|
|
486
|
+
},
|
|
487
|
+
error => reject( error )
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
} );
|
|
491
|
+
|
|
492
|
+
} finally {
|
|
493
|
+
|
|
494
|
+
this._disposeGLTFLoader( loader );
|
|
495
|
+
|
|
496
|
+
}
|
|
489
497
|
|
|
490
498
|
} else {
|
|
491
499
|
|
|
@@ -697,11 +705,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
697
705
|
|
|
698
706
|
}
|
|
699
707
|
|
|
700
|
-
//
|
|
708
|
+
// Returns a fresh loader each call — DRACOLoader/KTX2Loader hold persistent
|
|
709
|
+
// worker pools. Callers must invoke _disposeGLTFLoader() to terminate them.
|
|
701
710
|
async createGLTFLoader() {
|
|
702
711
|
|
|
703
|
-
if ( this.loaderCache.gltf ) return this.loaderCache.gltf;
|
|
704
|
-
|
|
705
712
|
const dracoLoader = new DRACOLoader();
|
|
706
713
|
dracoLoader.setDecoderConfig( { type: 'js' } );
|
|
707
714
|
dracoLoader.setDecoderPath( 'https://www.gstatic.com/draco/v1/decoders/' );
|
|
@@ -726,18 +733,23 @@ export class AssetLoader extends EventDispatcher {
|
|
|
726
733
|
|
|
727
734
|
}
|
|
728
735
|
|
|
729
|
-
this.loaderCache.ktx2 = ktx2Loader;
|
|
730
|
-
|
|
731
736
|
const loader = new GLTFLoader();
|
|
732
737
|
loader.setDRACOLoader( dracoLoader );
|
|
733
738
|
loader.setKTX2Loader( ktx2Loader );
|
|
734
739
|
loader.setMeshoptDecoder( MeshoptDecoder );
|
|
735
740
|
|
|
736
|
-
this.loaderCache.gltf = loader;
|
|
737
741
|
return loader;
|
|
738
742
|
|
|
739
743
|
}
|
|
740
744
|
|
|
745
|
+
_disposeGLTFLoader( loader ) {
|
|
746
|
+
|
|
747
|
+
if ( ! loader ) return;
|
|
748
|
+
loader.dracoLoader?.dispose();
|
|
749
|
+
loader.ktx2Loader?.dispose();
|
|
750
|
+
|
|
751
|
+
}
|
|
752
|
+
|
|
741
753
|
async loadExampleModels( index, modelFiles ) {
|
|
742
754
|
|
|
743
755
|
if ( ! modelFiles || ! modelFiles[ index ] ) {
|
|
@@ -753,9 +765,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
753
765
|
|
|
754
766
|
async loadModel( modelUrl ) {
|
|
755
767
|
|
|
768
|
+
const loader = await this.createGLTFLoader();
|
|
769
|
+
|
|
756
770
|
try {
|
|
757
771
|
|
|
758
|
-
const loader = await this.createGLTFLoader();
|
|
759
772
|
updateLoading( { status: "Loading Model...", progress: 2 } );
|
|
760
773
|
const data = await loader.loadAsync( modelUrl );
|
|
761
774
|
updateLoading( { status: "Processing Data...", progress: 10 } );
|
|
@@ -774,15 +787,20 @@ export class AssetLoader extends EventDispatcher {
|
|
|
774
787
|
this.dispatchEvent( { type: 'error', message: error.message, filename: modelUrl } );
|
|
775
788
|
throw error;
|
|
776
789
|
|
|
790
|
+
} finally {
|
|
791
|
+
|
|
792
|
+
this._disposeGLTFLoader( loader );
|
|
793
|
+
|
|
777
794
|
}
|
|
778
795
|
|
|
779
796
|
}
|
|
780
797
|
|
|
781
798
|
async loadGLBFromArrayBuffer( arrayBuffer, filename = 'model.glb' ) {
|
|
782
799
|
|
|
800
|
+
const loader = await this.createGLTFLoader();
|
|
801
|
+
|
|
783
802
|
try {
|
|
784
803
|
|
|
785
|
-
const loader = await this.createGLTFLoader();
|
|
786
804
|
updateLoading( { isLoading: true, status: "Processing GLB Data...", progress: 5 } );
|
|
787
805
|
await new Promise( r => setTimeout( r, 0 ) );
|
|
788
806
|
|
|
@@ -804,6 +822,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
804
822
|
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
805
823
|
throw error;
|
|
806
824
|
|
|
825
|
+
} finally {
|
|
826
|
+
|
|
827
|
+
this._disposeGLTFLoader( loader );
|
|
828
|
+
|
|
807
829
|
}
|
|
808
830
|
|
|
809
831
|
}
|