threepipe
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

AssetManager.ts 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. import {ImportAssetOptions, ImportResult, ProcessRawOptions, RootSceneImportResult} from './IAssetImporter'
  2. import {
  3. BaseEvent,
  4. Cache as threeCache,
  5. Camera,
  6. EventDispatcher,
  7. LinearFilter,
  8. LinearMipmapLinearFilter,
  9. LoadingManager,
  10. PerspectiveCamera,
  11. TextureLoader,
  12. } from 'three'
  13. import {ISerializedConfig, IViewerPlugin, ThreeViewer} from '../viewer'
  14. import {AssetImporter} from './AssetImporter'
  15. import {generateUUID, getTextureDataType, overrideThreeCache} from '../three'
  16. import {IAsset} from './IAsset'
  17. import {
  18. AddObjectOptions,
  19. ICamera,
  20. iCameraCommons,
  21. IMaterial,
  22. iMaterialCommons,
  23. IObject3D,
  24. iObjectCommons,
  25. ISceneEvent,
  26. ITexture,
  27. PerspectiveCamera2,
  28. upgradeTexture,
  29. } from '../core'
  30. import {Importer} from './Importer'
  31. import {MaterialManager} from './MaterialManager'
  32. import {DRACOLoader2, GLTFLoader2, JSONMaterialLoader, MTLLoader2, OBJLoader2, ZipLoader} from './import'
  33. import {RGBELoader} from 'three/examples/jsm/loaders/RGBELoader.js'
  34. import {FBXLoader} from 'three/examples/jsm/loaders/FBXLoader.js'
  35. import {EXRLoader} from 'three/examples/jsm/loaders/EXRLoader.js'
  36. import {Class, ValOrArr} from 'ts-browser-helpers'
  37. import {ILoader} from './IImporter'
  38. import {AssetExporter} from './AssetExporter'
  39. import {IExporter} from './IExporter'
  40. import {GLTFExporter2} from './export'
  41. export interface AssetManagerOptions{
  42. /**
  43. * simple memory based cache for downloaded files, default = false
  44. */
  45. simpleCache?: boolean
  46. /**
  47. * Cache Storage for downloaded files, can use with `caches.open`
  48. * When true and by default uses `caches.open('threepipe-assetmanager')`, set to false to disable
  49. * @default true
  50. */
  51. storage?: Cache | Storage | boolean
  52. }
  53. export type AddAssetOptions = AddObjectOptions & {
  54. /**
  55. * Automatically set any loaded HDR, EXR file as the scene environment map
  56. * @default true
  57. */
  58. autoSetEnvironment?: boolean
  59. /**
  60. * Automatically set any loaded image(ITexture) file as the scene background
  61. */
  62. autoSetBackground?: boolean
  63. }
  64. export type ImportAddOptions = ImportAssetOptions & AddAssetOptions
  65. export type AddRawOptions = ProcessRawOptions & AddAssetOptions
  66. /**
  67. * Asset Manager
  68. *
  69. * Utility class to manage import, export, and material management.
  70. * @category Asset Manager
  71. */
  72. export class AssetManager extends EventDispatcher<BaseEvent&{data: ImportResult}, 'loadAsset'> {
  73. static readonly PluginType = 'AssetManager'
  74. readonly viewer: ThreeViewer
  75. readonly importer: AssetImporter
  76. readonly exporter: AssetExporter
  77. readonly materials: MaterialManager
  78. private _storage?: Cache | Storage
  79. get storage() {return this._storage}
  80. constructor(viewer: ThreeViewer, {simpleCache = false, storage}: AssetManagerOptions = {}) {
  81. super()
  82. this._sceneUpdated = this._sceneUpdated.bind(this)
  83. this.addAsset = this.addAsset.bind(this)
  84. this.addRaw = this.addRaw.bind(this)
  85. this.addImported = this.addImported.bind(this)
  86. this.importer = new AssetImporter(!!viewer.getPlugin('debug'))
  87. this.exporter = new AssetExporter()
  88. this.materials = new MaterialManager()
  89. this.viewer = viewer
  90. this.viewer.scene.addEventListener('addSceneObject', this._sceneUpdated)
  91. this.viewer.scene.addEventListener('materialChanged', this._sceneUpdated)
  92. this.viewer.scene.addEventListener('beforeDeserialize', this._sceneUpdated)
  93. this._initCacheStorage(simpleCache, storage ?? true)
  94. this.importer.addEventListener('processRaw', (event)=>{
  95. // console.log('preprocess mat', mat)
  96. const mat = event.data as IMaterial
  97. if (!mat || !mat.isMaterial || !mat.uuid) return
  98. if (this.materials?.findMaterial(mat.uuid)) {
  99. console.warn('imported material uuid already exists, creating new uuid')
  100. mat.uuid = generateUUID()
  101. if (mat.userData.uuid) mat.userData.uuid = mat.uuid
  102. }
  103. // todo: check for name exists also
  104. this.materials.registerMaterial(mat)
  105. })
  106. this.importer.addEventListener('processRawStart', (event)=>{
  107. // console.log('preprocess mat', mat)
  108. const res = event.data!
  109. // if (!res.assetType) {
  110. // if (res.isBufferGeometry) { // for eg stl todo
  111. // res = new Mesh(res, new MeshStandardMaterial())
  112. // }
  113. // if (res.isObject3D) {
  114. // }
  115. // }
  116. if (res.isObject3D) {
  117. // todo replace lights
  118. // if (res.isLight) {
  119. // res = upgradeThreejsLight(res)
  120. // } else {
  121. // const lights: any[] = []
  122. // res.traverse((rr: any)=>{
  123. // if (rr !== res && rr.isLight) lights.push(rr)
  124. // })
  125. // for (const light of lights) {
  126. // upgradeThreejsLight(light)
  127. // }
  128. // res = new Object3DModel(res, options as any)
  129. // }
  130. const cameras: Camera[] = []
  131. res.traverse((obj: any) => {
  132. if (obj.material) {
  133. const materials = Array.isArray(obj.material) ? obj.material : [obj.material]
  134. const newMaterials = []
  135. for (const material of materials) {
  136. const mat = this.materials.convertToIMaterial(material) || material
  137. mat.uuid = material.uuid
  138. mat.userData.uuid = material.uuid
  139. newMaterials.push(mat)
  140. }
  141. if (Array.isArray(obj.material)) obj.material = newMaterials
  142. else obj.material = newMaterials[0]
  143. }
  144. if (obj.isCamera) cameras.push(obj)
  145. })
  146. for (const camera of cameras) {
  147. // todo: OrthographicCamera
  148. if (!(camera as PerspectiveCamera).isPerspectiveCamera || !camera.parent) {
  149. iCameraCommons.upgradeCamera.call(camera)
  150. } else {
  151. const newCamera: ICamera = (camera as any).iCamera ?? new PerspectiveCamera2('', this.viewer.canvas).copy(camera)
  152. if (camera === newCamera) continue
  153. ;(newCamera as any).uuid = camera.uuid
  154. newCamera.userData.uuid = camera.uuid
  155. ;(camera as any).iCamera = newCamera
  156. camera.parent.children.splice(camera.parent.children.indexOf(camera), 1, newCamera)
  157. }
  158. }
  159. iObjectCommons.upgradeObject3D.call(res)
  160. } else if (res.isMaterial) {
  161. iMaterialCommons.upgradeMaterial.call(res)
  162. // todo update res by generating new material?
  163. } else if (res.isTexture) {
  164. upgradeTexture.call(res)
  165. if (event?.options?.generateMipmaps !== undefined)
  166. res.generateMipmaps = event?.options.generateMipmaps
  167. if (!res.generateMipmaps && !res.isRenderTargetTexture) { // todo: do we need to check more?
  168. res.minFilter = res.minFilter === LinearMipmapLinearFilter ? LinearFilter : res.minFilter
  169. res.magFilter = res.magFilter === LinearMipmapLinearFilter ? LinearFilter : res.magFilter
  170. }
  171. }
  172. // todo other asset/object types?
  173. })
  174. this._addImporters()
  175. this._addExporters()
  176. }
  177. async addAsset<T extends ImportResult = ImportResult>(assetOrPath?: string | IAsset | IAsset[], options?: ImportAddOptions): Promise<(T|undefined)[]> {
  178. if (!this.importer || !this.viewer) return []
  179. const imported = await this.importer.import<T>(assetOrPath, options)
  180. if (!imported) {
  181. console.warn('Unable to import', assetOrPath, imported)
  182. return []
  183. }
  184. return this.loadImported<(T|undefined)[]>(imported, options)
  185. }
  186. // materials: IMaterial[] = []
  187. // textures: ITexture[] = []
  188. async loadImported<T extends ValOrArr<ImportResult|undefined> = ImportResult>(imported: T, {autoSetEnvironment = true, autoSetBackground = false, ...options}: AddAssetOptions = {}): Promise<T | never[]> {
  189. const arr: (ImportResult|undefined)[] = Array.isArray(imported) ? imported : [imported]
  190. let ret: T = Array.isArray(imported) ? [] : undefined as any
  191. for (const obj of arr) {
  192. if (!obj) {
  193. if (Array.isArray(ret)) ret.push(undefined)
  194. continue
  195. }
  196. let r = obj
  197. switch (obj.assetType) {
  198. case 'material':
  199. this.materials.registerMaterial(<IMaterial>obj)
  200. break
  201. case 'texture':
  202. if (autoSetEnvironment && (
  203. obj.__rootPath?.endsWith('.hdr') || obj.__rootPath?.endsWith('.exr')
  204. )) this.viewer.scene.environment = <ITexture>obj
  205. if (autoSetBackground) this.viewer.scene.background = <ITexture>obj
  206. break
  207. case 'model':
  208. case 'light':
  209. case 'camera':
  210. r = await this.viewer.addSceneObject(<IObject3D|RootSceneImportResult>obj, options) // todo update references in scene update event
  211. break
  212. case 'config':
  213. if (options?.importConfig !== false) await this.viewer.importConfig(<ISerializedConfig>obj)
  214. break
  215. default:
  216. // legacy
  217. if (obj.type && typeof obj.type === 'string' && (Array.isArray((obj as any).plugins) ||
  218. (obj as any).type === 'ThreeViewer' || this.viewer.getPlugin((obj as any).type))) {
  219. await this.viewer.importConfig(<ISerializedConfig>obj)
  220. }
  221. break
  222. }
  223. this.dispatchEvent({type: 'loadAsset', data: obj})
  224. if (Array.isArray(ret)) ret.push(r)
  225. else ret = r as T
  226. }
  227. return ret || []
  228. }
  229. /**
  230. * same as {@link loadImported}
  231. * @param imported
  232. * @param options
  233. */
  234. async addProcessedAssets<T extends ImportResult|undefined = ImportResult>(imported: (T|undefined)[], options?: AddAssetOptions): Promise<(T | undefined)[]> {
  235. return this.loadImported(imported, options)
  236. }
  237. async addAssetSingle<T extends ImportResult = ImportResult>(asset?: IAsset | string, options?: ImportAssetOptions): Promise<T|undefined> {
  238. return !asset ? undefined : (await this.addAsset<T>(asset, options))?.[0]
  239. }
  240. // processAndAddObjects
  241. async addRaw<T extends (ImportResult|undefined) = ImportResult>(res: T|T[], options: AddRawOptions = {}): Promise<(T|undefined)[]> {
  242. const r = await this.importer.processRaw<T>(res, options)
  243. return this.loadImported<T[]>(r, options)
  244. }
  245. async addRawSingle<T extends ImportResult|undefined = ImportResult|undefined>(res: T, options: AddRawOptions = {}): Promise<T|undefined> {
  246. return (await this.addRaw<T>(res, options))?.[0]
  247. }
  248. private _sceneUpdated(event: ISceneEvent) { // todo: check if objects are added some other way.
  249. if (event.type === 'addSceneObject') {
  250. const target = event.object as ImportResult
  251. switch (target.assetType) {
  252. case 'material':
  253. this.materials.registerMaterial(<IMaterial>target)
  254. break
  255. case 'texture':
  256. break
  257. case 'model':
  258. case 'light':
  259. case 'camera':
  260. break
  261. default:
  262. break
  263. }
  264. } else if (event.type === 'materialChanged') {
  265. const target = event.material as IMaterial | IMaterial[] | undefined
  266. const targets = Array.isArray(target) ? target : target ? [target] : []
  267. for (const t of targets) {
  268. this.materials.registerMaterial(t)
  269. }
  270. } else if (event.type === 'beforeDeserialize') {
  271. // object/material/texture to be deserialized
  272. const data = event.data
  273. const meta = event.meta
  274. if (!data.metadata) {
  275. console.warn('Invalid data(no metadata)', data)
  276. }
  277. console.log(data, event)
  278. if (event.material) {
  279. if (data.metadata?.type !== 'Material') {
  280. console.warn('Invalid material data', data)
  281. }
  282. JSONMaterialLoader.DeserializeMaterialJSON(data, this.viewer, meta, event.material).then(()=>{
  283. //
  284. })
  285. }
  286. } else {
  287. console.error('Unexpected')
  288. }
  289. }
  290. dispose() {
  291. this.importer.dispose()
  292. this.materials.dispose()
  293. this.viewer.scene.removeEventListener('addSceneObject', this._sceneUpdated)
  294. this.viewer.scene.removeEventListener('materialChanged', this._sceneUpdated)
  295. this.exporter.dispose()
  296. }
  297. protected _addImporters() {
  298. const viewer = this.viewer
  299. if (!viewer) return
  300. const importers: Importer[] = [
  301. new Importer(TextureLoader, ['webp', 'png', 'jpeg', 'jpg', 'svg', 'ico', 'data:image'], [
  302. 'image/webp', 'image/png', 'image/jpeg', 'image/svg+xml', 'image/gif', 'image/bmp', 'image/tiff', 'image/x-icon',
  303. ], false), // todo: use ImageBitmapLoader if supported (better performance)
  304. new Importer<JSONMaterialLoader>(JSONMaterialLoader,
  305. ['mat', ...this.materials.templates.map(t=>t.typeSlug!).filter(v=>v)], // todo add others
  306. [], false, (loader)=>{
  307. if (loader) loader.viewer = this.viewer
  308. return loader
  309. }),
  310. new Importer(class extends RGBELoader {
  311. constructor(manager: LoadingManager) {
  312. super(manager)
  313. this.setDataType(getTextureDataType(viewer.renderManager.renderer))
  314. }
  315. }, ['hdr'], ['image/vnd.radiance'], false),
  316. new Importer(class extends EXRLoader {
  317. constructor(manager: LoadingManager) {
  318. super(manager)
  319. this.setDataType(getTextureDataType(viewer.renderManager.renderer))
  320. }
  321. }, ['exr'], ['image/x-exr'], false),
  322. new Importer(FBXLoader, ['fbx'], ['model/fbx'], true),
  323. new Importer(ZipLoader, ['zip'], ['application/zip'], true),
  324. new Importer(OBJLoader2 as any as Class<ILoader>, ['obj'], ['model/obj'], true),
  325. new Importer(MTLLoader2 as any as Class<ILoader>, ['mtl'], ['model/mtl'], false),
  326. new Importer<GLTFLoader2>(GLTFLoader2, ['gltf', 'glb', 'data:model/gltf'], ['model/gltf', 'model/gltf+json', 'model/gltf-binary'], true, (l, _, i) => l?.setup(this.viewer, i.extensions)),
  327. new Importer(DRACOLoader2, ['drc'], ['model/mesh+draco'], true),
  328. ]
  329. this.importer.addImporter(...importers)
  330. }
  331. protected _addExporters() {
  332. const exporters: IExporter[] = [
  333. {ext: ['gltf', 'glb'], extensions: [], ctor: (_, exporter)=>{
  334. const ex = new GLTFExporter2()
  335. // This should be added at the end.
  336. ex.setup(this.viewer, exporter.extensions)
  337. return ex
  338. }},
  339. ]
  340. this.exporter.addExporter(...exporters)
  341. }
  342. private _initCacheStorage(simpleCache?: boolean, storage?: Cache | Storage | boolean) {
  343. if (storage === true && window?.caches) {
  344. window.caches.open?.('threepipe-assetmanager').then(c => {
  345. this._initCacheStorage(simpleCache, c)
  346. this._storage = c
  347. })
  348. return
  349. }
  350. if (simpleCache || storage) {
  351. // three.js built-in simple memory cache. used in FileLoader.js todo: use local storage somehow
  352. if (simpleCache) threeCache.enabled = true
  353. if (storage && window.Cache && typeof window.Cache === 'function' && storage instanceof window.Cache) {
  354. overrideThreeCache(storage)
  355. // todo: clear cache
  356. }
  357. }
  358. this._storage = typeof storage === 'boolean' ? undefined : storage
  359. }
  360. // region deprecated
  361. /**
  362. * @deprecated use addRaw instead
  363. * @param res
  364. * @param options
  365. */
  366. async addImported<T extends (ImportResult|undefined) = ImportResult>(res: T|T[], options: AddRawOptions = {}): Promise<(T|undefined)[]> {
  367. console.error('addImported is deprecated, use addRaw instead')
  368. return this.addRaw(res, options)
  369. }
  370. /**
  371. * @deprecated use addAsset instead
  372. * @param path
  373. * @param options
  374. */
  375. public async addFromPath(path: string, options: ImportAddOptions = {}): Promise<any[]> {
  376. console.error('addFromPath is deprecated, use addAsset instead')
  377. return this.addAsset(path, options)
  378. }
  379. /**
  380. * @deprecated use {@link ThreeViewer.exportConfig} instead
  381. * @param binary - if set to false, encodes all the array buffers to base64
  382. */
  383. exportViewerConfig(binary = true): Record<string, any> {
  384. if (!this.viewer) return {}
  385. console.error('exportViewerConfig is deprecated, use viewer.toJSON instead')
  386. return this.viewer.toJSON(binary, undefined)
  387. }
  388. /**
  389. * @deprecated use {@link ThreeViewer.exportPluginsConfig} instead
  390. * @param filter
  391. */
  392. exportPluginPresets(filter?: string[]) {
  393. console.error('exportPluginPresets is deprecated, use viewer.exportPluginsConfig instead')
  394. return this.viewer?.exportPluginsConfig(filter)
  395. }
  396. /**
  397. * @deprecated use {@link ThreeViewer.exportPluginConfig} instead
  398. * @param plugin
  399. */
  400. exportPluginPreset(plugin: IViewerPlugin) {
  401. console.error('exportPluginPreset is deprecated, use viewer.exportPluginConfig instead')
  402. return this.viewer?.exportPluginConfig(plugin)
  403. }
  404. /**
  405. * @deprecated use {@link ThreeViewer.importPluginConfig} instead
  406. * @param json
  407. * @param plugin
  408. */
  409. async importPluginPreset(json: any, plugin?: IViewerPlugin) {
  410. console.error('importPluginPreset is deprecated, use viewer.importPluginConfig instead')
  411. return this.viewer?.importPluginConfig(json, plugin)
  412. }
  413. // todo continue from here by moving functions to the viewer.
  414. /**
  415. * @deprecated use {@link ThreeViewer.importConfig} instead
  416. * @param viewerConfig
  417. */
  418. async importViewerConfig(viewerConfig: any) {
  419. return this.viewer?.importConfig(viewerConfig)
  420. }
  421. /**
  422. * @deprecated use {@link ThreeViewer.fromJSON} instead
  423. * @param viewerConfig
  424. */
  425. applyViewerConfig(viewerConfig: any, resources?: any) {
  426. console.error('applyViewerConfig is deprecated, use viewer.fromJSON instead')
  427. return this.viewer?.fromJSON(viewerConfig, resources)
  428. }
  429. /**
  430. * @deprecated moved to {@link ThreeViewer.loadConfigResources}
  431. * @param json
  432. * @param extraResources - preloaded resources in the format of viewer config resources.
  433. */
  434. async importConfigResources(json: any, extraResources?: any) {
  435. if (!this.importer) throw 'Importer not initialized yet.'
  436. if (json.__isLoadedResources) return json
  437. return this.viewer?.loadConfigResources(json, extraResources)
  438. }
  439. // endregion
  440. }