| const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas')}) | const viewer = new ThreeViewer({canvas: document.getElementById('mcanvas')}) | ||||
| async function init() { | |||||
| async function init(loadAsFile = true) { | |||||
| const url = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf' | |||||
| let file | |||||
| if(loadAsFile){ | |||||
| file = new File([await (await fetch(url)).blob()], url) | |||||
| } | |||||
| const load = async ()=> { | |||||
| return viewer.load(file ?? url, { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| } | |||||
| await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | await viewer.setEnvironmentMap('https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr') | ||||
| viewer.scene.background = viewer.scene.environment | viewer.scene.background = viewer.scene.environment | ||||
| const model = await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const model = await load() | |||||
| console.log(model.uuid) | console.log(model.uuid) | ||||
| await timeout(500) | await timeout(500) | ||||
| viewer.scene.clearSceneModels(false) | viewer.scene.clearSceneModels(false) | ||||
| await timeout(500) | await timeout(500) | ||||
| const model2 = await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const model2 = await load() | |||||
| console.log(model2.uuid) | console.log(model2.uuid) | ||||
| if(model !== model2) throw new Error('Error in Test - Models should be the same after clearing scene models'); | if(model !== model2) throw new Error('Error in Test - Models should be the same after clearing scene models'); | ||||
| await timeout(500) | await timeout(500) | ||||
| viewer.scene.disposeSceneModels() | viewer.scene.disposeSceneModels() | ||||
| await timeout(500) | await timeout(500) | ||||
| const model3 = await viewer.load('https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf', { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| const model3 = await load() | |||||
| console.log(model3.uuid) | console.log(model3.uuid) | ||||
| if(model2 === model3) throw new Error('Error in Test - Models should not be the same after disposing scene models'); | if(model2 === model3) throw new Error('Error in Test - Models should not be the same after disposing scene models'); | ||||
| await timeout(500) | await timeout(500) | ||||
| viewer.scene.addObject(model.translateX(-0.5)) // add back cleared model | viewer.scene.addObject(model.translateX(-0.5)) // add back cleared model | ||||
| viewer.scene.addObject(model2.translateX(-0.5)) // again, shouldn't change anything | viewer.scene.addObject(model2.translateX(-0.5)) // again, shouldn't change anything | ||||
| viewer.scene.addObject(model3.translateX(1)) // add back disposed model | viewer.scene.addObject(model3.translateX(1)) // add back disposed model | ||||
| await timeout(1000) | |||||
| viewer.scene.disposeSceneModels() | |||||
| } | } | ||||
| _testStart() | _testStart() | ||||
| init().finally(_testFinish) | |||||
| init(false).then(()=>init(true)).finally(_testFinish) | |||||
| </script> | </script> | ||||
| </head> | </head> | ||||
| <body> | <body> |
| <!DOCTYPE html> | |||||
| <html lang="en"> | |||||
| <head> | |||||
| <meta charset="UTF-8"> | |||||
| <title>File/Blob Load</title> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <!-- Import maps polyfill --> | |||||
| <!-- Remove this when import maps will be widely supported --> | |||||
| <script async src="https://unpkg.com/es-module-shims@1.6.3/dist/es-module-shims.js"></script> | |||||
| <script type="importmap"> | |||||
| { | |||||
| "imports": { | |||||
| "threepipe": "./../../dist/index.mjs" | |||||
| } | |||||
| } | |||||
| </script> | |||||
| <style id="example-style"> | |||||
| html, body, #canvas-container, #mcanvas { | |||||
| width: 100%; | |||||
| height: 100%; | |||||
| margin: 0; | |||||
| overflow: hidden; | |||||
| } | |||||
| </style> | |||||
| <script type="module" src="../examples-utils/global-loading.mjs"></script> | |||||
| <script type="module" src="../examples-utils/simple-code-preview.mjs"></script> | |||||
| <script id="example-script" type="module" src="./script.js" data-scripts="./script.ts;./script.js"></script> | |||||
| </head> | |||||
| <body> | |||||
| <div id="canvas-container"> | |||||
| <canvas id="mcanvas"></canvas> | |||||
| </div> | |||||
| </body> |
| import {_testFinish, _testStart, LoadingScreenPlugin, ThreeViewer} from 'threepipe' | |||||
| async function init() { | |||||
| const viewer = new ThreeViewer({ | |||||
| canvas: document.getElementById('mcanvas') as HTMLCanvasElement, | |||||
| msaa: true, | |||||
| dropzone: { | |||||
| addOptions: { | |||||
| disposeSceneObjects: true, | |||||
| autoSetEnvironment: true, // when hdr is dropped | |||||
| autoSetBackground: true, | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| viewer.addPluginSync(LoadingScreenPlugin) | |||||
| const env = 'https://threejs.org/examples/textures/equirectangular/venice_sunset_1k.hdr' | |||||
| const url = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf' | |||||
| const responses = await Promise.all([ | |||||
| fetch(env), | |||||
| fetch(url), | |||||
| ]) | |||||
| const envFile = new File([await responses[0].blob()], 'venice_sunset_1k.hdr') | |||||
| await viewer.setEnvironmentMap(envFile, { | |||||
| setBackground: true, | |||||
| }) | |||||
| const blob = await responses[1].blob() | |||||
| const file = new File([blob], url) // Set the file name to the URL, so that internal textures can be resolved correctly from the base path | |||||
| const result = await viewer.load(file, { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| console.log(result) | |||||
| } | |||||
| _testStart() | |||||
| init().finally(_testFinish) |
| <li><a href="./slippy-map-tiles/">Slippy Map Tiles Load<br/>(OpenStreetMap ZYX) </a></li> | <li><a href="./slippy-map-tiles/">Slippy Map Tiles Load<br/>(OpenStreetMap ZYX) </a></li> | ||||
| <li><a href="./ogc-tiles-google-maps/">Google Maps Globe (OGC Tiles)</a></li> | <li><a href="./ogc-tiles-google-maps/">Google Maps Globe (OGC Tiles)</a></li> | ||||
| <li><a href="./ogc-tiles-google-maps-3d/">Google Maps 3D (OGC Tiles)</a></li> | <li><a href="./ogc-tiles-google-maps-3d/">Google Maps 3D (OGC Tiles)</a></li> | ||||
| <li><a href="./file-load/">File/Blob load</a></li> | |||||
| </ul> | </ul> | ||||
| <h2 class="category">Export</h2> | <h2 class="category">Export</h2> | ||||
| <ul> | <ul> | ||||
| <li><a href="./uint8-rgbm-hdr-test/">Uint8 RGBM HDR Test </a></li> | <li><a href="./uint8-rgbm-hdr-test/">Uint8 RGBM HDR Test </a></li> | ||||
| <li><a href="./half-float-hdr-test/">Half-float HDR Test </a></li> | <li><a href="./half-float-hdr-test/">Half-float HDR Test </a></li> | ||||
| <li><a href="./sphere-rgbm-test/">RGBM Test </a></li> | <li><a href="./sphere-rgbm-test/">RGBM Test </a></li> | ||||
| <li><a href="./sphere-half-float-test/">Half Float Test </a></li> | |||||
| <li><a href="./sphere-half-float-test/">Half-float Test </a></li> | |||||
| <li><a href="./sphere-msaa-test/">MSAA Test </a></li> | <li><a href="./sphere-msaa-test/">MSAA Test </a></li> | ||||
| <li><a href="./z-prepass/">Z-Prepass Test </a></li> | <li><a href="./z-prepass/">Z-Prepass Test </a></li> | ||||
| <li><a href="./import-test/">Import Test</a></li> | <li><a href="./import-test/">Import Test</a></li> |
| ImportFilesOptions, | ImportFilesOptions, | ||||
| ImportResult, | ImportResult, | ||||
| LoadFileOptions, | LoadFileOptions, | ||||
| ProcessRawOptions, RootSceneImportResult, | |||||
| ProcessRawOptions, | |||||
| RootSceneImportResult, | |||||
| } from './IAssetImporter' | } from './IAssetImporter' | ||||
| import {IAsset, IFile} from './IAsset' | import {IAsset, IFile} from './IAsset' | ||||
| import {IImporter, ILoader} from './IImporter' | import {IImporter, ILoader} from './IImporter' | ||||
| import {Importer} from './Importer' | import {Importer} from './Importer' | ||||
| import {SimpleJSONLoader} from './import' | import {SimpleJSONLoader} from './import' | ||||
| import {parseFileExtension} from 'ts-browser-helpers' | import {parseFileExtension} from 'ts-browser-helpers' | ||||
| import {IObject3D} from '../core' | |||||
| // export type IAssetImporterEvent = Event&{ | // export type IAssetImporterEvent = Event&{ | ||||
| // type: IAssetImporterEventTypes, | // type: IAssetImporterEventTypes, | ||||
| console.error('AssetImporter: Invalid asset or path', assetOrPath) | console.error('AssetImporter: Invalid asset or path', assetOrPath) | ||||
| return [] | return [] | ||||
| } | } | ||||
| async importSingle<T extends ImportResult|undefined = ImportResult>(asset?: IAsset | string, options?: ImportAssetOptions): Promise<T|undefined> { | |||||
| async importSingle<T extends ImportResult|undefined = ImportResult>(asset?: string | IAsset | File, options?: ImportAssetOptions): Promise<T|undefined> { | |||||
| return (await this.import<T>(asset, options))?.[0] | return (await this.import<T>(asset, options))?.[0] | ||||
| } | } | ||||
| // load a single file | // load a single file | ||||
| private async _loadFile(path: string, file?: IFile, options: LoadFileOptions = {}, onDownloadProgress?: (e: ProgressEvent)=>void): Promise<ImportResult | ImportResult[] | undefined> { | private async _loadFile(path: string, file?: IFile, options: LoadFileOptions = {}, onDownloadProgress?: (e: ProgressEvent)=>void): Promise<ImportResult | ImportResult[] | undefined> { | ||||
| if (file?.__loadedAsset) return file.__loadedAsset | |||||
| // if (file?.__loadedAsset) return file.__loadedAsset | |||||
| this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 0}) | this.dispatchEvent({type: 'importFile', path, state:'downloading', progress: 0}) | ||||
| let res: ImportResult | ImportResult[] | undefined | let res: ImportResult | ImportResult[] | undefined | ||||
| return [] | return [] | ||||
| } | } | ||||
| this.dispatchEvent({type: 'importFile', path, state: 'done'}) // todo: do this after processing? | this.dispatchEvent({type: 'importFile', path, state: 'done'}) // todo: do this after processing? | ||||
| if (file) { | |||||
| file.__loadedAsset = res | |||||
| // todo: recheck below code after dispose logic change | |||||
| // Clear the reference __loadedAsset when any one asset is disposed. | |||||
| // it's a bit hacky to do this here, but it works for now. todo: move to a better place | |||||
| let ress: any[] = [] | |||||
| if (Array.isArray(res)) ress = res.flat(2) | |||||
| else if ((<RootSceneImportResult>res)?.userData?.rootSceneModelRoot) ress.push(...(<IObject3D>res).children) | |||||
| else ress.push(res) | |||||
| for (const r of ress) r?.addEventListener?.('dispose', () => file.__loadedAsset = undefined) | |||||
| } | |||||
| // if (file) { | |||||
| // file.__loadedAsset = res | |||||
| // | |||||
| // | |||||
| // // todo: recheck below code after dispose logic change | |||||
| // | |||||
| // // Clear the reference __loadedAsset when any one asset is disposed. | |||||
| // // it's a bit hacky to do this here, but it works for now. todo: move to a better place | |||||
| // let ress: any[] = [] | |||||
| // if (Array.isArray(res)) ress = res.flat(2) | |||||
| // else if ((<RootSceneImportResult>res)?.userData?.rootSceneModelRoot) ress.push(...(<IObject3D>res).children) | |||||
| // else ress.push(res) | |||||
| // for (const r of ress) r?.addEventListener?.('dispose', () => file.__loadedAsset = undefined) | |||||
| // | |||||
| // } | |||||
| if (res && typeof res === 'object' && !Array.isArray(res)) { | if (res && typeof res === 'object' && !Array.isArray(res)) { | ||||
| res.__rootPath = path | res.__rootPath = path | ||||
| const f = file || this._fileDatabase.get(path) | const f = file || this._fileDatabase.get(path) |
| * @param obj | * @param obj | ||||
| * @param options | * @param options | ||||
| */ | */ | ||||
| async import<T extends ImportResult = ImportResult>(obj: string | IAsset | null, options?: ImportAddOptions) { | |||||
| async import<T extends ImportResult = ImportResult>(obj: string | IAsset | File | null, options?: ImportAddOptions) { | |||||
| if (!obj) return | if (!obj) return | ||||
| return await this.assetManager.importer.importSingle<T>(obj, options) | return await this.assetManager.importer.importSingle<T>(obj, options) | ||||
| } | } | ||||
| * @param setBackground - Set the background image of the scene from the same map. | * @param setBackground - Set the background image of the scene from the same map. | ||||
| * @param options - Options for importing the asset. See {@link ImportAssetOptions} | * @param options - Options for importing the asset. See {@link ImportAssetOptions} | ||||
| */ | */ | ||||
| async setEnvironmentMap(map: string | IAsset | null | ITexture | undefined, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | |||||
| async setEnvironmentMap(map: string | IAsset | null | ITexture | File | undefined, {setBackground = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| this._scene.environment = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset|File, options) || null : <ITexture>map || null | |||||
| if (setBackground) return this.setBackgroundMap(this._scene.environment) | if (setBackground) return this.setBackgroundMap(this._scene.environment) | ||||
| return this._scene.environment | return this._scene.environment | ||||
| } | } | ||||
| * @param setEnvironment - Set the environment map of the scene from the same map. | * @param setEnvironment - Set the environment map of the scene from the same map. | ||||
| * @param options - Options for importing the asset. See {@link ImportAssetOptions} | * @param options - Options for importing the asset. See {@link ImportAssetOptions} | ||||
| */ | */ | ||||
| async setBackgroundMap(map: string | IAsset | null | ITexture | undefined, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| async setBackgroundMap(map: string | IAsset | null | ITexture | File | undefined, {setEnvironment = false, ...options}: ImportAssetOptions&{setBackground?: boolean} = {}): Promise<ITexture | null> { | |||||
| this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | this._scene.background = map && !(<ITexture>map).isTexture ? await this.assetManager.importer.importSingle<ITexture>(map as string|IAsset, options) || null : <ITexture>map || null | ||||
| if (setEnvironment) return this.setEnvironmentMap(this._scene.background) | if (setEnvironment) return this.setEnvironmentMap(this._scene.background) | ||||
| return this._scene.background | return this._scene.background |
| The AssetManager has support for loading files from URLs, local files and data URLs. | The AssetManager has support for loading files from URLs, local files and data URLs. | ||||
| The AssetManager also adds support for loading files from a zip archive. The zip files are automatically unzipped, and the files are loaded from the zip archive. | The AssetManager also adds support for loading files from a zip archive. The zip files are automatically unzipped, and the files are loaded from the zip archive. | ||||
| [`viewer.load()`](https://threepipe.org/docs/classes/ThreeViewer.html#load) is a simple wrapper for loading files from the AssetManager. | |||||
| It automatically adds the loaded object to the scene(if possible) and returns a promise that resolves to the loaded object, the materials are also automatically registered to the material manager. | |||||
| [`viewer.load()`](https://threepipe.org/docs/classes/ThreeViewer.html#load) is a simple helper for loading files from the AssetManager. It accepts urls, local `File`/`Blob`, data URLs, zip files, `IAsset`. | |||||
| It automatically adds the loaded object to the scene(if possible) and returns a promise that resolves to the loaded object/texture/material/etc, the materials are also automatically registered to the material manager. | |||||
| ```typescript | |||||
| const object = await viewer.load<IObject3D>('https://example.com/file.glb') | |||||
| ``` | |||||
| ::: details AssetManager | ::: details AssetManager | ||||
| AssetManager internally uses [AssetImporter](https://threepipe.org/docs/classes/AssetImporter.html), which provides an API for managing three.js [LoadingManager](https://threejs.org/docs/#api/en/loaders/LoadingManager) and adding and registering loaders for different file types. | AssetManager internally uses [AssetImporter](https://threepipe.org/docs/classes/AssetImporter.html), which provides an API for managing three.js [LoadingManager](https://threejs.org/docs/#api/en/loaders/LoadingManager) and adding and registering loaders for different file types. | ||||
| ```typescript | ```typescript | ||||
| const file: File|Blob = fileObject // create a new file, blob or get from input element | const file: File|Blob = fileObject // create a new file, blob or get from input element | ||||
| const text = await viewer.load<IObject>({ | |||||
| const res = await viewer.load(file) | |||||
| const res2 = await viewer.load<IObject>({ | |||||
| // a path/name is required to determine the proper importer by extension. `file.name` can also be used if available | // a path/name is required to determine the proper importer by extension. `file.name` can also be used if available | ||||
| path: 'file.glb', | path: 'file.glb', | ||||
| file | file | ||||
| To load a `Map` of files(like when multiple files are dragged and dropped on the webpage) with internal references to other files, use `viewer.assetManager.importer.importFiles` method. Check the source for [DropzonePlugin](../plugin/DropzonePlugin) for an example. | To load a `Map` of files(like when multiple files are dragged and dropped on the webpage) with internal references to other files, use `viewer.assetManager.importer.importFiles` method. Check the source for [DropzonePlugin](../plugin/DropzonePlugin) for an example. | ||||
| ## File/Blob with URL | |||||
| Files with references can be loaded with path, by setting the path as the name of the `File` object, or by specifying the `path` parameter. | |||||
| ```typescript | |||||
| const url = 'https://threejs.org/examples/models/gltf/DamagedHelmet/glTF/DamagedHelmet.gltf' | |||||
| const blob = await fetch(url).then(res => res.blob()) | |||||
| const file = new File([blob], url) // Set the file name to the URL, so that internal textures can be resolved correctly from the base path | |||||
| const result = await viewer.load(file, { | |||||
| autoCenter: true, | |||||
| autoScale: true, | |||||
| }) | |||||
| ``` | |||||
| Check the complete example - [file-load](https://threepipe.org/examples/#file-load/) | |||||
| ## Background, Environment maps | ## Background, Environment maps | ||||
| The background and environment maps can be set using the `viewer.setBackgroundMap` and `viewer.setEnvironmentMap` methods respectively. These accept both loaded textures from `viewer.load` and direct URLs. Files can be of any image format including hdr, exr. | The background and environment maps can be set using the `viewer.setBackgroundMap` and `viewer.setEnvironmentMap` methods respectively. These accept both loaded textures from `viewer.load` and direct URLs. Files can be of any image format including hdr, exr. |