threepipe
Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

serialization.ts 36KB

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