rayzee 7.25.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/dist/rayzee.es.js +319 -205
- package/dist/rayzee.umd.js +4 -4
- package/package.json +1 -1
- package/src/PathTracerApp.js +193 -15
- package/src/Processor/AssetLoader.js +11 -1
- package/src/Processor/SceneMetadata.js +123 -0
- package/src/index.js +3 -0
- package/src/managers/EnvironmentManager.js +34 -2
package/package.json
CHANGED
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 );
|
|
@@ -674,17 +677,26 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
674
677
|
|
|
675
678
|
}
|
|
676
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
|
+
|
|
677
693
|
/**
|
|
678
694
|
* Loads an environment map and rebuilds CDF.
|
|
679
695
|
* @param {string} url - Environment URL
|
|
680
696
|
*/
|
|
681
697
|
async loadEnvironment( url ) {
|
|
682
698
|
|
|
683
|
-
if ( this._loadingInProgress )
|
|
684
|
-
|
|
685
|
-
throw new Error( 'PathTracerApp.loadEnvironment: another load is already in progress' );
|
|
686
|
-
|
|
687
|
-
}
|
|
699
|
+
if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp.loadEnvironment' );
|
|
688
700
|
|
|
689
701
|
this._loadingInProgress = true;
|
|
690
702
|
|
|
@@ -711,6 +723,63 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
711
723
|
|
|
712
724
|
}
|
|
713
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
|
+
|
|
714
783
|
/**
|
|
715
784
|
* Loads example models by index.
|
|
716
785
|
* @param {number} index
|
|
@@ -784,11 +853,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
784
853
|
/** Shared pipeline: load asset → sync controls → build BVH → reset → dispatch events */
|
|
785
854
|
async _loadWithSceneRebuild( loadFn, eventPayload ) {
|
|
786
855
|
|
|
787
|
-
if ( this._loadingInProgress )
|
|
788
|
-
|
|
789
|
-
throw new Error( 'PathTracerApp: another load is already in progress' );
|
|
790
|
-
|
|
791
|
-
}
|
|
856
|
+
if ( this._loadingInProgress ) throw this._busyError( 'PathTracerApp' );
|
|
792
857
|
|
|
793
858
|
this._loadingInProgress = true;
|
|
794
859
|
|
|
@@ -803,7 +868,7 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
803
868
|
// (The old primary was already released by releaseTargetModel() in loadFn.)
|
|
804
869
|
this._clearAppendedModels();
|
|
805
870
|
this._syncControlsAfterLoad();
|
|
806
|
-
await this.loadSceneData();
|
|
871
|
+
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
807
872
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
808
873
|
this.reset();
|
|
809
874
|
this.cameraManager.currentCameraIndex = 0;
|
|
@@ -818,11 +883,99 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
818
883
|
|
|
819
884
|
}
|
|
820
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
|
+
|
|
821
970
|
/**
|
|
822
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.
|
|
823
976
|
* @returns {boolean}
|
|
824
977
|
*/
|
|
825
|
-
async loadSceneData() {
|
|
978
|
+
async loadSceneData( { pendingEnvironment = null } = {} ) {
|
|
826
979
|
|
|
827
980
|
// Clear selection before rebuilding — the old object leaves the scene graph.
|
|
828
981
|
// Skipped on the append path (addModel): the selected object persists, so its
|
|
@@ -841,7 +994,23 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
841
994
|
|
|
842
995
|
// Environment CDF build in parallel with BVH
|
|
843
996
|
let cdfPromise = null;
|
|
844
|
-
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 ) {
|
|
845
1014
|
|
|
846
1015
|
timer.start( 'Environment CDF build (worker)' );
|
|
847
1016
|
this.stages.pathTracer.scene.environment = environmentTexture;
|
|
@@ -861,7 +1030,9 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
861
1030
|
await new Promise( r => setTimeout( r, 0 ) );
|
|
862
1031
|
timer.start( 'GPU data transfer' );
|
|
863
1032
|
|
|
864
|
-
|
|
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;
|
|
865
1036
|
|
|
866
1037
|
// Patch per-mesh visibility into the TLAS leaves we just uploaded
|
|
867
1038
|
this.stages.pathTracer._meshRefs = this.stages.pathTracer._collectMeshRefs( this.meshScene );
|
|
@@ -2612,11 +2783,18 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
2612
2783
|
|
|
2613
2784
|
if ( this._loadingInProgress ) return;
|
|
2614
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
|
+
|
|
2615
2793
|
if ( event.model ) {
|
|
2616
2794
|
|
|
2617
2795
|
// Drag-drop / file load is a replace: clear any appended models first.
|
|
2618
2796
|
this._clearAppendedModels();
|
|
2619
|
-
await this.loadSceneData();
|
|
2797
|
+
await this.loadSceneData( { pendingEnvironment: this._beginSceneMetadataEnvironment() } );
|
|
2620
2798
|
|
|
2621
2799
|
} else if ( event.texture ) {
|
|
2622
2800
|
|
|
@@ -14,6 +14,7 @@ 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 = {
|
|
@@ -55,6 +56,10 @@ export class AssetLoader extends EventDispatcher {
|
|
|
55
56
|
this.animations = [];
|
|
56
57
|
this.renderer = null;
|
|
57
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
|
+
|
|
58
63
|
// Shared across every loader so cancelActiveLoad() can abort whichever
|
|
59
64
|
// fetch is in flight (three r185 FileLoader wires the manager's abort
|
|
60
65
|
// signal into its fetch). One load runs at a time (guarded upstream).
|
|
@@ -134,6 +139,8 @@ export class AssetLoader extends EventDispatcher {
|
|
|
134
139
|
*/
|
|
135
140
|
releaseTargetModel() {
|
|
136
141
|
|
|
142
|
+
this.sceneMetadata = null;
|
|
143
|
+
|
|
137
144
|
if ( ! this.targetModel ) return;
|
|
138
145
|
|
|
139
146
|
if ( this.targetModel === this._externalModel ) {
|
|
@@ -298,7 +305,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
298
305
|
texture.generateMipmaps = true;
|
|
299
306
|
|
|
300
307
|
this.applyEnvironmentToScene( texture );
|
|
301
|
-
this.dispatchEvent( { type: 'load', texture } );
|
|
308
|
+
this.dispatchEvent( { type: 'load', texture, url: envUrl, filename: envUrl.split( /[?#]/ )[ 0 ].split( '/' ).pop() } );
|
|
302
309
|
return texture;
|
|
303
310
|
|
|
304
311
|
} catch ( error ) {
|
|
@@ -804,6 +811,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
804
811
|
|
|
805
812
|
this.releaseTargetModel();
|
|
806
813
|
this.targetModel = gltf.scene;
|
|
814
|
+
this.sceneMetadata = extractSceneMetadata( gltf );
|
|
807
815
|
this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
|
|
808
816
|
|
|
809
817
|
},
|
|
@@ -1104,6 +1112,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1104
1112
|
|
|
1105
1113
|
this.targetModel = data.scene;
|
|
1106
1114
|
this.animations = data.animations || [];
|
|
1115
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
1107
1116
|
await this.onModelLoad( this.targetModel );
|
|
1108
1117
|
this.dispatchEvent( { type: 'load', model: data.scene, filename: modelUrl.split( '/' ).pop() } );
|
|
1109
1118
|
return data;
|
|
@@ -1204,6 +1213,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1204
1213
|
|
|
1205
1214
|
this.targetModel = data.scene;
|
|
1206
1215
|
this.animations = data.animations || [];
|
|
1216
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
1207
1217
|
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
1208
1218
|
await this.onModelLoad( this.targetModel );
|
|
1209
1219
|
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SceneMetadata.js
|
|
3
|
+
* Scene-level authoring metadata carried inside an asset file.
|
|
4
|
+
*
|
|
5
|
+
* glTF/GLB carries it in `extras`, which three's GLTFLoader surfaces in three
|
|
6
|
+
* places depending on where the exporter wrote it: top-level `extras` becomes
|
|
7
|
+
* `gltf.userData`, `asset.extras` stays on `gltf.asset.extras`, and a scene's
|
|
8
|
+
* `extras` becomes `gltf.scene.userData`. All three are scanned, and the payload
|
|
9
|
+
* may sit at the root or be namespaced under a `rayzee` key.
|
|
10
|
+
*
|
|
11
|
+
* Recognised payload:
|
|
12
|
+
* {
|
|
13
|
+
* "environment": {
|
|
14
|
+
* "sourceFile": "https://.../brown_photostudio_02_1k.hdr",
|
|
15
|
+
* "rotation": 0, // degrees around Y, matches the environmentRotation setting
|
|
16
|
+
* "intensity": 1 // drives both environmentIntensity and backgroundIntensity
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
*
|
|
20
|
+
* Values may also arrive as JSON strings — DCC custom-property round-trips often
|
|
21
|
+
* stringify nested objects — so every container is parsed leniently.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const ENV_URL_KEYS = [ 'sourceFile', 'url', 'file' ];
|
|
25
|
+
|
|
26
|
+
function asObject( value ) {
|
|
27
|
+
|
|
28
|
+
if ( typeof value === 'string' ) {
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
|
|
32
|
+
const parsed = JSON.parse( value );
|
|
33
|
+
return ( parsed && typeof parsed === 'object' ) ? parsed : null;
|
|
34
|
+
|
|
35
|
+
} catch {
|
|
36
|
+
|
|
37
|
+
return null;
|
|
38
|
+
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return ( value && typeof value === 'object' ) ? value : null;
|
|
44
|
+
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function asNumber( value ) {
|
|
48
|
+
|
|
49
|
+
const n = typeof value === 'string' ? parseFloat( value ) : value;
|
|
50
|
+
return Number.isFinite( n ) ? n : undefined;
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pickPayload( container ) {
|
|
55
|
+
|
|
56
|
+
const obj = asObject( container );
|
|
57
|
+
if ( ! obj ) return null;
|
|
58
|
+
|
|
59
|
+
const scoped = asObject( obj.rayzee );
|
|
60
|
+
if ( scoped && scoped.environment !== undefined ) return scoped;
|
|
61
|
+
|
|
62
|
+
return obj.environment !== undefined ? obj : null;
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeEnvironment( raw ) {
|
|
67
|
+
|
|
68
|
+
const env = asObject( raw );
|
|
69
|
+
if ( ! env ) return null;
|
|
70
|
+
|
|
71
|
+
const sourceFile = ENV_URL_KEYS
|
|
72
|
+
.map( key => env[ key ] )
|
|
73
|
+
.find( value => typeof value === 'string' && value.trim() );
|
|
74
|
+
if ( ! sourceFile ) return null;
|
|
75
|
+
|
|
76
|
+
const rotation = asNumber( env.rotation );
|
|
77
|
+
const intensity = asNumber( env.intensity );
|
|
78
|
+
|
|
79
|
+
const normalized = { sourceFile: sourceFile.trim() };
|
|
80
|
+
if ( rotation !== undefined ) normalized.rotation = rotation;
|
|
81
|
+
if ( intensity !== undefined ) normalized.intensity = Math.max( 0, intensity );
|
|
82
|
+
|
|
83
|
+
return normalized;
|
|
84
|
+
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Reads scene metadata out of one `extras`-shaped container.
|
|
89
|
+
* @param {Object|string|null} container
|
|
90
|
+
* @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
|
|
91
|
+
*/
|
|
92
|
+
export function parseSceneMetadata( container ) {
|
|
93
|
+
|
|
94
|
+
const payload = pickPayload( container );
|
|
95
|
+
if ( ! payload ) return null;
|
|
96
|
+
|
|
97
|
+
const environment = normalizeEnvironment( payload.environment );
|
|
98
|
+
return environment ? { environment } : null;
|
|
99
|
+
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Reads scene metadata out of a GLTFLoader parse result.
|
|
104
|
+
* @param {Object} gltf - The object GLTFLoader resolves with
|
|
105
|
+
* @returns {{ environment?: { sourceFile: string, rotation?: number, intensity?: number } }|null}
|
|
106
|
+
*/
|
|
107
|
+
export function extractSceneMetadata( gltf ) {
|
|
108
|
+
|
|
109
|
+
if ( ! gltf ) return null;
|
|
110
|
+
|
|
111
|
+
const containers = [ gltf.userData, gltf.asset?.extras, gltf.scene?.userData ];
|
|
112
|
+
if ( Array.isArray( gltf.scenes ) ) containers.push( ...gltf.scenes.map( scene => scene?.userData ) );
|
|
113
|
+
|
|
114
|
+
for ( const container of containers ) {
|
|
115
|
+
|
|
116
|
+
const metadata = parseSceneMetadata( container );
|
|
117
|
+
if ( metadata ) return metadata;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
|
|
123
|
+
}
|
package/src/index.js
CHANGED
|
@@ -17,6 +17,9 @@ export { EngineEvents } from './EngineEvents.js';
|
|
|
17
17
|
// Asset URL / cache namespace overrides (call before constructing PathTracerApp)
|
|
18
18
|
export { configureAssets, getAssetConfig } from './AssetConfig.js';
|
|
19
19
|
|
|
20
|
+
// Scene-level authoring metadata embedded in model files (glTF `extras`)
|
|
21
|
+
export { extractSceneMetadata, parseSceneMetadata } from './Processor/SceneMetadata.js';
|
|
22
|
+
|
|
20
23
|
// Logging — leveled/namespaced console output shared with the workers
|
|
21
24
|
export { Logger, createLogger, fmt, LOG_LEVELS } from './utils/Logger.js';
|
|
22
25
|
|
|
@@ -360,8 +360,11 @@ export class EnvironmentManager {
|
|
|
360
360
|
/**
|
|
361
361
|
* Set environment map, build CDF, and update shader texture nodes.
|
|
362
362
|
* @param {import('three').Texture|null} envMap
|
|
363
|
+
* @param {Object} [options]
|
|
364
|
+
* @param {boolean} [options.buildCDF=true] - Skip when the caller builds the CDF itself
|
|
365
|
+
* (PathTracerApp.loadSceneData builds it in parallel with the BVH, then calls applyCDFResults).
|
|
363
366
|
*/
|
|
364
|
-
async setEnvironmentMap( envMap ) {
|
|
367
|
+
async setEnvironmentMap( envMap, { buildCDF = true } = {} ) {
|
|
365
368
|
|
|
366
369
|
// Free the outgoing env texture's GPU memory before it is orphaned. Skip: the
|
|
367
370
|
// reusable placeholder, an idempotent re-set of the same texture, and the HDRI
|
|
@@ -379,7 +382,7 @@ export class EnvironmentManager {
|
|
|
379
382
|
|
|
380
383
|
if ( envMap ) {
|
|
381
384
|
|
|
382
|
-
await this.buildEnvironmentCDF();
|
|
385
|
+
if ( buildCDF ) await this.buildEnvironmentCDF();
|
|
383
386
|
|
|
384
387
|
} else {
|
|
385
388
|
|
|
@@ -411,6 +414,35 @@ export class EnvironmentManager {
|
|
|
411
414
|
|
|
412
415
|
}
|
|
413
416
|
|
|
417
|
+
/**
|
|
418
|
+
* Enter 'hdri' mode ahead of an async HDRI install, and drop the texture stashed by a
|
|
419
|
+
* previous setMode( 'gradient' | 'color' | 'procedural' ): a new HDRI makes it
|
|
420
|
+
* unreachable, so leaving it in place both leaks it and lets a setMode( 'hdri' )
|
|
421
|
+
* arriving mid-download restore it over the map we are about to install.
|
|
422
|
+
* @param {import('three').Texture} [incoming] - Never disposed, even if it is the stash.
|
|
423
|
+
*/
|
|
424
|
+
beginHDRI( incoming = null ) {
|
|
425
|
+
|
|
426
|
+
if ( this._previousHDRI && this._previousHDRI !== incoming ) this._previousHDRI.dispose?.();
|
|
427
|
+
|
|
428
|
+
this._previousHDRI = null;
|
|
429
|
+
this.envParams.mode = 'hdri';
|
|
430
|
+
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Install an HDRI as the active environment, switching the mode state machine to 'hdri'.
|
|
435
|
+
* @param {import('three').Texture} envMap
|
|
436
|
+
* @param {Object} [options]
|
|
437
|
+
* @param {boolean} [options.buildCDF=true]
|
|
438
|
+
*/
|
|
439
|
+
async applyHDRI( envMap, { buildCDF = true } = {} ) {
|
|
440
|
+
|
|
441
|
+
this.beginHDRI( envMap );
|
|
442
|
+
await this.setEnvironmentMap( envMap, { buildCDF } );
|
|
443
|
+
|
|
444
|
+
}
|
|
445
|
+
|
|
414
446
|
// ===== SKY GENERATORS =====
|
|
415
447
|
|
|
416
448
|
/**
|