threepipe
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

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