rayzee 9.4.2 → 9.5.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 +223 -21
- package/dist/ActiveColor-YvuQq44O.js +13 -0
- package/dist/ArchiveImporter-xuUJiklP.js +4301 -0
- package/dist/AssetConfig-DSEntE65.js +30 -0
- package/dist/BidirectionalIntegrator-C5PKjJoT.js +934 -0
- package/dist/ColorManagement-CheBCPaW.js +1488 -0
- package/dist/EngineDefaults-DH3bI5t-.js +844 -0
- package/dist/EngineEvents-Ci8dVWCP.js +73 -0
- package/dist/EngineIssues-CIZnXHsF.js +72 -0
- package/dist/FileFormats-sPGJeCRc.js +102 -0
- package/dist/Logger-BnHghprG.js +97 -0
- package/dist/{NeuralRetouch-vkdXXyK2.js → NeuralRetouch-CW11yLZ_.js} +8 -6
- package/dist/{NeuralSuperRes-FVqRJKQV.js → NeuralSuperRes-BekaecYg.js} +55 -120
- package/dist/PathTracer-Ccl9KOuF.js +29729 -0
- package/dist/PhysicalSky-Dv2PMFGW.js +1219 -0
- package/dist/RayzeeRenderer-BQxnzCTD.js +3542 -0
- package/dist/SolarLimb-ken-JbDK.js +721 -0
- package/dist/ToneMapGPU-BHi5JzIq.js +177 -0
- package/dist/ViewTransforms-D0rTLBsX.js +255 -0
- package/dist/WorkingMatrix-C6y233hT.js +27 -0
- package/dist/ZipReader-gDSWr0I5.js +98 -0
- package/dist/addons/archives.es.js +3 -0
- package/dist/addons/bidirectional.es.js +2 -0
- package/dist/addons/color.es.js +3 -0
- package/dist/addons/formats.es.js +2 -0
- package/dist/addons/physical-sky.es.js +2 -0
- package/dist/addons/storage.es.js +3 -0
- package/dist/archiveFormats-BZw7TiDp.js +75 -0
- package/dist/identity-DrQsGT2o.js +39 -0
- package/dist/openStorage-DwcBRUj7.js +820 -0
- package/dist/rayzee-core.es.js +12 -0
- package/dist/rayzee.es.js +2266 -44079
- package/dist/rayzee.umd.js +485 -473
- package/dist/sceneCachePolicy-BmwYOYDu.js +183 -0
- package/dist/shared-BGBcSxBQ.js +17 -0
- package/package.json +23 -2
- package/src/Color/ActiveColor.js +34 -0
- package/src/Color/BasicColor.js +147 -0
- package/src/Color/ColorManagement.js +7 -31
- package/src/EngineDefaults.js +22 -8
- package/src/EngineEvents.js +2 -0
- package/src/EngineIssues.js +1 -0
- package/src/PathTracerApp.js +617 -4016
- package/src/Pipeline/PipelineContext.js +0 -4
- package/src/Platform.js +36 -0
- package/src/Processor/ArchiveCache.js +1 -1
- package/src/Processor/ArchiveImporter.js +1259 -0
- package/src/Processor/AssetLoader.js +322 -1746
- package/src/Processor/AtmosphereModel.js +1 -2
- package/src/Processor/BVHBuilder.js +4 -95
- package/src/Processor/EnvironmentExactTable.js +187 -0
- package/src/Processor/EquirectHDRInfo.js +30 -223
- package/src/Processor/FileFormats.js +116 -0
- package/src/Processor/GLTFDecoders.js +85 -0
- package/src/Processor/GLTFDiffuseTransmission.js +63 -0
- package/src/Processor/GeometryExtractor.js +12 -1
- package/src/Processor/IESParser.js +47 -35
- package/src/Processor/KernelManager.js +35 -2
- package/src/Processor/MaterialPacking.js +8 -7
- package/src/Processor/PBRT/PBRTMaterials.js +129 -41
- package/src/Processor/PBRT/PBRTParser.js +30 -17
- package/src/Processor/PBRT/PBRTSceneBuilder.js +548 -37
- package/src/Processor/PBRT/PBRTTextureBake.js +154 -0
- package/src/Processor/PBRT/PFM.js +59 -0
- package/src/Processor/PackedRayBuffer.js +116 -9
- package/src/Processor/ParallelBVHBuilder.js +3 -6
- package/src/Processor/PhysicalSky.js +5 -4
- package/src/Processor/PlatformImageLoader.js +50 -6
- package/src/Processor/QueueManager.js +12 -3
- package/src/Processor/ReinsertionOptimizer.js +2 -2
- package/src/Processor/SceneProcessor.js +15 -55
- package/src/Processor/ShaderBuilder.js +6 -16
- package/src/Processor/SolarLimb.js +3 -0
- package/src/Processor/TextureCreator.js +1 -1
- package/src/Processor/Workers/BVHSubtreeWorker.js +1 -34
- package/src/Processor/Workers/BVHWorker.js +5 -18
- package/src/Processor/Workers/CDFWorker.js +6 -183
- package/src/Processor/archiveFormats.js +7 -0
- package/src/Processor/utils.js +2 -1
- package/src/RayzeeRenderer.js +3902 -0
- package/src/RenderSettings.js +49 -41
- package/src/Stages/ASVGF.js +5 -7
- package/src/Stages/AutoExposure.js +3 -12
- package/src/Stages/Compositor.js +8 -23
- package/src/Stages/NRD.js +9 -2
- package/src/Stages/NormalDepth.js +9 -6
- package/src/Stages/PathTracer.js +254 -38
- package/src/Stages/PathTracerStage.js +11 -16
- package/src/Storage/BLASCache.js +1 -1
- package/src/Storage/CDFCache.js +18 -17
- package/src/Storage/DownloadCache.js +1 -1
- package/src/Storage/SceneGraphCodec.js +46 -3
- package/src/Storage/SpillStore.js +1 -1
- package/src/Storage/StorageManager.js +2 -4
- package/src/Storage/areas.js +4 -0
- package/src/TSL/BVHTraversal.js +62 -24
- package/src/TSL/Bidirectional.js +204 -0
- package/src/TSL/BidirectionalLamps.js +132 -0
- package/src/TSL/Clearcoat.js +15 -6
- package/src/TSL/Common.js +19 -14
- package/src/TSL/ConnectKernel.js +147 -0
- package/src/TSL/Debugger.js +1 -1
- package/src/TSL/Displacement.js +4 -3
- package/src/TSL/EmissiveSampling.js +64 -17
- package/src/TSL/Environment.js +77 -106
- package/src/TSL/EnvironmentCDF.js +85 -39
- package/src/TSL/ExtendKernel.js +8 -3
- package/src/TSL/FinalWriteKernel.js +4 -4
- package/src/TSL/GenerateKernel.js +18 -4
- package/src/TSL/HitFacet.js +35 -5
- package/src/TSL/LightBVHSampling.js +40 -9
- package/src/TSL/LightGenerateKernel.js +293 -0
- package/src/TSL/LightGuide.js +198 -0
- package/src/TSL/LightSplatKernel.js +165 -0
- package/src/TSL/LightsCore.js +13 -38
- package/src/TSL/LightsDirect.js +28 -29
- package/src/TSL/LightsIndirect.js +3 -1
- package/src/TSL/LightsSampling.js +90 -38
- package/src/TSL/MaterialEvaluation.js +113 -31
- package/src/TSL/MaterialProperties.js +44 -17
- package/src/TSL/MaterialTransmission.js +52 -47
- package/src/TSL/MergeKernel.js +218 -0
- package/src/TSL/PathTracerCore.js +65 -29
- package/src/TSL/Random.js +4 -0
- package/src/TSL/SceneResources.js +51 -0
- package/src/TSL/ShadeKernel.js +881 -103
- package/src/TSL/Struct.js +10 -0
- package/src/TSL/Sun.js +1 -1
- package/src/TSL/TextureSampling.js +65 -60
- package/src/addons/archives.js +14 -0
- package/src/addons/bidirectional.js +13 -0
- package/src/addons/color.js +32 -0
- package/src/addons/formats.js +16 -0
- package/src/addons/physicalSky.js +14 -0
- package/src/addons/storage.js +19 -0
- package/src/core.js +35 -0
- package/src/index.js +3 -1
- package/src/integrators/BidirectionalIntegrator.js +832 -0
- package/src/managers/EnvironmentManager.js +85 -33
- package/src/managers/MaterialDataManager.js +42 -0
- package/src/managers/UniformManager.js +0 -1
- package/dist/ToneMapGPU-DKQM1pMd.js +0 -552
- package/dist/rolldown-runtime-CAFD8bLK.js +0 -11
- package/src/Processor/TreeletOptimizer.js +0 -574
|
@@ -1,71 +1,42 @@
|
|
|
1
1
|
import { Box3, BufferGeometry, Vector3, RectAreaLight, Color, FloatType, LinearFilter, EquirectangularReflectionMapping,
|
|
2
|
-
TextureLoader,
|
|
3
|
-
CircleGeometry,
|
|
2
|
+
TextureLoader, SRGBColorSpace, Mesh, MeshPhysicalMaterial,
|
|
3
|
+
CircleGeometry, 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';
|
|
7
|
-
import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';
|
|
8
|
-
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js';
|
|
9
|
-
import { EXRLoader } from 'three/addons/loaders/EXRLoader.js';
|
|
10
7
|
import { createMeshesFromMultiMaterialMesh } from 'three/addons/utils/SceneUtils.js';
|
|
11
8
|
import { clone as cloneWithSkeletons } from 'three/addons/utils/SkeletonUtils.js';
|
|
12
|
-
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js';
|
|
13
|
-
import { zipSync, strFromU8 } from 'three/addons/libs/fflate.module.js';
|
|
14
|
-
import {
|
|
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
9
|
import { DownloadCache, nameFromUrl, cachedObjectURL } from '../Storage/DownloadCache.js';
|
|
19
10
|
import { setEnvironmentSource } from '../Storage/CDFCache.js';
|
|
20
|
-
import { fileIdentity, identityKey
|
|
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';
|
|
11
|
+
import { fileIdentity, identityKey } from '../Storage/identity.js';
|
|
25
12
|
import { disposeEngineOwnedResources, disposeObjectFromMemory, updateLoading } from './utils';
|
|
26
13
|
import { BuildTimer } from './BuildTimer.js';
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
14
|
+
import { getPlatform, hasImageDecoder, withHostWorker } from '../Platform.js';
|
|
15
|
+
import { loadPlatformImage, platformImagesPlugin, missingImageDecoderPlugin } from './PlatformImageLoader.js';
|
|
16
|
+
import { diffuseTransmissionPlugin } from './GLTFDiffuseTransmission.js';
|
|
17
|
+
import { decodersOnDemand } from './GLTFDecoders.js';
|
|
31
18
|
import { extractSceneMetadata } from './SceneMetadata.js';
|
|
32
|
-
import { ISSUE_CODES
|
|
19
|
+
import { ISSUE_CODES } from '../EngineIssues.js';
|
|
33
20
|
import { getRenderProfile } from '../EngineDefaults.js';
|
|
34
21
|
|
|
35
22
|
// Define supported file formats
|
|
36
|
-
|
|
23
|
+
// What the core reads itself; every other format is registered (registerFormat) or comes with an add-on.
|
|
24
|
+
const CORE_FORMATS = {
|
|
37
25
|
'glb': { type: 'model', name: 'GLB (GLTF Binary)' }, 'gltf': { type: 'model', name: 'GLTF' },
|
|
38
|
-
'
|
|
39
|
-
'stl': { type: 'model', name: 'STL' }, 'ply': { type: 'model', name: 'PLY (Polygon File Format)' },
|
|
40
|
-
'dae': { type: 'model', name: 'Collada' }, '3mf': { type: 'model', name: '3D Manufacturing Format' },
|
|
41
|
-
'usd': { type: 'model', name: 'USD (Universal Scene Description)' },
|
|
42
|
-
'usda': { type: 'model', name: 'USDA (USD ASCII)' },
|
|
43
|
-
'usdc': { type: 'model', name: 'USDC (USD Crate)' },
|
|
44
|
-
'usdz': { type: 'model', name: 'USDZ (USD Archive)' },
|
|
45
|
-
'hdr': { type: 'environment', name: 'HDR (High Dynamic Range)' }, 'exr': { type: 'environment', name: 'EXR (OpenEXR)' },
|
|
26
|
+
'hdr': { type: 'environment', name: 'HDR (High Dynamic Range)' },
|
|
46
27
|
'png': { type: 'image', name: 'PNG' }, 'jpg': { type: 'image', name: 'JPEG' },
|
|
47
28
|
'jpeg': { type: 'image', name: 'JPEG' }, 'webp': { type: 'image', name: 'WebP' },
|
|
48
|
-
'zip': { type: 'archive', name: 'ZIP Archive' },
|
|
49
|
-
'gz': { type: 'archive', name: 'Gzipped TAR Archive' },
|
|
50
|
-
'tgz': { type: 'archive', name: 'Gzipped TAR Archive' },
|
|
51
|
-
'tar': { type: 'archive', name: 'TAR Archive' }
|
|
52
29
|
};
|
|
53
30
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
* Measured on Moana: a 7.5 GB archive of 15 parts parses to 40M triangles and ~7.3 GB of CPU
|
|
64
|
-
* memory, which loads on a freshly started browser and fails on one that has been up a while.
|
|
65
|
-
* Anything near that is worth a question, because the failure past it kills the tab rather than
|
|
66
|
-
* throwing. Override per load with `promptBytes`.
|
|
67
|
-
*/
|
|
68
|
-
export const ARCHIVE_ELEMENT_PROMPT_BYTES = 4_000_000_000;
|
|
31
|
+
// Formats an add-on reads, for the error that names it.
|
|
32
|
+
const ARCHIVES = { addOn: 'rayzee/addons/archives', install: 'assetLoader.setArchiveImporter()' };
|
|
33
|
+
const FORMATS = { addOn: 'rayzee/addons/formats', install: 'assetLoader.registerFormat()' };
|
|
34
|
+
const ADDON_FORMATS = {
|
|
35
|
+
zip: ARCHIVES, gz: ARCHIVES, tgz: ARCHIVES, tar: ARCHIVES,
|
|
36
|
+
fbx: FORMATS, obj: FORMATS, stl: FORMATS, ply: FORMATS, dae: FORMATS, '3mf': FORMATS,
|
|
37
|
+
usd: FORMATS, usda: FORMATS, usdc: FORMATS, usdz: FORMATS, exr: FORMATS,
|
|
38
|
+
};
|
|
39
|
+
|
|
69
40
|
// A throwaway stand-in for a geometry the engine must not mutate: the split's mergeGroups()
|
|
70
41
|
// reorders and disposes what it is given, but never writes the attributes.
|
|
71
42
|
function standInForSplit( source ) {
|
|
@@ -130,6 +101,12 @@ export class AssetLoader extends EventDispatcher {
|
|
|
130
101
|
this._downloads = null;
|
|
131
102
|
this._sourceKey = null;
|
|
132
103
|
this._appended = false;
|
|
104
|
+
/** @type {?import('./ArchiveImporter.js').ArchiveImporter} reads archives; see setArchiveImporter() */
|
|
105
|
+
this.archives = null;
|
|
106
|
+
/** @type {?{load: function(): Promise<Object>, formats: Object, pending: ?Promise}} see setArchiveImporterLoader() */
|
|
107
|
+
this._archiveLoader = null;
|
|
108
|
+
/** @type {Map<string, Object>} extension → a format registerFormat() added */
|
|
109
|
+
this._formats = new Map();
|
|
133
110
|
|
|
134
111
|
this._issues = issues;
|
|
135
112
|
this._profile = profile ?? getRenderProfile();
|
|
@@ -321,7 +298,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
321
298
|
this.sceneMetadata = null;
|
|
322
299
|
this._sourceKey = null;
|
|
323
300
|
this._appended = false;
|
|
324
|
-
this.
|
|
301
|
+
this.archives?.release();
|
|
325
302
|
|
|
326
303
|
if ( ! this.targetModel ) return;
|
|
327
304
|
|
|
@@ -340,11 +317,88 @@ export class AssetLoader extends EventDispatcher {
|
|
|
340
317
|
|
|
341
318
|
}
|
|
342
319
|
|
|
320
|
+
/**
|
|
321
|
+
* Installs the importer that reads scene archives and the pbrt scenes in them — `ArchiveImporter` from
|
|
322
|
+
* rayzee/addons/archives. Without one, archives are not a supported format.
|
|
323
|
+
* @param {?import('./ArchiveImporter.js').ArchiveImporter} importer
|
|
324
|
+
*/
|
|
325
|
+
setArchiveImporter( importer ) {
|
|
326
|
+
|
|
327
|
+
this.archives = importer;
|
|
328
|
+
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Installs the archive importer on first use: `load()` resolves to one, the first time an archive is read. `formats`
|
|
333
|
+
* are the extensions it reads (its class's `FORMATS`), known before its code is loaded.
|
|
334
|
+
* @param {function(): Promise<import('./ArchiveImporter.js').ArchiveImporter>} load
|
|
335
|
+
* @param {Object<string, {type: string, name: string}>} formats
|
|
336
|
+
*/
|
|
337
|
+
setArchiveImporterLoader( load, formats ) {
|
|
338
|
+
|
|
339
|
+
this._archiveLoader = { load, formats, pending: null };
|
|
340
|
+
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/** The archive importer, loading it first if it was installed with setArchiveImporterLoader(). @private */
|
|
344
|
+
async _archiveImporter() {
|
|
345
|
+
|
|
346
|
+
if ( ! this.archives && this._archiveLoader ) this.archives = await ( this._archiveLoader.pending ??= this._archiveLoader.load() );
|
|
347
|
+
return this.archives;
|
|
348
|
+
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** @private */
|
|
352
|
+
get _archiveFormats() {
|
|
353
|
+
|
|
354
|
+
return this.archives?.constructor.FORMATS ?? this._archiveLoader?.formats ?? null;
|
|
355
|
+
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Adds file formats beyond glTF and .hdr — those of rayzee/addons/formats, or a host's own. Each is
|
|
360
|
+
* `{ name, label, type: 'model' | 'environment', extensions, parse | createLoader }` (see Processor/FileFormats.js).
|
|
361
|
+
* @param {...Object} formats
|
|
362
|
+
* @returns {this}
|
|
363
|
+
*/
|
|
364
|
+
registerFormat( ...formats ) {
|
|
365
|
+
|
|
366
|
+
for ( const format of formats ) for ( const extension of format.extensions ) this._formats.set( extension, format );
|
|
367
|
+
return this;
|
|
368
|
+
|
|
369
|
+
}
|
|
370
|
+
|
|
343
371
|
// File utilities
|
|
344
372
|
getFileFormat( filename ) {
|
|
345
373
|
|
|
346
374
|
const extension = filename.split( '.' ).pop().toLowerCase();
|
|
347
|
-
return
|
|
375
|
+
return CORE_FORMATS[ extension ] || this._formats.get( extension ) || this._archiveFormats?.[ extension ] || null;
|
|
376
|
+
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/** The error for a file no installed reader takes, naming the add-on that would. */
|
|
380
|
+
formatError( filename ) {
|
|
381
|
+
|
|
382
|
+
const addOn = ADDON_FORMATS[ filename.split( '.' ).pop().toLowerCase() ];
|
|
383
|
+
return new Error( addOn
|
|
384
|
+
? `${filename}: this format is read by ${addOn.addOn} — install it with ${addOn.install}`
|
|
385
|
+
: `Unsupported file format: ${filename}` );
|
|
386
|
+
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/** @see ArchiveImporter#inspectArchive */
|
|
390
|
+
async inspectArchive( file ) {
|
|
391
|
+
|
|
392
|
+
const archives = await this._archiveImporter();
|
|
393
|
+
if ( ! archives ) throw this.formatError( file.name ?? 'archive.zip' );
|
|
394
|
+
return await archives.inspectArchive( file );
|
|
395
|
+
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** @see ArchiveImporter#flushPendingGraph */
|
|
399
|
+
flushPendingGraph( buildMs ) {
|
|
400
|
+
|
|
401
|
+
this.archives?.flushPendingGraph( buildMs );
|
|
348
402
|
|
|
349
403
|
}
|
|
350
404
|
|
|
@@ -366,7 +420,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
366
420
|
|
|
367
421
|
const filename = file.name;
|
|
368
422
|
const format = this.getFileFormat( filename );
|
|
369
|
-
if ( ! format ) throw
|
|
423
|
+
if ( ! format ) throw this.formatError( filename );
|
|
370
424
|
|
|
371
425
|
updateLoading( { isLoading: true, status: `Loading ${format.name}...`, progress: 2 } );
|
|
372
426
|
try {
|
|
@@ -377,7 +431,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
377
431
|
case 'model': result = await this.loadModelFromFile( file, filename ); break;
|
|
378
432
|
case 'environment':
|
|
379
433
|
case 'image': result = await this.loadEnvironmentFromFile( file, filename ); break;
|
|
380
|
-
case 'archive': result = await this.loadArchiveFromFile( file, filename, options ); break;
|
|
434
|
+
case 'archive': result = await ( await this._archiveImporter() ).loadArchiveFromFile( file, filename, options ); break;
|
|
381
435
|
default: throw new Error( `Unknown asset type: ${format.type}` );
|
|
382
436
|
|
|
383
437
|
}
|
|
@@ -403,7 +457,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
403
457
|
async loadAssetFromUrl( url, options = {} ) {
|
|
404
458
|
|
|
405
459
|
const { filename = nameFromUrl( url ), cacheKey = url, cachePolicy, ...loadOptions } = options;
|
|
406
|
-
if ( ! this.getFileFormat( filename ) ) throw
|
|
460
|
+
if ( ! this.getFileFormat( filename ) ) throw this.formatError( filename );
|
|
407
461
|
|
|
408
462
|
this._loadCancelled = false;
|
|
409
463
|
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
@@ -463,23 +517,36 @@ export class AssetLoader extends EventDispatcher {
|
|
|
463
517
|
async _loadModelFileByExtension( file, filename ) {
|
|
464
518
|
|
|
465
519
|
const extension = filename.split( '.' ).pop().toLowerCase();
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
520
|
+
if ( extension === 'glb' || extension === 'gltf' ) return await this.loadGLBFromArrayBuffer( await this.readFileAsArrayBuffer( file ), filename );
|
|
521
|
+
|
|
522
|
+
const format = this._formats.get( extension );
|
|
523
|
+
if ( format?.type !== 'model' ) throw this.formatError( filename );
|
|
524
|
+
return await this._loadModelWithFormat( format, file, filename );
|
|
525
|
+
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async _loadModelWithFormat( format, file, filename ) {
|
|
529
|
+
|
|
530
|
+
try {
|
|
531
|
+
|
|
532
|
+
updateLoading( { isLoading: true, status: `Processing ${format.label} Data...`, progress: 5 } );
|
|
533
|
+
await new Promise( r => setTimeout( r, 0 ) );
|
|
534
|
+
|
|
535
|
+
const { model, result = model } = await format.parse( file, { filename, cache: this.loaderCache } );
|
|
536
|
+
this.releaseTargetModel();
|
|
537
|
+
this.targetModel = model;
|
|
538
|
+
|
|
539
|
+
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
540
|
+
await this.onModelLoad( this.targetModel );
|
|
541
|
+
|
|
542
|
+
this.dispatchEvent( { type: 'load', model, filename } );
|
|
543
|
+
return result;
|
|
544
|
+
|
|
545
|
+
} catch ( error ) {
|
|
546
|
+
|
|
547
|
+
console.error( `Error loading ${format.label}:`, error );
|
|
548
|
+
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
549
|
+
throw error;
|
|
483
550
|
|
|
484
551
|
}
|
|
485
552
|
|
|
@@ -599,7 +666,17 @@ export class AssetLoader extends EventDispatcher {
|
|
|
599
666
|
|
|
600
667
|
}
|
|
601
668
|
|
|
602
|
-
|
|
669
|
+
/**
|
|
670
|
+
* @param {string} url
|
|
671
|
+
* @param {string} extension
|
|
672
|
+
* @param {{loader?: Object}} [options] - a three.js loader to read the file with, for a format nothing registered
|
|
673
|
+
*/
|
|
674
|
+
async loadEnvironmentByExtension( url, extension, { loader = null } = {} ) {
|
|
675
|
+
|
|
676
|
+
const format = this._formats.get( extension );
|
|
677
|
+
if ( ! loader && format?.type === 'environment' ) loader = this.loaderCache[ extension ] ??= await format.createLoader( this._loadingManager );
|
|
678
|
+
if ( ! loader && extension === 'hdr' ) loader = this.loaderCache.hdr ??= new HDRLoader( this._loadingManager ).setDataType( FloatType );
|
|
679
|
+
if ( ! loader && ADDON_FORMATS[ extension ] ) throw this.formatError( `environment.${extension}` );
|
|
603
680
|
|
|
604
681
|
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
605
682
|
const status = "Downloading Environment...";
|
|
@@ -609,11 +686,8 @@ export class AssetLoader extends EventDispatcher {
|
|
|
609
686
|
let texture;
|
|
610
687
|
try {
|
|
611
688
|
|
|
612
|
-
if (
|
|
689
|
+
if ( loader ) {
|
|
613
690
|
|
|
614
|
-
const loader = extension === 'hdr'
|
|
615
|
-
? ( this.loaderCache.hdr || ( this.loaderCache.hdr = new HDRLoader( this._loadingManager ).setDataType( FloatType ) ) )
|
|
616
|
-
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader( this._loadingManager ).setDataType( FloatType ) ) );
|
|
617
691
|
texture = await loader.loadAsync( source.url, onProgress );
|
|
618
692
|
|
|
619
693
|
} else if ( getPlatform().decodeImage ) {
|
|
@@ -653,1756 +727,249 @@ export class AssetLoader extends EventDispatcher {
|
|
|
653
727
|
|
|
654
728
|
}
|
|
655
729
|
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
async
|
|
730
|
+
// Returns a fresh loader each call — the DRACOLoader/KTX2Loader a file needs hold persistent
|
|
731
|
+
// worker pools. Callers must invoke _disposeGLTFLoader() to terminate them.
|
|
732
|
+
async createGLTFLoader() {
|
|
659
733
|
|
|
660
|
-
|
|
661
|
-
|
|
734
|
+
const loader = decodersOnDemand( new GLTFLoader( this._loadingManager ), this.renderer );
|
|
735
|
+
const onImageFailure = ( where, error ) => this._reportImageFailure( where, error );
|
|
736
|
+
if ( getPlatform().decodeImage ) loader.register( ( parser ) => platformImagesPlugin( parser, onImageFailure ) );
|
|
737
|
+
else if ( ! hasImageDecoder() ) loader.register( ( parser ) => missingImageDecoderPlugin( parser, onImageFailure ) );
|
|
738
|
+
loader.register( ( parser ) => diffuseTransmissionPlugin( parser ) );
|
|
662
739
|
|
|
663
|
-
|
|
740
|
+
return loader;
|
|
664
741
|
|
|
665
|
-
|
|
742
|
+
}
|
|
666
743
|
|
|
667
|
-
|
|
744
|
+
// GLTFLoader swallows the failure and loads the model untextured, so a strict host's throw is kept
|
|
745
|
+
// for the end of the load (_throwDeferred).
|
|
746
|
+
_reportImageFailure( where, error ) {
|
|
668
747
|
|
|
669
|
-
|
|
748
|
+
try {
|
|
670
749
|
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
async inspectArchive( file ) {
|
|
750
|
+
this._issues?.record(
|
|
751
|
+
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
752
|
+
`image ${where} could not be decoded (${error?.message ?? error}) — the surfaces using it render untextured`,
|
|
753
|
+
{ image: where, cause: String( error?.message ?? error ) }
|
|
754
|
+
);
|
|
677
755
|
|
|
678
|
-
|
|
679
|
-
const scanning = p => updateLoading( {
|
|
680
|
-
isLoading: true, status: `Scanning archive… ${( p.bytes / 1e9 ).toFixed( 1 )} GB`, progress: 4
|
|
681
|
-
} );
|
|
756
|
+
} catch ( strictError ) {
|
|
682
757
|
|
|
683
|
-
|
|
684
|
-
if ( kind === 'gzip' ) {
|
|
758
|
+
this._deferredError ??= strictError;
|
|
685
759
|
|
|
686
|
-
|
|
687
|
-
if ( unpacked ) {
|
|
760
|
+
}
|
|
688
761
|
|
|
689
|
-
|
|
690
|
-
unpacked.release();
|
|
762
|
+
}
|
|
691
763
|
|
|
692
|
-
|
|
764
|
+
_throwDeferred() {
|
|
693
765
|
|
|
694
|
-
|
|
766
|
+
const error = this._deferredError;
|
|
767
|
+
this._deferredError = null;
|
|
768
|
+
if ( error ) throw error;
|
|
695
769
|
|
|
696
|
-
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
_disposeGLTFLoader( loader ) {
|
|
697
773
|
|
|
698
|
-
|
|
774
|
+
if ( ! loader ) return;
|
|
775
|
+
loader.dracoLoader?.dispose();
|
|
776
|
+
loader.ktx2Loader?.dispose();
|
|
699
777
|
|
|
700
|
-
|
|
778
|
+
}
|
|
701
779
|
|
|
702
|
-
|
|
780
|
+
async loadExampleModels( index, modelFiles ) {
|
|
781
|
+
|
|
782
|
+
if ( ! modelFiles || ! modelFiles[ index ] ) {
|
|
703
783
|
|
|
704
|
-
|
|
784
|
+
throw new Error( `No model file at index ${index}` );
|
|
705
785
|
|
|
706
786
|
}
|
|
707
787
|
|
|
708
|
-
const
|
|
709
|
-
return
|
|
710
|
-
kind: kind ?? 'zip', root, elements,
|
|
711
|
-
entryCount: listing.length,
|
|
712
|
-
totalBytes: listing.reduce( ( n, e ) => n + e.size, 0 )
|
|
713
|
-
};
|
|
788
|
+
const modelUrl = `${modelFiles[ index ].url}`;
|
|
789
|
+
return await this.loadModel( modelUrl );
|
|
714
790
|
|
|
715
791
|
}
|
|
716
792
|
|
|
717
793
|
/**
|
|
718
|
-
*
|
|
719
|
-
* @
|
|
794
|
+
* @param {string} modelUrl
|
|
795
|
+
* @param {{cacheKey?: string, cachePolicy?: string}} [options] - key a download cache entry by
|
|
796
|
+
* something steadier than a signed, expiring URL
|
|
720
797
|
*/
|
|
721
|
-
async
|
|
798
|
+
async loadModel( modelUrl, options = {} ) {
|
|
799
|
+
|
|
800
|
+
this._loadCancelled = false;
|
|
801
|
+
const loader = await this.createGLTFLoader();
|
|
802
|
+
const cancelable = AssetLoader._isNetworkUrl( modelUrl );
|
|
803
|
+
let source = null;
|
|
722
804
|
|
|
723
805
|
try {
|
|
724
806
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
807
|
+
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
808
|
+
source = await this._viaCache( modelUrl, "Downloading Model...", cancelable, options );
|
|
809
|
+
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( modelUrl ) );
|
|
810
|
+
const data = await withHostWorker( () => loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) ) );
|
|
811
|
+
this._downloadComplete();
|
|
812
|
+
this._throwDeferred();
|
|
813
|
+
|
|
814
|
+
this.releaseTargetModel();
|
|
815
|
+
this._sourceKey = cancelable ? this._keyed( 'url', options.cacheKey ?? modelUrl ) : null;
|
|
816
|
+
|
|
817
|
+
this.targetModel = data.scene;
|
|
818
|
+
this.animations = data.animations || [];
|
|
819
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
820
|
+
await this.onModelLoad( this.targetModel );
|
|
821
|
+
this.dispatchEvent( { type: 'load', model: data.scene, filename: modelUrl.split( '/' ).pop() } );
|
|
822
|
+
return data;
|
|
732
823
|
|
|
733
824
|
} catch ( error ) {
|
|
734
825
|
|
|
735
|
-
|
|
736
|
-
console.
|
|
737
|
-
|
|
826
|
+
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
827
|
+
console.error( "Error loading model:", error );
|
|
828
|
+
this.dispatchEvent( { type: 'error', message: error.message, filename: modelUrl } );
|
|
829
|
+
throw error;
|
|
738
830
|
|
|
739
|
-
}
|
|
831
|
+
} finally {
|
|
740
832
|
|
|
741
|
-
|
|
833
|
+
source?.release();
|
|
834
|
+
this._disposeGLTFLoader( loader );
|
|
742
835
|
|
|
743
|
-
|
|
744
|
-
* @param {File|Blob} file
|
|
745
|
-
* @param {string} filename
|
|
746
|
-
* @param {object} [options]
|
|
747
|
-
* @param {string} [options.pbrtEntry] - which .pbrt to load when the archive holds several
|
|
748
|
-
* independent scenes; the issue log lists what else was available.
|
|
749
|
-
* @param {string|string[]} [options.element] - path prefix of one subtree to load on its own,
|
|
750
|
-
* or several to load together, as reported by `inspectArchive()`. Everything above them
|
|
751
|
-
* (scene file, materials, textures) comes along; subtrees left out are skipped without ever
|
|
752
|
-
* being held in memory, and the Includes that point at them only warn.
|
|
753
|
-
* @param {number} [options.byteBudget] - cap on retained bytes when no element is chosen.
|
|
754
|
-
* @param {number} [options.promptBytes] - size past which a multi-part archive asks which
|
|
755
|
-
* parts to load instead of taking all of them.
|
|
756
|
-
* @param {number} [options.maxTriangles] - stop past this many STORED triangles.
|
|
757
|
-
* @param {number} [options.maxPlacements] - stop past this many instance placements.
|
|
758
|
-
* @param {number} [options.mergeShapesAbove] - merge small non-instanced shapes past this count.
|
|
759
|
-
* @param {number} [options.curveSteps] - samples per spline span when tessellating curves.
|
|
760
|
-
* @param {number} [options.curveSides] - 1 ribbon, 2 crossed ribbons, >=3 closed tube.
|
|
761
|
-
* @param {number} [options.curveTolerance] - how far a curve strip may stray from the curve, as a
|
|
762
|
-
* fraction of its half-width (default 0.05); 0 samples every span `curveSteps` times.
|
|
763
|
-
* @param {boolean} [options.instanceIncludes] - place a file included again under the same
|
|
764
|
-
* material as another instance of its first reading (default true).
|
|
765
|
-
*/
|
|
766
|
-
async loadArchiveFromFile( file, filename, { pbrtEntry = null, element = null, byteBudget, promptBytes, ...pbrt } = {} ) {
|
|
836
|
+
}
|
|
767
837
|
|
|
768
|
-
|
|
838
|
+
}
|
|
769
839
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
840
|
+
// ─────────────────────────────────────────────────────────────
|
|
841
|
+
// Append / remove primitives (dynamic object add/remove).
|
|
842
|
+
// These deliberately do NOT touch targetModel/animations and do NOT
|
|
843
|
+
// reframe the camera or dispatch load/modelProcessed — the caller
|
|
844
|
+
// (PathTracerApp) drives a reframe-free scene rebuild.
|
|
845
|
+
// ─────────────────────────────────────────────────────────────
|
|
773
846
|
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
// being refused and loading. A .tar.gz becomes seekable by unpacking it to storage.
|
|
777
|
-
if ( kind === 'tar' ) return await this._loadSeekable( file, filename, lazy );
|
|
847
|
+
// Multi-material split + area-light placeholders, then parent into meshScene.
|
|
848
|
+
_processAndParent( model ) {
|
|
778
849
|
|
|
779
|
-
|
|
850
|
+
this.processModelObjects( model );
|
|
851
|
+
this.scene.add( model );
|
|
852
|
+
// Refresh world matrices for the whole subtree — the reframe-free rebuild
|
|
853
|
+
// extracts geometry immediately (rAF is gated off), so nothing else would
|
|
854
|
+
// update matrices first and any ancestor-node transform would be dropped.
|
|
855
|
+
model.updateMatrixWorld( true );
|
|
780
856
|
|
|
781
|
-
|
|
782
|
-
if ( unpacked ) {
|
|
857
|
+
}
|
|
783
858
|
|
|
784
|
-
|
|
859
|
+
// Append a model from URL without releasing prior models or reframing.
|
|
860
|
+
// Reuses createGLTFLoader() so appended KTX2 textures stay RGBA DataArrayTexture.
|
|
861
|
+
async appendModel( url, options = {} ) {
|
|
785
862
|
|
|
786
|
-
|
|
863
|
+
this._loadCancelled = false;
|
|
864
|
+
const loader = await this.createGLTFLoader();
|
|
865
|
+
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
866
|
+
let source = null;
|
|
787
867
|
|
|
788
|
-
|
|
868
|
+
try {
|
|
789
869
|
|
|
790
|
-
|
|
870
|
+
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
871
|
+
source = await this._viaCache( url, "Downloading Model...", cancelable, options );
|
|
872
|
+
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( url ) );
|
|
873
|
+
const data = await withHostWorker( () => loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) ) );
|
|
874
|
+
this._downloadComplete();
|
|
875
|
+
this._throwDeferred();
|
|
876
|
+
this._appended = true;
|
|
877
|
+
this._processAndParent( data.scene );
|
|
878
|
+
return { root: data.scene, animations: data.animations || [] };
|
|
791
879
|
|
|
792
|
-
|
|
880
|
+
} catch ( error ) {
|
|
793
881
|
|
|
794
|
-
|
|
882
|
+
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
883
|
+
throw error;
|
|
795
884
|
|
|
796
|
-
|
|
885
|
+
} finally {
|
|
797
886
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const result = await this._loadNonPBRTArchive( entries, filename );
|
|
801
|
-
this._sourceKey = this._keyed( 'archive', archiveId );
|
|
802
|
-
return result;
|
|
887
|
+
source?.release();
|
|
888
|
+
this._disposeGLTFLoader( loader );
|
|
803
889
|
|
|
804
|
-
|
|
890
|
+
}
|
|
805
891
|
|
|
806
|
-
|
|
892
|
+
}
|
|
807
893
|
|
|
808
|
-
|
|
894
|
+
// Append a copy of a caller-owned Object3D without releasing prior models or reframing.
|
|
895
|
+
appendObject3D( object3d, name = 'object3d' ) {
|
|
809
896
|
|
|
810
|
-
|
|
811
|
-
throw error;
|
|
897
|
+
this._appended = true;
|
|
812
898
|
|
|
813
|
-
|
|
899
|
+
const root = this._adoptExternalObject( object3d );
|
|
900
|
+
root.name = object3d.name || name;
|
|
901
|
+
this._processAndParent( root );
|
|
902
|
+
return { root };
|
|
814
903
|
|
|
815
904
|
}
|
|
816
905
|
|
|
817
|
-
|
|
906
|
+
// Detach + dispose a model root. An adopted root's geometry/materials belong to the
|
|
907
|
+
// caller, so only the engine's own allocations go.
|
|
908
|
+
removeModelRoot( root ) {
|
|
818
909
|
|
|
819
|
-
|
|
820
|
-
if ( result ) return result;
|
|
821
|
-
return await this.findAndLoadModelFromZip( entries, filename );
|
|
910
|
+
if ( ! root ) return;
|
|
822
911
|
|
|
823
|
-
|
|
912
|
+
if ( root.userData.__rayzeeExternal ) {
|
|
824
913
|
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
* entry only when the scene asks for it.
|
|
828
|
-
* @private
|
|
829
|
-
*/
|
|
830
|
-
async _loadSeekable( file, filename, { element, promptBytes, pbrtEntry, pbrt, index = null, zip = false, archiveId = null } ) {
|
|
914
|
+
disposeEngineOwnedResources( root );
|
|
915
|
+
root.parent?.remove( root );
|
|
831
916
|
|
|
832
|
-
|
|
833
|
-
source.archiveId = archiveId;
|
|
834
|
-
source.elements = ( Array.isArray( element ) ? element : [ element ] ).filter( Boolean );
|
|
835
|
-
if ( source.listing.some( e => e.path.toLowerCase().endsWith( '.pbrt' ) ) ) {
|
|
917
|
+
} else {
|
|
836
918
|
|
|
837
|
-
|
|
919
|
+
disposeObjectFromMemory( root );
|
|
838
920
|
|
|
839
921
|
}
|
|
840
922
|
|
|
841
|
-
// Not a pbrt scene: fall back to materialising it, which those paths still expect.
|
|
842
|
-
for ( const e of source.listing ) if ( e.offset !== undefined ) source.entries[ e.path ] ??= await source.read( e.path );
|
|
843
|
-
const result = await this._loadNonPBRTArchive( source.entries, filename );
|
|
844
|
-
this._sourceKey = this._keyed( 'archive', archiveId );
|
|
845
|
-
return result;
|
|
846
|
-
|
|
847
923
|
}
|
|
848
924
|
|
|
849
|
-
|
|
850
|
-
* The parsed scene stored for this archive and these options, decoded — its textures read back
|
|
851
|
-
* out of the archive — or null.
|
|
852
|
-
* @private
|
|
853
|
-
*/
|
|
854
|
-
async _loadStoredGraph( key, source, { imageFromBytes, envFromBytes } ) {
|
|
925
|
+
async loadGLBFromArrayBuffer( arrayBuffer, filename = 'model.glb' ) {
|
|
855
926
|
|
|
856
|
-
const
|
|
857
|
-
const entry = area ? await area.open( key ) : null;
|
|
858
|
-
if ( ! entry ) return null;
|
|
927
|
+
const loader = await this.createGLTFLoader();
|
|
859
928
|
|
|
860
|
-
const start = performance.now();
|
|
861
|
-
let failure = null;
|
|
862
929
|
try {
|
|
863
930
|
|
|
864
|
-
updateLoading( { isLoading: true, status:
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
const
|
|
868
|
-
|
|
869
|
-
loadTexture: async ( path, loader ) => {
|
|
931
|
+
updateLoading( { isLoading: true, status: "Processing GLB Data...", progress: 5 } );
|
|
932
|
+
await new Promise( r => setTimeout( r, 0 ) );
|
|
933
|
+
|
|
934
|
+
const data = await withHostWorker( () => loader.parseAsync( arrayBuffer, '' ) );
|
|
935
|
+
this._throwDeferred();
|
|
870
936
|
|
|
871
|
-
|
|
872
|
-
if ( ! bytes ) return null;
|
|
873
|
-
return loader === 'environment' ? envFromBytes( bytes, path ) : imageFromBytes( bytes, path );
|
|
937
|
+
this.releaseTargetModel();
|
|
874
938
|
|
|
875
|
-
|
|
876
|
-
|
|
939
|
+
this.targetModel = data.scene;
|
|
940
|
+
this.animations = data.animations || [];
|
|
941
|
+
this.sceneMetadata = extractSceneMetadata( data );
|
|
942
|
+
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
943
|
+
await this.onModelLoad( this.targetModel );
|
|
877
944
|
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
group: decoded.root,
|
|
881
|
-
environment: decoded.environment ? { texture: decoded.environment } : null,
|
|
882
|
-
animations: decoded.animations,
|
|
883
|
-
report: null,
|
|
884
|
-
parseMs: 0,
|
|
885
|
-
buildMs: performance.now() - start,
|
|
886
|
-
fromStorage: true,
|
|
887
|
-
};
|
|
945
|
+
this.dispatchEvent( { type: 'load', model: data.scene, filename } );
|
|
946
|
+
return data;
|
|
888
947
|
|
|
889
948
|
} catch ( error ) {
|
|
890
949
|
|
|
891
|
-
|
|
892
|
-
|
|
950
|
+
console.error( 'Error loading GLB:', error );
|
|
951
|
+
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
952
|
+
throw error;
|
|
893
953
|
|
|
894
954
|
} finally {
|
|
895
955
|
|
|
896
|
-
|
|
897
|
-
if ( failure ) {
|
|
898
|
-
|
|
899
|
-
console.warn( `The stored scene could not be used, parsing the archive instead: ${failure.message}` );
|
|
900
|
-
this._issues?.warn( ISSUE_CODES.STORAGE_ENTRY_CORRUPT, `stored scene unusable: ${failure.message}`, { key } );
|
|
901
|
-
area.remove( key ).catch( () => {} );
|
|
902
|
-
|
|
903
|
-
}
|
|
956
|
+
this._disposeGLTFLoader( loader );
|
|
904
957
|
|
|
905
958
|
}
|
|
906
959
|
|
|
907
960
|
}
|
|
908
961
|
|
|
909
|
-
|
|
910
|
-
* Stores a freshly parsed scene for the next open. Encoded here, before onModelLoad or the
|
|
911
|
-
* build touch it; written in the background.
|
|
912
|
-
* @private
|
|
913
|
-
*/
|
|
914
|
-
_storeGraph( key, built, label ) {
|
|
915
|
-
|
|
916
|
-
const area = this.storage?.area( ENGINE_AREAS.SCENES );
|
|
917
|
-
if ( ! area ) return;
|
|
962
|
+
async loadObject3D( object3d, name = 'object3d' ) {
|
|
918
963
|
|
|
919
|
-
|
|
920
|
-
try {
|
|
964
|
+
this.releaseTargetModel();
|
|
921
965
|
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
animations: built.animations ?? [],
|
|
925
|
-
stats: {
|
|
926
|
-
meshCount: built.meshCount, entryPath: built.entryPath, candidates: built.candidates, frames: built.frames,
|
|
927
|
-
warnings: built.warnings, triangleCount: built.triangleCount, placementCount: built.placementCount,
|
|
928
|
-
mergedShapes: built.mergedShapes, skippedForBudget: built.skippedForBudget, droppedNoTemplate: built.droppedNoTemplate,
|
|
929
|
-
},
|
|
930
|
-
} );
|
|
966
|
+
const model = this._adoptExternalObject( object3d );
|
|
967
|
+
model.name = object3d.name || name;
|
|
931
968
|
|
|
932
|
-
|
|
969
|
+
this.targetModel = model;
|
|
933
970
|
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
return;
|
|
937
|
-
|
|
938
|
-
}
|
|
939
|
-
|
|
940
|
-
const pending = { key, encoded, label, parseMs: ( built.parseMs ?? 0 ) + ( built.buildMs ?? 0 ), triangles: built.triangleCount ?? null };
|
|
941
|
-
|
|
942
|
-
// A parse slow enough on its own is written now, during the build. Held until the build
|
|
943
|
-
// ends, it kept every array the build replaces alive with it — the float normals and the
|
|
944
|
-
// instance matrices, ~1 GB on the whole Moana subset, at the build's peak.
|
|
945
|
-
if ( worthStoring( pending.parseMs, encoded.byteLength ) ) this._writeGraph( area, pending );
|
|
946
|
-
else this._pendingGraph = pending;
|
|
947
|
-
|
|
948
|
-
}
|
|
949
|
-
|
|
950
|
-
/**
|
|
951
|
-
* Writes the scene encoded at parse time once the build is done, when the whole cold load —
|
|
952
|
-
* parse plus build — was slow enough to be worth it (decision D6).
|
|
953
|
-
* @param {number} buildMs - the SceneProcessor build that followed the parse
|
|
954
|
-
*/
|
|
955
|
-
flushPendingGraph( buildMs ) {
|
|
956
|
-
|
|
957
|
-
const pending = this._pendingGraph;
|
|
958
|
-
this._pendingGraph = null;
|
|
959
|
-
const area = this.storage?.area( ENGINE_AREAS.SCENES );
|
|
960
|
-
if ( ! pending || ! area || ! worthStoring( pending.parseMs + buildMs, pending.encoded.byteLength ) ) return;
|
|
961
|
-
this._writeGraph( area, pending );
|
|
962
|
-
|
|
963
|
-
}
|
|
964
|
-
|
|
965
|
-
/** Writes an encoded graph in the background, letting each array go once it is on disk. @private */
|
|
966
|
-
_writeGraph( area, pending ) {
|
|
967
|
-
|
|
968
|
-
( async () => {
|
|
969
|
-
|
|
970
|
-
const writer = await area.create( pending.key, { label: `${pending.label} (scene)`, expectedBytes: pending.encoded.byteLength } );
|
|
971
|
-
if ( ! writer ) return;
|
|
972
|
-
try {
|
|
973
|
-
|
|
974
|
-
await writeSceneGraph( writer, pending.encoded, { release: true } );
|
|
975
|
-
await writer.commit( { triangles: pending.triangles } );
|
|
976
|
-
|
|
977
|
-
} catch ( error ) {
|
|
978
|
-
|
|
979
|
-
await writer.abort();
|
|
980
|
-
console.warn( 'Storing the scene failed:', error );
|
|
981
|
-
|
|
982
|
-
}
|
|
983
|
-
|
|
984
|
-
} )().catch( error => console.warn( 'Storing the scene failed:', error ) );
|
|
985
|
-
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
/**
|
|
989
|
-
* Index a seekable archive without retaining it. Entries are read back from the File on
|
|
990
|
-
* demand, so residency is the open include chain rather than the whole archive.
|
|
991
|
-
*
|
|
992
|
-
* Indexing is cheap and the whole archive is never held, but *parsing* all of it is not:
|
|
993
|
-
* the scene that comes out is what runs the tab out of memory. So a large multi-part archive
|
|
994
|
-
* stops here and asks which parts to load, the same as the streamed path does. The index is
|
|
995
|
-
* already built at that point, so the question costs nothing.
|
|
996
|
-
* @private
|
|
997
|
-
*/
|
|
998
|
-
async _openSeekableArchive( file, filename, element, promptBytes, { index = null, zip = false } = {} ) {
|
|
999
|
-
|
|
1000
|
-
const chosen = Array.isArray( element ) ? element.filter( Boolean ) : ( element ? [ element ] : [] );
|
|
1001
|
-
const filter = chosen.length ? elementFilter( chosen ) : null;
|
|
1002
|
-
|
|
1003
|
-
let source;
|
|
1004
|
-
if ( zip ) {
|
|
1005
|
-
|
|
1006
|
-
source = await openZip( file, { filter } );
|
|
1007
|
-
|
|
1008
|
-
} else {
|
|
1009
|
-
|
|
1010
|
-
const saved = index ?? await loadTarIndex( file, this.storage );
|
|
1011
|
-
source = await openTar( file, {
|
|
1012
|
-
index: saved,
|
|
1013
|
-
headersOnly: true,
|
|
1014
|
-
filter,
|
|
1015
|
-
onProgress: p => updateLoading( {
|
|
1016
|
-
isLoading: true, status: `Indexing archive… ${( p.bytes / 1e9 ).toFixed( 1 )} GB`, progress: 4
|
|
1017
|
-
} ),
|
|
1018
|
-
} );
|
|
1019
|
-
if ( ! saved ) saveTarIndex( file, this.storage, source.index ).catch( () => {} );
|
|
1020
|
-
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
const totalBytes = source.listing.reduce( ( n, e ) => n + e.size, 0 );
|
|
1024
|
-
|
|
1025
|
-
if ( chosen.length === 0 ) this._requireElementChoice( filename, source.listing, totalBytes, promptBytes );
|
|
1026
|
-
|
|
1027
|
-
console.info(
|
|
1028
|
-
`Archive "${filename}": indexed ${source.indexed} entries, ` +
|
|
1029
|
-
`${( totalBytes / 1e9 ).toFixed( 1 )} GB, none resident` +
|
|
1030
|
-
( chosen.length ? `, ${chosen.length} part${chosen.length > 1 ? 's' : ''} selected.` : '.' )
|
|
1031
|
-
);
|
|
1032
|
-
|
|
1033
|
-
return source;
|
|
1034
|
-
|
|
1035
|
-
}
|
|
1036
|
-
|
|
1037
|
-
/**
|
|
1038
|
-
* Stop and ask which parts to load, when the archive is big enough that the answer matters
|
|
1039
|
-
* and it actually has parts to choose between. Not a capability limit — selecting every part
|
|
1040
|
-
* is a valid answer and loads the whole scene.
|
|
1041
|
-
* @private
|
|
1042
|
-
*/
|
|
1043
|
-
_requireElementChoice( filename, listing, totalBytes, promptBytes = ARCHIVE_ELEMENT_PROMPT_BYTES ) {
|
|
1044
|
-
|
|
1045
|
-
if ( totalBytes < promptBytes ) return;
|
|
1046
|
-
|
|
1047
|
-
const { root, elements } = listArchiveElements( listing );
|
|
1048
|
-
if ( elements.length < 2 ) return;
|
|
1049
|
-
|
|
1050
|
-
const error = new Error(
|
|
1051
|
-
`"${filename}" holds ${( totalBytes / 1e9 ).toFixed( 1 )} GB across ${elements.length} parts. ` +
|
|
1052
|
-
'Choose which to load — loading all of them at once may exhaust memory.'
|
|
1053
|
-
);
|
|
1054
|
-
error.code = 'ARCHIVE_NEEDS_ELEMENT';
|
|
1055
|
-
error.root = root;
|
|
1056
|
-
error.elements = elements;
|
|
1057
|
-
error.totalBytes = totalBytes;
|
|
1058
|
-
|
|
1059
|
-
// Logged as a warning on purpose: recording an ERROR makes a strict host throw an
|
|
1060
|
-
// EngineIssueError from inside record(), and the caller never learns which parts it
|
|
1061
|
-
// could have chosen. The typed throw below is the refusal, and it carries the list.
|
|
1062
|
-
this._issues?.record(
|
|
1063
|
-
ISSUE_CODES.ASSET_ARCHIVE_TOO_LARGE,
|
|
1064
|
-
`archive holds ${( totalBytes / 1e9 ).toFixed( 1 )} GB across ${elements.length} parts; choose which to load`,
|
|
1065
|
-
{ root, elements: elements.map( e => e.prefix ), totalBytes },
|
|
1066
|
-
ISSUE_SEVERITY.WARNING
|
|
1067
|
-
);
|
|
1068
|
-
|
|
1069
|
-
throw error;
|
|
1070
|
-
|
|
1071
|
-
}
|
|
1072
|
-
|
|
1073
|
-
async _readStreamedArchive( file, filename, kind, element, byteBudget ) {
|
|
1074
|
-
|
|
1075
|
-
const chosen = Array.isArray( element ) ? element.filter( Boolean ) : ( element ? [ element ] : [] );
|
|
1076
|
-
const { entries, listing, retainedBytes, truncated } = await readTarGz( file, {
|
|
1077
|
-
filter: chosen.length ? elementFilter( chosen ) : null,
|
|
1078
|
-
...( byteBudget === undefined ? {} : { byteBudget } ),
|
|
1079
|
-
onProgress: p => updateLoading( {
|
|
1080
|
-
isLoading: true,
|
|
1081
|
-
status: `Reading archive… ${( p.bytes / 1e9 ).toFixed( 1 )} GB`,
|
|
1082
|
-
progress: 4
|
|
1083
|
-
} )
|
|
1084
|
-
} );
|
|
1085
|
-
|
|
1086
|
-
// Choosing every part is a valid answer, so a selection is not refused again for being
|
|
1087
|
-
// large: the read budget exists to stop an unasked-for whole-archive load. Past this
|
|
1088
|
-
// point the scene's own memory preflight is what refuses, with a figure to act on.
|
|
1089
|
-
if ( truncated && chosen.length === 0 ) {
|
|
1090
|
-
|
|
1091
|
-
const { root, elements } = listArchiveElements( listing );
|
|
1092
|
-
const total = listing.reduce( ( n, e ) => n + e.size, 0 );
|
|
1093
|
-
const error = new Error(
|
|
1094
|
-
`"${filename}" unpacks to ${( total / 1e9 ).toFixed( 1 )} GB, past the load budget. ` +
|
|
1095
|
-
`Load one of its ${elements.length} parts instead.`
|
|
1096
|
-
);
|
|
1097
|
-
error.code = 'ARCHIVE_NEEDS_ELEMENT';
|
|
1098
|
-
error.root = root;
|
|
1099
|
-
error.elements = elements;
|
|
1100
|
-
error.totalBytes = total;
|
|
1101
|
-
// Warning, not error: a strict host's throw from record() would replace the typed
|
|
1102
|
-
// error below and take the part list with it.
|
|
1103
|
-
this._issues?.record(
|
|
1104
|
-
ISSUE_CODES.ASSET_ARCHIVE_TOO_LARGE,
|
|
1105
|
-
`archive unpacks to ${( total / 1e9 ).toFixed( 1 )} GB; choose one of ${elements.length} parts`,
|
|
1106
|
-
{ root, elements: elements.map( e => e.prefix ), totalBytes: total },
|
|
1107
|
-
ISSUE_SEVERITY.WARNING
|
|
1108
|
-
);
|
|
1109
|
-
throw error;
|
|
1110
|
-
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
|
-
if ( truncated ) {
|
|
1114
|
-
|
|
1115
|
-
const keep = elementFilter( chosen );
|
|
1116
|
-
const missing = listing.filter( e => keep( e.path, e.size ) && ! entries[ e.path ] ).length;
|
|
1117
|
-
this._issues?.record(
|
|
1118
|
-
ISSUE_CODES.ASSET_ARCHIVE_TOO_LARGE,
|
|
1119
|
-
`${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`,
|
|
1120
|
-
{ elements: chosen, missing, retainedBytes },
|
|
1121
|
-
ISSUE_SEVERITY.WARNING
|
|
1122
|
-
);
|
|
1123
|
-
|
|
1124
|
-
}
|
|
1125
|
-
|
|
1126
|
-
if ( chosen.length ) {
|
|
1127
|
-
|
|
1128
|
-
console.info(
|
|
1129
|
-
`Archive "${filename}": loaded ${chosen.map( c => `"${c}"` ).join( ', ' )} — ` +
|
|
1130
|
-
`${Object.keys( entries ).length} of ${listing.length} files, ${( retainedBytes / 1048576 ).toFixed( 1 )} MB.`
|
|
1131
|
-
);
|
|
1132
|
-
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
return entries;
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
/**
|
|
1140
|
-
* Loads a pbrt-v4 scene from an unzipped archive. Parses the entry .pbrt
|
|
1141
|
-
* (following Include/Import), builds a THREE.Group, sets the infinite light
|
|
1142
|
-
* as the scene environment, and runs the standard onModelLoad pipeline.
|
|
1143
|
-
* @param {Object<string, Uint8Array>} zip - unzipped entries (path → bytes); consumed, entry by entry
|
|
1144
|
-
* @param {string} filename - original archive name (for display/events)
|
|
1145
|
-
*/
|
|
1146
|
-
async loadPBRTFromZip( zip, filename, entryPath = null, options = {}, source = null ) {
|
|
1147
|
-
|
|
1148
|
-
updateLoading( { isLoading: true, status: 'Parsing PBRT scene...', progress: 5 } );
|
|
1149
|
-
|
|
1150
|
-
// Geometry decoder — reuse the cached PLYLoader (pbrt leans on .ply meshes).
|
|
1151
|
-
if ( ! this.loaderCache.ply ) {
|
|
1152
|
-
|
|
1153
|
-
const { PLYLoader } = await import( 'three/examples/jsm/loaders/PLYLoader.js' );
|
|
1154
|
-
this.loaderCache.ply = new PLYLoader();
|
|
1155
|
-
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
const plyParser = ( buf ) => this.loaderCache.ply.parse( buf );
|
|
1159
|
-
|
|
1160
|
-
// Texture maps — decode by extension (pbrt uses .png/.jpg but also .exr/.hdr/.tga). Tagged
|
|
1161
|
-
// with their archive path so a stored scene can read them back instead of keeping pixels.
|
|
1162
|
-
const imageFromBytes = async ( bytes, fname ) => {
|
|
1163
|
-
|
|
1164
|
-
const texture = await this._pbrtTextureFromBytes( bytes, fname );
|
|
1165
|
-
if ( texture ) texture.userData[ ARCHIVE_PATH ] = fname;
|
|
1166
|
-
return texture;
|
|
1167
|
-
|
|
1168
|
-
};
|
|
1169
|
-
|
|
1170
|
-
// Infinite-light maps → HDR/EXR/LDR via the shared environment decoder.
|
|
1171
|
-
const envFromBytes = async ( bytes, fname ) => {
|
|
1172
|
-
|
|
1173
|
-
const ext = fname.split( '.' ).pop().toLowerCase();
|
|
1174
|
-
const blob = new Blob( [ bytes ] );
|
|
1175
|
-
const url = URL.createObjectURL( blob );
|
|
1176
|
-
try {
|
|
1177
|
-
|
|
1178
|
-
const texture = await this.loadEnvironmentByExtension( url, ext );
|
|
1179
|
-
setEnvironmentSource( texture, `bytes:${await sampleHash( blob )}` );
|
|
1180
|
-
texture.userData[ ARCHIVE_PATH ] = fname;
|
|
1181
|
-
texture.userData[ ARCHIVE_LOADER ] = 'environment';
|
|
1182
|
-
return texture;
|
|
1183
|
-
|
|
1184
|
-
} finally {
|
|
1185
|
-
|
|
1186
|
-
URL.revokeObjectURL( url );
|
|
1187
|
-
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
};
|
|
1191
|
-
|
|
1192
|
-
const pbrtStart = performance.now();
|
|
1193
|
-
const shape = {
|
|
1194
|
-
animation: options.animation, maxTriangles: options.maxTriangles, maxPlacements: options.maxPlacements,
|
|
1195
|
-
mergeShapesAbove: options.mergeShapesAbove, curveSteps: options.curveSteps, curveSides: options.curveSides,
|
|
1196
|
-
curveTolerance: options.curveTolerance, instanceIncludes: options.instanceIncludes,
|
|
1197
|
-
};
|
|
1198
|
-
const graphKey = source?.archiveId
|
|
1199
|
-
? this._keyed( `pbrt:${SCENE_GRAPH_FORMAT}.${PBRT_BUILD_REVISION}:${VERSION}`, `${source.archiveId}|${entryPath ?? ''}|${[ ...( source.elements ?? [] ) ].sort().join( ',' )}|${JSON.stringify( shape )}` )
|
|
1200
|
-
: null;
|
|
1201
|
-
|
|
1202
|
-
const stored = graphKey ? await this._loadStoredGraph( graphKey, source, { imageFromBytes, envFromBytes } ) : null;
|
|
1203
|
-
const built = stored ?? await loadPBRTScene( { vfs: zip, source, entryPath, plyParser, imageFromBytes, envFromBytes, ...shape } );
|
|
1204
|
-
|
|
1205
|
-
const { group, environment, animations, report, warnings, meshCount, entryPath: loadedEntry, candidates,
|
|
1206
|
-
frames, parseMs, buildMs, triangleCount, placementCount, mergedShapes, skippedForBudget,
|
|
1207
|
-
droppedNoTemplate } = built;
|
|
1208
|
-
if ( stored ) console.info( `PBRT scene "${loadedEntry}" opened from storage in ${( buildMs / 1000 ).toFixed( 1 )} s, skipping the parse` );
|
|
1209
|
-
|
|
1210
|
-
// Phase breakdown for scaling work; the engine's own build timings live in
|
|
1211
|
-
// SceneProcessor.performanceMetrics.
|
|
1212
|
-
this.lastPBRTStats = {
|
|
1213
|
-
parseMs, buildMs, loaderMs: performance.now() - pbrtStart,
|
|
1214
|
-
triangleCount, placementCount, mergedShapes, skippedForBudget, droppedNoTemplate, meshCount,
|
|
1215
|
-
frames: frames?.length ?? 1
|
|
1216
|
-
};
|
|
1217
|
-
|
|
1218
|
-
// An archive can hold several independent scenes (bistro ships three views). Only one is
|
|
1219
|
-
// loaded, so name it and the alternatives rather than leave the user comparing against a
|
|
1220
|
-
// reference of a different scene. A frame sequence loads whole, as its animation.
|
|
1221
|
-
const loaded = new Set( frames ?? [ loadedEntry ] );
|
|
1222
|
-
const others = candidates.filter( p => ! loaded.has( p ) );
|
|
1223
|
-
if ( others.length > 0 ) {
|
|
1224
|
-
|
|
1225
|
-
console.warn( `PBRT archive holds ${candidates.length} scenes; loaded "${loadedEntry}". Others: ${others.join( ', ' )}` );
|
|
1226
|
-
this._issues?.record(
|
|
1227
|
-
ISSUE_CODES.ASSET_AMBIGUOUS_ENTRY,
|
|
1228
|
-
`archive holds ${candidates.length} pbrt scenes; loaded "${loadedEntry}"`,
|
|
1229
|
-
{ loaded: loadedEntry, alternatives: others },
|
|
1230
|
-
ISSUE_SEVERITY.WARNING
|
|
1231
|
-
);
|
|
1232
|
-
|
|
1233
|
-
}
|
|
1234
|
-
|
|
1235
|
-
// Diagnostics — surface what each mesh resolved to (helps debug black/wrong materials).
|
|
1236
|
-
if ( report && report.length && typeof console.table === 'function' ) {
|
|
1237
|
-
|
|
1238
|
-
const shown = report.length < meshCount ? ` (first ${report.length} of ${meshCount})` : '';
|
|
1239
|
-
console.groupCollapsed( `PBRT loader: ${meshCount} mesh(es) from "${loadedEntry}"${shown}` );
|
|
1240
|
-
console.table( report );
|
|
1241
|
-
console.groupEnd();
|
|
1242
|
-
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
if ( warnings && warnings.length ) {
|
|
1246
|
-
|
|
1247
|
-
console.warn( `PBRT loader: ${warnings.length} warning(s) parsing "${loadedEntry}"` );
|
|
1248
|
-
warnings.forEach( w => console.warn( ' •', w ) );
|
|
1249
|
-
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
// Infinite light → scene environment (CDF is built later in loadSceneData).
|
|
1253
|
-
if ( environment?.texture ) {
|
|
1254
|
-
|
|
1255
|
-
environment.texture.generateMipmaps = true;
|
|
1256
|
-
this.applyEnvironmentToScene( environment.texture );
|
|
1257
|
-
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1260
|
-
group.name = loadedEntry || filename;
|
|
1261
|
-
this.releaseTargetModel();
|
|
1262
|
-
this._sourceKey = graphKey;
|
|
1263
|
-
// Before onModelLoad and the build touch the new group.
|
|
1264
|
-
if ( graphKey && ! stored ) this._storeGraph( graphKey, built, filename );
|
|
1265
|
-
this.targetModel = group;
|
|
1266
|
-
this.animations = animations ?? [];
|
|
1267
|
-
|
|
1268
|
-
// The light's own orientation and `scale` are already baked into the texture, so the
|
|
1269
|
-
// scene is only correct at rotation 0 / intensity 1 — pinned, whatever the host's defaults.
|
|
1270
|
-
if ( environment?.texture ) this.sceneMetadata = { environment: { rotation: 0, intensity: 1 } };
|
|
1271
|
-
|
|
1272
|
-
updateLoading( { isLoading: true, status: 'Processing PBRT geometry...', progress: 10 } );
|
|
1273
|
-
await this.onModelLoad( this.targetModel );
|
|
1274
|
-
|
|
1275
|
-
this.dispatchEvent( { type: 'load', model: group, filename: `${loadedEntry} (from ZIP)` } );
|
|
1276
|
-
return group;
|
|
1277
|
-
|
|
1278
|
-
}
|
|
1279
|
-
|
|
1280
|
-
/**
|
|
1281
|
-
* Decodes a pbrt texture map from raw bytes, picking a decoder by extension.
|
|
1282
|
-
* ImageBitmap can't handle EXR/HDR/TGA, which pbrt scenes use freely.
|
|
1283
|
-
* @param {Uint8Array} bytes
|
|
1284
|
-
* @param {string} fname
|
|
1285
|
-
* @returns {Promise<import('three').Texture>}
|
|
1286
|
-
*/
|
|
1287
|
-
async _pbrtTextureFromBytes( bytes, fname ) {
|
|
1288
|
-
|
|
1289
|
-
const ext = fname.split( '.' ).pop().toLowerCase();
|
|
1290
|
-
let tex;
|
|
1291
|
-
|
|
1292
|
-
if ( ext === 'exr' || ext === 'hdr' ) {
|
|
1293
|
-
|
|
1294
|
-
const loader = ext === 'hdr'
|
|
1295
|
-
? ( this.loaderCache.hdr || ( this.loaderCache.hdr = new HDRLoader().setDataType( FloatType ) ) )
|
|
1296
|
-
: ( this.loaderCache.exr || ( this.loaderCache.exr = new EXRLoader().setDataType( FloatType ) ) );
|
|
1297
|
-
tex = await this._loadViaObjectURL( loader, bytes );
|
|
1298
|
-
// HDR/EXR maps are linear — leave colorSpace as the loader set it.
|
|
1299
|
-
|
|
1300
|
-
} else if ( ext === 'tga' ) {
|
|
1301
|
-
|
|
1302
|
-
if ( ! this.loaderCache.tga ) {
|
|
1303
|
-
|
|
1304
|
-
const { TGALoader } = await import( 'three/examples/jsm/loaders/TGALoader.js' );
|
|
1305
|
-
this.loaderCache.tga = new TGALoader();
|
|
1306
|
-
|
|
1307
|
-
}
|
|
1308
|
-
|
|
1309
|
-
tex = await this._loadViaObjectURL( this.loaderCache.tga, bytes );
|
|
1310
|
-
tex.colorSpace = SRGBColorSpace;
|
|
1311
|
-
|
|
1312
|
-
} else {
|
|
1313
|
-
|
|
1314
|
-
// png / jpg / webp / gif / bmp
|
|
1315
|
-
const bitmap = await createImageBitmap( new Blob( [ bytes ] ) );
|
|
1316
|
-
tex = new Texture( bitmap );
|
|
1317
|
-
tex.colorSpace = SRGBColorSpace;
|
|
1318
|
-
|
|
1319
|
-
}
|
|
1320
|
-
|
|
1321
|
-
tex.wrapS = tex.wrapT = RepeatWrapping;
|
|
1322
|
-
tex.needsUpdate = true;
|
|
1323
|
-
return tex;
|
|
1324
|
-
|
|
1325
|
-
}
|
|
1326
|
-
|
|
1327
|
-
/** Decode bytes through a three loader's loadAsync via a transient object URL. */
|
|
1328
|
-
async _loadViaObjectURL( loader, bytes ) {
|
|
1329
|
-
|
|
1330
|
-
const url = URL.createObjectURL( new Blob( [ bytes ] ) );
|
|
1331
|
-
try {
|
|
1332
|
-
|
|
1333
|
-
return await loader.loadAsync( url );
|
|
1334
|
-
|
|
1335
|
-
} finally {
|
|
1336
|
-
|
|
1337
|
-
URL.revokeObjectURL( url );
|
|
1338
|
-
|
|
1339
|
-
}
|
|
1340
|
-
|
|
1341
|
-
}
|
|
1342
|
-
|
|
1343
|
-
async processObjMtlPairsInZip( zip, filename ) {
|
|
1344
|
-
|
|
1345
|
-
const objFiles = [];
|
|
1346
|
-
const mtlFiles = [];
|
|
1347
|
-
|
|
1348
|
-
for ( const path in zip ) {
|
|
1349
|
-
|
|
1350
|
-
const lowerPath = path.toLowerCase();
|
|
1351
|
-
if ( lowerPath.endsWith( '.obj' ) ) objFiles.push( { path, content: zip[ path ] } );
|
|
1352
|
-
else if ( lowerPath.endsWith( '.mtl' ) ) mtlFiles.push( { path, content: zip[ path ] } );
|
|
1353
|
-
|
|
1354
|
-
}
|
|
1355
|
-
|
|
1356
|
-
if ( objFiles.length > 0 && mtlFiles.length > 0 ) {
|
|
1357
|
-
|
|
1358
|
-
console.log( `Found ${objFiles.length} OBJ files and ${mtlFiles.length} MTL files in ZIP` );
|
|
1359
|
-
const matches = this.findMatchingObjMtlPairs( objFiles, mtlFiles );
|
|
1360
|
-
|
|
1361
|
-
if ( matches.length > 0 ) {
|
|
1362
|
-
|
|
1363
|
-
console.log( `Found ${matches.length} matching OBJ+MTL pairs` );
|
|
1364
|
-
return await this.loadOBJMTLPairFromZip( matches[ 0 ].obj, matches[ 0 ].mtl, zip, filename );
|
|
1365
|
-
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
|
-
if ( matches.length === 0 ) {
|
|
1369
|
-
|
|
1370
|
-
console.log( 'No matching pairs by name, using first OBJ and MTL files' );
|
|
1371
|
-
return await this.loadOBJMTLPairFromZip( objFiles[ 0 ], mtlFiles[ 0 ], zip, filename );
|
|
1372
|
-
|
|
1373
|
-
}
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
return null;
|
|
1378
|
-
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
findMatchingObjMtlPairs( objFiles, mtlFiles ) {
|
|
1382
|
-
|
|
1383
|
-
const matches = [];
|
|
1384
|
-
for ( const objFile of objFiles ) {
|
|
1385
|
-
|
|
1386
|
-
const objBaseName = objFile.path.split( '/' ).pop().replace( /\.obj$/i, '' ).toLowerCase();
|
|
1387
|
-
|
|
1388
|
-
for ( const mtlFile of mtlFiles ) {
|
|
1389
|
-
|
|
1390
|
-
const mtlBaseName = mtlFile.path.split( '/' ).pop().replace( /\.mtl$/i, '' ).toLowerCase();
|
|
1391
|
-
if ( objBaseName === mtlBaseName || objBaseName.includes( mtlBaseName ) || mtlBaseName.includes( objBaseName ) ) {
|
|
1392
|
-
|
|
1393
|
-
matches.push( { obj: objFile, mtl: mtlFile } );
|
|
1394
|
-
break;
|
|
1395
|
-
|
|
1396
|
-
}
|
|
1397
|
-
|
|
1398
|
-
}
|
|
1399
|
-
|
|
1400
|
-
}
|
|
1401
|
-
|
|
1402
|
-
return matches;
|
|
1403
|
-
|
|
1404
|
-
}
|
|
1405
|
-
|
|
1406
|
-
async findAndLoadModelFromZip( zip ) {
|
|
1407
|
-
|
|
1408
|
-
const mainModelFiles = [
|
|
1409
|
-
'scene.gltf', 'scene.glb', 'model.gltf', 'model.glb',
|
|
1410
|
-
'main.gltf', 'main.glb', 'asset.gltf', 'asset.glb'
|
|
1411
|
-
];
|
|
1412
|
-
|
|
1413
|
-
for ( const mainFile of mainModelFiles ) {
|
|
1414
|
-
|
|
1415
|
-
if ( zip[ mainFile ] ) {
|
|
1416
|
-
|
|
1417
|
-
console.log( `Found main model file: ${mainFile}` );
|
|
1418
|
-
const extension = mainFile.split( '.' ).pop().toLowerCase();
|
|
1419
|
-
return await this.loadModelFromZipEntry( zip[ mainFile ], mainFile, extension, zip );
|
|
1420
|
-
|
|
1421
|
-
}
|
|
1422
|
-
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
|
-
for ( const path in zip ) {
|
|
1426
|
-
|
|
1427
|
-
const extension = path.split( '.' ).pop().toLowerCase();
|
|
1428
|
-
if ( SUPPORTED_FORMATS[ extension ] && SUPPORTED_FORMATS[ extension ].type === 'model' ) {
|
|
1429
|
-
|
|
1430
|
-
// A loose layer is only one slice of a USD scene — hand the whole
|
|
1431
|
-
// archive over so its references and payloads can resolve.
|
|
1432
|
-
if ( USD_LAYER_RE.test( path ) ) return await this.loadUSDHierarchyFromZip( zip );
|
|
1433
|
-
|
|
1434
|
-
console.log( `Loading model file from ZIP: ${path}` );
|
|
1435
|
-
return await this.loadModelFromZipEntry( zip[ path ], path, extension, zip );
|
|
1436
|
-
|
|
1437
|
-
}
|
|
1438
|
-
|
|
1439
|
-
}
|
|
1440
|
-
|
|
1441
|
-
throw new Error( 'No supported model files found in the ZIP archive' );
|
|
1442
|
-
|
|
1443
|
-
}
|
|
1444
|
-
|
|
1445
|
-
// USDLoader only builds a cross-layer asset map on its USDZ branch, so repack
|
|
1446
|
-
// the archive as USDZ — root layer first, per AOUSD core spec 16.4.1.2 — and
|
|
1447
|
-
// let the loader resolve the references/payloads itself.
|
|
1448
|
-
async loadUSDHierarchyFromZip( zip ) {
|
|
1449
|
-
|
|
1450
|
-
const layers = Object.keys( zip ).filter( name => USD_LAYER_RE.test( name ) );
|
|
1451
|
-
if ( layers.length === 0 ) throw new Error( 'No USD layers found in the ZIP archive' );
|
|
1452
|
-
|
|
1453
|
-
const root = AssetLoader._pickUSDRootLayer( layers );
|
|
1454
|
-
console.log( `Loading USD scene from ZIP: ${root} (${layers.length} layers)` );
|
|
1455
|
-
|
|
1456
|
-
const packed = { [ root ]: [ zip[ root ], { level: 0 } ] };
|
|
1457
|
-
for ( const name of Object.keys( zip ) ) {
|
|
1458
|
-
|
|
1459
|
-
if ( name === root ) continue;
|
|
1460
|
-
if ( USD_LAYER_RE.test( name ) || USD_IMAGE_RE.test( name ) ) packed[ name ] = [ zip[ name ], { level: 0 } ];
|
|
1461
|
-
|
|
1462
|
-
}
|
|
1463
|
-
|
|
1464
|
-
return await this.loadUSDFromArrayBuffer( zipSync( packed ), root );
|
|
1465
|
-
|
|
1466
|
-
}
|
|
1467
|
-
|
|
1468
|
-
// Shallowest layer wins; among ties prefer the <dir>/<dir>.usd convention so a
|
|
1469
|
-
// set's entry point beats its sibling variants.
|
|
1470
|
-
static _pickUSDRootLayer( layers ) {
|
|
1471
|
-
|
|
1472
|
-
const depth = name => name.split( '/' ).length;
|
|
1473
|
-
const minDepth = Math.min( ...layers.map( depth ) );
|
|
1474
|
-
const candidates = layers.filter( name => depth( name ) === minDepth );
|
|
1475
|
-
|
|
1476
|
-
const conventional = candidates.find( name => {
|
|
1477
|
-
|
|
1478
|
-
const parts = name.split( '/' );
|
|
1479
|
-
return parts.length > 1 && parts[ parts.length - 1 ].replace( USD_LAYER_RE, '' ) === parts[ parts.length - 2 ];
|
|
1480
|
-
|
|
1481
|
-
} );
|
|
1482
|
-
|
|
1483
|
-
return conventional || candidates[ 0 ];
|
|
1484
|
-
|
|
1485
|
-
}
|
|
1486
|
-
|
|
1487
|
-
async loadModelFromZipEntry( fileContent, filePath, extension, zipContents ) {
|
|
1488
|
-
|
|
1489
|
-
try {
|
|
1490
|
-
|
|
1491
|
-
updateLoading( { isLoading: true, status: `Processing ${extension.toUpperCase()} from ZIP...`, progress: 5 } );
|
|
1492
|
-
const blob = new Blob( [ fileContent.buffer ], { type: 'application/octet-stream' } );
|
|
1493
|
-
const blobUrl = URL.createObjectURL( blob );
|
|
1494
|
-
let result;
|
|
1495
|
-
|
|
1496
|
-
switch ( extension ) {
|
|
1497
|
-
|
|
1498
|
-
case 'glb':
|
|
1499
|
-
case 'gltf':
|
|
1500
|
-
result = await this.handleGltfFromZip( extension, fileContent, filePath, zipContents );
|
|
1501
|
-
break;
|
|
1502
|
-
case 'fbx':
|
|
1503
|
-
result = await this.loadFBXFromArrayBuffer( fileContent.buffer, filePath );
|
|
1504
|
-
break;
|
|
1505
|
-
case 'obj':
|
|
1506
|
-
result = await this.handleObjFromZip( fileContent, filePath, zipContents );
|
|
1507
|
-
break;
|
|
1508
|
-
case 'stl':
|
|
1509
|
-
result = await this.loadSTLFromArrayBuffer( fileContent.buffer, filePath );
|
|
1510
|
-
break;
|
|
1511
|
-
case 'ply':
|
|
1512
|
-
result = await this.loadPLYFromArrayBuffer( fileContent.buffer, filePath );
|
|
1513
|
-
break;
|
|
1514
|
-
case 'dae':
|
|
1515
|
-
{
|
|
1516
|
-
|
|
1517
|
-
const daeContent = strFromU8( fileContent );
|
|
1518
|
-
const daeFile = new File( [ new Blob( [ daeContent ] ) ], filePath );
|
|
1519
|
-
result = await this.loadColladaFromFile( daeFile, filePath );
|
|
1520
|
-
|
|
1521
|
-
}
|
|
1522
|
-
|
|
1523
|
-
break;
|
|
1524
|
-
case '3mf':
|
|
1525
|
-
result = await this.load3MFFromArrayBuffer( fileContent.buffer, filePath );
|
|
1526
|
-
break;
|
|
1527
|
-
case 'usd':
|
|
1528
|
-
case 'usda':
|
|
1529
|
-
case 'usdc':
|
|
1530
|
-
case 'usdz':
|
|
1531
|
-
result = await this.loadUSDFromArrayBuffer( fileContent, filePath );
|
|
1532
|
-
break;
|
|
1533
|
-
default:
|
|
1534
|
-
throw new Error( `Support for ${extension} files is not yet implemented` );
|
|
1535
|
-
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
URL.revokeObjectURL( blobUrl );
|
|
1539
|
-
this.dispatchEvent( {
|
|
1540
|
-
type: 'load',
|
|
1541
|
-
model: this.targetModel,
|
|
1542
|
-
filename: `${filePath} (from ZIP)`
|
|
1543
|
-
} );
|
|
1544
|
-
return result;
|
|
1545
|
-
|
|
1546
|
-
} catch ( error ) {
|
|
1547
|
-
|
|
1548
|
-
console.error( `Error loading ${extension} from ZIP:`, error );
|
|
1549
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename: filePath } );
|
|
1550
|
-
throw error;
|
|
1551
|
-
|
|
1552
|
-
}
|
|
1553
|
-
|
|
1554
|
-
}
|
|
1555
|
-
|
|
1556
|
-
async handleGltfFromZip( extension, fileContent, filePath, zipContents ) {
|
|
1557
|
-
|
|
1558
|
-
if ( extension === 'gltf' ) {
|
|
1559
|
-
|
|
1560
|
-
const gltfContent = strFromU8( fileContent );
|
|
1561
|
-
const manager = new LoadingManager();
|
|
1562
|
-
const gltfDir = filePath.split( '/' ).slice( 0, - 1 ).join( '/' );
|
|
1563
|
-
|
|
1564
|
-
manager.setURLModifier( url => this.resolveZipResource( url, gltfDir, zipContents ) );
|
|
1565
|
-
const loader = await this.createGLTFLoader();
|
|
1566
|
-
loader.manager = manager;
|
|
1567
|
-
|
|
1568
|
-
try {
|
|
1569
|
-
|
|
1570
|
-
return await new Promise( ( resolve, reject ) => {
|
|
1571
|
-
|
|
1572
|
-
loader.parse( gltfContent, '',
|
|
1573
|
-
gltf => {
|
|
1574
|
-
|
|
1575
|
-
try {
|
|
1576
|
-
|
|
1577
|
-
this._throwDeferred();
|
|
1578
|
-
|
|
1579
|
-
} catch ( error ) {
|
|
1580
|
-
|
|
1581
|
-
reject( error );
|
|
1582
|
-
return;
|
|
1583
|
-
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
this.releaseTargetModel();
|
|
1587
|
-
this.targetModel = gltf.scene;
|
|
1588
|
-
this.sceneMetadata = extractSceneMetadata( gltf );
|
|
1589
|
-
this.onModelLoad( this.targetModel ).then( () => resolve( gltf ) );
|
|
1590
|
-
|
|
1591
|
-
},
|
|
1592
|
-
error => reject( error )
|
|
1593
|
-
);
|
|
1594
|
-
|
|
1595
|
-
} );
|
|
1596
|
-
|
|
1597
|
-
} finally {
|
|
1598
|
-
|
|
1599
|
-
this._disposeGLTFLoader( loader );
|
|
1600
|
-
|
|
1601
|
-
}
|
|
1602
|
-
|
|
1603
|
-
} else {
|
|
1604
|
-
|
|
1605
|
-
return await this.loadGLBFromArrayBuffer( fileContent.buffer, filePath );
|
|
1606
|
-
|
|
1607
|
-
}
|
|
1608
|
-
|
|
1609
|
-
}
|
|
1610
|
-
|
|
1611
|
-
async handleObjFromZip( fileContent, filePath, zipContents ) {
|
|
1612
|
-
|
|
1613
|
-
const objContent = strFromU8( fileContent );
|
|
1614
|
-
const mtlMatch = objContent.match( /mtllib\s+([^\s]+)/ );
|
|
1615
|
-
let materials = null;
|
|
1616
|
-
|
|
1617
|
-
if ( mtlMatch && mtlMatch[ 1 ] ) {
|
|
1618
|
-
|
|
1619
|
-
materials = await this.loadMtlFromZip( mtlMatch[ 1 ], filePath, zipContents );
|
|
1620
|
-
|
|
1621
|
-
}
|
|
1622
|
-
|
|
1623
|
-
const { OBJLoader } = await import( 'three/examples/jsm/loaders/OBJLoader.js' );
|
|
1624
|
-
const objLoader = new OBJLoader();
|
|
1625
|
-
if ( materials ) objLoader.setMaterials( materials );
|
|
1626
|
-
|
|
1627
|
-
const object = objLoader.parse( objContent );
|
|
1628
|
-
object.name = filePath;
|
|
1629
|
-
|
|
1630
|
-
this.releaseTargetModel();
|
|
1631
|
-
this.targetModel = object;
|
|
1632
|
-
await this.onModelLoad( this.targetModel );
|
|
1633
|
-
return object;
|
|
1634
|
-
|
|
1635
|
-
}
|
|
1636
|
-
|
|
1637
|
-
// MTLLoader returns Texture objects whose `.image` only lands on a later tick, and
|
|
1638
|
-
// SceneProcessor._bucketTextures drops any texture without one — so handing the model
|
|
1639
|
-
// over before the maps decode renders the whole scene untextured. Replaces preload().
|
|
1640
|
-
async preloadMtlTextures( materials ) {
|
|
1641
|
-
|
|
1642
|
-
const pending = [];
|
|
1643
|
-
const loadTexture = materials.loadTexture.bind( materials );
|
|
1644
|
-
|
|
1645
|
-
materials.loadTexture = ( url, mapping, onLoad, onProgress, onError ) => {
|
|
1646
|
-
|
|
1647
|
-
let settle;
|
|
1648
|
-
pending.push( new Promise( resolve => ( settle = resolve ) ) );
|
|
1649
|
-
return loadTexture( url, mapping,
|
|
1650
|
-
texture => {
|
|
1651
|
-
|
|
1652
|
-
settle();
|
|
1653
|
-
onLoad?.( texture );
|
|
1654
|
-
|
|
1655
|
-
},
|
|
1656
|
-
onProgress,
|
|
1657
|
-
error => {
|
|
1658
|
-
|
|
1659
|
-
settle();
|
|
1660
|
-
onError?.( error );
|
|
1661
|
-
|
|
1662
|
-
}
|
|
1663
|
-
);
|
|
1664
|
-
|
|
1665
|
-
};
|
|
1666
|
-
|
|
1667
|
-
materials.preload();
|
|
1668
|
-
if ( pending.length === 0 ) return materials;
|
|
1669
|
-
|
|
1670
|
-
let timer;
|
|
1671
|
-
const decoded = await Promise.race( [
|
|
1672
|
-
Promise.all( pending ).then( () => true ),
|
|
1673
|
-
new Promise( resolve => ( timer = setTimeout( () => resolve( false ), MTL_TEXTURE_TIMEOUT_MS ) ) )
|
|
1674
|
-
] );
|
|
1675
|
-
clearTimeout( timer );
|
|
1676
|
-
|
|
1677
|
-
if ( ! decoded ) this._issues?.record(
|
|
1678
|
-
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
1679
|
-
`MTL textures did not decode within ${MTL_TEXTURE_TIMEOUT_MS}ms; affected materials render untextured`,
|
|
1680
|
-
{ pending: pending.length }
|
|
1681
|
-
);
|
|
1682
|
-
|
|
1683
|
-
return materials;
|
|
1684
|
-
|
|
1685
|
-
}
|
|
1686
|
-
|
|
1687
|
-
async loadMtlFromZip( mtlFilename, objPath, zipContents ) {
|
|
1688
|
-
|
|
1689
|
-
const objDir = objPath.split( '/' ).slice( 0, - 1 ).join( '/' );
|
|
1690
|
-
const possibleMtlPaths = [
|
|
1691
|
-
mtlFilename,
|
|
1692
|
-
`${objDir}/${mtlFilename}`,
|
|
1693
|
-
mtlFilename.split( '/' ).pop()
|
|
1694
|
-
];
|
|
1695
|
-
|
|
1696
|
-
for ( const path of possibleMtlPaths ) {
|
|
1697
|
-
|
|
1698
|
-
if ( zipContents[ path ] ) {
|
|
1699
|
-
|
|
1700
|
-
const { MTLLoader } = await import( 'three/examples/jsm/loaders/MTLLoader.js' );
|
|
1701
|
-
const mtlContent = strFromU8( zipContents[ path ] );
|
|
1702
|
-
const manager = new LoadingManager();
|
|
1703
|
-
manager.setURLModifier( url => this.resolveZipResource( url, objDir, zipContents ) );
|
|
1704
|
-
const mtlLoader = new MTLLoader( manager );
|
|
1705
|
-
const materials = mtlLoader.parse( mtlContent, objDir );
|
|
1706
|
-
await this.preloadMtlTextures( materials );
|
|
1707
|
-
return materials;
|
|
1708
|
-
|
|
1709
|
-
}
|
|
1710
|
-
|
|
1711
|
-
}
|
|
1712
|
-
|
|
1713
|
-
return null;
|
|
1714
|
-
|
|
1715
|
-
}
|
|
1716
|
-
|
|
1717
|
-
resolveZipResource( url, baseDir, zipContents ) {
|
|
1718
|
-
|
|
1719
|
-
const normalizedUrl = url.replace( /^\.\/|^\//, '' );
|
|
1720
|
-
const possiblePaths = [
|
|
1721
|
-
normalizedUrl,
|
|
1722
|
-
`${baseDir}/${normalizedUrl}`,
|
|
1723
|
-
normalizedUrl.split( '/' ).pop()
|
|
1724
|
-
];
|
|
1725
|
-
|
|
1726
|
-
for ( const path of possiblePaths ) {
|
|
1727
|
-
|
|
1728
|
-
if ( zipContents[ path ] ) {
|
|
1729
|
-
|
|
1730
|
-
const fileBlob = new Blob( [ zipContents[ path ].buffer ], { type: 'application/octet-stream' } );
|
|
1731
|
-
return URL.createObjectURL( fileBlob );
|
|
1732
|
-
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
|
-
}
|
|
1736
|
-
|
|
1737
|
-
console.warn( `Resource not found in ZIP: ${url}` );
|
|
1738
|
-
return url;
|
|
1739
|
-
|
|
1740
|
-
}
|
|
1741
|
-
|
|
1742
|
-
async loadOBJMTLPairFromZip( objFile, mtlFile, zip, filename ) {
|
|
1743
|
-
|
|
1744
|
-
const { MTLLoader } = await import( 'three/examples/jsm/loaders/MTLLoader.js' );
|
|
1745
|
-
const { OBJLoader } = await import( 'three/examples/jsm/loaders/OBJLoader.js' );
|
|
1746
|
-
const createdUrls = [];
|
|
1747
|
-
const manager = new LoadingManager();
|
|
1748
|
-
const objDir = objFile.path.split( '/' ).slice( 0, - 1 ).join( '/' );
|
|
1749
|
-
const mtlDir = mtlFile.path.split( '/' ).slice( 0, - 1 ).join( '/' );
|
|
1750
|
-
|
|
1751
|
-
manager.setURLModifier( url => this.resolveTextureInZip( url, objDir, mtlDir, mtlFile, zip, createdUrls ) );
|
|
1752
|
-
const mtlContent = this.prepareFixedMtlContent( mtlFile );
|
|
1753
|
-
const materials = new MTLLoader( manager ).parse( mtlContent, mtlDir );
|
|
1754
|
-
await this.preloadMtlTextures( materials );
|
|
1755
|
-
|
|
1756
|
-
const objLoader = new OBJLoader( manager );
|
|
1757
|
-
objLoader.setMaterials( materials );
|
|
1758
|
-
const objContent = strFromU8( objFile.content );
|
|
1759
|
-
const object = objLoader.parse( objContent );
|
|
1760
|
-
|
|
1761
|
-
this.releaseTargetModel();
|
|
1762
|
-
this.targetModel = object;
|
|
1763
|
-
await this.onModelLoad( this.targetModel );
|
|
1764
|
-
|
|
1765
|
-
createdUrls.forEach( url => URL.revokeObjectURL( url ) );
|
|
1766
|
-
this.dispatchEvent( {
|
|
1767
|
-
type: 'load',
|
|
1768
|
-
model: object,
|
|
1769
|
-
filename: `${objFile.path} (from ${filename})`
|
|
1770
|
-
} );
|
|
1771
|
-
|
|
1772
|
-
return object;
|
|
1773
|
-
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
prepareFixedMtlContent( mtlFile ) {
|
|
1777
|
-
|
|
1778
|
-
const mtlContent = strFromU8( mtlFile.content );
|
|
1779
|
-
return mtlContent
|
|
1780
|
-
.replace( new RegExp( `${mtlFile.path.split( '/' ).pop()}\\s+`, 'g' ), ' ' )
|
|
1781
|
-
.replace( /([a-zA-Z_]+)([\\/])/g, '$1 $2' );
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
|
|
1785
|
-
resolveTextureInZip( url, objDir, mtlDir, mtlFile, zip, createdUrls ) {
|
|
1786
|
-
|
|
1787
|
-
const cleanUrl = url.split( '?' )[ 0 ].split( '#' )[ 0 ];
|
|
1788
|
-
let normalizedUrl = cleanUrl.replace( /^\.\/|^\//, '' );
|
|
1789
|
-
|
|
1790
|
-
const mtlFilename = mtlFile.path.split( '/' ).pop();
|
|
1791
|
-
if ( normalizedUrl.startsWith( mtlFilename ) ) {
|
|
1792
|
-
|
|
1793
|
-
normalizedUrl = normalizedUrl.substring( mtlFilename.length ).replace( /^\.\/|^\/|^\./, '' );
|
|
1794
|
-
|
|
1795
|
-
}
|
|
1796
|
-
|
|
1797
|
-
const possibleLocations = [
|
|
1798
|
-
normalizedUrl,
|
|
1799
|
-
`${objDir}/${normalizedUrl}`,
|
|
1800
|
-
`${mtlDir}/${normalizedUrl}`,
|
|
1801
|
-
`textures/${normalizedUrl}`,
|
|
1802
|
-
`texture/${normalizedUrl}`,
|
|
1803
|
-
`materials/${normalizedUrl}`,
|
|
1804
|
-
normalizedUrl.split( '/' ).pop()
|
|
1805
|
-
];
|
|
1806
|
-
|
|
1807
|
-
for ( const location of possibleLocations ) {
|
|
1808
|
-
|
|
1809
|
-
if ( zip[ location ] ) {
|
|
1810
|
-
|
|
1811
|
-
const blob = new Blob( [ zip[ location ].buffer ], { type: 'application/octet-stream' } );
|
|
1812
|
-
const blobUrl = URL.createObjectURL( blob );
|
|
1813
|
-
createdUrls.push( blobUrl );
|
|
1814
|
-
return blobUrl;
|
|
1815
|
-
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
|
-
}
|
|
1819
|
-
|
|
1820
|
-
return this.findTextureWithFuzzyMatch( normalizedUrl, zip, createdUrls ) || url;
|
|
1821
|
-
|
|
1822
|
-
}
|
|
1823
|
-
|
|
1824
|
-
findTextureWithFuzzyMatch( normalizedUrl, zip, createdUrls ) {
|
|
1825
|
-
|
|
1826
|
-
const textureFilename = normalizedUrl.split( '/' ).pop();
|
|
1827
|
-
|
|
1828
|
-
for ( const zipPath in zip ) {
|
|
1829
|
-
|
|
1830
|
-
if ( zipPath.endsWith( textureFilename ) ) {
|
|
1831
|
-
|
|
1832
|
-
const blob = new Blob( [ zip[ zipPath ].buffer ], { type: 'application/octet-stream' } );
|
|
1833
|
-
const blobUrl = URL.createObjectURL( blob );
|
|
1834
|
-
createdUrls.push( blobUrl );
|
|
1835
|
-
return blobUrl;
|
|
1836
|
-
|
|
1837
|
-
}
|
|
1838
|
-
|
|
1839
|
-
}
|
|
1840
|
-
|
|
1841
|
-
if ( textureFilename && textureFilename.length > 5 ) {
|
|
1842
|
-
|
|
1843
|
-
for ( const zipPath in zip ) {
|
|
1844
|
-
|
|
1845
|
-
const zipFilename = zipPath.split( '/' ).pop();
|
|
1846
|
-
if ( zipFilename.includes( textureFilename ) || textureFilename.includes( zipFilename ) ) {
|
|
1847
|
-
|
|
1848
|
-
const blob = new Blob( [ zip[ zipPath ].buffer ], { type: 'application/octet-stream' } );
|
|
1849
|
-
const blobUrl = URL.createObjectURL( blob );
|
|
1850
|
-
createdUrls.push( blobUrl );
|
|
1851
|
-
return blobUrl;
|
|
1852
|
-
|
|
1853
|
-
}
|
|
1854
|
-
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
}
|
|
1858
|
-
|
|
1859
|
-
return null;
|
|
1860
|
-
|
|
1861
|
-
}
|
|
1862
|
-
|
|
1863
|
-
// Returns a fresh loader each call — DRACOLoader/KTX2Loader hold persistent
|
|
1864
|
-
// worker pools. Callers must invoke _disposeGLTFLoader() to terminate them.
|
|
1865
|
-
async createGLTFLoader() {
|
|
1866
|
-
|
|
1867
|
-
const { dracoDecoderPath, ktx2TranscoderPath } = getAssetConfig();
|
|
1868
|
-
|
|
1869
|
-
const dracoLoader = new DRACOLoader();
|
|
1870
|
-
dracoLoader.setDecoderConfig( { type: 'js' } );
|
|
1871
|
-
dracoLoader.setDecoderPath( dracoDecoderPath );
|
|
1872
|
-
|
|
1873
|
-
const ktx2Loader = new KTX2Loader();
|
|
1874
|
-
ktx2Loader.setTranscoderPath( ktx2TranscoderPath );
|
|
1875
|
-
|
|
1876
|
-
if ( this.renderer ) {
|
|
1877
|
-
|
|
1878
|
-
ktx2Loader.detectSupport( this.renderer );
|
|
1879
|
-
|
|
1880
|
-
// Force RGBA output for Basis Universal textures. GPU-compressed
|
|
1881
|
-
// texture arrays (CompressedArrayTexture) are blocked by a Three.js
|
|
1882
|
-
// TSL limitation: the node compiler maintains global state that
|
|
1883
|
-
// survives dispose(), so swapping texture array formats between
|
|
1884
|
-
// DataArrayTexture and CompressedArrayTexture at runtime causes
|
|
1885
|
-
// WGSL compilation failures (unresolved uniform bindings).
|
|
1886
|
-
ktx2Loader.workerConfig = {
|
|
1887
|
-
astcSupported: false, etc1Supported: false, etc2Supported: false,
|
|
1888
|
-
dxtSupported: false, bptcSupported: false, pvrtcSupported: false,
|
|
1889
|
-
};
|
|
1890
|
-
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
const loader = new GLTFLoader( this._loadingManager );
|
|
1894
|
-
loader.setDRACOLoader( dracoLoader );
|
|
1895
|
-
loader.setKTX2Loader( ktx2Loader );
|
|
1896
|
-
loader.setMeshoptDecoder( MeshoptDecoder );
|
|
1897
|
-
if ( getPlatform().decodeImage ) loader.register( ( parser ) => platformImagesPlugin( parser, ( where, error ) => this._reportImageFailure( where, error ) ) );
|
|
1898
|
-
|
|
1899
|
-
return loader;
|
|
1900
|
-
|
|
1901
|
-
}
|
|
1902
|
-
|
|
1903
|
-
// GLTFLoader swallows the failure and loads the model untextured, so a strict host's throw is kept
|
|
1904
|
-
// for the end of the load (_throwDeferred).
|
|
1905
|
-
_reportImageFailure( where, error ) {
|
|
1906
|
-
|
|
1907
|
-
try {
|
|
1908
|
-
|
|
1909
|
-
this._issues?.record(
|
|
1910
|
-
ISSUE_CODES.TEXTURE_BUILD_FAILED,
|
|
1911
|
-
`image ${where} could not be decoded — the surfaces using it render untextured`,
|
|
1912
|
-
{ image: where, cause: String( error?.message ?? error ) }
|
|
1913
|
-
);
|
|
1914
|
-
|
|
1915
|
-
} catch ( strictError ) {
|
|
1916
|
-
|
|
1917
|
-
this._deferredError ??= strictError;
|
|
1918
|
-
|
|
1919
|
-
}
|
|
1920
|
-
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
_throwDeferred() {
|
|
1924
|
-
|
|
1925
|
-
const error = this._deferredError;
|
|
1926
|
-
this._deferredError = null;
|
|
1927
|
-
if ( error ) throw error;
|
|
1928
|
-
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
_disposeGLTFLoader( loader ) {
|
|
1932
|
-
|
|
1933
|
-
if ( ! loader ) return;
|
|
1934
|
-
loader.dracoLoader?.dispose();
|
|
1935
|
-
loader.ktx2Loader?.dispose();
|
|
1936
|
-
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
async loadExampleModels( index, modelFiles ) {
|
|
1940
|
-
|
|
1941
|
-
if ( ! modelFiles || ! modelFiles[ index ] ) {
|
|
1942
|
-
|
|
1943
|
-
throw new Error( `No model file at index ${index}` );
|
|
1944
|
-
|
|
1945
|
-
}
|
|
1946
|
-
|
|
1947
|
-
const modelUrl = `${modelFiles[ index ].url}`;
|
|
1948
|
-
return await this.loadModel( modelUrl );
|
|
1949
|
-
|
|
1950
|
-
}
|
|
1951
|
-
|
|
1952
|
-
/**
|
|
1953
|
-
* @param {string} modelUrl
|
|
1954
|
-
* @param {{cacheKey?: string, cachePolicy?: string}} [options] - key a download cache entry by
|
|
1955
|
-
* something steadier than a signed, expiring URL
|
|
1956
|
-
*/
|
|
1957
|
-
async loadModel( modelUrl, options = {} ) {
|
|
1958
|
-
|
|
1959
|
-
this._loadCancelled = false;
|
|
1960
|
-
const loader = await this.createGLTFLoader();
|
|
1961
|
-
const cancelable = AssetLoader._isNetworkUrl( modelUrl );
|
|
1962
|
-
let source = null;
|
|
1963
|
-
|
|
1964
|
-
try {
|
|
1965
|
-
|
|
1966
|
-
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
1967
|
-
source = await this._viaCache( modelUrl, "Downloading Model...", cancelable, options );
|
|
1968
|
-
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( modelUrl ) );
|
|
1969
|
-
const data = await loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
1970
|
-
this._downloadComplete();
|
|
1971
|
-
this._throwDeferred();
|
|
1972
|
-
|
|
1973
|
-
this.releaseTargetModel();
|
|
1974
|
-
this._sourceKey = cancelable ? this._keyed( 'url', options.cacheKey ?? modelUrl ) : null;
|
|
1975
|
-
|
|
1976
|
-
this.targetModel = data.scene;
|
|
1977
|
-
this.animations = data.animations || [];
|
|
1978
|
-
this.sceneMetadata = extractSceneMetadata( data );
|
|
1979
|
-
await this.onModelLoad( this.targetModel );
|
|
1980
|
-
this.dispatchEvent( { type: 'load', model: data.scene, filename: modelUrl.split( '/' ).pop() } );
|
|
1981
|
-
return data;
|
|
1982
|
-
|
|
1983
|
-
} catch ( error ) {
|
|
1984
|
-
|
|
1985
|
-
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
1986
|
-
console.error( "Error loading model:", error );
|
|
1987
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename: modelUrl } );
|
|
1988
|
-
throw error;
|
|
1989
|
-
|
|
1990
|
-
} finally {
|
|
1991
|
-
|
|
1992
|
-
source?.release();
|
|
1993
|
-
this._disposeGLTFLoader( loader );
|
|
1994
|
-
|
|
1995
|
-
}
|
|
1996
|
-
|
|
1997
|
-
}
|
|
1998
|
-
|
|
1999
|
-
// ─────────────────────────────────────────────────────────────
|
|
2000
|
-
// Append / remove primitives (dynamic object add/remove).
|
|
2001
|
-
// These deliberately do NOT touch targetModel/animations and do NOT
|
|
2002
|
-
// reframe the camera or dispatch load/modelProcessed — the caller
|
|
2003
|
-
// (PathTracerApp) drives a reframe-free scene rebuild.
|
|
2004
|
-
// ─────────────────────────────────────────────────────────────
|
|
2005
|
-
|
|
2006
|
-
// Multi-material split + area-light placeholders, then parent into meshScene.
|
|
2007
|
-
_processAndParent( model ) {
|
|
2008
|
-
|
|
2009
|
-
this.processModelObjects( model );
|
|
2010
|
-
this.scene.add( model );
|
|
2011
|
-
// Refresh world matrices for the whole subtree — the reframe-free rebuild
|
|
2012
|
-
// extracts geometry immediately (rAF is gated off), so nothing else would
|
|
2013
|
-
// update matrices first and any ancestor-node transform would be dropped.
|
|
2014
|
-
model.updateMatrixWorld( true );
|
|
2015
|
-
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// Append a model from URL without releasing prior models or reframing.
|
|
2019
|
-
// Reuses createGLTFLoader() so appended KTX2 textures stay RGBA DataArrayTexture.
|
|
2020
|
-
async appendModel( url, options = {} ) {
|
|
2021
|
-
|
|
2022
|
-
this._loadCancelled = false;
|
|
2023
|
-
const loader = await this.createGLTFLoader();
|
|
2024
|
-
const cancelable = AssetLoader._isNetworkUrl( url );
|
|
2025
|
-
let source = null;
|
|
2026
|
-
|
|
2027
|
-
try {
|
|
2028
|
-
|
|
2029
|
-
updateLoading( { isLoading: true, status: "Downloading Model...", progress: 2, canCancel: cancelable, loadedBytes: 0, totalBytes: 0 } );
|
|
2030
|
-
source = await this._viaCache( url, "Downloading Model...", cancelable, options );
|
|
2031
|
-
if ( source.cached ) loader.setResourcePath( LoaderUtils.extractUrlBase( url ) );
|
|
2032
|
-
const data = await loader.loadAsync( source.url, source.cached ? undefined : this._downloadProgress( "Downloading Model...", cancelable ) );
|
|
2033
|
-
this._downloadComplete();
|
|
2034
|
-
this._throwDeferred();
|
|
2035
|
-
this._appended = true;
|
|
2036
|
-
this._processAndParent( data.scene );
|
|
2037
|
-
return { root: data.scene, animations: data.animations || [] };
|
|
2038
|
-
|
|
2039
|
-
} catch ( error ) {
|
|
2040
|
-
|
|
2041
|
-
if ( this._isCancellation( error ) ) throw this._cancellationError();
|
|
2042
|
-
throw error;
|
|
2043
|
-
|
|
2044
|
-
} finally {
|
|
2045
|
-
|
|
2046
|
-
source?.release();
|
|
2047
|
-
this._disposeGLTFLoader( loader );
|
|
2048
|
-
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
}
|
|
2052
|
-
|
|
2053
|
-
// Append a copy of a caller-owned Object3D without releasing prior models or reframing.
|
|
2054
|
-
appendObject3D( object3d, name = 'object3d' ) {
|
|
2055
|
-
|
|
2056
|
-
this._appended = true;
|
|
2057
|
-
|
|
2058
|
-
const root = this._adoptExternalObject( object3d );
|
|
2059
|
-
root.name = object3d.name || name;
|
|
2060
|
-
this._processAndParent( root );
|
|
2061
|
-
return { root };
|
|
2062
|
-
|
|
2063
|
-
}
|
|
2064
|
-
|
|
2065
|
-
// Detach + dispose a model root. An adopted root's geometry/materials belong to the
|
|
2066
|
-
// caller, so only the engine's own allocations go.
|
|
2067
|
-
removeModelRoot( root ) {
|
|
2068
|
-
|
|
2069
|
-
if ( ! root ) return;
|
|
2070
|
-
|
|
2071
|
-
if ( root.userData.__rayzeeExternal ) {
|
|
2072
|
-
|
|
2073
|
-
disposeEngineOwnedResources( root );
|
|
2074
|
-
root.parent?.remove( root );
|
|
2075
|
-
|
|
2076
|
-
} else {
|
|
2077
|
-
|
|
2078
|
-
disposeObjectFromMemory( root );
|
|
2079
|
-
|
|
2080
|
-
}
|
|
2081
|
-
|
|
2082
|
-
}
|
|
2083
|
-
|
|
2084
|
-
async loadGLBFromArrayBuffer( arrayBuffer, filename = 'model.glb' ) {
|
|
2085
|
-
|
|
2086
|
-
const loader = await this.createGLTFLoader();
|
|
2087
|
-
|
|
2088
|
-
try {
|
|
2089
|
-
|
|
2090
|
-
updateLoading( { isLoading: true, status: "Processing GLB Data...", progress: 5 } );
|
|
2091
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2092
|
-
|
|
2093
|
-
const data = await loader.parseAsync( arrayBuffer, '' );
|
|
2094
|
-
this._throwDeferred();
|
|
2095
|
-
|
|
2096
|
-
this.releaseTargetModel();
|
|
2097
|
-
|
|
2098
|
-
this.targetModel = data.scene;
|
|
2099
|
-
this.animations = data.animations || [];
|
|
2100
|
-
this.sceneMetadata = extractSceneMetadata( data );
|
|
2101
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2102
|
-
await this.onModelLoad( this.targetModel );
|
|
2103
|
-
|
|
2104
|
-
this.dispatchEvent( { type: 'load', model: data.scene, filename } );
|
|
2105
|
-
return data;
|
|
2106
|
-
|
|
2107
|
-
} catch ( error ) {
|
|
2108
|
-
|
|
2109
|
-
console.error( 'Error loading GLB:', error );
|
|
2110
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2111
|
-
throw error;
|
|
2112
|
-
|
|
2113
|
-
} finally {
|
|
2114
|
-
|
|
2115
|
-
this._disposeGLTFLoader( loader );
|
|
2116
|
-
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
async loadFBXFromArrayBuffer( arrayBuffer, filename = 'model.fbx' ) {
|
|
2122
|
-
|
|
2123
|
-
try {
|
|
2124
|
-
|
|
2125
|
-
updateLoading( { isLoading: true, status: "Processing FBX Data...", progress: 5 } );
|
|
2126
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2127
|
-
|
|
2128
|
-
if ( ! this.loaderCache.fbx ) {
|
|
2129
|
-
|
|
2130
|
-
const { FBXLoader } = await import( 'three/examples/jsm/loaders/FBXLoader.js' );
|
|
2131
|
-
this.loaderCache.fbx = new FBXLoader();
|
|
2132
|
-
|
|
2133
|
-
}
|
|
2134
|
-
|
|
2135
|
-
const object = this.loaderCache.fbx.parse( arrayBuffer );
|
|
2136
|
-
this.releaseTargetModel();
|
|
2137
|
-
this.targetModel = object;
|
|
2138
|
-
|
|
2139
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2140
|
-
await this.onModelLoad( this.targetModel );
|
|
2141
|
-
|
|
2142
|
-
this.dispatchEvent( { type: 'load', model: object, filename } );
|
|
2143
|
-
return object;
|
|
2144
|
-
|
|
2145
|
-
} catch ( error ) {
|
|
2146
|
-
|
|
2147
|
-
console.error( 'Error loading FBX:', error );
|
|
2148
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2149
|
-
throw error;
|
|
2150
|
-
|
|
2151
|
-
}
|
|
2152
|
-
|
|
2153
|
-
}
|
|
2154
|
-
|
|
2155
|
-
async loadOBJFromFile( file, filename = 'model.obj' ) {
|
|
2156
|
-
|
|
2157
|
-
try {
|
|
2158
|
-
|
|
2159
|
-
updateLoading( { isLoading: true, status: "Processing OBJ Data...", progress: 5 } );
|
|
2160
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2161
|
-
|
|
2162
|
-
if ( ! this.loaderCache.obj ) {
|
|
2163
|
-
|
|
2164
|
-
const { OBJLoader } = await import( 'three/examples/jsm/loaders/OBJLoader.js' );
|
|
2165
|
-
this.loaderCache.obj = new OBJLoader();
|
|
2166
|
-
|
|
2167
|
-
}
|
|
2168
|
-
|
|
2169
|
-
const contents = await this.readFileAsText( file );
|
|
2170
|
-
const object = this.loaderCache.obj.parse( contents );
|
|
2171
|
-
object.name = filename;
|
|
2172
|
-
|
|
2173
|
-
this.releaseTargetModel();
|
|
2174
|
-
this.targetModel = object;
|
|
2175
|
-
|
|
2176
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2177
|
-
await this.onModelLoad( this.targetModel );
|
|
2178
|
-
|
|
2179
|
-
this.dispatchEvent( { type: 'load', model: object, filename } );
|
|
2180
|
-
return object;
|
|
2181
|
-
|
|
2182
|
-
} catch ( error ) {
|
|
2183
|
-
|
|
2184
|
-
console.error( 'Error loading OBJ:', error );
|
|
2185
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2186
|
-
throw error;
|
|
2187
|
-
|
|
2188
|
-
}
|
|
2189
|
-
|
|
2190
|
-
}
|
|
2191
|
-
|
|
2192
|
-
async loadSTLFromArrayBuffer( arrayBuffer, filename = 'model.stl' ) {
|
|
2193
|
-
|
|
2194
|
-
try {
|
|
2195
|
-
|
|
2196
|
-
updateLoading( { isLoading: true, status: "Processing STL Data...", progress: 5 } );
|
|
2197
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2198
|
-
|
|
2199
|
-
if ( ! this.loaderCache.stl ) {
|
|
2200
|
-
|
|
2201
|
-
const { STLLoader } = await import( 'three/examples/jsm/loaders/STLLoader.js' );
|
|
2202
|
-
this.loaderCache.stl = new STLLoader();
|
|
2203
|
-
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
const geometry = this.loaderCache.stl.parse( arrayBuffer );
|
|
2207
|
-
const material = new MeshStandardMaterial();
|
|
2208
|
-
const mesh = new Mesh( geometry, material );
|
|
2209
|
-
mesh.name = filename;
|
|
2210
|
-
|
|
2211
|
-
this.releaseTargetModel();
|
|
2212
|
-
this.targetModel = mesh;
|
|
2213
|
-
|
|
2214
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2215
|
-
await this.onModelLoad( this.targetModel );
|
|
2216
|
-
|
|
2217
|
-
this.dispatchEvent( { type: 'load', model: mesh, filename } );
|
|
2218
|
-
return mesh;
|
|
2219
|
-
|
|
2220
|
-
} catch ( error ) {
|
|
2221
|
-
|
|
2222
|
-
console.error( 'Error loading STL:', error );
|
|
2223
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2224
|
-
throw error;
|
|
2225
|
-
|
|
2226
|
-
}
|
|
2227
|
-
|
|
2228
|
-
}
|
|
2229
|
-
|
|
2230
|
-
async loadPLYFromArrayBuffer( arrayBuffer, filename = 'model.ply' ) {
|
|
2231
|
-
|
|
2232
|
-
try {
|
|
2233
|
-
|
|
2234
|
-
updateLoading( { isLoading: true, status: "Processing PLY Data...", progress: 5 } );
|
|
2235
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2236
|
-
|
|
2237
|
-
if ( ! this.loaderCache.ply ) {
|
|
2238
|
-
|
|
2239
|
-
const { PLYLoader } = await import( 'three/examples/jsm/loaders/PLYLoader.js' );
|
|
2240
|
-
this.loaderCache.ply = new PLYLoader();
|
|
2241
|
-
|
|
2242
|
-
}
|
|
2243
|
-
|
|
2244
|
-
const geometry = this.loaderCache.ply.parse( arrayBuffer );
|
|
2245
|
-
let object;
|
|
2246
|
-
|
|
2247
|
-
if ( geometry.index !== null ) {
|
|
2248
|
-
|
|
2249
|
-
const material = new MeshStandardMaterial();
|
|
2250
|
-
object = new Mesh( geometry, material );
|
|
2251
|
-
|
|
2252
|
-
} else {
|
|
2253
|
-
|
|
2254
|
-
const material = new PointsMaterial( { size: 0.01 } );
|
|
2255
|
-
material.vertexColors = geometry.hasAttribute( 'color' );
|
|
2256
|
-
object = new Points( geometry, material );
|
|
2257
|
-
|
|
2258
|
-
}
|
|
2259
|
-
|
|
2260
|
-
object.name = filename;
|
|
2261
|
-
this.releaseTargetModel();
|
|
2262
|
-
this.targetModel = object;
|
|
2263
|
-
|
|
2264
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2265
|
-
await this.onModelLoad( this.targetModel );
|
|
2266
|
-
|
|
2267
|
-
this.dispatchEvent( { type: 'load', model: object, filename } );
|
|
2268
|
-
return object;
|
|
2269
|
-
|
|
2270
|
-
} catch ( error ) {
|
|
2271
|
-
|
|
2272
|
-
console.error( 'Error loading PLY:', error );
|
|
2273
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2274
|
-
throw error;
|
|
2275
|
-
|
|
2276
|
-
}
|
|
2277
|
-
|
|
2278
|
-
}
|
|
2279
|
-
|
|
2280
|
-
async loadColladaFromFile( file, filename = 'model.dae' ) {
|
|
2281
|
-
|
|
2282
|
-
try {
|
|
2283
|
-
|
|
2284
|
-
updateLoading( { isLoading: true, status: "Processing Collada Data...", progress: 5 } );
|
|
2285
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2286
|
-
|
|
2287
|
-
if ( ! this.loaderCache.collada ) {
|
|
2288
|
-
|
|
2289
|
-
const { ColladaLoader } = await import( 'three/examples/jsm/loaders/ColladaLoader.js' );
|
|
2290
|
-
this.loaderCache.collada = new ColladaLoader();
|
|
2291
|
-
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
const contents = await this.readFileAsText( file );
|
|
2295
|
-
const collada = this.loaderCache.collada.parse( contents );
|
|
2296
|
-
collada.scene.name = filename;
|
|
2297
|
-
|
|
2298
|
-
this.releaseTargetModel();
|
|
2299
|
-
this.targetModel = collada.scene;
|
|
2300
|
-
|
|
2301
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2302
|
-
await this.onModelLoad( this.targetModel );
|
|
2303
|
-
|
|
2304
|
-
this.dispatchEvent( { type: 'load', model: collada.scene, filename } );
|
|
2305
|
-
return collada;
|
|
2306
|
-
|
|
2307
|
-
} catch ( error ) {
|
|
2308
|
-
|
|
2309
|
-
console.error( 'Error loading Collada:', error );
|
|
2310
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2311
|
-
throw error;
|
|
2312
|
-
|
|
2313
|
-
}
|
|
2314
|
-
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
async load3MFFromArrayBuffer( arrayBuffer, filename = 'model.3mf' ) {
|
|
2318
|
-
|
|
2319
|
-
try {
|
|
2320
|
-
|
|
2321
|
-
updateLoading( { isLoading: true, status: "Processing 3MF Data...", progress: 5 } );
|
|
2322
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2323
|
-
|
|
2324
|
-
if ( ! this.loaderCache.threemf ) {
|
|
2325
|
-
|
|
2326
|
-
const { ThreeMFLoader } = await import( 'three/examples/jsm/loaders/3MFLoader.js' );
|
|
2327
|
-
this.loaderCache.threemf = new ThreeMFLoader();
|
|
2328
|
-
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
const object = this.loaderCache.threemf.parse( arrayBuffer );
|
|
2332
|
-
|
|
2333
|
-
this.releaseTargetModel();
|
|
2334
|
-
this.targetModel = object;
|
|
2335
|
-
|
|
2336
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2337
|
-
await this.onModelLoad( this.targetModel );
|
|
2338
|
-
|
|
2339
|
-
this.dispatchEvent( { type: 'load', model: object, filename } );
|
|
2340
|
-
return object;
|
|
2341
|
-
|
|
2342
|
-
} catch ( error ) {
|
|
2343
|
-
|
|
2344
|
-
console.error( 'Error loading 3MF:', error );
|
|
2345
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2346
|
-
throw error;
|
|
2347
|
-
|
|
2348
|
-
}
|
|
2349
|
-
|
|
2350
|
-
}
|
|
2351
|
-
|
|
2352
|
-
async loadUSDFromArrayBuffer( data, filename = 'model.usd' ) {
|
|
2353
|
-
|
|
2354
|
-
try {
|
|
2355
|
-
|
|
2356
|
-
updateLoading( { isLoading: true, status: "Processing USD Data...", progress: 5 } );
|
|
2357
|
-
await new Promise( r => setTimeout( r, 0 ) );
|
|
2358
|
-
|
|
2359
|
-
if ( ! this.loaderCache.usd ) {
|
|
2360
|
-
|
|
2361
|
-
const { USDLoader } = await import( 'three/examples/jsm/loaders/USDLoader.js' );
|
|
2362
|
-
this.loaderCache.usd = new USDLoader();
|
|
2363
|
-
|
|
2364
|
-
}
|
|
2365
|
-
|
|
2366
|
-
// parse() returns the group synchronously but resolves textures async;
|
|
2367
|
-
// setupPathTracing snapshots materials, so maps must land before it runs.
|
|
2368
|
-
const object = await new Promise( ( resolve, reject ) => {
|
|
2369
|
-
|
|
2370
|
-
this.loaderCache.usd.parse( data, '', resolve, reject );
|
|
2371
|
-
|
|
2372
|
-
} );
|
|
2373
|
-
|
|
2374
|
-
object.name = filename;
|
|
2375
|
-
|
|
2376
|
-
this.releaseTargetModel();
|
|
2377
|
-
this.targetModel = object;
|
|
2378
|
-
|
|
2379
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2380
|
-
await this.onModelLoad( this.targetModel );
|
|
2381
|
-
|
|
2382
|
-
this.dispatchEvent( { type: 'load', model: object, filename } );
|
|
2383
|
-
return object;
|
|
2384
|
-
|
|
2385
|
-
} catch ( error ) {
|
|
2386
|
-
|
|
2387
|
-
console.error( 'Error loading USD:', error );
|
|
2388
|
-
this.dispatchEvent( { type: 'error', message: error.message, filename } );
|
|
2389
|
-
throw error;
|
|
2390
|
-
|
|
2391
|
-
}
|
|
2392
|
-
|
|
2393
|
-
}
|
|
2394
|
-
|
|
2395
|
-
async loadObject3D( object3d, name = 'object3d' ) {
|
|
2396
|
-
|
|
2397
|
-
this.releaseTargetModel();
|
|
2398
|
-
|
|
2399
|
-
const model = this._adoptExternalObject( object3d );
|
|
2400
|
-
model.name = object3d.name || name;
|
|
2401
|
-
|
|
2402
|
-
this.targetModel = model;
|
|
2403
|
-
|
|
2404
|
-
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
2405
|
-
await this.onModelLoad( this.targetModel );
|
|
971
|
+
updateLoading( { isLoading: true, status: "Processing Data...", progress: 10 } );
|
|
972
|
+
await this.onModelLoad( this.targetModel );
|
|
2406
973
|
|
|
2407
974
|
this.dispatchEvent( { type: 'load', model, filename: name } );
|
|
2408
975
|
return model;
|
|
@@ -2425,7 +992,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2425
992
|
const center = box.getCenter( new Vector3() );
|
|
2426
993
|
const size = box.getSize( new Vector3() );
|
|
2427
994
|
|
|
2428
|
-
this.controls
|
|
995
|
+
this.controls?.target.copy( center );
|
|
2429
996
|
|
|
2430
997
|
const maxDim = Math.max( size.x, size.y, size.z );
|
|
2431
998
|
const fov = this.camera.fov * ( Math.PI / 180 );
|
|
@@ -2445,9 +1012,14 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2445
1012
|
this.camera.near = maxDim / 100;
|
|
2446
1013
|
this.camera.far = maxDim * 100;
|
|
2447
1014
|
this.camera.updateProjectionMatrix();
|
|
2448
|
-
this.controls
|
|
2449
|
-
|
|
2450
|
-
|
|
1015
|
+
if ( this.controls ) {
|
|
1016
|
+
|
|
1017
|
+
this.controls.maxDistance = cameraDistance * 10;
|
|
1018
|
+
this.controls.saveState();
|
|
1019
|
+
this.controls.update();
|
|
1020
|
+
|
|
1021
|
+
}
|
|
1022
|
+
|
|
2451
1023
|
buildTimer.end( 'Camera setup' );
|
|
2452
1024
|
|
|
2453
1025
|
// Adjust floor plane
|
|
@@ -2715,10 +1287,12 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2715
1287
|
|
|
2716
1288
|
getSupportedFormats( type = null ) {
|
|
2717
1289
|
|
|
1290
|
+
const formats = { ...CORE_FORMATS, ...this._archiveFormats };
|
|
1291
|
+
for ( const [ extension, { type, name } ] of this._formats ) formats[ extension ] = { type, name };
|
|
2718
1292
|
if ( type ) {
|
|
2719
1293
|
|
|
2720
1294
|
const filtered = {};
|
|
2721
|
-
for ( const [ ext, info ] of Object.entries(
|
|
1295
|
+
for ( const [ ext, info ] of Object.entries( formats ) ) {
|
|
2722
1296
|
|
|
2723
1297
|
if ( info.type === type ) filtered[ ext ] = info;
|
|
2724
1298
|
|
|
@@ -2728,7 +1302,7 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2728
1302
|
|
|
2729
1303
|
}
|
|
2730
1304
|
|
|
2731
|
-
return
|
|
1305
|
+
return formats;
|
|
2732
1306
|
|
|
2733
1307
|
}
|
|
2734
1308
|
|
|
@@ -2759,7 +1333,9 @@ export class AssetLoader extends EventDispatcher {
|
|
|
2759
1333
|
this._urlAbort = null;
|
|
2760
1334
|
this.storage = null;
|
|
2761
1335
|
this._downloads = null;
|
|
2762
|
-
this.
|
|
1336
|
+
this.archives?.release();
|
|
1337
|
+
this.archives = null;
|
|
1338
|
+
this._archiveLoader = null;
|
|
2763
1339
|
|
|
2764
1340
|
this.releaseTargetModel();
|
|
2765
1341
|
|