rayzee 8.8.2 → 9.1.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 +210 -20
- package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
- package/dist/{NeuralSuperRes-Dc6jRYke.js → NeuralSuperRes-CbNvQjCw.js} +5 -2
- package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
- package/dist/rayzee.es.js +10795 -5324
- package/dist/rayzee.umd.js +279 -268
- package/package.json +1 -1
- package/src/AssetConfig.js +4 -0
- package/src/EngineDefaults.js +60 -76
- package/src/EngineEvents.js +7 -0
- package/src/EngineIssues.js +6 -0
- package/src/Headless.js +3 -1
- package/src/Passes/AIUpscaler.js +2 -1
- package/src/Passes/OIDNDenoiser.js +3 -6
- package/src/Passes/OIDNTemporalHistory.js +25 -7
- package/src/PathTracerApp.js +313 -25
- package/src/Processor/ArchiveCache.js +120 -0
- package/src/Processor/ArchiveReader.js +179 -32
- package/src/Processor/AssetLoader.js +523 -76
- package/src/Processor/AtmosphereModel.js +465 -0
- package/src/Processor/BVHBuilder.js +4 -1
- package/src/Processor/BVHLeafFold.js +100 -0
- package/src/Processor/BVHRefitter.js +98 -214
- package/src/Processor/ChunkedRecords.js +189 -4
- package/src/Processor/EmissiveTriangleBuilder.js +8 -0
- package/src/Processor/GeometryExtractor.js +238 -40
- package/src/Processor/InstanceTable.js +55 -37
- package/src/Processor/PBRT/PBRTCurves.js +104 -27
- package/src/Processor/PBRT/PBRTParser.js +132 -22
- package/src/Processor/PBRT/PBRTSceneBuilder.js +210 -55
- package/src/Processor/PBRT/buffers.js +27 -0
- package/src/Processor/PBRT/index.js +55 -15
- package/src/Processor/PackedRayBuffer.js +10 -13
- package/src/Processor/ParallelBVHBuilder.js +36 -15
- package/src/Processor/PhysicalSky.js +308 -0
- package/src/Processor/QueueManager.js +1 -0
- package/src/Processor/SceneProcessor.js +1132 -181
- package/src/Processor/SplitBLAS.js +249 -0
- package/src/Processor/SunPosition.js +41 -0
- package/src/Processor/TextureCreator.js +21 -0
- package/src/Processor/Workers/AIUpscalerWorker.js +79 -52
- package/src/Processor/Workers/BVHWorker.js +17 -13
- package/src/Processor/ZipReader.js +207 -0
- package/src/RenderSettings.js +49 -8
- package/src/SceneState/SceneState.js +367 -0
- package/src/SceneState/portable.js +52 -0
- package/src/Stages/MotionVector.js +8 -42
- package/src/Stages/NRD.js +30 -21
- package/src/Stages/NormalDepth.js +12 -23
- package/src/Stages/PathTracer.js +114 -6
- package/src/Stages/PathTracerStage.js +251 -104
- package/src/Storage/BLASCache.js +190 -0
- package/src/Storage/CDFCache.js +77 -0
- package/src/Storage/DownloadCache.js +266 -0
- package/src/Storage/GeometrySpill.js +217 -0
- package/src/Storage/SceneGraphCodec.js +503 -0
- package/src/Storage/SpillStore.js +112 -0
- package/src/Storage/StorageManager.js +934 -0
- package/src/Storage/StorageOps.js +137 -0
- package/src/Storage/StorageWorker.js +37 -0
- package/src/Storage/events.js +29 -0
- package/src/Storage/identity.js +77 -0
- package/src/Storage/index.js +9 -0
- package/src/Storage/inlineTransport.js +24 -0
- package/src/Storage/locks.js +99 -0
- package/src/Storage/openStorage.js +87 -0
- package/src/Storage/sceneCachePolicy.js +12 -0
- package/src/Storage/shared.js +9 -0
- package/src/Storage/transport.js +62 -0
- package/src/TSL/Atmosphere.js +684 -0
- package/src/TSL/BVHTraversal.js +288 -239
- package/src/TSL/CameraRay.js +99 -14
- package/src/TSL/Common.js +11 -2
- package/src/TSL/DebugKernel.js +2 -2
- package/src/TSL/Displacement.js +6 -6
- package/src/TSL/EmissiveSampling.js +5 -5
- package/src/TSL/EnvironmentCDF.js +176 -0
- package/src/TSL/GenerateKernel.js +5 -5
- package/src/TSL/HitFacet.js +4 -4
- package/src/TSL/LightsDirect.js +8 -8
- package/src/TSL/LightsSampling.js +55 -37
- package/src/TSL/Random.js +1 -0
- package/src/TSL/ShadeKernel.js +75 -19
- package/src/TSL/Sun.js +40 -0
- package/src/TSL/TextureSampling.js +6 -7
- package/src/index.js +22 -0
- package/src/managers/CameraManager.js +412 -56
- package/src/managers/DenoisingManager.js +4 -3
- package/src/managers/EnvironmentManager.js +263 -33
- package/src/managers/GoboManager.js +19 -8
- package/src/managers/IESManager.js +3 -3
- package/src/managers/InteractionManager.js +30 -2
- package/src/managers/LightManager.js +98 -5
- package/src/managers/MaterialDataManager.js +22 -2
- package/src/managers/TransformManager.js +26 -0
- package/src/managers/UniformManager.js +11 -7
- package/src/managers/VideoRenderManager.js +38 -17
- package/src/managers/ViewCamera.js +77 -0
- package/src/managers/WalkControls.js +262 -0
- package/src/managers/helpers/OutlineHelper.js +22 -2
- package/src/managers/timeline/CameraTrack.js +235 -0
- package/src/managers/timeline/TimelineManager.js +146 -0
- package/src/version.js +3 -0
- package/src/Processor/ProceduralSky.js +0 -291
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box3, BufferGeometry, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping,
|
|
2
2
|
TextureLoader, Texture, SRGBColorSpace, RepeatWrapping, Mesh, MeshStandardMaterial, MeshPhysicalMaterial,
|
|
3
|
-
CircleGeometry, Points, PointsMaterial, LoadingManager, EventDispatcher
|
|
3
|
+
CircleGeometry, Points, PointsMaterial, LoadingManager, EventDispatcher, LoaderUtils
|
|
4
4
|
} from 'three';
|
|
5
5
|
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
|
|
6
6
|
import { HDRLoader } from 'three/addons/loaders/HDRLoader.js';
|
|
@@ -10,13 +10,22 @@ import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
|
10
10
|
import { createMeshesFromMultiMaterialMesh } from 'three/addons/utils/SceneUtils.js';
|
|
11
11
|
import { clone as cloneWithSkeletons } from 'three/addons/utils/SkeletonUtils.js';
|
|
12
12
|
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
13
|
-
import {
|
|
13
|
+
import { zipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
|
|
14
14
|
import {
|
|
15
|
-
detectArchiveKind, readTarGz,
|
|
15
|
+
detectArchiveKind, readTarGz, elementFilter, listArchiveElements, openTar, indexTarHeaders } from './ArchiveReader.js';
|
|
16
|
+
import { openZip, readZipDirectory } from './ZipReader.js';
|
|
17
|
+
import { unpackTarGz, loadTarIndex, saveTarIndex } from './ArchiveCache.js';
|
|
18
|
+
import { DownloadCache, nameFromUrl, cachedObjectURL } from '../Storage/DownloadCache.js';
|
|
19
|
+
import { setEnvironmentSource } from '../Storage/CDFCache.js';
|
|
20
|
+
import { fileIdentity, identityKey, sampleHash } from '../Storage/identity.js';
|
|
21
|
+
import { ENGINE_AREAS } from '../Storage/StorageManager.js';
|
|
22
|
+
import { encodeSceneGraph, writeSceneGraph, decodeSceneGraph, SceneGraphUnsupported, SCENE_GRAPH_FORMAT, ARCHIVE_PATH, ARCHIVE_LOADER } from '../Storage/SceneGraphCodec.js';
|
|
23
|
+
import { worthStoring } from '../Storage/sceneCachePolicy.js';
|
|
24
|
+
import { VERSION } from '../version.js';
|
|
16
25
|
import { disposeEngineOwnedResources, disposeObjectFromMemory, updateLoading } from './utils';
|
|
17
26
|
import { BuildTimer } from './BuildTimer.js';
|
|
18
27
|
import { getAssetConfig } from '../AssetConfig.js';
|
|
19
|
-
import { loadPBRTScene, pickEntryPath } from './PBRT/index.js';
|
|
28
|
+
import { loadPBRTScene, pickEntryPath, VirtualFS, PBRT_BUILD_REVISION } from './PBRT/index.js';
|
|
20
29
|
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
21
30
|
import { ISSUE_CODES, ISSUE_SEVERITY } from '../EngineIssues.js';
|
|
22
31
|
import { getRenderProfile } from '../EngineDefaults.js';
|
|
@@ -97,6 +106,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
97
106
|
this.targetModel = null;
|
|
98
107
|
this.floorPlane = null;
|
|
99
108
|
this.sceneScale = 1.0;
|
|
109
|
+
this.sceneSize = new Vector3( 1, 1, 1 );
|
|
100
110
|
this.loaderCache = {};
|
|
101
111
|
this.uploadedFileInfo = null;
|
|
102
112
|
this.animations = [];
|
|
@@ -111,6 +121,13 @@ export class AssetLoader extends EventDispatcher {
|
|
|
111
121
|
// signal into its fetch). One load runs at a time (guarded upstream).
|
|
112
122
|
this._loadingManager = new LoadingManager();
|
|
113
123
|
this._loadCancelled = false;
|
|
124
|
+
this._urlAbort = null;
|
|
125
|
+
|
|
126
|
+
/** @type {?import('../Storage/StorageManager.js').StorageManager} set by the app once storage opens */
|
|
127
|
+
this.storage = null;
|
|
128
|
+
this._downloads = null;
|
|
129
|
+
this._sourceKey = null;
|
|
130
|
+
this._appended = false;
|
|
114
131
|
|
|
115
132
|
this._issues = issues;
|
|
116
133
|
this._profile = profile ?? getRenderProfile();
|
|
@@ -140,6 +157,66 @@ export class AssetLoader extends EventDispatcher {
|
|
|
140
157
|
|
|
141
158
|
this._loadCancelled = true;
|
|
142
159
|
this._loadingManager.abort();
|
|
160
|
+
this._urlAbort?.abort();
|
|
161
|
+
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* `url` swapped for an object URL of its stored copy, downloading it first if needed. A .gltf
|
|
166
|
+
* with its resources beside it, and non-network URLs, pass through untouched.
|
|
167
|
+
* @private
|
|
168
|
+
*/
|
|
169
|
+
async _viaCache( url, status, cancelable, { cacheKey = url, cachePolicy } = {} ) {
|
|
170
|
+
|
|
171
|
+
if ( ! this.storage || ! AssetLoader._isNetworkUrl( url ) || /\.gltf$/i.test( url.split( /[?#]/ )[ 0 ] ) ) {
|
|
172
|
+
|
|
173
|
+
return { url, release: () => {}, cached: false };
|
|
174
|
+
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
this._urlAbort = controller;
|
|
179
|
+
try {
|
|
180
|
+
|
|
181
|
+
return await cachedObjectURL( url, {
|
|
182
|
+
downloads: this.downloads, key: cacheKey, policy: cachePolicy, name: nameFromUrl( url ),
|
|
183
|
+
signal: controller.signal, onProgress: this._downloadProgress( status, cancelable ),
|
|
184
|
+
} );
|
|
185
|
+
|
|
186
|
+
} catch ( error ) {
|
|
187
|
+
|
|
188
|
+
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
189
|
+
throw error;
|
|
190
|
+
|
|
191
|
+
} finally {
|
|
192
|
+
|
|
193
|
+
this._urlAbort = null;
|
|
194
|
+
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* What the scene was loaded from, stable across reloads, or null — for the stored-BVH cache.
|
|
201
|
+
* Null once anything was appended: the scene is no longer one source's.
|
|
202
|
+
*/
|
|
203
|
+
get sceneSourceKey() {
|
|
204
|
+
|
|
205
|
+
return this._appended ? null : this._sourceKey;
|
|
206
|
+
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
_keyed( kind, id ) {
|
|
210
|
+
|
|
211
|
+
return id ? `${kind}:${this._profile?.name ?? ''}:${id}` : null;
|
|
212
|
+
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/** Downloads go through storage when there is some, into memory otherwise. */
|
|
216
|
+
get downloads() {
|
|
217
|
+
|
|
218
|
+
if ( this._downloads?._storage !== this.storage ) this._downloads = new DownloadCache( this.storage );
|
|
219
|
+
return this._downloads;
|
|
143
220
|
|
|
144
221
|
}
|
|
145
222
|
|
|
@@ -240,6 +317,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
240
317
|
releaseTargetModel() {
|
|
241
318
|
|
|
242
319
|
this.sceneMetadata = null;
|
|
320
|
+
this._sourceKey = null;
|
|
321
|
+
this._appended = false;
|
|
322
|
+
this._pendingGraph = null;
|
|
243
323
|
|
|
244
324
|
if ( ! this.targetModel ) return;
|
|
245
325
|
|
|
@@ -324,8 +404,75 @@ export class AssetLoader extends EventDispatcher {
|
|
|
324
404
|
|
|
325
405
|
}
|
|
326
406
|
|
|
407
|
+
/**
|
|
408
|
+
* Downloads a URL (through the download cache) and loads it as `loadAssetFromFile` would —
|
|
409
|
+
* archives included. An ARCHIVE_NEEDS_ELEMENT error carries the downloaded `file` to retry with.
|
|
410
|
+
* @param {string} url
|
|
411
|
+
* @param {object} [options] - as loadAssetFromFile, plus `filename` to name the download and
|
|
412
|
+
* `cacheKey` for URLs that expire (signed links)
|
|
413
|
+
*/
|
|
414
|
+
async loadAssetFromUrl( url, options = {} ) {
|
|
415
|
+
|
|
416
|
+
const { filename = nameFromUrl( url ), cacheKey = url, cachePolicy, ...loadOptions } = options;
|
|
417
|
+
if ( ! this.getFileFormat( filename ) ) throw new Error( `Unsupported file format: ${filename}` );
|
|
418
|
+
|
|
419
|
+
this._loadCancelled = false;
|
|
420
|
+
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
421
|
+
const status = `Downloading ${filename}...`;
|
|
422
|
+
const controller = new AbortController();
|
|
423
|
+
this._urlAbort = controller;
|
|
424
|
+
|
|
425
|
+
updateLoading( { isLoading: true, status, progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
426
|
+
|
|
427
|
+
let download;
|
|
428
|
+
try {
|
|
429
|
+
|
|
430
|
+
download = await this.downloads.fetch( url, {
|
|
431
|
+
key: cacheKey, name: filename, policy: cachePolicy, signal: controller.signal,
|
|
432
|
+
onProgress: this._downloadProgress( status, cancelable ),
|
|
433
|
+
} );
|
|
434
|
+
|
|
435
|
+
} catch ( error ) {
|
|
436
|
+
|
|
437
|
+
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
438
|
+
throw error;
|
|
439
|
+
|
|
440
|
+
} finally {
|
|
441
|
+
|
|
442
|
+
this._urlAbort = null;
|
|
443
|
+
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
this._downloadComplete();
|
|
447
|
+
|
|
448
|
+
try {
|
|
449
|
+
|
|
450
|
+
return await this.loadAssetFromFile( download.file, loadOptions );
|
|
451
|
+
|
|
452
|
+
} catch ( error ) {
|
|
453
|
+
|
|
454
|
+
if ( error?.code === 'ARCHIVE_NEEDS_ELEMENT' ) error.file = download.file;
|
|
455
|
+
throw error;
|
|
456
|
+
|
|
457
|
+
} finally {
|
|
458
|
+
|
|
459
|
+
download.release();
|
|
460
|
+
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
}
|
|
464
|
+
|
|
327
465
|
async loadModelFromFile( file, filename ) {
|
|
328
466
|
|
|
467
|
+
const key = this.storage ? this._keyed( 'file', identityKey( await fileIdentity( file ) ) ) : null;
|
|
468
|
+
const result = await this._loadModelFileByExtension( file, filename );
|
|
469
|
+
this._sourceKey = key;
|
|
470
|
+
return result;
|
|
471
|
+
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
async _loadModelFileByExtension( file, filename ) {
|
|
475
|
+
|
|
329
476
|
const extension = filename.split( '.' ).pop().toLowerCase();
|
|
330
477
|
const arrayBuffer = await this.readFileAsArrayBuffer( file );
|
|
331
478
|
|
|
@@ -356,6 +503,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
356
503
|
try {
|
|
357
504
|
|
|
358
505
|
const texture = await this.loadEnvironment( url );
|
|
506
|
+
setEnvironmentSource( texture, identityKey( await fileIdentity( file ) ) );
|
|
359
507
|
this.dispatchEvent( { type: 'load', texture, filename } );
|
|
360
508
|
return texture;
|
|
361
509
|
|
|
@@ -394,6 +542,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
394
542
|
}
|
|
395
543
|
|
|
396
544
|
texture.generateMipmaps = true;
|
|
545
|
+
if ( AssetLoader._isNetworkUrl( envUrl ) ) setEnvironmentSource( texture, envUrl );
|
|
397
546
|
|
|
398
547
|
this.applyEnvironmentToScene( texture );
|
|
399
548
|
this.dispatchEvent( { type: 'load', texture, url: envUrl, filename: envUrl.split( /[?#]/ )[ 0 ].split( '/' ).pop() } );
|
|
@@ -464,23 +613,33 @@ export class AssetLoader extends EventDispatcher {
|
|
|
464
613
|
async loadEnvironmentByExtension( url, extension ) {
|
|
465
614
|
|
|
466
615
|
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
467
|
-
const
|
|
616
|
+
const status = "Downloading Environment...";
|
|
617
|
+
const source = await this._viaCache( url, status, cancelable );
|
|
618
|
+
const onProgress = source.cached ? undefined : this._downloadProgress( status, cancelable );
|
|
468
619
|
|
|
469
620
|
let texture;
|
|
470
|
-
|
|
621
|
+
try {
|
|
471
622
|
|
|
472
|
-
|
|
473
|
-
? ( this.loaderCache.hdr || ( this.loaderCache.hdr = new HDRLoader( this._loadingManager ).setDataType( FloatType ) ) )
|
|
474
|
-
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader( this._loadingManager ).setDataType( FloatType ) ) );
|
|
475
|
-
texture = await loader.loadAsync( url, onProgress );
|
|
623
|
+
if ( extension === 'hdr' || extension === 'exr' ) {
|
|
476
624
|
|
|
477
|
-
|
|
625
|
+
const loader = extension === 'hdr'
|
|
626
|
+
? ( this.loaderCache.hdr || ( this.loaderCache.hdr = new HDRLoader( this._loadingManager ).setDataType( FloatType ) ) )
|
|
627
|
+
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader( this._loadingManager ).setDataType( FloatType ) ) );
|
|
628
|
+
texture = await loader.loadAsync( source.url, onProgress );
|
|
478
629
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
630
|
+
} else {
|
|
631
|
+
|
|
632
|
+
if ( ! this.loaderCache.texture ) this.loaderCache.texture = new TextureLoader( this._loadingManager );
|
|
633
|
+
texture = await this.loaderCache.texture.loadAsync( source.url, onProgress );
|
|
634
|
+
// LDR env maps (jpg/png/webp) are authored in sRGB; tag them so the backend
|
|
635
|
+
// decodes to linear. HDR/EXR are already linear and keep the loader's setting.
|
|
636
|
+
texture.colorSpace = SRGBColorSpace;
|
|
637
|
+
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
} finally {
|
|
641
|
+
|
|
642
|
+
source.release();
|
|
484
643
|
|
|
485
644
|
}
|
|
486
645
|
|
|
@@ -523,36 +682,70 @@ export class AssetLoader extends EventDispatcher {
|
|
|
523
682
|
async inspectArchive( file ) {
|
|
524
683
|
|
|
525
684
|
const kind = detectArchiveKind( await this._readHead( file ) );
|
|
685
|
+
const scanning = p => updateLoading( {
|
|
686
|
+
isLoading: true, status: `Scanning archive… ${( p.bytes / 1e9 ).toFixed( 1 )} GB`, progress: 4
|
|
687
|
+
} );
|
|
526
688
|
|
|
527
|
-
|
|
689
|
+
let listing;
|
|
690
|
+
if ( kind === 'gzip' ) {
|
|
528
691
|
|
|
529
|
-
const
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
692
|
+
const unpacked = await this._unpackGzip( file, file.name );
|
|
693
|
+
if ( unpacked ) {
|
|
694
|
+
|
|
695
|
+
listing = unpacked.index.listing;
|
|
696
|
+
unpacked.release();
|
|
697
|
+
|
|
698
|
+
} else {
|
|
699
|
+
|
|
700
|
+
( { listing } = await readTarGz( file, { filter: () => false, onProgress: scanning } ) );
|
|
701
|
+
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
} else if ( kind === 'tar' ) {
|
|
705
|
+
|
|
706
|
+
listing = ( await loadTarIndex( file, this.storage ) )?.listing ?? await indexTarHeaders( file, { onProgress: scanning } );
|
|
707
|
+
|
|
708
|
+
} else {
|
|
709
|
+
|
|
710
|
+
listing = ( await readZipDirectory( file ) ).map( e => ( { path: e.name, size: e.size } ) );
|
|
542
711
|
|
|
543
712
|
}
|
|
544
713
|
|
|
545
|
-
const zip = unzipSync( new Uint8Array( await this.readFileAsArrayBuffer( file ) ) );
|
|
546
|
-
const listing = Object.keys( zip ).map( path => ( { path, size: zip[ path ].length } ) );
|
|
547
714
|
const { root, elements } = listArchiveElements( listing );
|
|
548
715
|
return {
|
|
549
|
-
kind: 'zip', root, elements,
|
|
716
|
+
kind: kind ?? 'zip', root, elements,
|
|
550
717
|
entryCount: listing.length,
|
|
551
718
|
totalBytes: listing.reduce( ( n, e ) => n + e.size, 0 )
|
|
552
719
|
};
|
|
553
720
|
|
|
554
721
|
}
|
|
555
722
|
|
|
723
|
+
/**
|
|
724
|
+
* A .tar.gz unpacked to storage, or null to read it the old way (no storage, or no room).
|
|
725
|
+
* @private
|
|
726
|
+
*/
|
|
727
|
+
async _unpackGzip( file, filename ) {
|
|
728
|
+
|
|
729
|
+
try {
|
|
730
|
+
|
|
731
|
+
return await unpackTarGz( file, {
|
|
732
|
+
storage: this.storage,
|
|
733
|
+
label: filename,
|
|
734
|
+
onProgress: bytes => updateLoading( {
|
|
735
|
+
isLoading: true, status: `Unpacking archive… ${( bytes / 1e9 ).toFixed( 1 )} GB`, progress: 4
|
|
736
|
+
} ),
|
|
737
|
+
} );
|
|
738
|
+
|
|
739
|
+
} catch ( error ) {
|
|
740
|
+
|
|
741
|
+
// A corrupt stream fails the same way in memory, where the error reads better.
|
|
742
|
+
console.warn( `Unpacking "${filename}" to storage failed, reading it in memory: ${error.message}` );
|
|
743
|
+
return null;
|
|
744
|
+
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
}
|
|
748
|
+
|
|
556
749
|
/**
|
|
557
750
|
* @param {File|Blob} file
|
|
558
751
|
* @param {string} filename
|
|
@@ -571,39 +764,52 @@ export class AssetLoader extends EventDispatcher {
|
|
|
571
764
|
* @param {number} [options.mergeShapesAbove] - merge small non-instanced shapes past this count.
|
|
572
765
|
* @param {number} [options.curveSteps] - samples per spline span when tessellating curves.
|
|
573
766
|
* @param {number} [options.curveSides] - 1 ribbon, 2 crossed ribbons, >=3 closed tube.
|
|
767
|
+
* @param {number} [options.curveTolerance] - how far a curve strip may stray from the curve, as a
|
|
768
|
+
* fraction of its half-width (default 0.05); 0 samples every span `curveSteps` times.
|
|
769
|
+
* @param {boolean} [options.instanceIncludes] - place a file included again under the same
|
|
770
|
+
* material as another instance of its first reading (default true).
|
|
574
771
|
*/
|
|
575
772
|
async loadArchiveFromFile( file, filename, { pbrtEntry = null, element = null, byteBudget, promptBytes, ...pbrt } = {} ) {
|
|
576
773
|
|
|
577
774
|
try {
|
|
578
775
|
|
|
579
776
|
const kind = detectArchiveKind( await this._readHead( file ) );
|
|
777
|
+
const archiveId = this.storage ? identityKey( await fileIdentity( file ) ) : null;
|
|
778
|
+
const lazy = { element, promptBytes, pbrtEntry, pbrt, archiveId };
|
|
580
779
|
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
if ( kind === 'tar' )
|
|
780
|
+
// A seekable archive is indexed rather than read: entries are pulled out as the scene
|
|
781
|
+
// asks for them and never all held at once. That is the difference between a 7 GB archive
|
|
782
|
+
// being refused and loading. A .tar.gz becomes seekable by unpacking it to storage.
|
|
783
|
+
if ( kind === 'tar' ) return await this._loadSeekable( file, filename, lazy );
|
|
585
784
|
|
|
586
|
-
|
|
587
|
-
if ( source.listing.some( e => e.path.toLowerCase().endsWith( '.pbrt' ) ) ) {
|
|
785
|
+
if ( kind === 'gzip' ) {
|
|
588
786
|
|
|
589
|
-
|
|
787
|
+
const unpacked = await this._unpackGzip( file, filename );
|
|
788
|
+
if ( unpacked ) {
|
|
590
789
|
|
|
591
|
-
|
|
790
|
+
try {
|
|
592
791
|
|
|
593
|
-
|
|
594
|
-
for ( const e of source.listing ) source.entries[ e.path ] ??= await source.read( e.path );
|
|
595
|
-
return await this._loadNonPBRTArchive( source.entries, filename );
|
|
792
|
+
return await this._loadSeekable( unpacked.file, filename, { ...lazy, index: unpacked.index } );
|
|
596
793
|
|
|
597
|
-
|
|
794
|
+
} finally {
|
|
795
|
+
|
|
796
|
+
unpacked.release();
|
|
598
797
|
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
}
|
|
602
801
|
|
|
603
|
-
|
|
604
|
-
if ( pickEntryPath( entries ) ) return await this.loadPBRTFromZip( entries, filename, pbrtEntry, pbrt );
|
|
802
|
+
const entries = await this._readStreamedArchive( file, filename, kind, element, byteBudget );
|
|
605
803
|
|
|
606
|
-
|
|
804
|
+
// A pbrt scene archive takes priority — it owns its own geometry/texture refs.
|
|
805
|
+
if ( pickEntryPath( entries ) ) return await this.loadPBRTFromZip( entries, filename, pbrtEntry, pbrt );
|
|
806
|
+
const result = await this._loadNonPBRTArchive( entries, filename );
|
|
807
|
+
this._sourceKey = this._keyed( 'archive', archiveId );
|
|
808
|
+
return result;
|
|
809
|
+
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
return await this._loadSeekable( file, filename, { ...lazy, zip: true } );
|
|
607
813
|
|
|
608
814
|
} catch ( error ) {
|
|
609
815
|
|
|
@@ -623,7 +829,170 @@ export class AssetLoader extends EventDispatcher {
|
|
|
623
829
|
}
|
|
624
830
|
|
|
625
831
|
/**
|
|
626
|
-
*
|
|
832
|
+
* Loads a seekable archive — a .tar (or a .tar.gz unpacked to one) or a .zip — reading each
|
|
833
|
+
* entry only when the scene asks for it.
|
|
834
|
+
* @private
|
|
835
|
+
*/
|
|
836
|
+
async _loadSeekable( file, filename, { element, promptBytes, pbrtEntry, pbrt, index = null, zip = false, archiveId = null } ) {
|
|
837
|
+
|
|
838
|
+
const source = await this._openSeekableArchive( file, filename, element, promptBytes, { index, zip } );
|
|
839
|
+
source.archiveId = archiveId;
|
|
840
|
+
source.elements = ( Array.isArray( element ) ? element : [ element ] ).filter( Boolean );
|
|
841
|
+
if ( source.listing.some( e => e.path.toLowerCase().endsWith( '.pbrt' ) ) ) {
|
|
842
|
+
|
|
843
|
+
return await this.loadPBRTFromZip( {}, filename, pbrtEntry, pbrt, source );
|
|
844
|
+
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
// Not a pbrt scene: fall back to materialising it, which those paths still expect.
|
|
848
|
+
for ( const e of source.listing ) if ( e.offset !== undefined ) source.entries[ e.path ] ??= await source.read( e.path );
|
|
849
|
+
const result = await this._loadNonPBRTArchive( source.entries, filename );
|
|
850
|
+
this._sourceKey = this._keyed( 'archive', archiveId );
|
|
851
|
+
return result;
|
|
852
|
+
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
/**
|
|
856
|
+
* The parsed scene stored for this archive and these options, decoded — its textures read back
|
|
857
|
+
* out of the archive — or null.
|
|
858
|
+
* @private
|
|
859
|
+
*/
|
|
860
|
+
async _loadStoredGraph( key, source, { imageFromBytes, envFromBytes } ) {
|
|
861
|
+
|
|
862
|
+
const area = this.storage?.area( ENGINE_AREAS.SCENES );
|
|
863
|
+
const entry = area ? await area.open( key ) : null;
|
|
864
|
+
if ( ! entry ) return null;
|
|
865
|
+
|
|
866
|
+
const start = performance.now();
|
|
867
|
+
let failure = null;
|
|
868
|
+
try {
|
|
869
|
+
|
|
870
|
+
updateLoading( { isLoading: true, status: 'Opening the stored scene...', progress: 6 } );
|
|
871
|
+
const manifest = await entry.json( 'graph.json' );
|
|
872
|
+
const data = await entry.file( 'data.bin' );
|
|
873
|
+
const vfs = new VirtualFS( {}, source );
|
|
874
|
+
const decoded = await decodeSceneGraph( manifest, data, {
|
|
875
|
+
loadTexture: async ( path, loader ) => {
|
|
876
|
+
|
|
877
|
+
const bytes = await vfs.readPath( path );
|
|
878
|
+
if ( ! bytes ) return null;
|
|
879
|
+
return loader === 'environment' ? envFromBytes( bytes, path ) : imageFromBytes( bytes, path );
|
|
880
|
+
|
|
881
|
+
},
|
|
882
|
+
} );
|
|
883
|
+
|
|
884
|
+
return {
|
|
885
|
+
...decoded.stats,
|
|
886
|
+
group: decoded.root,
|
|
887
|
+
environment: decoded.environment ? { texture: decoded.environment } : null,
|
|
888
|
+
animations: decoded.animations,
|
|
889
|
+
report: null,
|
|
890
|
+
parseMs: 0,
|
|
891
|
+
buildMs: performance.now() - start,
|
|
892
|
+
fromStorage: true,
|
|
893
|
+
};
|
|
894
|
+
|
|
895
|
+
} catch ( error ) {
|
|
896
|
+
|
|
897
|
+
failure = error;
|
|
898
|
+
return null;
|
|
899
|
+
|
|
900
|
+
} finally {
|
|
901
|
+
|
|
902
|
+
entry.release();
|
|
903
|
+
if ( failure ) {
|
|
904
|
+
|
|
905
|
+
console.warn( `The stored scene could not be used, parsing the archive instead: ${failure.message}` );
|
|
906
|
+
this._issues?.warn( ISSUE_CODES.STORAGE_ENTRY_CORRUPT, `stored scene unusable: ${failure.message}`, { key } );
|
|
907
|
+
area.remove( key ).catch( () => {} );
|
|
908
|
+
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Stores a freshly parsed scene for the next open. Encoded here, before onModelLoad or the
|
|
917
|
+
* build touch it; written in the background.
|
|
918
|
+
* @private
|
|
919
|
+
*/
|
|
920
|
+
_storeGraph( key, built, label ) {
|
|
921
|
+
|
|
922
|
+
const area = this.storage?.area( ENGINE_AREAS.SCENES );
|
|
923
|
+
if ( ! area ) return;
|
|
924
|
+
|
|
925
|
+
let encoded;
|
|
926
|
+
try {
|
|
927
|
+
|
|
928
|
+
encoded = encodeSceneGraph( built.group, {
|
|
929
|
+
environment: built.environment?.texture ?? null,
|
|
930
|
+
animations: built.animations ?? [],
|
|
931
|
+
stats: {
|
|
932
|
+
meshCount: built.meshCount, entryPath: built.entryPath, candidates: built.candidates, frames: built.frames,
|
|
933
|
+
warnings: built.warnings, triangleCount: built.triangleCount, placementCount: built.placementCount,
|
|
934
|
+
mergedShapes: built.mergedShapes, skippedForBudget: built.skippedForBudget, droppedNoTemplate: built.droppedNoTemplate,
|
|
935
|
+
},
|
|
936
|
+
} );
|
|
937
|
+
|
|
938
|
+
} catch ( error ) {
|
|
939
|
+
|
|
940
|
+
if ( error instanceof SceneGraphUnsupported ) console.info( `Scene not stored for next time: ${error.message}` );
|
|
941
|
+
else console.warn( 'Scene not stored for next time:', error );
|
|
942
|
+
return;
|
|
943
|
+
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const pending = { key, encoded, label, parseMs: ( built.parseMs ?? 0 ) + ( built.buildMs ?? 0 ), triangles: built.triangleCount ?? null };
|
|
947
|
+
|
|
948
|
+
// A parse slow enough on its own is written now, during the build. Held until the build
|
|
949
|
+
// ends, it kept every array the build replaces alive with it — the float normals and the
|
|
950
|
+
// instance matrices, ~1 GB on the whole Moana subset, at the build's peak.
|
|
951
|
+
if ( worthStoring( pending.parseMs, encoded.byteLength ) ) this._writeGraph( area, pending );
|
|
952
|
+
else this._pendingGraph = pending;
|
|
953
|
+
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Writes the scene encoded at parse time once the build is done, when the whole cold load —
|
|
958
|
+
* parse plus build — was slow enough to be worth it (decision D6).
|
|
959
|
+
* @param {number} buildMs - the SceneProcessor build that followed the parse
|
|
960
|
+
*/
|
|
961
|
+
flushPendingGraph( buildMs ) {
|
|
962
|
+
|
|
963
|
+
const pending = this._pendingGraph;
|
|
964
|
+
this._pendingGraph = null;
|
|
965
|
+
const area = this.storage?.area( ENGINE_AREAS.SCENES );
|
|
966
|
+
if ( ! pending || ! area || ! worthStoring( pending.parseMs + buildMs, pending.encoded.byteLength ) ) return;
|
|
967
|
+
this._writeGraph( area, pending );
|
|
968
|
+
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/** Writes an encoded graph in the background, letting each array go once it is on disk. @private */
|
|
972
|
+
_writeGraph( area, pending ) {
|
|
973
|
+
|
|
974
|
+
( async () => {
|
|
975
|
+
|
|
976
|
+
const writer = await area.create( pending.key, { label: `${pending.label} (scene)`, expectedBytes: pending.encoded.byteLength } );
|
|
977
|
+
if ( ! writer ) return;
|
|
978
|
+
try {
|
|
979
|
+
|
|
980
|
+
await writeSceneGraph( writer, pending.encoded, { release: true } );
|
|
981
|
+
await writer.commit( { triangles: pending.triangles } );
|
|
982
|
+
|
|
983
|
+
} catch ( error ) {
|
|
984
|
+
|
|
985
|
+
await writer.abort();
|
|
986
|
+
console.warn( 'Storing the scene failed:', error );
|
|
987
|
+
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
} )().catch( error => console.warn( 'Storing the scene failed:', error ) );
|
|
991
|
+
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
/**
|
|
995
|
+
* Index a seekable archive without retaining it. Entries are read back from the File on
|
|
627
996
|
* demand, so residency is the open include chain rather than the whole archive.
|
|
628
997
|
*
|
|
629
998
|
* Indexing is cheap and the whole archive is never held, but *parsing* all of it is not:
|
|
@@ -632,14 +1001,30 @@ export class AssetLoader extends EventDispatcher {
|
|
|
632
1001
|
* already built at that point, so the question costs nothing.
|
|
633
1002
|
* @private
|
|
634
1003
|
*/
|
|
635
|
-
async _openSeekableArchive( file, filename, element, promptBytes ) {
|
|
1004
|
+
async _openSeekableArchive( file, filename, element, promptBytes, { index = null, zip = false } = {} ) {
|
|
636
1005
|
|
|
637
1006
|
const chosen = Array.isArray( element ) ? element.filter( Boolean ) : ( element ? [ element ] : [] );
|
|
1007
|
+
const filter = chosen.length ? elementFilter( chosen ) : null;
|
|
638
1008
|
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
1009
|
+
let source;
|
|
1010
|
+
if ( zip ) {
|
|
1011
|
+
|
|
1012
|
+
source = await openZip( file, { filter } );
|
|
1013
|
+
|
|
1014
|
+
} else {
|
|
1015
|
+
|
|
1016
|
+
const saved = index ?? await loadTarIndex( file, this.storage );
|
|
1017
|
+
source = await openTar( file, {
|
|
1018
|
+
index: saved,
|
|
1019
|
+
headersOnly: true,
|
|
1020
|
+
filter,
|
|
1021
|
+
onProgress: p => updateLoading( {
|
|
1022
|
+
isLoading: true, status: `Indexing archive… ${( p.bytes / 1e9 ).toFixed( 1 )} GB`, progress: 4
|
|
1023
|
+
} ),
|
|
1024
|
+
} );
|
|
1025
|
+
if ( ! saved ) saveTarIndex( file, this.storage, source.index ).catch( () => {} );
|
|
1026
|
+
|
|
1027
|
+
}
|
|
643
1028
|
|
|
644
1029
|
const totalBytes = source.listing.reduce( ( n, e ) => n + e.size, 0 );
|
|
645
1030
|
|
|
@@ -694,8 +1079,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
694
1079
|
async _readStreamedArchive( file, filename, kind, element, byteBudget ) {
|
|
695
1080
|
|
|
696
1081
|
const chosen = Array.isArray( element ) ? element.filter( Boolean ) : ( element ? [ element ] : [] );
|
|
697
|
-
const
|
|
698
|
-
const { entries, listing, retainedBytes, truncated } = await read( file, {
|
|
1082
|
+
const { entries, listing, retainedBytes, truncated } = await readTarGz( file, {
|
|
699
1083
|
filter: chosen.length ? elementFilter( chosen ) : null,
|
|
700
1084
|
...( byteBudget === undefined ? {} : { byteBudget } ),
|
|
701
1085
|
onProgress: p => updateLoading( {
|
|
@@ -732,6 +1116,19 @@ export class AssetLoader extends EventDispatcher {
|
|
|
732
1116
|
|
|
733
1117
|
}
|
|
734
1118
|
|
|
1119
|
+
if ( truncated ) {
|
|
1120
|
+
|
|
1121
|
+
const keep = elementFilter( chosen );
|
|
1122
|
+
const missing = listing.filter( e => keep( e.path, e.size ) && ! entries[ e.path ] ).length;
|
|
1123
|
+
this._issues?.record(
|
|
1124
|
+
ISSUE_CODES.ASSET_ARCHIVE_TOO_LARGE,
|
|
1125
|
+
`${missing} files of the chosen parts were left out: they did not fit the ${( ( byteBudget ?? 1.5e9 ) / 1e9 ).toFixed( 1 )} GB in-memory read budget`,
|
|
1126
|
+
{ elements: chosen, missing, retainedBytes },
|
|
1127
|
+
ISSUE_SEVERITY.WARNING
|
|
1128
|
+
);
|
|
1129
|
+
|
|
1130
|
+
}
|
|
1131
|
+
|
|
735
1132
|
if ( chosen.length ) {
|
|
736
1133
|
|
|
737
1134
|
console.info(
|
|
@@ -766,17 +1163,29 @@ export class AssetLoader extends EventDispatcher {
|
|
|
766
1163
|
|
|
767
1164
|
const plyParser = ( buf ) => this.loaderCache.ply.parse( buf );
|
|
768
1165
|
|
|
769
|
-
// Texture maps — decode by extension (pbrt uses .png/.jpg but also .exr/.hdr/.tga).
|
|
770
|
-
|
|
1166
|
+
// Texture maps — decode by extension (pbrt uses .png/.jpg but also .exr/.hdr/.tga). Tagged
|
|
1167
|
+
// with their archive path so a stored scene can read them back instead of keeping pixels.
|
|
1168
|
+
const imageFromBytes = async ( bytes, fname ) => {
|
|
1169
|
+
|
|
1170
|
+
const texture = await this._pbrtTextureFromBytes( bytes, fname );
|
|
1171
|
+
if ( texture ) texture.userData[ ARCHIVE_PATH ] = fname;
|
|
1172
|
+
return texture;
|
|
1173
|
+
|
|
1174
|
+
};
|
|
771
1175
|
|
|
772
1176
|
// Infinite-light maps → HDR/EXR/LDR via the shared environment decoder.
|
|
773
1177
|
const envFromBytes = async ( bytes, fname ) => {
|
|
774
1178
|
|
|
775
1179
|
const ext = fname.split( '.' ).pop().toLowerCase();
|
|
776
|
-
const
|
|
1180
|
+
const blob = new Blob( [ bytes ] );
|
|
1181
|
+
const url = URL.createObjectURL( blob );
|
|
777
1182
|
try {
|
|
778
1183
|
|
|
779
|
-
|
|
1184
|
+
const texture = await this.loadEnvironmentByExtension( url, ext );
|
|
1185
|
+
setEnvironmentSource( texture, `bytes:${await sampleHash( blob )}` );
|
|
1186
|
+
texture.userData[ ARCHIVE_PATH ] = fname;
|
|
1187
|
+
texture.userData[ ARCHIVE_LOADER ] = 'environment';
|
|
1188
|
+
return texture;
|
|
780
1189
|
|
|
781
1190
|
} finally {
|
|
782
1191
|
|
|
@@ -787,17 +1196,22 @@ export class AssetLoader extends EventDispatcher {
|
|
|
787
1196
|
};
|
|
788
1197
|
|
|
789
1198
|
const pbrtStart = performance.now();
|
|
1199
|
+
const shape = {
|
|
1200
|
+
animation: options.animation, maxTriangles: options.maxTriangles, maxPlacements: options.maxPlacements,
|
|
1201
|
+
mergeShapesAbove: options.mergeShapesAbove, curveSteps: options.curveSteps, curveSides: options.curveSides,
|
|
1202
|
+
curveTolerance: options.curveTolerance, instanceIncludes: options.instanceIncludes,
|
|
1203
|
+
};
|
|
1204
|
+
const graphKey = source?.archiveId
|
|
1205
|
+
? this._keyed( `pbrt:${SCENE_GRAPH_FORMAT}.${PBRT_BUILD_REVISION}:${VERSION}`, `${source.archiveId}|${entryPath ?? ''}|${[ ...( source.elements ?? [] ) ].sort().join( ',' )}|${JSON.stringify( shape )}` )
|
|
1206
|
+
: null;
|
|
1207
|
+
|
|
1208
|
+
const stored = graphKey ? await this._loadStoredGraph( graphKey, source, { imageFromBytes, envFromBytes } ) : null;
|
|
1209
|
+
const built = stored ?? await loadPBRTScene( { vfs: zip, source, entryPath, plyParser, imageFromBytes, envFromBytes, ...shape } );
|
|
1210
|
+
|
|
790
1211
|
const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
|
|
791
1212
|
frames, parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
|
|
792
|
-
droppedNoTemplate } =
|
|
793
|
-
|
|
794
|
-
animation: options.animation,
|
|
795
|
-
maxTriangles: options.maxTriangles,
|
|
796
|
-
maxPlacements: options.maxPlacements,
|
|
797
|
-
mergeShapesAbove: options.mergeShapesAbove,
|
|
798
|
-
curveSteps: options.curveSteps,
|
|
799
|
-
curveSides: options.curveSides
|
|
800
|
-
} );
|
|
1213
|
+
droppedNoTemplate } = built;
|
|
1214
|
+
if ( stored ) console.info( `PBRT scene "${loadedEntry}" opened from storage in ${( buildMs / 1000 ).toFixed( 1 )} s, skipping the parse` );
|
|
801
1215
|
|
|
802
1216
|
// Phase breakdown for scaling work; the engine's own build timings live in
|
|
803
1217
|
// SceneProcessor.performanceMetrics.
|
|
@@ -851,6 +1265,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
851
1265
|
|
|
852
1266
|
group.name = loadedEntry || filename;
|
|
853
1267
|
this.releaseTargetModel();
|
|
1268
|
+
this._sourceKey = graphKey;
|
|
1269
|
+
// Before onModelLoad and the build touch the new group.
|
|
1270
|
+
if ( graphKey && ! stored ) this._storeGraph( graphKey, built, filename );
|
|
854
1271
|
this.targetModel = group;
|
|
855
1272
|
this.animations = animations ?? [];
|
|
856
1273
|
|
|
@@ -1498,19 +1915,28 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1498
1915
|
|
|
1499
1916
|
}
|
|
1500
1917
|
|
|
1501
|
-
|
|
1918
|
+
/**
|
|
1919
|
+
* @param {string} modelUrl
|
|
1920
|
+
* @param {{cacheKey?: string, cachePolicy?: string}} [options] - key a download cache entry by
|
|
1921
|
+
* something steadier than a signed, expiring URL
|
|
1922
|
+
*/
|
|
1923
|
+
async loadModel( modelUrl, options = {} ) {
|
|
1502
1924
|
|
|
1503
1925
|
this._loadCancelled = false;
|
|
1504
1926
|
const loader = await this.createGLTFLoader();
|
|
1505
1927
|
const cancelable = AssetLoader._isNetworkUrl( modelUrl );
|
|
1928
|
+
let source = null;
|
|
1506
1929
|
|
|
1507
1930
|
try {
|
|
1508
1931
|
|
|
1509
1932
|
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
1510
|
-
|
|
1933
|
+
source = await this._viaCache( modelUrl, "Downloading Model...", cancelable, options );
|
|
1934
|
+
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( modelUrl ) );
|
|
1935
|
+
const data = await loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
1511
1936
|
this._downloadComplete();
|
|
1512
1937
|
|
|
1513
1938
|
this.releaseTargetModel();
|
|
1939
|
+
this._sourceKey = cancelable ? this._keyed( 'url', options.cacheKey ?? modelUrl ) : null;
|
|
1514
1940
|
|
|
1515
1941
|
this.targetModel = data.scene;
|
|
1516
1942
|
this.animations = data.animations || [];
|
|
@@ -1528,6 +1954,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1528
1954
|
|
|
1529
1955
|
} finally {
|
|
1530
1956
|
|
|
1957
|
+
source?.release();
|
|
1531
1958
|
this._disposeGLTFLoader( loader );
|
|
1532
1959
|
|
|
1533
1960
|
}
|
|
@@ -1555,17 +1982,21 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1555
1982
|
|
|
1556
1983
|
// Append a model from URL without releasing prior models or reframing.
|
|
1557
1984
|
// Reuses createGLTFLoader() so appended KTX2 textures stay RGBA DataArrayTexture.
|
|
1558
|
-
async appendModel( url ) {
|
|
1985
|
+
async appendModel( url, options = {} ) {
|
|
1559
1986
|
|
|
1560
1987
|
this._loadCancelled = false;
|
|
1561
1988
|
const loader = await this.createGLTFLoader();
|
|
1562
1989
|
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
1990
|
+
let source = null;
|
|
1563
1991
|
|
|
1564
1992
|
try {
|
|
1565
1993
|
|
|
1566
1994
|
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
1567
|
-
|
|
1995
|
+
source = await this._viaCache( url, "Downloading Model...", cancelable, options );
|
|
1996
|
+
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( url ) );
|
|
1997
|
+
const data = await loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
1568
1998
|
this._downloadComplete();
|
|
1999
|
+
this._appended = true;
|
|
1569
2000
|
this._processAndParent( data.scene );
|
|
1570
2001
|
return { root: data.scene, animations: data.animations || [] };
|
|
1571
2002
|
|
|
@@ -1576,6 +2007,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1576
2007
|
|
|
1577
2008
|
} finally {
|
|
1578
2009
|
|
|
2010
|
+
source?.release();
|
|
1579
2011
|
this._disposeGLTFLoader( loader );
|
|
1580
2012
|
|
|
1581
2013
|
}
|
|
@@ -1585,6 +2017,8 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1585
2017
|
// Append a copy of a caller-owned Object3D without releasing prior models or reframing.
|
|
1586
2018
|
appendObject3D( object3d, name = 'object3d' ) {
|
|
1587
2019
|
|
|
2020
|
+
this._appended = true;
|
|
2021
|
+
|
|
1588
2022
|
const root = this._adoptExternalObject( object3d );
|
|
1589
2023
|
root.name = object3d.name || name;
|
|
1590
2024
|
this._processAndParent( root );
|
|
@@ -2000,6 +2434,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2000
2434
|
|
|
2001
2435
|
// Calculate scene scale factor based on model size
|
|
2002
2436
|
const sceneScale = maxDim;
|
|
2437
|
+
this.sceneSize.copy( size );
|
|
2003
2438
|
|
|
2004
2439
|
// Rebuild path tracing
|
|
2005
2440
|
buildTimer.start( 'setupPathTracing' );
|
|
@@ -2216,6 +2651,13 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2216
2651
|
|
|
2217
2652
|
}
|
|
2218
2653
|
|
|
2654
|
+
/** The loaded model's bounding-box size along x, y and z, in scene units. */
|
|
2655
|
+
getSceneSize() {
|
|
2656
|
+
|
|
2657
|
+
return this.sceneSize.clone();
|
|
2658
|
+
|
|
2659
|
+
}
|
|
2660
|
+
|
|
2219
2661
|
getTargetModel() {
|
|
2220
2662
|
|
|
2221
2663
|
return this.targetModel;
|
|
@@ -2264,6 +2706,11 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2264
2706
|
// onError captures `this`, and a manager outlives the loader via an in-flight fetch.
|
|
2265
2707
|
this._loadingManager.onError = undefined;
|
|
2266
2708
|
this._issues = null;
|
|
2709
|
+
this._urlAbort?.abort();
|
|
2710
|
+
this._urlAbort = null;
|
|
2711
|
+
this.storage = null;
|
|
2712
|
+
this._downloads = null;
|
|
2713
|
+
this._pendingGraph = null;
|
|
2267
2714
|
|
|
2268
2715
|
this.releaseTargetModel();
|
|
2269
2716
|
|