rayzee 9.0.0 → 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 +139 -8
- package/dist/{NeuralRetouch-CYOJ21Wn.js → NeuralRetouch-BRquqfmZ.js} +1 -1
- package/dist/{NeuralSuperRes-CQLCLpcC.js → NeuralSuperRes-CbNvQjCw.js} +3 -2
- package/dist/{ToneMapGPU-BVE852iN.js → ToneMapGPU-DpUZDMbQ.js} +2 -1
- package/dist/rayzee.es.js +10344 -5334
- package/dist/rayzee.umd.js +265 -265
- package/package.json +1 -1
- package/src/AssetConfig.js +4 -0
- package/src/EngineDefaults.js +38 -56
- package/src/EngineEvents.js +3 -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/PathTracerApp.js +290 -19
- package/src/Processor/ArchiveCache.js +120 -0
- package/src/Processor/ArchiveReader.js +179 -32
- package/src/Processor/AssetLoader.js +514 -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 +192 -49
- 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 +38 -0
- package/src/SceneState/SceneState.js +367 -0
- package/src/SceneState/portable.js +52 -0
- package/src/Stages/NormalDepth.js +7 -6
- package/src/Stages/PathTracer.js +108 -4
- 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/Common.js +11 -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 +3 -3
- 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 +18 -0
- package/src/managers/CameraManager.js +169 -1
- package/src/managers/EnvironmentManager.js +263 -33
- package/src/managers/GoboManager.js +19 -8
- package/src/managers/IESManager.js +3 -3
- 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 +5 -3
- package/src/managers/VideoRenderManager.js +3 -1
- package/src/managers/timeline/CameraTrack.js +22 -0
- package/src/managers/timeline/TimelineManager.js +14 -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';
|
|
@@ -112,6 +121,13 @@ export class AssetLoader extends EventDispatcher {
|
|
|
112
121
|
// signal into its fetch). One load runs at a time (guarded upstream).
|
|
113
122
|
this._loadingManager = new LoadingManager();
|
|
114
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;
|
|
115
131
|
|
|
116
132
|
this._issues = issues;
|
|
117
133
|
this._profile = profile ?? getRenderProfile();
|
|
@@ -141,6 +157,66 @@ export class AssetLoader extends EventDispatcher {
|
|
|
141
157
|
|
|
142
158
|
this._loadCancelled = true;
|
|
143
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;
|
|
144
220
|
|
|
145
221
|
}
|
|
146
222
|
|
|
@@ -241,6 +317,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
241
317
|
releaseTargetModel() {
|
|
242
318
|
|
|
243
319
|
this.sceneMetadata = null;
|
|
320
|
+
this._sourceKey = null;
|
|
321
|
+
this._appended = false;
|
|
322
|
+
this._pendingGraph = null;
|
|
244
323
|
|
|
245
324
|
if ( ! this.targetModel ) return;
|
|
246
325
|
|
|
@@ -325,8 +404,75 @@ export class AssetLoader extends EventDispatcher {
|
|
|
325
404
|
|
|
326
405
|
}
|
|
327
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
|
+
|
|
328
465
|
async loadModelFromFile( file, filename ) {
|
|
329
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
|
+
|
|
330
476
|
const extension = filename.split( '.' ).pop().toLowerCase();
|
|
331
477
|
const arrayBuffer = await this.readFileAsArrayBuffer( file );
|
|
332
478
|
|
|
@@ -357,6 +503,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
357
503
|
try {
|
|
358
504
|
|
|
359
505
|
const texture = await this.loadEnvironment( url );
|
|
506
|
+
setEnvironmentSource( texture, identityKey( await fileIdentity( file ) ) );
|
|
360
507
|
this.dispatchEvent( { type: 'load', texture, filename } );
|
|
361
508
|
return texture;
|
|
362
509
|
|
|
@@ -395,6 +542,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
395
542
|
}
|
|
396
543
|
|
|
397
544
|
texture.generateMipmaps = true;
|
|
545
|
+
if ( AssetLoader._isNetworkUrl( envUrl ) ) setEnvironmentSource( texture, envUrl );
|
|
398
546
|
|
|
399
547
|
this.applyEnvironmentToScene( texture );
|
|
400
548
|
this.dispatchEvent( { type: 'load', texture, url: envUrl, filename: envUrl.split( /[?#]/ )[ 0 ].split( '/' ).pop() } );
|
|
@@ -465,23 +613,33 @@ export class AssetLoader extends EventDispatcher {
|
|
|
465
613
|
async loadEnvironmentByExtension( url, extension ) {
|
|
466
614
|
|
|
467
615
|
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
468
|
-
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 );
|
|
469
619
|
|
|
470
620
|
let texture;
|
|
471
|
-
|
|
621
|
+
try {
|
|
472
622
|
|
|
473
|
-
|
|
474
|
-
? ( this.loaderCache.hdr || ( this.loaderCache.hdr = new HDRLoader( this._loadingManager ).setDataType( FloatType ) ) )
|
|
475
|
-
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader( this._loadingManager ).setDataType( FloatType ) ) );
|
|
476
|
-
texture = await loader.loadAsync( url, onProgress );
|
|
623
|
+
if ( extension === 'hdr' || extension === 'exr' ) {
|
|
477
624
|
|
|
478
|
-
|
|
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 );
|
|
629
|
+
|
|
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 {
|
|
479
641
|
|
|
480
|
-
|
|
481
|
-
texture = await this.loaderCache.texture.loadAsync( url, onProgress );
|
|
482
|
-
// LDR env maps (jpg/png/webp) are authored in sRGB; tag them so the backend
|
|
483
|
-
// decodes to linear. HDR/EXR are already linear and keep the loader's setting.
|
|
484
|
-
texture.colorSpace = SRGBColorSpace;
|
|
642
|
+
source.release();
|
|
485
643
|
|
|
486
644
|
}
|
|
487
645
|
|
|
@@ -524,36 +682,70 @@ export class AssetLoader extends EventDispatcher {
|
|
|
524
682
|
async inspectArchive( file ) {
|
|
525
683
|
|
|
526
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
|
+
} );
|
|
527
688
|
|
|
528
|
-
|
|
689
|
+
let listing;
|
|
690
|
+
if ( kind === 'gzip' ) {
|
|
529
691
|
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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 } ) );
|
|
543
711
|
|
|
544
712
|
}
|
|
545
713
|
|
|
546
|
-
const zip = unzipSync( new Uint8Array( await this.readFileAsArrayBuffer( file ) ) );
|
|
547
|
-
const listing = Object.keys( zip ).map( path => ( { path, size: zip[ path ].length } ) );
|
|
548
714
|
const { root, elements } = listArchiveElements( listing );
|
|
549
715
|
return {
|
|
550
|
-
kind: 'zip', root, elements,
|
|
716
|
+
kind: kind ?? 'zip', root, elements,
|
|
551
717
|
entryCount: listing.length,
|
|
552
718
|
totalBytes: listing.reduce( ( n, e ) => n + e.size, 0 )
|
|
553
719
|
};
|
|
554
720
|
|
|
555
721
|
}
|
|
556
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
|
+
|
|
557
749
|
/**
|
|
558
750
|
* @param {File|Blob} file
|
|
559
751
|
* @param {string} filename
|
|
@@ -572,39 +764,52 @@ export class AssetLoader extends EventDispatcher {
|
|
|
572
764
|
* @param {number} [options.mergeShapesAbove] - merge small non-instanced shapes past this count.
|
|
573
765
|
* @param {number} [options.curveSteps] - samples per spline span when tessellating curves.
|
|
574
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).
|
|
575
771
|
*/
|
|
576
772
|
async loadArchiveFromFile( file, filename, { pbrtEntry = null, element = null, byteBudget, promptBytes, ...pbrt } = {} ) {
|
|
577
773
|
|
|
578
774
|
try {
|
|
579
775
|
|
|
580
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 };
|
|
581
779
|
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
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 );
|
|
586
784
|
|
|
587
|
-
|
|
588
|
-
if ( source.listing.some( e => e.path.toLowerCase().endsWith( '.pbrt' ) ) ) {
|
|
785
|
+
if ( kind === 'gzip' ) {
|
|
589
786
|
|
|
590
|
-
|
|
787
|
+
const unpacked = await this._unpackGzip( file, filename );
|
|
788
|
+
if ( unpacked ) {
|
|
591
789
|
|
|
592
|
-
|
|
790
|
+
try {
|
|
593
791
|
|
|
594
|
-
|
|
595
|
-
for ( const e of source.listing ) source.entries[ e.path ] ??= await source.read( e.path );
|
|
596
|
-
return await this._loadNonPBRTArchive( source.entries, filename );
|
|
792
|
+
return await this._loadSeekable( unpacked.file, filename, { ...lazy, index: unpacked.index } );
|
|
597
793
|
|
|
598
|
-
|
|
794
|
+
} finally {
|
|
795
|
+
|
|
796
|
+
unpacked.release();
|
|
599
797
|
|
|
600
|
-
|
|
601
|
-
? await this._readStreamedArchive( file, filename, kind, element, byteBudget )
|
|
602
|
-
: unzipSync( new Uint8Array( await this.readFileAsArrayBuffer( file ) ) );
|
|
798
|
+
}
|
|
603
799
|
|
|
604
|
-
|
|
605
|
-
if ( pickEntryPath( entries ) ) return await this.loadPBRTFromZip( entries, filename, pbrtEntry, pbrt );
|
|
800
|
+
}
|
|
606
801
|
|
|
607
|
-
|
|
802
|
+
const entries = await this._readStreamedArchive( file, filename, kind, element, byteBudget );
|
|
803
|
+
|
|
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 } );
|
|
608
813
|
|
|
609
814
|
} catch ( error ) {
|
|
610
815
|
|
|
@@ -624,7 +829,170 @@ export class AssetLoader extends EventDispatcher {
|
|
|
624
829
|
}
|
|
625
830
|
|
|
626
831
|
/**
|
|
627
|
-
*
|
|
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
|
|
628
996
|
* demand, so residency is the open include chain rather than the whole archive.
|
|
629
997
|
*
|
|
630
998
|
* Indexing is cheap and the whole archive is never held, but *parsing* all of it is not:
|
|
@@ -633,14 +1001,30 @@ export class AssetLoader extends EventDispatcher {
|
|
|
633
1001
|
* already built at that point, so the question costs nothing.
|
|
634
1002
|
* @private
|
|
635
1003
|
*/
|
|
636
|
-
async _openSeekableArchive( file, filename, element, promptBytes ) {
|
|
1004
|
+
async _openSeekableArchive( file, filename, element, promptBytes, { index = null, zip = false } = {} ) {
|
|
637
1005
|
|
|
638
1006
|
const chosen = Array.isArray( element ) ? element.filter( Boolean ) : ( element ? [ element ] : [] );
|
|
1007
|
+
const filter = chosen.length ? elementFilter( chosen ) : null;
|
|
639
1008
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
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
|
+
}
|
|
644
1028
|
|
|
645
1029
|
const totalBytes = source.listing.reduce( ( n, e ) => n + e.size, 0 );
|
|
646
1030
|
|
|
@@ -695,8 +1079,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
695
1079
|
async _readStreamedArchive( file, filename, kind, element, byteBudget ) {
|
|
696
1080
|
|
|
697
1081
|
const chosen = Array.isArray( element ) ? element.filter( Boolean ) : ( element ? [ element ] : [] );
|
|
698
|
-
const
|
|
699
|
-
const { entries, listing, retainedBytes, truncated } = await read( file, {
|
|
1082
|
+
const { entries, listing, retainedBytes, truncated } = await readTarGz( file, {
|
|
700
1083
|
filter: chosen.length ? elementFilter( chosen ) : null,
|
|
701
1084
|
...( byteBudget === undefined ? {} : { byteBudget } ),
|
|
702
1085
|
onProgress: p => updateLoading( {
|
|
@@ -733,6 +1116,19 @@ export class AssetLoader extends EventDispatcher {
|
|
|
733
1116
|
|
|
734
1117
|
}
|
|
735
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
|
+
|
|
736
1132
|
if ( chosen.length ) {
|
|
737
1133
|
|
|
738
1134
|
console.info(
|
|
@@ -767,17 +1163,29 @@ export class AssetLoader extends EventDispatcher {
|
|
|
767
1163
|
|
|
768
1164
|
const plyParser = ( buf ) => this.loaderCache.ply.parse( buf );
|
|
769
1165
|
|
|
770
|
-
// Texture maps — decode by extension (pbrt uses .png/.jpg but also .exr/.hdr/.tga).
|
|
771
|
-
|
|
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
|
+
};
|
|
772
1175
|
|
|
773
1176
|
// Infinite-light maps → HDR/EXR/LDR via the shared environment decoder.
|
|
774
1177
|
const envFromBytes = async ( bytes, fname ) => {
|
|
775
1178
|
|
|
776
1179
|
const ext = fname.split( '.' ).pop().toLowerCase();
|
|
777
|
-
const
|
|
1180
|
+
const blob = new Blob( [ bytes ] );
|
|
1181
|
+
const url = URL.createObjectURL( blob );
|
|
778
1182
|
try {
|
|
779
1183
|
|
|
780
|
-
|
|
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;
|
|
781
1189
|
|
|
782
1190
|
} finally {
|
|
783
1191
|
|
|
@@ -788,17 +1196,22 @@ export class AssetLoader extends EventDispatcher {
|
|
|
788
1196
|
};
|
|
789
1197
|
|
|
790
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
|
+
|
|
791
1211
|
const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
|
|
792
1212
|
frames, parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
|
|
793
|
-
droppedNoTemplate } =
|
|
794
|
-
|
|
795
|
-
animation: options.animation,
|
|
796
|
-
maxTriangles: options.maxTriangles,
|
|
797
|
-
maxPlacements: options.maxPlacements,
|
|
798
|
-
mergeShapesAbove: options.mergeShapesAbove,
|
|
799
|
-
curveSteps: options.curveSteps,
|
|
800
|
-
curveSides: options.curveSides
|
|
801
|
-
} );
|
|
1213
|
+
droppedNoTemplate } = built;
|
|
1214
|
+
if ( stored ) console.info( `PBRT scene "${loadedEntry}" opened from storage in ${( buildMs / 1000 ).toFixed( 1 )} s, skipping the parse` );
|
|
802
1215
|
|
|
803
1216
|
// Phase breakdown for scaling work; the engine's own build timings live in
|
|
804
1217
|
// SceneProcessor.performanceMetrics.
|
|
@@ -852,6 +1265,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
852
1265
|
|
|
853
1266
|
group.name = loadedEntry || filename;
|
|
854
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 );
|
|
855
1271
|
this.targetModel = group;
|
|
856
1272
|
this.animations = animations ?? [];
|
|
857
1273
|
|
|
@@ -1499,19 +1915,28 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1499
1915
|
|
|
1500
1916
|
}
|
|
1501
1917
|
|
|
1502
|
-
|
|
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 = {} ) {
|
|
1503
1924
|
|
|
1504
1925
|
this._loadCancelled = false;
|
|
1505
1926
|
const loader = await this.createGLTFLoader();
|
|
1506
1927
|
const cancelable = AssetLoader._isNetworkUrl( modelUrl );
|
|
1928
|
+
let source = null;
|
|
1507
1929
|
|
|
1508
1930
|
try {
|
|
1509
1931
|
|
|
1510
1932
|
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
1511
|
-
|
|
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 ) );
|
|
1512
1936
|
this._downloadComplete();
|
|
1513
1937
|
|
|
1514
1938
|
this.releaseTargetModel();
|
|
1939
|
+
this._sourceKey = cancelable ? this._keyed( 'url', options.cacheKey ?? modelUrl ) : null;
|
|
1515
1940
|
|
|
1516
1941
|
this.targetModel = data.scene;
|
|
1517
1942
|
this.animations = data.animations || [];
|
|
@@ -1529,6 +1954,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1529
1954
|
|
|
1530
1955
|
} finally {
|
|
1531
1956
|
|
|
1957
|
+
source?.release();
|
|
1532
1958
|
this._disposeGLTFLoader( loader );
|
|
1533
1959
|
|
|
1534
1960
|
}
|
|
@@ -1556,17 +1982,21 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1556
1982
|
|
|
1557
1983
|
// Append a model from URL without releasing prior models or reframing.
|
|
1558
1984
|
// Reuses createGLTFLoader() so appended KTX2 textures stay RGBA DataArrayTexture.
|
|
1559
|
-
async appendModel( url ) {
|
|
1985
|
+
async appendModel( url, options = {} ) {
|
|
1560
1986
|
|
|
1561
1987
|
this._loadCancelled = false;
|
|
1562
1988
|
const loader = await this.createGLTFLoader();
|
|
1563
1989
|
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
1990
|
+
let source = null;
|
|
1564
1991
|
|
|
1565
1992
|
try {
|
|
1566
1993
|
|
|
1567
1994
|
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
1568
|
-
|
|
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 ) );
|
|
1569
1998
|
this._downloadComplete();
|
|
1999
|
+
this._appended = true;
|
|
1570
2000
|
this._processAndParent( data.scene );
|
|
1571
2001
|
return { root: data.scene, animations: data.animations || [] };
|
|
1572
2002
|
|
|
@@ -1577,6 +2007,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1577
2007
|
|
|
1578
2008
|
} finally {
|
|
1579
2009
|
|
|
2010
|
+
source?.release();
|
|
1580
2011
|
this._disposeGLTFLoader( loader );
|
|
1581
2012
|
|
|
1582
2013
|
}
|
|
@@ -1586,6 +2017,8 @@ export class AssetLoader extends EventDispatcher {
|
|
|
1586
2017
|
// Append a copy of a caller-owned Object3D without releasing prior models or reframing.
|
|
1587
2018
|
appendObject3D( object3d, name = 'object3d' ) {
|
|
1588
2019
|
|
|
2020
|
+
this._appended = true;
|
|
2021
|
+
|
|
1589
2022
|
const root = this._adoptExternalObject( object3d );
|
|
1590
2023
|
root.name = object3d.name || name;
|
|
1591
2024
|
this._processAndParent( root );
|
|
@@ -2273,6 +2706,11 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2273
2706
|
// onError captures `this`, and a manager outlives the loader via an in-flight fetch.
|
|
2274
2707
|
this._loadingManager.onError = undefined;
|
|
2275
2708
|
this._issues = null;
|
|
2709
|
+
this._urlAbort?.abort();
|
|
2710
|
+
this._urlAbort = null;
|
|
2711
|
+
this.storage = null;
|
|
2712
|
+
this._downloads = null;
|
|
2713
|
+
this._pendingGraph = null;
|
|
2276
2714
|
|
|
2277
2715
|
this.releaseTargetModel();
|
|
2278
2716
|
|