rayzee 7.24.0 → 7.26.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 +1 -1
- package/dist/rayzee.es.js +1921 -1755
- package/dist/rayzee.umd.js +7 -7
- package/package.json +2 -2
- package/src/Passes/AIUpscaler.js +3 -3
- package/src/Passes/OIDNDenoiser.js +2 -2
- package/src/PathTracerApp.js +206 -19
- package/src/Pipeline/CompletionTracker.js +45 -6
- package/src/Processor/AssetLoader.js +88 -12
- package/src/Processor/SceneMetadata.js +123 -0
- package/src/Processor/ToneMapCPU.js +37 -7
- package/src/RenderSettings.js +1 -0
- package/src/Stages/PathTracerStage.js +6 -16
- package/src/index.js +3 -0
- package/src/managers/EnvironmentManager.js +34 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rayzee",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.26.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",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
],
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/atul-mourya/
|
|
21
|
+
"url": "git+https://github.com/atul-mourya/rayzee-renderer.git",
|
|
22
22
|
"directory": "rayzee"
|
|
23
23
|
},
|
|
24
24
|
"keywords": [
|
package/src/Passes/AIUpscaler.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { EventDispatcher, ACESFilmicToneMapping } from 'three';
|
|
2
|
-
import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation } from '../Processor/ToneMapCPU.js';
|
|
2
|
+
import { TONE_MAP_FNS, SRGB_GAMMA, applySaturation, effectiveExposure } from '../Processor/ToneMapCPU.js';
|
|
3
3
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
4
4
|
import AIUpscalerWorker from '../Processor/Workers/AIUpscalerWorker.js?worker&inline';
|
|
5
5
|
|
|
@@ -400,7 +400,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
400
400
|
if ( sourceImageData.isHDR ) {
|
|
401
401
|
|
|
402
402
|
this._hdrToneMapFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
403
|
-
this._hdrExposure = this.getExposure();
|
|
403
|
+
this._hdrExposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
|
|
404
404
|
this._hdrSaturation = this.getSaturation();
|
|
405
405
|
this._tmOut = new Float32Array( 3 );
|
|
406
406
|
|
|
@@ -793,7 +793,7 @@ export class AIUpscaler extends EventDispatcher {
|
|
|
793
793
|
const imageData = ctx.createImageData( width, height );
|
|
794
794
|
const pixels = imageData.data;
|
|
795
795
|
const tmFn = TONE_MAP_FNS.get( this.getToneMapping() ) || TONE_MAP_FNS.get( ACESFilmicToneMapping );
|
|
796
|
-
const exposure = this.getExposure();
|
|
796
|
+
const exposure = effectiveExposure( this.getExposure(), this.getToneMapping() );
|
|
797
797
|
const saturation = this.getSaturation();
|
|
798
798
|
const out = new Float32Array( 3 );
|
|
799
799
|
|
|
@@ -16,7 +16,7 @@ async function getInitUNetFromURL() {
|
|
|
16
16
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
import { TONE_MAP_FNS, linearToSRGB, applySaturation } from '../Processor/ToneMapCPU.js';
|
|
19
|
+
import { TONE_MAP_FNS, linearToSRGB, applySaturation, effectiveExposure } from '../Processor/ToneMapCPU.js';
|
|
20
20
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
21
21
|
|
|
22
22
|
/** Reusable RGB output buffer (avoids per-pixel allocation). */
|
|
@@ -764,7 +764,7 @@ export class OIDNDenoiser extends EventDispatcher {
|
|
|
764
764
|
/** Display exposure with the input scale divided back out — output arrives pre-multiplied. */
|
|
765
765
|
_outputExposure() {
|
|
766
766
|
|
|
767
|
-
return this.getExposure() / this._oidnInputScale;
|
|
767
|
+
return effectiveExposure( this.getExposure(), this.getToneMapping() ) / this._oidnInputScale;
|
|
768
768
|
|
|
769
769
|
}
|
|
770
770
|
|
package/src/PathTracerApp.js
CHANGED
|
@@ -109,6 +109,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
109
109
|
this.canvas = canvas;
|
|
110
110
|
this._autoResize = options.autoResize !== false;
|
|
111
111
|
this._container = options.container || null;
|
|
112
|
+
// Apply the environment authored into a model file's metadata on load. See _beginSceneMetadataEnvironment().
|
|
113
|
+
this._applySceneMetadataEnabled = options.applySceneMetadata !== false;
|
|
114
|
+
this._applyingSceneMetadata = false;
|
|
112
115
|
|
|
113
116
|
// ── Settings (single source of truth for all render parameters) ──
|
|
114
117
|
this.settings = new RenderSettings( DEFAULT_STATE );
|
|
@@ -366,8 +369,11 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
366
369
|
memoryPeak: tracker?.peak ?? 0,
|
|
367
370
|
} );
|
|
368
371
|
|
|
369
|
-
//
|
|
370
|
-
|
|
372
|
+
// Only the wall-clock stop — PathTracer.render() retires the ceiling and convergence
|
|
373
|
+
// itself, so whichever of the three arrives first wins.
|
|
374
|
+
if ( this.completion.isTimeLimitReached(
|
|
375
|
+
this.stages.pathTracer, this.settings.get( 'renderLimitMode' ), this.settings.get( 'renderTimeLimit' )
|
|
376
|
+
) ) {
|
|
371
377
|
|
|
372
378
|
this.stages.pathTracer.isComplete = true;
|
|
373
379
|
|
|
@@ -381,8 +387,14 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
381
387
|
context: this.pipeline?.context,
|
|
382
388
|
} );
|
|
383
389
|
|
|
384
|
-
|
|
385
|
-
|
|
390
|
+
const completionInfo = {
|
|
391
|
+
samples: this.stages.pathTracer.frameCount,
|
|
392
|
+
timeElapsed: this.completion.timeElapsed,
|
|
393
|
+
budgetOverrun: this.completion.budgetOverrun,
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
this.dispatchEvent( { type: 'RenderComplete', ...completionInfo } );
|
|
397
|
+
this.dispatchEvent( { type: EngineEvents.RENDER_COMPLETE, ...completionInfo } );
|
|
386
398
|
|
|
387
399
|
}
|
|
388
400
|
|
|
@@ -665,17 +677,26 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
665
677
|
|
|
666
678
|
}
|
|
667
679
|
|
|
680
|
+
/**
|
|
681
|
+
* A load is already running. Typed so hosts can tell "you clicked too fast" apart from
|
|
682
|
+
* a genuine load failure and say so, instead of dropping the request on the floor.
|
|
683
|
+
* @private
|
|
684
|
+
*/
|
|
685
|
+
_busyError( where ) {
|
|
686
|
+
|
|
687
|
+
const error = new Error( `${where}: another load is already in progress` );
|
|
688
|
+
error.code = 'LOAD_IN_PROGRESS';
|
|
689
|
+
return error;
|
|
690
|
+
|
|
691
|
+
}
|
|
692
|
+
|
|
668
693
|
/**
|
|
669
694
|
* Loads an environment map and rebuilds CDF.
|
|
670
695
|
* @param {string} url - Environment URL
|
|
671
696
|
*/
|
|
672
697
|
async loadEnvironment( url ) {
|
|
673
698
|
|
|
674
|
-
if ( this._loadingInProgress )
|
|
675
|
-
|
|
676
|
-
throw new Error( 'PathTracerApp.loadEnvironment: another load is already in progress' );
|
|
677
|
-
|
|
678
|
-
}
|
|
699
|
+
if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp.loadEnvironment' );
|
|
679
700
|
|
|
680
701
|
this._loadingInProgress = true;
|
|
681
702
|
|
|
@@ -702,6 +723,63 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
702
723
|
|
|
703
724
|
}
|
|
704
725
|
|
|
726
|
+
/**
|
|
727
|
+
* Loads a user-supplied File (drag-drop, file picker) — model, archive, or environment
|
|
728
|
+
* map, dispatched by extension.
|
|
729
|
+
*
|
|
730
|
+
* Prefer this over driving `assetLoader.loadAssetFromFile()` directly. That bypasses the
|
|
731
|
+
* in-progress guard, and AssetLoader disposes the outgoing model before it knows whether
|
|
732
|
+
* the engine will rebuild: a file dropped mid-load would tear down the live scene, add the
|
|
733
|
+
* new one to the graph, and then be discarded by the `load` handler — leaving the path
|
|
734
|
+
* tracer rendering buffers whose geometry has been freed. Here a concurrent call throws
|
|
735
|
+
* LOAD_IN_PROGRESS before anything is touched.
|
|
736
|
+
*
|
|
737
|
+
* @param {File} file
|
|
738
|
+
* @returns {Promise<void>}
|
|
739
|
+
*/
|
|
740
|
+
async loadFile( file ) {
|
|
741
|
+
|
|
742
|
+
const format = this.assetLoader?.getFileFormat( file?.name || '' );
|
|
743
|
+
if ( ! format ) throw new Error( `Unsupported file format: ${file?.name}` );
|
|
744
|
+
|
|
745
|
+
if ( format.type !== 'environment' && format.type !== 'image' ) {
|
|
746
|
+
|
|
747
|
+
await this._loadWithSceneRebuild(
|
|
748
|
+
() => this.assetLoader.loadAssetFromFile( file ),
|
|
749
|
+
{ type: 'ModelLoaded', filename: file.name }
|
|
750
|
+
);
|
|
751
|
+
return;
|
|
752
|
+
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// Environment drop: install it, no scene rebuild.
|
|
756
|
+
if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp.loadFile' );
|
|
757
|
+
|
|
758
|
+
this._loadingInProgress = true;
|
|
759
|
+
|
|
760
|
+
try {
|
|
761
|
+
|
|
762
|
+
await this.assetLoader.loadAssetFromFile( file );
|
|
763
|
+
|
|
764
|
+
const texture = this.meshScene.environment;
|
|
765
|
+
if ( texture && this.stages.pathTracer ) {
|
|
766
|
+
|
|
767
|
+
await this.stages.pathTracer.environment.applyHDRI( texture );
|
|
768
|
+
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
772
|
+
this.reset();
|
|
773
|
+
this.dispatchEvent( { type: 'EnvironmentLoaded', filename: file.name } );
|
|
774
|
+
|
|
775
|
+
} finally {
|
|
776
|
+
|
|
777
|
+
this._loadingInProgress = false;
|
|
778
|
+
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
}
|
|
782
|
+
|
|
705
783
|
/**
|
|
706
784
|
* Loads example models by index.
|
|
707
785
|
* @param {number} index
|
|
@@ -775,11 +853,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
775
853
|
/** Shared pipeline: load asset → sync controls → build BVH → reset → dispatch events */
|
|
776
854
|
async _loadWithSceneRebuild( loadFn, eventPayload ) {
|
|
777
855
|
|
|
778
|
-
if ( this._loadingInProgress )
|
|
779
|
-
|
|
780
|
-
throw new Error( 'PathTracerApp: another load is already in progress' );
|
|
781
|
-
|
|
782
|
-
}
|
|
856
|
+
if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp' );
|
|
783
857
|
|
|
784
858
|
this._loadingInProgress = true;
|
|
785
859
|
|
|
@@ -794,7 +868,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
794
868
|
// (The old primary was already released by releaseTargetModel() in loadFn.)
|
|
795
869
|
this._clearAppendedModels();
|
|
796
870
|
this._syncControlsAfterLoad();
|
|
797
|
-
await this.loadSceneData();
|
|
871
|
+
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
798
872
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
799
873
|
this.reset();
|
|
800
874
|
this.cameraManager.currentCameraIndex = 0;
|
|
@@ -809,11 +883,99 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
809
883
|
|
|
810
884
|
}
|
|
811
885
|
|
|
886
|
+
/**
|
|
887
|
+
* Scene-level authoring metadata carried by the current model file (glTF `extras`),
|
|
888
|
+
* or null when the file has none. See {@link module:Processor/SceneMetadata}.
|
|
889
|
+
* @type {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
|
|
890
|
+
*/
|
|
891
|
+
get sceneMetadata() {
|
|
892
|
+
|
|
893
|
+
return this.assetLoader?.sceneMetadata ?? null;
|
|
894
|
+
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/**
|
|
898
|
+
* Starts fetching the environment authored into the just-loaded model's metadata and
|
|
899
|
+
* returns a promise for the texture — deliberately NOT awaited here. The fetch is the
|
|
900
|
+
* longest single step of such a load (measured 1448 ms for a 1k HDRI against 325 ms to
|
|
901
|
+
* build and upload a 112k-triangle scene), so the promise is handed to loadSceneData(),
|
|
902
|
+
* which installs it and builds its CDF while the BVH builds.
|
|
903
|
+
*
|
|
904
|
+
* Called from both replace-load seams: the URL/catalog path (_loadWithSceneRebuild) and
|
|
905
|
+
* the drag-drop / File-open path, which drives the AssetLoader directly and rebuilds via
|
|
906
|
+
* the 'load' event (_onAssetLoaded). Appending a model never applies it — an appended
|
|
907
|
+
* model must not hijack the scene's environment.
|
|
908
|
+
*
|
|
909
|
+
* Resolves to null and never rejects: a failed fetch leaves the model on the current
|
|
910
|
+
* environment. Returns null outright when there is nothing to apply.
|
|
911
|
+
* Opt out per app with `new PathTracerApp( canvas, { applySceneMetadata: false } )`.
|
|
912
|
+
*
|
|
913
|
+
* @returns {Promise<import('three').Texture|null>|null}
|
|
914
|
+
*/
|
|
915
|
+
_beginSceneMetadataEnvironment() {
|
|
916
|
+
|
|
917
|
+
const env = this.sceneMetadata?.environment;
|
|
918
|
+
if ( ! this._applySceneMetadataEnabled || ! env?.sourceFile || ! this.stages.pathTracer ) return null;
|
|
919
|
+
|
|
920
|
+
// Claim 'hdri' mode up front: loadEnvironment() fires beforeEnvironmentLoad, and a
|
|
921
|
+
// host that answers it with setMode( 'hdri' ) would otherwise restore the HDRI
|
|
922
|
+
// stashed by the current sky mode on top of the download now in flight.
|
|
923
|
+
this.stages.pathTracer.environment.beginHDRI();
|
|
924
|
+
this._applyingSceneMetadata = true;
|
|
925
|
+
|
|
926
|
+
return this.assetLoader.loadEnvironment( env.sourceFile )
|
|
927
|
+
.catch( error => {
|
|
928
|
+
|
|
929
|
+
log.warn( `scene metadata: environment "${env.sourceFile}" failed to load`, error );
|
|
930
|
+
return null;
|
|
931
|
+
|
|
932
|
+
} )
|
|
933
|
+
.finally( () => {
|
|
934
|
+
|
|
935
|
+
this._applyingSceneMetadata = false;
|
|
936
|
+
|
|
937
|
+
} );
|
|
938
|
+
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
/**
|
|
942
|
+
* Mirrors the authored environment onto the settings once its texture is installed.
|
|
943
|
+
* The authored env is both the light and the backdrop, so the backdrop axis
|
|
944
|
+
* (showBackground / transparentBackground) is forced to the image.
|
|
945
|
+
* @private
|
|
946
|
+
*/
|
|
947
|
+
_applySceneMetadataSettings( texture ) {
|
|
948
|
+
|
|
949
|
+
const metadata = this.sceneMetadata;
|
|
950
|
+
const env = metadata?.environment;
|
|
951
|
+
if ( ! env ) return;
|
|
952
|
+
|
|
953
|
+
const updates = { enableEnvironment: true, showBackground: true, transparentBackground: false };
|
|
954
|
+
if ( env.intensity !== undefined ) {
|
|
955
|
+
|
|
956
|
+
updates.environmentIntensity = env.intensity;
|
|
957
|
+
updates.backgroundIntensity = env.intensity;
|
|
958
|
+
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
if ( env.rotation !== undefined ) updates.environmentRotation = env.rotation;
|
|
962
|
+
|
|
963
|
+
this.settings.setMany( updates, { reset: false } );
|
|
964
|
+
if ( this.scene ) this.scene.background = texture;
|
|
965
|
+
|
|
966
|
+
this.dispatchEvent( { type: 'SceneMetadataApplied', metadata, environment: { ...env } } );
|
|
967
|
+
|
|
968
|
+
}
|
|
969
|
+
|
|
812
970
|
/**
|
|
813
971
|
* Builds BVH from meshScene and uploads all scene data to the path tracer.
|
|
972
|
+
* @param {Object} [options]
|
|
973
|
+
* @param {Promise<import('three').Texture|null>} [options.pendingEnvironment] - An
|
|
974
|
+
* environment still being fetched (see _beginSceneMetadataEnvironment). Installed and
|
|
975
|
+
* CDF'd concurrently with the BVH build rather than before it.
|
|
814
976
|
* @returns {boolean}
|
|
815
977
|
*/
|
|
816
|
-
async loadSceneData() {
|
|
978
|
+
async loadSceneData( { pendingEnvironment = null } = {} ) {
|
|
817
979
|
|
|
818
980
|
// Clear selection before rebuilding — the old object leaves the scene graph.
|
|
819
981
|
// Skipped on the append path (addModel): the selected object persists, so its
|
|
@@ -832,7 +994,23 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
832
994
|
|
|
833
995
|
// Environment CDF build in parallel with BVH
|
|
834
996
|
let cdfPromise = null;
|
|
835
|
-
if (
|
|
997
|
+
if ( pendingEnvironment ) {
|
|
998
|
+
|
|
999
|
+
// The authored environment is still downloading. Install + CDF it off to the side so
|
|
1000
|
+
// the fetch overlaps the BVH build below instead of gating it.
|
|
1001
|
+
timer.start( 'Environment fetch + CDF (concurrent)' );
|
|
1002
|
+
cdfPromise = pendingEnvironment
|
|
1003
|
+
.then( async texture => {
|
|
1004
|
+
|
|
1005
|
+
if ( ! texture ) return;
|
|
1006
|
+
await this.stages.pathTracer.environment.applyHDRI( texture );
|
|
1007
|
+
this._applySceneMetadataSettings( texture );
|
|
1008
|
+
|
|
1009
|
+
} )
|
|
1010
|
+
.catch( error => log.warn( 'scene metadata: environment install failed', error ) )
|
|
1011
|
+
.finally( () => timer.end( 'Environment fetch + CDF (concurrent)' ) );
|
|
1012
|
+
|
|
1013
|
+
} else if ( environmentTexture?.image?.data ) {
|
|
836
1014
|
|
|
837
1015
|
timer.start( 'Environment CDF build (worker)' );
|
|
838
1016
|
this.stages.pathTracer.scene.environment = environmentTexture;
|
|
@@ -852,7 +1030,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
852
1030
|
await new Promise( r => setTimeout( r, 0 ) );
|
|
853
1031
|
timer.start( 'GPU data transfer' );
|
|
854
1032
|
|
|
855
|
-
|
|
1033
|
+
// Re-read rather than reusing the snapshot above: a pendingEnvironment may have landed
|
|
1034
|
+
// during the BVH build, and the snapshot is then a disposed texture.
|
|
1035
|
+
if ( ! this._sdf.uploadToPathTracer( this.stages.pathTracer, this.lightManager, this.meshScene, this.meshScene.environment ) ) return false;
|
|
856
1036
|
|
|
857
1037
|
// Patch per-mesh visibility into the TLAS leaves we just uploaded
|
|
858
1038
|
this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
|
|
@@ -2603,11 +2783,18 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2603
2783
|
|
|
2604
2784
|
if ( this._loadingInProgress ) return;
|
|
2605
2785
|
|
|
2786
|
+
// The authored-environment fetch runs through this same AssetLoader, so its 'load'
|
|
2787
|
+
// event lands right back here. We install that texture ourselves — without the
|
|
2788
|
+
// guard the texture branch below would rebuild the CDF a second time and
|
|
2789
|
+
// resetLoading() would tear down the overlay mid-upload. Model events still pass:
|
|
2790
|
+
// a user can drop another file while the environment is in flight.
|
|
2791
|
+
if ( this._applyingSceneMetadata && event.texture ) return;
|
|
2792
|
+
|
|
2606
2793
|
if ( event.model ) {
|
|
2607
2794
|
|
|
2608
2795
|
// Drag-drop / file load is a replace: clear any appended models first.
|
|
2609
2796
|
this._clearAppendedModels();
|
|
2610
|
-
await this.loadSceneData();
|
|
2797
|
+
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
2611
2798
|
|
|
2612
2799
|
} else if ( event.texture ) {
|
|
2613
2800
|
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createLogger } from '../utils/Logger.js';
|
|
2
|
+
|
|
3
|
+
const log = createLogger( 'engine' );
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* Tracks render completion state, time limits, and sample limits.
|
|
3
7
|
*
|
|
@@ -12,6 +16,10 @@ export class CompletionTracker {
|
|
|
12
16
|
this.lastResetTime = performance.now();
|
|
13
17
|
this.renderCompleteDispatched = false;
|
|
14
18
|
|
|
19
|
+
/** Whether the deadline — rather than the ceiling or convergence — retired the frame. */
|
|
20
|
+
this.budgetOverrun = false;
|
|
21
|
+
this._budgetHeldWarned = false;
|
|
22
|
+
|
|
15
23
|
}
|
|
16
24
|
|
|
17
25
|
/**
|
|
@@ -24,19 +32,48 @@ export class CompletionTracker {
|
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
/**
|
|
27
|
-
* Checks whether the
|
|
28
|
-
*
|
|
35
|
+
* Checks whether the wall-clock render budget has expired. An additional stop condition,
|
|
36
|
+
* never a replacement — the sample ceiling and convergence still bind when they come first.
|
|
37
|
+
*
|
|
38
|
+
* Floored at one sample: frameCount does not advance while the stage compiles kernels or
|
|
39
|
+
* while the camera is in interaction mode, and retiring in that window denoises an empty
|
|
40
|
+
* buffer and captures a blank canvas.
|
|
41
|
+
*
|
|
42
|
+
* @param {Object} pathTracer - The PathTracer stage
|
|
43
|
+
* @param {string} renderLimitMode - 'time' or 'frames'
|
|
29
44
|
* @param {number} renderTimeLimit - Time limit in seconds
|
|
30
45
|
* @returns {boolean}
|
|
31
46
|
*/
|
|
32
|
-
isTimeLimitReached( renderLimitMode, renderTimeLimit ) {
|
|
47
|
+
isTimeLimitReached( pathTracer, renderLimitMode, renderTimeLimit ) {
|
|
48
|
+
|
|
49
|
+
if ( renderLimitMode !== 'time' || ! ( renderTimeLimit > 0 ) ) return false;
|
|
50
|
+
if ( this.timeElapsed < renderTimeLimit ) return false;
|
|
51
|
+
|
|
52
|
+
if ( ! ( pathTracer?.frameCount > 0 ) ) {
|
|
33
53
|
|
|
34
|
-
|
|
54
|
+
if ( ! this._budgetHeldWarned ) {
|
|
55
|
+
|
|
56
|
+
this._budgetHeldWarned = true;
|
|
57
|
+
log.warn(
|
|
58
|
+
`render time limit (${renderTimeLimit}s) expired before the first sample landed — ` +
|
|
59
|
+
'holding the deadline open to avoid capturing an empty frame'
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return false;
|
|
65
|
+
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this.budgetOverrun = true;
|
|
69
|
+
return true;
|
|
35
70
|
|
|
36
71
|
}
|
|
37
72
|
|
|
38
73
|
/**
|
|
39
|
-
* Checks whether ANY render limit
|
|
74
|
+
* Checks whether ANY render limit is reached: sample ceiling, convergence, or time budget,
|
|
75
|
+
* whichever arrives first.
|
|
76
|
+
*
|
|
40
77
|
* @param {Object} pathTracer - The PathTracer stage
|
|
41
78
|
* @param {string} renderLimitMode
|
|
42
79
|
* @param {number} renderTimeLimit
|
|
@@ -46,7 +83,7 @@ export class CompletionTracker {
|
|
|
46
83
|
|
|
47
84
|
if ( ! pathTracer ) return false;
|
|
48
85
|
|
|
49
|
-
if ( this.isTimeLimitReached( renderLimitMode, renderTimeLimit ) ) return true;
|
|
86
|
+
if ( this.isTimeLimitReached( pathTracer, renderLimitMode, renderTimeLimit ) ) return true;
|
|
50
87
|
|
|
51
88
|
// Tier-1 convergence early-stop must agree with PathTracer.render()'s own check, else the app-level
|
|
52
89
|
// reconcile would flip isComplete back off and keep dispatching after the frame converged.
|
|
@@ -75,6 +112,8 @@ export class CompletionTracker {
|
|
|
75
112
|
this.timeElapsed = 0;
|
|
76
113
|
this.lastResetTime = performance.now();
|
|
77
114
|
this.renderCompleteDispatched = false;
|
|
115
|
+
this.budgetOverrun = false;
|
|
116
|
+
this._budgetHeldWarned = false;
|
|
78
117
|
|
|
79
118
|
}
|
|
80
119
|
|
|
@@ -9,11 +9,12 @@ import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
|
|
9
9
|
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
10
10
|
import { createMeshesFromMultiMaterialMesh } from 'three/addons/utils/SceneUtils.js';
|
|
11
11
|
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
12
|
-
import { unzipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
|
|
12
|
+
import { unzipSync, zipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
|
|
13
13
|
import { disposeObjectFromMemory, updateLoading } from './utils';
|
|
14
14
|
import { BuildTimer } from './BuildTimer.js';
|
|
15
15
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
16
16
|
import { loadPBRTScene, pickEntryPath } from './PBRT/index.js';
|
|
17
|
+
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
17
18
|
|
|
18
19
|
// Define supported file formats
|
|
19
20
|
const SUPPORTED_FORMATS = {
|
|
@@ -21,13 +22,20 @@ const SUPPORTED_FORMATS = {
|
|
|
21
22
|
'fbx': { type: 'model', name: 'FBX' }, 'obj': { type: 'model', name: 'OBJ' },
|
|
22
23
|
'stl': { type: 'model', name: 'STL' }, 'ply': { type: 'model', name: 'PLY (Polygon File Format)' },
|
|
23
24
|
'dae': { type: 'model', name: 'Collada' }, '3mf': { type: 'model', name: '3D Manufacturing Format' },
|
|
24
|
-
'
|
|
25
|
+
'usd': { type: 'model', name: 'USD (Universal Scene Description)' },
|
|
26
|
+
'usda': { type: 'model', name: 'USDA (USD ASCII)' },
|
|
27
|
+
'usdc': { type: 'model', name: 'USDC (USD Crate)' },
|
|
28
|
+
'usdz': { type: 'model', name: 'USDZ (USD Archive)' },
|
|
25
29
|
'hdr': { type: 'environment', name: 'HDR (High Dynamic Range)' }, 'exr': { type: 'environment', name: 'EXR (OpenEXR)' },
|
|
26
30
|
'png': { type: 'image', name: 'PNG' }, 'jpg': { type: 'image', name: 'JPEG' },
|
|
27
31
|
'jpeg': { type: 'image', name: 'JPEG' }, 'webp': { type: 'image', name: 'WebP' },
|
|
28
32
|
'zip': { type: 'archive', name: 'ZIP Archive' }
|
|
29
33
|
};
|
|
30
34
|
|
|
35
|
+
// Loose USD layers inside a ZIP compose into one scene; these pick out the
|
|
36
|
+
// layers and the image assets they reference.
|
|
37
|
+
const USD_LAYER_RE = /\.(usd|usda|usdc)$/i;
|
|
38
|
+
const USD_IMAGE_RE = /\.(png|jpg|jpeg|avif)$/i;
|
|
31
39
|
/**
|
|
32
40
|
* AssetLoader class - handles loading of 3D models, environment maps, and archives
|
|
33
41
|
*/
|
|
@@ -48,6 +56,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
48
56
|
this.animations = [];
|
|
49
57
|
this.renderer = null;
|
|
50
58
|
|
|
59
|
+
// Scene-level authoring metadata from the current model (glTF `extras`), or null.
|
|
60
|
+
// See SceneMetadata.js. Cleared by releaseTargetModel() on every replace-load.
|
|
61
|
+
this.sceneMetadata = null;
|
|
62
|
+
|
|
51
63
|
// Shared across every loader so cancelActiveLoad() can abort whichever
|
|
52
64
|
// fetch is in flight (three r185 FileLoader wires the manager's abort
|
|
53
65
|
// signal into its fetch). One load runs at a time (guarded upstream).
|
|
@@ -127,6 +139,8 @@ export class AssetLoader extends EventDispatcher {
|
|
|
127
139
|
*/
|
|
128
140
|
releaseTargetModel() {
|
|
129
141
|
|
|
142
|
+
this.sceneMetadata = null;
|
|
143
|
+
|
|
130
144
|
if ( ! this.targetModel ) return;
|
|
131
145
|
|
|
132
146
|
if ( this.targetModel === this._externalModel ) {
|
|
@@ -234,7 +248,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
234
248
|
case 'ply': return await this.loadPLYFromArrayBuffer( arrayBuffer, filename );
|
|
235
249
|
case 'dae': return await this.loadColladaFromFile( file, filename );
|
|
236
250
|
case '3mf': return await this.load3MFFromArrayBuffer( arrayBuffer, filename );
|
|
237
|
-
case '
|
|
251
|
+
case 'usd':
|
|
252
|
+
case 'usda':
|
|
253
|
+
case 'usdc':
|
|
254
|
+
case 'usdz': return await this.loadUSDFromArrayBuffer( arrayBuffer, filename );
|
|
238
255
|
default: throw new Error( `Support for ${extension} files is not yet implemented` );
|
|
239
256
|
|
|
240
257
|
}
|
|
@@ -288,7 +305,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
288
305
|
texture.generateMipmaps = true;
|
|
289
306
|
|
|
290
307
|
this.applyEnvironmentToScene( texture );
|
|
291
|
-
this.dispatchEvent( { type: 'load', texture } );
|
|
308
|
+
this.dispatchEvent( { type: 'load', texture, url: envUrl, filename: envUrl.split( /[?#]/ )[ 0 ].split( '/' ).pop() } );
|
|
292
309
|
return texture;
|
|
293
310
|
|
|
294
311
|
} catch ( error ) {
|
|
@@ -647,6 +664,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
647
664
|
const extension = path.split( '.' ).pop().toLowerCase();
|
|
648
665
|
if ( SUPPORTED_FORMATS[ extension ] && SUPPORTED_FORMATS[ extension ].type === 'model' ) {
|
|
649
666
|
|
|
667
|
+
// A loose layer is only one slice of a USD scene — hand the whole
|
|
668
|
+
// archive over so its references and payloads can resolve.
|
|
669
|
+
if ( USD_LAYER_RE.test( path ) ) return await this.loadUSDHierarchyFromZip( zip );
|
|
670
|
+
|
|
650
671
|
console.log( `Loading model file from ZIP: ${path}` );
|
|
651
672
|
return await this.loadModelFromZipEntry( zip[ path ], path, extension, zip );
|
|
652
673
|
|
|
@@ -658,6 +679,48 @@ export class AssetLoader extends EventDispatcher {
|
|
|
658
679
|
|
|
659
680
|
}
|
|
660
681
|
|
|
682
|
+
// USDLoader only builds a cross-layer asset map on its USDZ branch, so repack
|
|
683
|
+
// the archive as USDZ — root layer first, per AOUSD core spec 16.4.1.2 — and
|
|
684
|
+
// let the loader resolve the references/payloads itself.
|
|
685
|
+
async loadUSDHierarchyFromZip( zip ) {
|
|
686
|
+
|
|
687
|
+
const layers = Object.keys( zip ).filter( name => USD_LAYER_RE.test( name ) );
|
|
688
|
+
if ( layers.length === 0 ) throw new Error( 'No USD layers found in the ZIP archive' );
|
|
689
|
+
|
|
690
|
+
const root = AssetLoader._pickUSDRootLayer( layers );
|
|
691
|
+
console.log( `Loading USD scene from ZIP: ${root} (${layers.length} layers)` );
|
|
692
|
+
|
|
693
|
+
const packed = { [ root ]: [ zip[ root ], { level: 0 } ] };
|
|
694
|
+
for ( const name of Object.keys( zip ) ) {
|
|
695
|
+
|
|
696
|
+
if ( name === root ) continue;
|
|
697
|
+
if ( USD_LAYER_RE.test( name ) || USD_IMAGE_RE.test( name ) ) packed[ name ] = [ zip[ name ], { level: 0 } ];
|
|
698
|
+
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
return await this.loadUSDFromArrayBuffer( zipSync( packed ), root );
|
|
702
|
+
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// Shallowest layer wins; among ties prefer the <dir>/<dir>.usd convention so a
|
|
706
|
+
// set's entry point beats its sibling variants.
|
|
707
|
+
static _pickUSDRootLayer( layers ) {
|
|
708
|
+
|
|
709
|
+
const depth = name => name.split( '/' ).length;
|
|
710
|
+
const minDepth = Math.min( ...layers.map( depth ) );
|
|
711
|
+
const candidates = layers.filter( name => depth( name ) === minDepth );
|
|
712
|
+
|
|
713
|
+
const conventional = candidates.find( name => {
|
|
714
|
+
|
|
715
|
+
const parts = name.split( '/' );
|
|
716
|
+
return parts.length > 1 && parts[ parts.length - 1 ].replace( USD_LAYER_RE, '' ) === parts[ parts.length - 2 ];
|
|
717
|
+
|
|
718
|
+
} );
|
|
719
|
+
|
|
720
|
+
return conventional || candidates[ 0 ];
|
|
721
|
+
|
|
722
|
+
}
|
|
723
|
+
|
|
661
724
|
async loadModelFromZipEntry( fileContent, filePath, extension, zipContents ) {
|
|
662
725
|
|
|
663
726
|
try {
|
|
@@ -698,8 +761,11 @@ export class AssetLoader extends EventDispatcher {
|
|
|
698
761
|
case '3mf':
|
|
699
762
|
result = await this.load3MFFromArrayBuffer( fileContent.buffer, filePath );
|
|
700
763
|
break;
|
|
764
|
+
case 'usd':
|
|
765
|
+
case 'usda':
|
|
766
|
+
case 'usdc':
|
|
701
767
|
case 'usdz':
|
|
702
|
-
result = await this.
|
|
768
|
+
result = await this.loadUSDFromArrayBuffer( fileContent, filePath );
|
|
703
769
|
break;
|
|
704
770
|
default:
|
|
705
771
|
throw new Error( `Support for ${extension} files is not yet implemented` );
|
|
@@ -745,6 +811,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
745
811
|
|
|
746
812
|
this.releaseTargetModel();
|
|
747
813
|
this.targetModel = gltf.scene;
|
|
814
|
+
this.sceneMetadata = extractSceneMetadata( gltf );
|
|
748
815
|
this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
|
|
749
816
|
|
|
750
817
|
},
|
|
@@ -1045,6 +1112,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1045
1112
|
|
|
1046
1113
|
this.targetModel = data.scene;
|
|
1047
1114
|
this.animations = data.animations || [];
|
|
1115
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
1048
1116
|
await this.onModelLoad( this.targetModel );
|
|
1049
1117
|
this.dispatchEvent( { type: 'load', model: data.scene, filename: modelUrl.split( '/' ).pop() } );
|
|
1050
1118
|
return data;
|
|
@@ -1145,6 +1213,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1145
1213
|
|
|
1146
1214
|
this.targetModel = data.scene;
|
|
1147
1215
|
this.animations = data.animations || [];
|
|
1216
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
1148
1217
|
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
1149
1218
|
await this.onModelLoad( this.targetModel );
|
|
1150
1219
|
|
|
@@ -1396,21 +1465,28 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1396
1465
|
|
|
1397
1466
|
}
|
|
1398
1467
|
|
|
1399
|
-
async
|
|
1468
|
+
async loadUSDFromArrayBuffer( data, filename = 'model.usd' ) {
|
|
1400
1469
|
|
|
1401
1470
|
try {
|
|
1402
1471
|
|
|
1403
|
-
updateLoading( { isLoading: true, status: "Processing
|
|
1472
|
+
updateLoading( { isLoading: true, status: "Processing USD Data...", progress: 5 } );
|
|
1404
1473
|
await new Promise( r => setTimeout( r, 0 ) );
|
|
1405
1474
|
|
|
1406
|
-
if ( ! this.loaderCache.
|
|
1475
|
+
if ( ! this.loaderCache.usd ) {
|
|
1407
1476
|
|
|
1408
|
-
const {
|
|
1409
|
-
this.loaderCache.
|
|
1477
|
+
const { USDLoader } = await import( 'three/examples/jsm/loaders/USDLoader.js' );
|
|
1478
|
+
this.loaderCache.usd = new USDLoader();
|
|
1410
1479
|
|
|
1411
1480
|
}
|
|
1412
1481
|
|
|
1413
|
-
|
|
1482
|
+
// parse() returns the group synchronously but resolves textures async;
|
|
1483
|
+
// setupPathTracing snapshots materials, so maps must land before it runs.
|
|
1484
|
+
const object = await new Promise( ( resolve, reject ) => {
|
|
1485
|
+
|
|
1486
|
+
this.loaderCache.usd.parse( data, '', resolve, reject );
|
|
1487
|
+
|
|
1488
|
+
} );
|
|
1489
|
+
|
|
1414
1490
|
object.name = filename;
|
|
1415
1491
|
|
|
1416
1492
|
this.releaseTargetModel();
|
|
@@ -1424,7 +1500,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1424
1500
|
|
|
1425
1501
|
} catch ( error ) {
|
|
1426
1502
|
|
|
1427
|
-
console.error( 'Error loading
|
|
1503
|
+
console.error( 'Error loading USD:', error );
|
|
1428
1504
|
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
1429
1505
|
throw error;
|
|
1430
1506
|
|