rayzee 7.9.2 → 7.10.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/README.md +1 -1
- package/dist/rayzee.es.js +178 -32
- package/dist/rayzee.es.js.map +1 -1
- package/dist/rayzee.umd.js +3 -3
- package/dist/rayzee.umd.js.map +1 -1
- package/package.json +1 -1
- package/src/PathTracerApp.js +268 -2
- package/src/Processor/AssetLoader.js +158 -9
- package/src/Stages/PathTracer.js +7 -3
- package/src/managers/TransformManager.js +4 -0
package/package.json
CHANGED
package/src/PathTracerApp.js
CHANGED
|
@@ -689,6 +689,21 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
689
689
|
|
|
690
690
|
}
|
|
691
691
|
|
|
692
|
+
/**
|
|
693
|
+
* Cancel the in-flight model/environment download, if any. The active
|
|
694
|
+
* loadAsync() rejects with a typed LOAD_CANCELLED error, which callers treat
|
|
695
|
+
* as a user cancellation (not a load failure). Only the network-download phase
|
|
696
|
+
* is cancelable — once processing (BVH/textures) has started this is a no-op.
|
|
697
|
+
* The scene is untouched: replace-loads release the old model only after the
|
|
698
|
+
* download succeeds, and appends parent nothing until then.
|
|
699
|
+
*/
|
|
700
|
+
cancelLoad() {
|
|
701
|
+
|
|
702
|
+
if ( ! this._loadingInProgress ) return;
|
|
703
|
+
this.assetLoader?.cancelActiveLoad();
|
|
704
|
+
|
|
705
|
+
}
|
|
706
|
+
|
|
692
707
|
/**
|
|
693
708
|
* Set the max material-texture dimension (longest edge) used when processing a
|
|
694
709
|
* scene's textures into GPU arrays. Clamped to the hardware ceiling. Larger =
|
|
@@ -744,6 +759,10 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
744
759
|
try {
|
|
745
760
|
|
|
746
761
|
await loadFn();
|
|
762
|
+
// Replace-load clears any dynamically-appended models — but only AFTER
|
|
763
|
+
// loadFn() succeeds, so a failed load leaves the current scene intact.
|
|
764
|
+
// (The old primary was already released by releaseTargetModel() in loadFn.)
|
|
765
|
+
this._clearAppendedModels();
|
|
747
766
|
this._syncControlsAfterLoad();
|
|
748
767
|
await this.loadSceneData();
|
|
749
768
|
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
@@ -770,13 +789,18 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
770
789
|
*/
|
|
771
790
|
async loadSceneData() {
|
|
772
791
|
|
|
773
|
-
// Clear selection before rebuilding — the old object leaves the scene graph
|
|
774
|
-
|
|
792
|
+
// Clear selection before rebuilding — the old object leaves the scene graph.
|
|
793
|
+
// Skipped on the append path (addModel): the selected object persists, so its
|
|
794
|
+
// selection + transform gizmo should survive the rebuild.
|
|
795
|
+
if ( ! this._preserveSelectionOnRebuild ) this.interactionManager?.deselect();
|
|
775
796
|
|
|
776
797
|
// Stop any running animation before rebuilding scene data
|
|
777
798
|
this.animationManager.dispose();
|
|
778
799
|
this._animRefitInFlight = false;
|
|
779
800
|
|
|
801
|
+
// Tag the primary (replace-loaded) model so it appears in the scene-object list.
|
|
802
|
+
this._tagPrimarySceneObject();
|
|
803
|
+
|
|
780
804
|
const timer = new BuildTimer( 'loadSceneData' );
|
|
781
805
|
const environmentTexture = this.meshScene.environment;
|
|
782
806
|
|
|
@@ -863,6 +887,189 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
863
887
|
|
|
864
888
|
}
|
|
865
889
|
|
|
890
|
+
// ═══════════════════════════════════════════════════════════════
|
|
891
|
+
// Dynamic scene objects (add / remove / list / visibility)
|
|
892
|
+
//
|
|
893
|
+
// Top-level objects are the auto-created "Ground" plane plus each loaded
|
|
894
|
+
// model root parented into meshScene. The scene graph + per-root userData
|
|
895
|
+
// tags are the single source of truth (no separate registry): ids are
|
|
896
|
+
// Object3D uuids (stable across rebuilds, since the same root persists).
|
|
897
|
+
// ═══════════════════════════════════════════════════════════════
|
|
898
|
+
|
|
899
|
+
/** Tag the primary (replace-loaded) model as a removable scene object (read by the Outliner + removeSceneObject). Idempotent. */
|
|
900
|
+
_tagPrimarySceneObject() {
|
|
901
|
+
|
|
902
|
+
const m = this.assetLoader?.targetModel;
|
|
903
|
+
if ( ! m ) return;
|
|
904
|
+
m.userData.__rayzeeSceneObject = true;
|
|
905
|
+
m.userData.__rayzeeExternal = ( m === this.assetLoader._externalModel );
|
|
906
|
+
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
/** Remove + dispose all dynamically-appended models (keeps Ground and the primary). */
|
|
910
|
+
_clearAppendedModels() {
|
|
911
|
+
|
|
912
|
+
const scene = this.meshScene;
|
|
913
|
+
if ( ! scene ) return;
|
|
914
|
+
const floor = this.assetLoader?.floorPlane;
|
|
915
|
+
const primary = this.assetLoader?.targetModel;
|
|
916
|
+
for ( const child of [ ...scene.children ] ) {
|
|
917
|
+
|
|
918
|
+
if ( child === floor || child === primary ) continue;
|
|
919
|
+
if ( ! child.userData?.__rayzeeSceneObject ) continue;
|
|
920
|
+
this.assetLoader.removeModelRoot( child, { external: !! child.userData.__rayzeeExternal } );
|
|
921
|
+
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
/** Reframe-free rebuild sequence. Assumes the _loadingInProgress guard is already held. */
|
|
927
|
+
async _finishRebuildNoReframe( eventPayload ) {
|
|
928
|
+
|
|
929
|
+
await this.loadSceneData(); // emits 'SceneRebuild'
|
|
930
|
+
this._recalibrateControlLimits(); // scene bounds changed — retune zoom limits + near/far (no camera move)
|
|
931
|
+
this.pipeline?.eventBus.emit( 'autoexposure:resetHistory' );
|
|
932
|
+
this.reset();
|
|
933
|
+
if ( eventPayload ) this.dispatchEvent( eventPayload );
|
|
934
|
+
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Append a model by URL to the current scene (does NOT replace it), then rebuild
|
|
939
|
+
* without reframing the camera.
|
|
940
|
+
* @param {string} url
|
|
941
|
+
* @param {Object} [opts]
|
|
942
|
+
* @param {string} [opts.name] - Display name for the scene-object list.
|
|
943
|
+
* @returns {Promise<string>} the new object's id (Object3D uuid).
|
|
944
|
+
*/
|
|
945
|
+
async addModel( url, { name } = {} ) {
|
|
946
|
+
|
|
947
|
+
if ( this._loadingInProgress ) {
|
|
948
|
+
|
|
949
|
+
throw new Error( 'PathTracerApp.addModel: another load is already in progress' );
|
|
950
|
+
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
this._loadingInProgress = true;
|
|
954
|
+
this._preserveSelectionOnRebuild = true;
|
|
955
|
+
try {
|
|
956
|
+
|
|
957
|
+
const { root } = await this.assetLoader.appendModel( url );
|
|
958
|
+
root.userData.__rayzeeSceneObject = true;
|
|
959
|
+
root.userData.__rayzeeExternal = false;
|
|
960
|
+
if ( name ) root.userData.__rayzeeName = name;
|
|
961
|
+
await this._finishRebuildNoReframe( { type: 'ModelAdded', url, id: root.uuid } );
|
|
962
|
+
return root.uuid;
|
|
963
|
+
|
|
964
|
+
} finally {
|
|
965
|
+
|
|
966
|
+
this._preserveSelectionOnRebuild = false;
|
|
967
|
+
this._loadingInProgress = false;
|
|
968
|
+
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
/**
|
|
974
|
+
* Append a caller-owned Object3D to the current scene, then rebuild (no reframe).
|
|
975
|
+
* The caller retains ownership — removal only detaches it.
|
|
976
|
+
* @param {import('three').Object3D} object3d
|
|
977
|
+
* @param {Object} [opts]
|
|
978
|
+
* @param {string} [opts.name]
|
|
979
|
+
* @returns {Promise<string>} the new object's id (Object3D uuid).
|
|
980
|
+
*/
|
|
981
|
+
async addModelFromObject3D( object3d, { name } = {} ) {
|
|
982
|
+
|
|
983
|
+
if ( this._loadingInProgress ) {
|
|
984
|
+
|
|
985
|
+
throw new Error( 'PathTracerApp.addModelFromObject3D: another load is already in progress' );
|
|
986
|
+
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
this._loadingInProgress = true;
|
|
990
|
+
this._preserveSelectionOnRebuild = true;
|
|
991
|
+
try {
|
|
992
|
+
|
|
993
|
+
const { root } = this.assetLoader.appendObject3D( object3d, name || 'object3d' );
|
|
994
|
+
root.userData.__rayzeeSceneObject = true;
|
|
995
|
+
root.userData.__rayzeeExternal = true;
|
|
996
|
+
if ( name ) root.userData.__rayzeeName = name;
|
|
997
|
+
await this._finishRebuildNoReframe( { type: 'ModelAdded', id: root.uuid } );
|
|
998
|
+
return root.uuid;
|
|
999
|
+
|
|
1000
|
+
} finally {
|
|
1001
|
+
|
|
1002
|
+
this._preserveSelectionOnRebuild = false;
|
|
1003
|
+
this._loadingInProgress = false;
|
|
1004
|
+
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Remove a scene object by id (Object3D uuid). The Ground plane is permanent.
|
|
1011
|
+
* @param {string} id
|
|
1012
|
+
* @returns {Promise<boolean>} true if removed.
|
|
1013
|
+
*/
|
|
1014
|
+
async removeSceneObject( id ) {
|
|
1015
|
+
|
|
1016
|
+
const scene = this.meshScene;
|
|
1017
|
+
if ( ! scene ) return false;
|
|
1018
|
+
|
|
1019
|
+
const floor = this.assetLoader?.floorPlane;
|
|
1020
|
+
if ( floor && floor.uuid === id ) return false; // Ground is not deletable
|
|
1021
|
+
|
|
1022
|
+
const root = scene.children.find( c => c.uuid === id && c.userData?.__rayzeeSceneObject );
|
|
1023
|
+
if ( ! root ) return false;
|
|
1024
|
+
|
|
1025
|
+
if ( this._loadingInProgress ) {
|
|
1026
|
+
|
|
1027
|
+
throw new Error( 'PathTracerApp.removeSceneObject: another load is already in progress' );
|
|
1028
|
+
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
this._loadingInProgress = true;
|
|
1032
|
+
try {
|
|
1033
|
+
|
|
1034
|
+
this.interactionManager?.deselect();
|
|
1035
|
+
this.transformManager?.detach?.();
|
|
1036
|
+
|
|
1037
|
+
if ( root === this.assetLoader.targetModel ) {
|
|
1038
|
+
|
|
1039
|
+
this.assetLoader.releaseTargetModel();
|
|
1040
|
+
|
|
1041
|
+
} else {
|
|
1042
|
+
|
|
1043
|
+
this.assetLoader.removeModelRoot( root, { external: !! root.userData.__rayzeeExternal } );
|
|
1044
|
+
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
// Ground is permanent (removal refused above), so the scene always keeps
|
|
1048
|
+
// renderable geometry — a full rebuild is always valid here.
|
|
1049
|
+
await this._finishRebuildNoReframe( { type: 'SceneObjectRemoved', id } );
|
|
1050
|
+
|
|
1051
|
+
return true;
|
|
1052
|
+
|
|
1053
|
+
} finally {
|
|
1054
|
+
|
|
1055
|
+
this._loadingInProgress = false;
|
|
1056
|
+
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Toggle a scene object's visibility without rebuilding (O(1) TLAS-leaf patch).
|
|
1063
|
+
* @param {string} id - Object3D uuid.
|
|
1064
|
+
* @param {boolean | ((prev:boolean)=>boolean)} visible
|
|
1065
|
+
* @returns {boolean|null} new visibility, or null if not found.
|
|
1066
|
+
*/
|
|
1067
|
+
setSceneObjectVisibility( id, visible ) {
|
|
1068
|
+
|
|
1069
|
+
return this.setMeshVisibilityByUuid( id, visible );
|
|
1070
|
+
|
|
1071
|
+
}
|
|
1072
|
+
|
|
866
1073
|
// ═══════════════════════════════════════════════════════════════
|
|
867
1074
|
// BVH Refit (Animation)
|
|
868
1075
|
// ═══════════════════════════════════════════════════════════════
|
|
@@ -1611,6 +1818,8 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1611
1818
|
|
|
1612
1819
|
if ( event.model ) {
|
|
1613
1820
|
|
|
1821
|
+
// Drag-drop / file load is a replace: clear any appended models first.
|
|
1822
|
+
this._clearAppendedModels();
|
|
1614
1823
|
await this.loadSceneData();
|
|
1615
1824
|
|
|
1616
1825
|
} else if ( event.texture ) {
|
|
@@ -1815,6 +2024,63 @@ export class PathTracerApp extends EventDispatcher {
|
|
|
1815
2024
|
|
|
1816
2025
|
}
|
|
1817
2026
|
|
|
2027
|
+
/**
|
|
2028
|
+
* Recompute OrbitControls zoom limits (+ default-camera near/far) from the CURRENT
|
|
2029
|
+
* model bounds without moving the camera or its target. Called after a dynamic
|
|
2030
|
+
* add/remove (the reframe-free path) so an enlarged scene stays reachable and
|
|
2031
|
+
* unclipped, and a shrunken one re-tightens. The replace-load (reframe) path owns
|
|
2032
|
+
* this via onModelLoad(). Bounds cover only the loaded model roots
|
|
2033
|
+
* (__rayzeeSceneObject) so the oversized, usually-hidden Ground plane can't inflate them.
|
|
2034
|
+
*/
|
|
2035
|
+
_recalibrateControlLimits() {
|
|
2036
|
+
|
|
2037
|
+
if ( ! this.meshScene || ! this.cameraManager ) return;
|
|
2038
|
+
|
|
2039
|
+
const bounds = new Box3();
|
|
2040
|
+
const tmp = new Box3();
|
|
2041
|
+
for ( const child of this.meshScene.children ) {
|
|
2042
|
+
|
|
2043
|
+
if ( ! child.userData?.__rayzeeSceneObject ) continue;
|
|
2044
|
+
tmp.setFromObject( child );
|
|
2045
|
+
if ( ! tmp.isEmpty() ) bounds.union( tmp );
|
|
2046
|
+
|
|
2047
|
+
}
|
|
2048
|
+
|
|
2049
|
+
if ( bounds.isEmpty() ) return;
|
|
2050
|
+
|
|
2051
|
+
const maxDim = Math.max(
|
|
2052
|
+
bounds.max.x - bounds.min.x,
|
|
2053
|
+
bounds.max.y - bounds.min.y,
|
|
2054
|
+
bounds.max.z - bounds.min.z,
|
|
2055
|
+
);
|
|
2056
|
+
if ( ! Number.isFinite( maxDim ) || maxDim <= 0 ) return;
|
|
2057
|
+
|
|
2058
|
+
const { camera, controls } = this.cameraManager;
|
|
2059
|
+
|
|
2060
|
+
// Same framing distance onModelLoad() uses for the initial reframe.
|
|
2061
|
+
const fov = camera.fov * ( Math.PI / 180 );
|
|
2062
|
+
const cameraDistance = Math.abs( maxDim / Math.sin( fov / 2 ) / 2 );
|
|
2063
|
+
|
|
2064
|
+
// Keep the (grown/shrunken) scene inside the frustum. Only touch near/far when the
|
|
2065
|
+
// default orbit camera is active — don't stomp an authored model camera's frustum.
|
|
2066
|
+
if ( this.cameraManager.currentCameraIndex === 0 ) {
|
|
2067
|
+
|
|
2068
|
+
camera.near = maxDim / 100;
|
|
2069
|
+
camera.far = maxDim * 100;
|
|
2070
|
+
camera.updateProjectionMatrix();
|
|
2071
|
+
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
// Reframe-free: never clamp past where the camera currently sits, so the rebuild
|
|
2075
|
+
// can't yank it (e.g. when the scene shrinks after a removal).
|
|
2076
|
+
const currentDist = camera.position.distanceTo( controls.target );
|
|
2077
|
+
controls.minDistance = Math.min( maxDim / 1000, currentDist );
|
|
2078
|
+
controls.maxDistance = Math.max( cameraDistance * 10, currentDist * 1.1 );
|
|
2079
|
+
|
|
2080
|
+
controls.update();
|
|
2081
|
+
|
|
2082
|
+
}
|
|
2083
|
+
|
|
1818
2084
|
/**
|
|
1819
2085
|
* Forwards events from a source EventDispatcher to this app instance.
|
|
1820
2086
|
*/
|
|
@@ -48,6 +48,75 @@ export class AssetLoader extends EventDispatcher {
|
|
|
48
48
|
this.animations = [];
|
|
49
49
|
this.renderer = null;
|
|
50
50
|
|
|
51
|
+
// Shared across every loader so cancelActiveLoad() can abort whichever
|
|
52
|
+
// fetch is in flight (three r185 FileLoader wires the manager's abort
|
|
53
|
+
// signal into its fetch). One load runs at a time (guarded upstream).
|
|
54
|
+
this._loadingManager = new LoadingManager();
|
|
55
|
+
this._loadCancelled = false;
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Abort the network download for the in-flight load, if any. The aborted
|
|
61
|
+
* loadAsync() rejects with an AbortError, which each load path re-throws as a
|
|
62
|
+
* typed LOAD_CANCELLED error. Only the download phase is cancelable — once the
|
|
63
|
+
* bytes are in and BVH/texture processing has begun, this is a no-op.
|
|
64
|
+
*/
|
|
65
|
+
cancelActiveLoad() {
|
|
66
|
+
|
|
67
|
+
this._loadCancelled = true;
|
|
68
|
+
this._loadingManager.abort();
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_isCancellation( error ) {
|
|
73
|
+
|
|
74
|
+
return this._loadCancelled || error?.name === 'AbortError' || error?.code === 'LOAD_CANCELLED';
|
|
75
|
+
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
_cancellationError() {
|
|
79
|
+
|
|
80
|
+
const err = new Error( 'Load cancelled' );
|
|
81
|
+
err.code = 'LOAD_CANCELLED';
|
|
82
|
+
return err;
|
|
83
|
+
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Build an onProgress handler that reports download byte counts to the UI.
|
|
87
|
+
// `cancelable` gates the Cancel affordance (true only for network URLs — blob
|
|
88
|
+
// and data URLs resolve locally and have nothing to abort). Download maps onto
|
|
89
|
+
// 2→60% of the bar, leaving headroom for the processing phases that follow.
|
|
90
|
+
_downloadProgress( status, cancelable ) {
|
|
91
|
+
|
|
92
|
+
return ( event ) => {
|
|
93
|
+
|
|
94
|
+
const loaded = event?.loaded || 0;
|
|
95
|
+
const total = event?.lengthComputable ? ( event.total || 0 ) : 0;
|
|
96
|
+
updateLoading( {
|
|
97
|
+
isLoading: true,
|
|
98
|
+
status,
|
|
99
|
+
loadedBytes: loaded,
|
|
100
|
+
totalBytes: total,
|
|
101
|
+
canCancel: !! cancelable,
|
|
102
|
+
progress: total ? Math.min( 60, 2 + Math.round( ( loaded / total ) * 58 ) ) : 2,
|
|
103
|
+
} );
|
|
104
|
+
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Called once bytes are in, before the (non-cancelable) processing phases.
|
|
110
|
+
_downloadComplete( status = 'Processing Data...', progress = 62 ) {
|
|
111
|
+
|
|
112
|
+
updateLoading( { status, progress, canCancel: false, loadedBytes: null, totalBytes: null } );
|
|
113
|
+
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
static _isNetworkUrl( url ) {
|
|
117
|
+
|
|
118
|
+
return typeof url === 'string' && /^https?:/i.test( url );
|
|
119
|
+
|
|
51
120
|
}
|
|
52
121
|
|
|
53
122
|
/**
|
|
@@ -72,6 +141,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
72
141
|
|
|
73
142
|
this.targetModel = null;
|
|
74
143
|
this._externalModel = null;
|
|
144
|
+
// Drop the released model's animation clips so a later rebuild doesn't rebind
|
|
145
|
+
// a mixer to disposed nodes. Every load path re-populates this.animations after.
|
|
146
|
+
this.animations = [];
|
|
75
147
|
|
|
76
148
|
}
|
|
77
149
|
|
|
@@ -189,6 +261,8 @@ export class AssetLoader extends EventDispatcher {
|
|
|
189
261
|
|
|
190
262
|
async loadEnvironment( envUrl ) {
|
|
191
263
|
|
|
264
|
+
this._loadCancelled = false;
|
|
265
|
+
|
|
192
266
|
try {
|
|
193
267
|
|
|
194
268
|
// Dispatch event before loading environment to allow UI to prepare
|
|
@@ -219,6 +293,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
219
293
|
|
|
220
294
|
} catch ( error ) {
|
|
221
295
|
|
|
296
|
+
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
222
297
|
console.error( "Error loading environment:", error );
|
|
223
298
|
this.dispatchEvent( { type: 'error', message: error.message, filename: envUrl } );
|
|
224
299
|
throw error;
|
|
@@ -280,24 +355,29 @@ export class AssetLoader extends EventDispatcher {
|
|
|
280
355
|
|
|
281
356
|
async loadEnvironmentByExtension( url, extension ) {
|
|
282
357
|
|
|
358
|
+
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
359
|
+
const onProgress = this._downloadProgress( "Downloading Environment...", cancelable );
|
|
360
|
+
|
|
283
361
|
let texture;
|
|
284
362
|
if ( extension === 'hdr' || extension === 'exr' ) {
|
|
285
363
|
|
|
286
364
|
const loader = extension === 'hdr'
|
|
287
|
-
? ( this.loaderCache.hdr || ( this.loaderCache.hdr = new HDRLoader().setDataType( FloatType ) ) )
|
|
288
|
-
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader().setDataType( FloatType ) ) );
|
|
289
|
-
texture = await loader.loadAsync( url );
|
|
365
|
+
? ( this.loaderCache.hdr || ( this.loaderCache.hdr = new HDRLoader( this._loadingManager ).setDataType( FloatType ) ) )
|
|
366
|
+
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader( this._loadingManager ).setDataType( FloatType ) ) );
|
|
367
|
+
texture = await loader.loadAsync( url, onProgress );
|
|
290
368
|
|
|
291
369
|
} else {
|
|
292
370
|
|
|
293
|
-
if ( ! this.loaderCache.texture ) this.loaderCache.texture = new TextureLoader();
|
|
294
|
-
texture = await this.loaderCache.texture.loadAsync( url );
|
|
371
|
+
if ( ! this.loaderCache.texture ) this.loaderCache.texture = new TextureLoader( this._loadingManager );
|
|
372
|
+
texture = await this.loaderCache.texture.loadAsync( url, onProgress );
|
|
295
373
|
// LDR env maps (jpg/png/webp) are authored in sRGB; tag them so the backend
|
|
296
374
|
// decodes to linear. HDR/EXR are already linear and keep the loader's setting.
|
|
297
375
|
texture.colorSpace = SRGBColorSpace;
|
|
298
376
|
|
|
299
377
|
}
|
|
300
378
|
|
|
379
|
+
this._downloadComplete( "Processing Environment...", 62 );
|
|
380
|
+
|
|
301
381
|
texture.mapping = EquirectangularReflectionMapping;
|
|
302
382
|
texture.minFilter = LinearFilter;
|
|
303
383
|
texture.magFilter = LinearFilter;
|
|
@@ -919,7 +999,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
919
999
|
|
|
920
1000
|
}
|
|
921
1001
|
|
|
922
|
-
const loader = new GLTFLoader();
|
|
1002
|
+
const loader = new GLTFLoader( this._loadingManager );
|
|
923
1003
|
loader.setDRACOLoader( dracoLoader );
|
|
924
1004
|
loader.setKTX2Loader( ktx2Loader );
|
|
925
1005
|
loader.setMeshoptDecoder( MeshoptDecoder );
|
|
@@ -951,13 +1031,15 @@ export class AssetLoader extends EventDispatcher {
|
|
|
951
1031
|
|
|
952
1032
|
async loadModel( modelUrl ) {
|
|
953
1033
|
|
|
1034
|
+
this._loadCancelled = false;
|
|
954
1035
|
const loader = await this.createGLTFLoader();
|
|
1036
|
+
const cancelable = AssetLoader._isNetworkUrl( modelUrl );
|
|
955
1037
|
|
|
956
1038
|
try {
|
|
957
1039
|
|
|
958
|
-
updateLoading( { status: "
|
|
959
|
-
const data = await loader.loadAsync( modelUrl );
|
|
960
|
-
|
|
1040
|
+
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
1041
|
+
const data = await loader.loadAsync( modelUrl, this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
1042
|
+
this._downloadComplete();
|
|
961
1043
|
|
|
962
1044
|
this.releaseTargetModel();
|
|
963
1045
|
|
|
@@ -969,6 +1051,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
969
1051
|
|
|
970
1052
|
} catch ( error ) {
|
|
971
1053
|
|
|
1054
|
+
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
972
1055
|
console.error( "Error loading model:", error );
|
|
973
1056
|
this.dispatchEvent( { type: 'error', message: error.message, filename: modelUrl } );
|
|
974
1057
|
throw error;
|
|
@@ -981,6 +1064,72 @@ export class AssetLoader extends EventDispatcher {
|
|
|
981
1064
|
|
|
982
1065
|
}
|
|
983
1066
|
|
|
1067
|
+
// ─────────────────────────────────────────────────────────────
|
|
1068
|
+
// Append / remove primitives (dynamic object add/remove).
|
|
1069
|
+
// These deliberately do NOT touch targetModel/animations and do NOT
|
|
1070
|
+
// reframe the camera or dispatch load/modelProcessed — the caller
|
|
1071
|
+
// (PathTracerApp) drives a reframe-free scene rebuild.
|
|
1072
|
+
// ─────────────────────────────────────────────────────────────
|
|
1073
|
+
|
|
1074
|
+
// Multi-material split + area-light placeholders, then parent into meshScene.
|
|
1075
|
+
_processAndParent( model ) {
|
|
1076
|
+
|
|
1077
|
+
this.processModelObjects( model );
|
|
1078
|
+
this.scene.add( model );
|
|
1079
|
+
// Refresh world matrices for the whole subtree — the reframe-free rebuild
|
|
1080
|
+
// extracts geometry immediately (rAF is gated off), so nothing else would
|
|
1081
|
+
// update matrices first and any ancestor-node transform would be dropped.
|
|
1082
|
+
model.updateMatrixWorld( true );
|
|
1083
|
+
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// Append a model from URL without releasing prior models or reframing.
|
|
1087
|
+
// Reuses createGLTFLoader() so appended KTX2 textures stay RGBA DataArrayTexture.
|
|
1088
|
+
async appendModel( url ) {
|
|
1089
|
+
|
|
1090
|
+
this._loadCancelled = false;
|
|
1091
|
+
const loader = await this.createGLTFLoader();
|
|
1092
|
+
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
1093
|
+
|
|
1094
|
+
try {
|
|
1095
|
+
|
|
1096
|
+
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
1097
|
+
const data = await loader.loadAsync( url, this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
1098
|
+
this._downloadComplete();
|
|
1099
|
+
this._processAndParent( data.scene );
|
|
1100
|
+
return { root: data.scene, animations: data.animations || [] };
|
|
1101
|
+
|
|
1102
|
+
} catch ( error ) {
|
|
1103
|
+
|
|
1104
|
+
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
1105
|
+
throw error;
|
|
1106
|
+
|
|
1107
|
+
} finally {
|
|
1108
|
+
|
|
1109
|
+
this._disposeGLTFLoader( loader );
|
|
1110
|
+
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
// Append a caller-owned Object3D without releasing prior models or reframing.
|
|
1116
|
+
appendObject3D( object3d, name = 'object3d' ) {
|
|
1117
|
+
|
|
1118
|
+
object3d.name = object3d.name || name;
|
|
1119
|
+
this._processAndParent( object3d );
|
|
1120
|
+
return { root: object3d };
|
|
1121
|
+
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Detach + dispose an appended root. External (caller-owned) roots are only detached.
|
|
1125
|
+
removeModelRoot( root, { external = false } = {} ) {
|
|
1126
|
+
|
|
1127
|
+
if ( ! root ) return;
|
|
1128
|
+
if ( external ) root.parent?.remove( root );
|
|
1129
|
+
else disposeObjectFromMemory( root );
|
|
1130
|
+
|
|
1131
|
+
}
|
|
1132
|
+
|
|
984
1133
|
async loadGLBFromArrayBuffer( arrayBuffer, filename = 'model.glb' ) {
|
|
985
1134
|
|
|
986
1135
|
const loader = await this.createGLTFLoader();
|
package/src/Stages/PathTracer.js
CHANGED
|
@@ -425,9 +425,13 @@ export class PathTracer extends PathTracerStage {
|
|
|
425
425
|
// Async readback of the per-bounce snapshot every N frames; never awaited, so the early-exit uses past-frame data.
|
|
426
426
|
_maybeReadbackCounters() {
|
|
427
427
|
|
|
428
|
-
// Never sample the survivor curve mid-motion
|
|
429
|
-
//
|
|
430
|
-
|
|
428
|
+
// Never sample the survivor curve mid-motion, nor while the CameraOptimizer is holding maxBounces
|
|
429
|
+
// down to its interaction value (1): mid-motion counts belong to a pose we're leaving, and a curve
|
|
430
|
+
// measured at the interaction budget stores _lastBounceCountsBudget=1 — when the real (high) budget
|
|
431
|
+
// is restored on exit that stale curve forces curveReliableUpto=1, killing the per-bounce early-exit
|
|
432
|
+
// and full-sizing the whole loopBound for a few frames (the dramatic FPS drop right when movement
|
|
433
|
+
// ends, worst at high maxBounces). Prime the counter so the first settled frame re-measures promptly.
|
|
434
|
+
if ( this.cameraChanged || this.cameraOptimizer?.isInInteractionMode() ) {
|
|
431
435
|
|
|
432
436
|
this._readbackFrameCounter = this._readbackEveryNFrames;
|
|
433
437
|
return;
|
|
@@ -75,6 +75,10 @@ export class TransformManager {
|
|
|
75
75
|
this._posBuffer = new Float32Array( offset * 9 );
|
|
76
76
|
this._normalBuffer = new Float32Array( offset * 9 );
|
|
77
77
|
|
|
78
|
+
// Mesh indices/buffers were just reallocated (e.g. after a scene rebuild) —
|
|
79
|
+
// force the next drag to recompute the transform baseline.
|
|
80
|
+
this._baselineComputed = false;
|
|
81
|
+
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
/**
|