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.

serialization.ts 30KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. import {arrayBufferToBase64, base64ToArrayBuffer, getTypedArray, Serialization} from 'ts-browser-helpers'
  2. import {
  3. Color,
  4. Material,
  5. MaterialLoader,
  6. Matrix3,
  7. Matrix4,
  8. ObjectLoader,
  9. Quaternion,
  10. Source,
  11. Texture,
  12. Vector2,
  13. Vector3,
  14. Vector4,
  15. } from 'three'
  16. import type {AssetImporter, AssetManager, MaterialManager} from '../assetmanager'
  17. import {IAssetImporter} from '../assetmanager'
  18. import {ThreeViewer} from '../viewer'
  19. import {ITexture} from '../core'
  20. const copier = (c: any) => (v: any, o: any) => o?.copy?.(v) ?? new c().copy(v)
  21. export class ThreeSerialization {
  22. static {
  23. const primitives = [
  24. [Vector2, 'isVector2', ['x', 'y']],
  25. [Vector3, 'isVector3', ['x', 'y', 'z']],
  26. [Vector4, 'isVector4', ['x', 'y', 'z', 'w']],
  27. [Quaternion, 'isQuaternion', ['x', 'y', 'z', 'w']],
  28. [Color, 'isColor', ['r', 'g', 'b']],
  29. [Matrix3, 'isMatrix3', ['elements']],
  30. [Matrix4, 'isMatrix4', ['elements']],
  31. ] as const
  32. Serialization.RegisterSerializer(...primitives.map(p=>({
  33. priority: 1,
  34. isType: (obj: any) => obj[p[1]],
  35. serialize: (obj: any) => {
  36. const ret = {[p[1]]: true}
  37. for (const k of p[2]) ret[k] = obj[k]
  38. return ret
  39. },
  40. deserialize: copier(p[0]),
  41. })))
  42. // texture
  43. Serialization.RegisterSerializer({
  44. priority: 2,
  45. isType: (obj: any) => obj.isTexture || obj.metadata?.type === 'Texture',
  46. serialize: (obj: any, meta?: SerializationMetaType) => {
  47. if (!obj?.isTexture) throw new Error('Expected a texture')
  48. if (obj.isRenderTargetTexture) return undefined // todo: support render targets
  49. if (meta?.textures[obj.uuid]) return {uuid: obj.uuid, resource: 'textures'}
  50. const imgData = obj.source.data
  51. if (obj.userData.rootPath) obj.source.data = null // if root-path exists we don't need to serialize the image data
  52. const ud = obj.userData
  53. obj.userData = {} // toJSON will call JSON.stringify, which will serialize userData
  54. const meta2 = {images: {} as any} // in-case meta is undefined
  55. let res = obj.toJSON(meta || meta2)
  56. if (!meta && res.image) res.image = obj.userData.rootPath ? undefined : meta2.images[res.image]
  57. obj.userData = ud
  58. res.userData = Serialization.Serialize(copyTextureUserData({}, ud), meta, false)
  59. if (obj.userData.rootPath) {
  60. if (meta) delete meta.images[obj.source.uuid] // because its empty. uuid still stored in the texture.image
  61. obj.source.data = imgData
  62. }
  63. if (meta?.textures && !res.resource) {
  64. if (!meta.textures[res.uuid])
  65. meta.textures[res.uuid] = res
  66. res = {uuid: res.uuid, resource: 'textures'}
  67. }
  68. return res
  69. },
  70. deserialize: (dat: any, obj: any, meta?: SerializationMetaType) => {
  71. if (dat.isTexture) return dat
  72. if (dat.resource === 'textures' && meta?.textures?.[dat.uuid]) return meta.textures[dat.uuid]
  73. console.warn('Cannot deserialize texture into object like primitive, since textures need to be loaded asynchronously. Trying with ObjectLoader. Load events might not work properly.', dat, obj)
  74. const loader = meta?._context.objectLoader ?? new ObjectLoader(meta?._context.assetImporter?.loadingManager)
  75. const data = {...dat}
  76. if (typeof data.image === 'string') {
  77. if (!meta?.images) {
  78. console.error('Cannot deserialize texture with image url without meta.images', data)
  79. } else {
  80. data.image = meta.images[data.image]
  81. }
  82. }
  83. if (!data.image || typeof data.image === 'string' || !data.image.isSource && !data.image.url) {
  84. console.error('Cannot deserialize texture', data)
  85. return obj
  86. }
  87. let imageOnLoad: undefined | (()=>void)
  88. if (meta && !data.image.isSource) {
  89. if (!meta._context.imagePromises) meta._context.imagePromises = []
  90. meta._context.imagePromises.push(new Promise<void>((resolve) => {
  91. imageOnLoad = resolve
  92. }))
  93. }
  94. const sources = data.image.isSource ? {[data.image.uuid]: data.image as Source} : loader.parseImages([data.image], imageOnLoad)
  95. data.image = Object.keys(sources)[0]
  96. if (meta?.images) meta.images[data.image] = sources[data.image]
  97. if (data.userData) data.userData = ThreeSerialization.Deserialize(data.userData, {}, meta)
  98. const textures = loader.parseTextures([data], sources)
  99. const uuid = Object.keys(textures)[0]
  100. if (!uuid || !textures[uuid]) {
  101. console.error('Cannot deserialize texture', data)
  102. return obj
  103. }
  104. if (meta?.textures) meta.textures[uuid] = textures[uuid]
  105. return textures[uuid]
  106. },
  107. })
  108. // material
  109. Serialization.RegisterSerializer({
  110. priority: 2,
  111. isType: (obj: any) => obj.isMaterial || obj.metadata?.type === 'Material',
  112. serialize: (obj: any, meta?: SerializationMetaType) => {
  113. if (!obj?.isMaterial) throw new Error('Expected a material')
  114. if (meta?.materials[obj.uuid]) return {uuid: obj.uuid, resource: 'materials'}
  115. if (obj.userData.rootPath) {
  116. // todo
  117. // it works for textures because image(Source) are immutable
  118. console.error('TODO: handle material with root path with material inheritance/hierarchy')
  119. }
  120. // serialize textures separately
  121. const meta2 = meta ?? {textures: {}, images: {}}
  122. const objTextures: any = {}
  123. const tempTextures: any = {}
  124. const propList = Object.keys(obj.constructor.MaterialProperties || obj)
  125. for (const k of propList) {
  126. if (k.startsWith('__')) continue // skip private/internal textures/properties
  127. const v = obj[k]
  128. if (v?.isTexture) {
  129. const ser = Serialization.Serialize(v, meta2)
  130. objTextures[k] = ser
  131. tempTextures[k] = v
  132. obj[k] = ser ? {isTexture: true, toJSON: ()=> ser} : null // because of how threejs Material.toJSON serializes textures
  133. }
  134. }
  135. // Serialize without userData because three.js tries to convert it to string. We are serializing it separately
  136. const userData = obj.userData
  137. obj.userData = {}
  138. let res = obj.toJSON(meta, true) // copying userData is handled in toJSON, see MeshStandardMaterial2
  139. obj.userData = userData
  140. serializeMaterialUserData(res, userData, meta)
  141. // todo: override generator to mention that this is a custom serializer?
  142. res.userData.uuid = obj.userData.uuid
  143. if (obj.constructor.TYPE) res.type = obj.constructor.TYPE // override type if specified as static property in the class
  144. // Remove undefined values. Note that null values are kept.
  145. for (const key of Object.keys(res)) if (res[key] === undefined) delete res[key]
  146. // Restore textures
  147. for (const [k, v] of Object.entries(tempTextures)) {
  148. obj[k] = v
  149. delete tempTextures[k]
  150. }
  151. // Add material, textures, images to meta
  152. // serialize textures are already added to meta by the texture serializer
  153. if (meta) {
  154. for (const [k, v] of Object.entries(objTextures)) {
  155. if (v) res[k] = v // can be undefined because of RenderTargetTexture...
  156. }
  157. if (meta.materials) {
  158. if (!meta.materials[res.uuid])
  159. meta.materials[res.uuid] = res
  160. res = {uuid: res.uuid, resource: 'materials'}
  161. }
  162. } else {
  163. for (const [k, v] of Object.entries(objTextures)) {
  164. if (v) res[k] = (v as any).uuid // to remain compatible with how three.js saves
  165. }
  166. res.textures = Object.values(meta2.textures)
  167. res.images = Object.values(meta2.images)
  168. }
  169. return res
  170. },
  171. deserialize: (dat: any, obj: any, meta?: SerializationMetaType) => {
  172. function finalCopy(material: Material) {
  173. if (material.isMaterial) {
  174. if (obj?.isMaterial && obj.uuid === material.uuid) {
  175. if (obj !== material && typeof obj.setValues === 'function') {
  176. console.warn('material uuid already exists, copying values to old material')
  177. obj.setValues(material)
  178. }
  179. return obj
  180. } else {
  181. return material
  182. }
  183. }
  184. return undefined
  185. }
  186. let ret = finalCopy(dat)
  187. if (ret !== undefined) return ret
  188. if (dat.resource === 'materials' && meta?.materials?.[dat.uuid]) {
  189. ret = finalCopy(meta.materials[dat.uuid])
  190. if (ret !== undefined) return ret
  191. console.error('cannot find material in meta', dat, ret)
  192. }
  193. const type = dat.type
  194. if (!type) {
  195. console.error('Cannot deserialize material without type', dat)
  196. return obj
  197. }
  198. const data = {...dat} as Record<string, any>
  199. if (data.userData) data.userData = Serialization.Deserialize(data.userData, undefined, meta, false)
  200. //
  201. const textures: Record<string, Texture> = {}
  202. for (const [k, v] of Object.entries(data)) { // for textures
  203. if (typeof v === 'string' && meta?.textures?.[v]) {
  204. data[k] = meta.textures[v]
  205. textures[k] = meta.textures[v]
  206. }
  207. if (!v || !v.resource || typeof v.resource !== 'string') continue
  208. const resource = meta?.[v.resource as 'textures'|'extras']?.[v.uuid]
  209. data[k] = resource || null
  210. if (v.resource === 'textures' && resource?.isTexture) {
  211. textures[k] = resource
  212. }
  213. }
  214. // we have 2 options, either obj is null or it is a material.
  215. // if the material is not the same type, we can't use it, should we throw an error or create a new material and assign it. maybe a warning and create a new material?
  216. // to create a material, we need to know the type, type->material initialization can be done in either material manager or MaterialLoader
  217. // data has deserialized textures and userData, assuming the rest can be deserialized by material.fromJSON
  218. if (!obj || !obj.isMaterial || obj.type !== type && obj.constructor?.TYPE !== type) {
  219. if (obj && Object.keys(obj).length) console.warn('Material type mismatch during deserialize, creating a new material', obj, data, type, obj.constructor?.type)
  220. obj = null
  221. }
  222. // if obj is not null
  223. if (obj && (!data.uuid || obj.uuid === data.uuid)) {
  224. if (obj.fromJSON) obj.fromJSON(data, meta, true)
  225. else if (obj.setValues) obj.setValues(data)
  226. else console.error('Cannot deserialize material, no fromJSON or setValues method', obj, data)
  227. return obj
  228. }
  229. // obj is null or type mismatch, so ignore obj and create a new material
  230. // generate from material manager generator and call fromJSON with internal true which will call setValues
  231. const materialManager = meta?._context.materialManager
  232. if (materialManager) {
  233. const material = materialManager.create(type)
  234. if (material) {
  235. if (material.fromJSON) material.fromJSON(data, meta, true)
  236. else if (material.setValues) material.setValues(data)
  237. else console.error('Cannot deserialize material, no fromJSON or setValues method', material, data)
  238. return material
  239. }
  240. }
  241. console.warn('Legacy three.js material deserialization')
  242. // normal three.js material
  243. const loader = new MaterialLoader() // todo: get loader from meta.loaders
  244. for (const [k, v] of Object.entries(textures)) {
  245. data[k] = v.uuid
  246. }
  247. const texs = {...loader.textures}
  248. loader.setTextures(textures)
  249. const mat = loader.parse(data)
  250. loader.setTextures(texs)
  251. ret = finalCopy(mat)
  252. if (ret !== undefined) return ret
  253. console.error('cannot deserialize material', dat, ret, mat)
  254. },
  255. })
  256. }
  257. /**
  258. * Serialize an object
  259. * {@link Serialization.Serialize}
  260. */
  261. static Serialize = Serialization.Serialize
  262. /**
  263. * Deserialize an object
  264. * {@link Serialization.Deserialize}
  265. */
  266. static Deserialize = Serialization.Deserialize
  267. }
  268. /**
  269. * Deep copy/clone from source to dest, assuming both are userData objects for three.js objects/materials/textures etc.
  270. * This will clone any property that can be cloned (apart from Object3D, Texture, Material) and deep copy the objects and arrays.
  271. * @note Keep synced with copyMaterialUserData in three.js -> Material.js todo: merge these functions? by putting this inside three.js?
  272. * @param dest
  273. * @param source
  274. * @param ignoredKeysInRoot - keys to ignore in the root object
  275. * @param isRoot - always true, used for recursion
  276. */
  277. export function copyUserData(dest: any, source: any, ignoredKeysInRoot: (string|symbol)[] = [], isRoot = true): any {
  278. if (!source) return dest
  279. for (const key of Object.keys(source)) {
  280. if (isRoot && ignoredKeysInRoot.includes(key)) continue
  281. if (key.startsWith('__')) continue // double underscore
  282. const src = source[key]
  283. if (typeof dest[key] === 'function' || typeof src === 'function') continue
  284. // todo only clone vectors, colors etc
  285. const skipClone = !src || src.isTexture || src.isObject3D || src.isMaterial
  286. if (!skipClone && typeof src.clone === 'function')
  287. dest[key] = src.clone()
  288. // else if (!skipClone && (typeof src === 'object' || Array.isArray(src)))
  289. else if (!skipClone && (src.constructor === Object || Array.isArray(src)))
  290. dest[key] = copyUserData(Array.isArray(src) ? [] : {}, src, ignoredKeysInRoot, false)
  291. else
  292. dest[key] = src
  293. }
  294. return dest
  295. }
  296. /**
  297. * Deep copy/clone from source to dest, assuming both are userData objects in Textures.
  298. * Same as {@link copyUserData} but ignores uuid in the root object.
  299. * @param dest
  300. * @param source
  301. * @param isRoot
  302. * @param ignoredKeysInRoot
  303. */
  304. export function copyTextureUserData(dest: any, source: any, ignoredKeysInRoot = ['uuid'], isRoot = true): any {
  305. return copyUserData(dest, source, ignoredKeysInRoot, isRoot)
  306. }
  307. /**
  308. * Deep copy/clone from source to dest, assuming both are userData objects in Materials.
  309. * Same as {@link copyUserData} but ignores uuid in the root object.
  310. * @note Keep synced with copyMaterialUserData in three.js -> Material.js
  311. * @param dest
  312. * @param source
  313. * @param isRoot
  314. * @param ignoredKeysInRoot
  315. */
  316. export function copyMaterialUserData(dest: any, source: any, ignoredKeysInRoot = ['uuid'], isRoot = true): any {
  317. return copyUserData(dest, source, ignoredKeysInRoot, isRoot)
  318. }
  319. /**
  320. * Deep copy/clone from source to dest, assuming both are userData objects in Object3D.
  321. * Same as {@link copyUserData} but ignores uuid in the root object.
  322. * @param dest
  323. * @param source
  324. * @param isRoot
  325. * @param ignoredKeysInRoot
  326. */
  327. export function copyObject3DUserData(dest: any, source: any, ignoredKeysInRoot = ['uuid'], isRoot = true): any {
  328. return copyUserData(dest, source, ignoredKeysInRoot, isRoot)
  329. }
  330. /**
  331. * Serialize userData and sets to data.userData. This is required because three.js Material.toJSON does not serialize userData.
  332. * @param data
  333. * @param userData
  334. * @param meta
  335. */
  336. function serializeMaterialUserData(data: any, userData: any, meta?: SerializationMetaType) {
  337. data.userData = {}
  338. copyMaterialUserData(data.userData, userData)
  339. // Serialize the userData
  340. const meta2 = meta || { // Make meta object for the Serializer from the data. This requires changing from Array to Object for textures and images
  341. textures: Object.fromEntries(data.textures?.map((t: any) => [t.uuid, t]) || []),
  342. images: Object.fromEntries(data.images?.map((t: any) => [t.uuid, t]) || []),
  343. }
  344. data.userData = Serialization.Serialize(data.userData, meta2) // here meta is required for textures otherwise images will be lost. Material.toJSON sets the result as meta if not provided.
  345. if (!meta) {
  346. // Add textures and images to the result if meta is not provided. This is to remain compatible with how three.js saves materials. See (MaterialLoader and ThreeMaterialLoader)
  347. if (Object.keys(meta2.textures).length > 0) data.textures = Object.values(meta2.textures)
  348. if (Object.keys(meta2.images).length > 0) data.images = Object.values(meta2.images)
  349. }
  350. }
  351. /**
  352. * Converts array buffers to base64 strings in meta.
  353. * This is useful when storing .json files, as storing as number arrays takes a lot of space.
  354. * Used in viewer.toJSON()
  355. * @param meta
  356. */
  357. export function convertArrayBufferToStringsInMeta(meta: SerializationMetaType) {
  358. Object.values(meta).forEach((res: any) => { // similar to processViewer in gltf export.
  359. if (res) Object.values(res).forEach((item: any) => {
  360. if (!item.url) return
  361. // console.log(item.url)
  362. if (!(item.url.data instanceof ArrayBuffer) && !Array.isArray(item.url.data)) return
  363. if (item.url.type === 'Uint16Array') {
  364. if (!(item.url.data instanceof Uint16Array)) { // because it can be a typed array
  365. item.url.data = new Uint16Array(item.url.data)
  366. }
  367. item.url.data = 'data:application/octet-stream;base64,' + arrayBufferToBase64(item.url.data.buffer)
  368. } else if (item.url.type === 'Uint8Array') {
  369. if (!(item.url.data instanceof Uint8Array)) { // because it can be a typed array
  370. item.url.data = new Uint8Array(item.url.data)
  371. }
  372. // todo: just use jpeg or PNG encoding for this ?
  373. item.url.data = 'data:application/octet-stream;base64,' + arrayBufferToBase64(item.url.data.buffer)
  374. } else if (item.url.data instanceof ArrayBuffer) {
  375. item.url.data = 'data:application/octet-stream;base64,' + arrayBufferToBase64(item.url.data.buffer)
  376. } else {
  377. console.warn('Unsupported buffer type', item.url.type)
  378. }
  379. })
  380. })
  381. }
  382. /**
  383. * Converts strings(base64 or utf-8) to array buffers in meta. This is the reverse of {@link convertArrayBufferToStringsInMeta}
  384. * Used in viewer.fromJSON()
  385. */
  386. export function convertStringsToArrayBuffersInMeta(meta: SerializationMetaType) {
  387. Object.values(meta).forEach((res: any) => { // similar to processViewer in gltf export.
  388. if (res) Object.values(res).forEach((item: any) => {
  389. if (!item || !item.url) return
  390. if (typeof item.url.data !== 'string') return
  391. // base64 data uri or any mime type
  392. // console.log(item.url.data?.match?.(/^data:.*;base64,(.*)$/))
  393. const dataUriMatch = item.url.data.match(/^data:.*;base64,(.*)$/)
  394. if (dataUriMatch?.[1]) {
  395. item.url.data = base64ToArrayBuffer(dataUriMatch?.[1])
  396. } else { // utf-8 string, not used at the moment
  397. if (item.url.type !== 'Uint8Array') {
  398. console.error('Unsupported buffer type string for ', item.url.type, 'use base64')
  399. }
  400. item.url.data = new TextEncoder().encode(item.url.data).buffer // todo: this doesnt work in ie/edge maybe, but this feature is not used.
  401. }
  402. })
  403. })
  404. }
  405. export function getEmptyMeta(): SerializationMetaType {
  406. return { // see Object3D.js toJSON for more details
  407. geometries: {},
  408. materials: {},
  409. textures: {},
  410. images: {},
  411. shapes: [],
  412. skeletons: {},
  413. animations: [],
  414. extras: {},
  415. _context: {},
  416. }
  417. }
  418. export interface SerializationResourcesType {
  419. geometries: Record<string, any>,
  420. materials: Record<string, any>,
  421. textures: Record<string, any>,
  422. images: Record<string, any>,
  423. shapes: Record<string, any>,
  424. skeletons: Record<string, any>,
  425. animations: Record<string, any>,
  426. extras: Record<string, any>,
  427. object?: any,
  428. [key: string]: any,
  429. }
  430. export interface SerializationMetaType extends SerializationResourcesType {
  431. _context: {
  432. assetImporter?: AssetImporter,
  433. objectLoader?: ObjectLoader,
  434. materialManager?: MaterialManager,
  435. assetManager?: AssetManager,
  436. imagePromises?: Promise<any>[],
  437. [key: string]: any,
  438. }
  439. __isLoadedResources?: boolean
  440. }
  441. export class MetaImporter {
  442. /**
  443. * @param json
  444. * @param objLoader
  445. * @param extraResources - preloaded resources in the format of viewer config resources.
  446. */
  447. static async ImportMeta(json: SerializationMetaType, extraResources?: Partial<SerializationResourcesType>) {
  448. // console.log(json)
  449. if (json.__isLoadedResources) return json
  450. const resources: SerializationMetaType = metaFromResources()
  451. resources._context = json._context
  452. convertStringsToArrayBuffersInMeta(json)
  453. // console.log(viewerConfig)
  454. const assetImporter = json._context.assetImporter
  455. if (!assetImporter) throw new Error('assetImporter not found in meta context, which is required for import meta.')
  456. const objLoader = json._context.objectLoader || new ObjectLoader(assetImporter.loadingManager)
  457. // see ObjectLoader.parseAsync
  458. resources.animations = json.animations ? objLoader.parseAnimations(Object.values(json.animations)) : {}
  459. if (extraResources && extraResources.animations) resources.animations = {...resources.animations, ...extraResources.animations}
  460. resources.shapes = json.shapes ? objLoader.parseShapes(Object.values(json.shapes)) : {}
  461. if (extraResources && extraResources.shapes) resources.shapes = {...resources.shapes, ...extraResources.shapes}
  462. resources.geometries = json.geometries ? objLoader.parseGeometries(Object.values(json.geometries), Object.values(resources.shapes)) : {}
  463. if (extraResources && extraResources.geometries) resources.geometries = {...resources.geometries, ...extraResources.geometries}
  464. resources.images = json.images ? await objLoader.parseImagesAsync(Object.values(json.images)) : {} // local images only like data url and data textures
  465. if (extraResources && extraResources.images) resources.images = {...resources.images, ...extraResources.images}
  466. // const onLoad = () => { // todo: do it after all the images not after one
  467. // Object.values(resources.textures).forEach((t: any) => {
  468. // if (t.isTexture && t.image?.complete) t.needsUpdate = true
  469. // })
  470. // }
  471. await MetaImporter.LoadRootPathTextures({textures: json.textures, images: resources.images}, assetImporter)
  472. // console.log(json.textures)
  473. const textures = []
  474. for (const texture of Object.values(json.textures)) {
  475. const tex = {...texture}
  476. if (tex.userData) tex.userData = ThreeSerialization.Deserialize(tex.userData, {}, resources)
  477. textures.push(tex)
  478. }
  479. resources.textures = json.textures ? objLoader.parseTextures(textures, resources.images) : {}
  480. for (const entry of Object.entries(resources.textures)) {
  481. entry[1] = await assetImporter.processRawSingle(entry[1], {})
  482. if (entry[1]) resources.textures[entry[0]] = entry[1]
  483. else delete resources.textures[entry[0]]
  484. }
  485. if (extraResources && extraResources.textures) resources.textures = {...resources.textures, ...extraResources.textures}
  486. const jsonMats: any[] = json.materials ? Object.values(json.materials) : []
  487. resources.materials = {}
  488. for (const material of jsonMats) {
  489. if (!material?.uuid) continue
  490. // Object.entries(material).forEach(([k, data]: [string, any]) => {
  491. // if (data && data.resource && data.uuid && data.resource === 'textures') { // for textures put in by serialize.ts
  492. // material[k] = data.uuid
  493. // }
  494. // })
  495. resources.materials[material.uuid] = ThreeSerialization.Deserialize(material, undefined, resources)
  496. }
  497. if (extraResources && extraResources.materials) resources.materials = {...resources.materials, ...extraResources.materials}
  498. if (json.object) {
  499. resources.object = objLoader.parseObject(json.object, resources.geometries, resources.materials, resources.textures, resources.animations)
  500. if (json.skeletons) {
  501. resources.skeletons = objLoader.parseSkeletons(Object.values(json.skeletons), resources.object as any)
  502. objLoader.bindSkeletons(resources.object as any, resources.skeletons)
  503. }
  504. }
  505. if (json.extras) {
  506. resources.extras = json.extras
  507. for (const e of (Object.values(json.extras) as any as any[])) {
  508. if (!e.uuid) continue
  509. if (!e.url) continue
  510. // see LUTCubeTextureWrapper, KTX2LoadPlugin for sample use
  511. if (typeof e.url === 'string') {
  512. const r = await assetImporter.importPath(e.url)
  513. if (r?.length > 0) resources.extras[e.uuid] = r[0]
  514. } else if (e.url.data) {
  515. const file = new File([getTypedArray(e.url.type, e.url.data)], e.url.path)
  516. // console.log(file, e)
  517. const r = await assetImporter.importAsset({path: file.name, file})
  518. // console.log(r)
  519. // todo: userdata? name? other properties?
  520. if (r?.length > 0) resources.extras[e.uuid] = r[0]
  521. } else {
  522. console.warn('invalid URL type while loading extra resource')
  523. }
  524. }
  525. // console.log(resources.extras)
  526. }
  527. if (extraResources && extraResources.extras) resources.extras = {...resources.extras, ...extraResources.extras}
  528. // console.log(resources, json)
  529. resources.__isLoadedResources = true
  530. return resources
  531. }
  532. static async LoadRootPathTextures({textures, images}: Pick<SerializationMetaType, 'textures'|'images'>, importer: IAssetImporter) {
  533. const pms = []
  534. for (const inpTexture of Object.values(textures ?? {} as any) as any as any[]) {
  535. const path = inpTexture?.userData?.rootPath // done separately(from parseTextures2) for hdr etc textures.
  536. if (path && (!inpTexture.image || !images[inpTexture.image])) {
  537. pms.push(importer.importSingle<ITexture>(path, {processRaw: false}).then(texture => {
  538. const source = texture?.source as any
  539. // const image = texture?.image as any
  540. if (!texture || !source) return
  541. // console.log(typeof image)
  542. const source2 = new Source(source.data)
  543. if (inpTexture.image) source2.uuid = inpTexture.image
  544. images[source2.uuid] = source2
  545. inpTexture.image = source2.uuid
  546. texture.dispose() // todo: what happens when we reimport a cached disposed texture asset, is three.js able to recreate the webgl texture on render?
  547. }).catch((e)=>{
  548. console.error(e)
  549. delete inpTexture.userData.rootPath
  550. }))
  551. }
  552. }
  553. await Promise.allSettled(pms)
  554. }
  555. }
  556. export function metaToResources(meta?: SerializationMetaType): Partial<SerializationResourcesType> {
  557. if (!meta) return {}
  558. const res: Partial<SerializationResourcesType> = {...meta}
  559. if (res._context) delete res._context
  560. return meta
  561. }
  562. export function metaFromResources(resources?: Partial<SerializationResourcesType>, viewer?: ThreeViewer): SerializationMetaType {
  563. return {
  564. ...getEmptyMeta(),
  565. ...resources,
  566. _context: {
  567. assetManager: viewer?.assetManager,
  568. assetImporter: viewer?.assetManager.importer,
  569. materialManager: viewer?.assetManager.materials,
  570. }, // clear context even if its present in resources
  571. }
  572. }