threepipe
選択できるのは25トピックまでです。 トピックは、先頭が英数字で、英数字とダッシュ('-')を使用した35文字以内のものにしてください。

AssetManager.ts 21KB

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