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