| { | { | ||||
| "name": "threepipe", | "name": "threepipe", | ||||
| "version": "0.0.5", | |||||
| "version": "0.0.6-dev", | |||||
| "description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.", | "description": "A 3D viewer framework built on top of three.js in TypeScript with a focus on quality rendering, modularity and extensibility.", | ||||
| "main": "src/index.ts", | "main": "src/index.ts", | ||||
| "module": "dist/index.mjs", | "module": "dist/index.mjs", | ||||
| "rollup-plugin-license": "^3.0.1", | "rollup-plugin-license": "^3.0.1", | ||||
| "rollup-plugin-postcss": "^4.0.2", | "rollup-plugin-postcss": "^4.0.2", | ||||
| "stats.js": "^0.17.0", | "stats.js": "^0.17.0", | ||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2006/package.tgz", | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2007/package.tgz", | |||||
| "tslib": "^2.5.0", | "tslib": "^2.5.0", | ||||
| "tweakpane": "^3.1.9", | "tweakpane": "^3.1.9", | ||||
| "@tweakpane/core": "^1.1.8", | "@tweakpane/core": "^1.1.8", | ||||
| "tweakpane-image-plugin": "https://github.com/repalash/tweakpane-image-plugin/releases/download/v1.1.403/package.tgz" | "tweakpane-image-plugin": "https://github.com/repalash/tweakpane-image-plugin/releases/download/v1.1.403/package.tgz" | ||||
| }, | }, | ||||
| "dependencies": { | "dependencies": { | ||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1006/package.tgz", | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1008/package.tgz", | |||||
| "@types/webxr": "^0.5.1", | "@types/webxr": "^0.5.1", | ||||
| "@types/wicg-file-system-access": "^2020.9.5", | "@types/wicg-file-system-access": "^2020.9.5", | ||||
| "ts-browser-helpers": "^0.6.0" | "ts-browser-helpers": "^0.6.0" | ||||
| "uiconfig.js": "^0.0.4", | "uiconfig.js": "^0.0.4", | ||||
| "ts-browser-helpers": "^0.5.0", | "ts-browser-helpers": "^0.5.0", | ||||
| "uiconfig-tweakpane": "^0.0.3", | "uiconfig-tweakpane": "^0.0.3", | ||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2006/package.tgz", | |||||
| "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2006.tar.gz", | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1006/package.tgz", | |||||
| "@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.1006.tar.gz", | |||||
| "three": "https://github.com/repalash/three.js-modded/releases/download/v0.152.2007/package.tgz", | |||||
| "three-f": "https://github.com/repalash/three.js-modded/archive/refs/tags/v0.152.2007.tar.gz", | |||||
| "@types/three": "https://github.com/repalash/three-ts-types/releases/download/v0.152.1008/package.tgz", | |||||
| "@types/three-f": "https://github.com/repalash/three-ts-types/archive/refs/tags/v0.152.1008.tar.gz", | |||||
| "@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three", | "@types/three-pkg": "https://gitpkg.now.sh/repalash/three-ts-types/types/three?modded_three", | ||||
| "tweakpane-image-plugin": "git+ssh://github.com/repalash/tweakpane-image-plugin.git#52d5542047fd07d2e7225b5b67c9f7620366f2c7" | "tweakpane-image-plugin": "git+ssh://github.com/repalash/tweakpane-image-plugin.git#52d5542047fd07d2e7225b5b67c9f7620366f2c7" | ||||
| }, | }, |
| import {GLTFExporter, GLTFExporterOptions, GLTFExporterPlugin} from 'three/examples/jsm/exporters/GLTFExporter.js' | import {GLTFExporter, GLTFExporterOptions, GLTFExporterPlugin} from 'three/examples/jsm/exporters/GLTFExporter.js' | ||||
| import {IExportParser} from '../IExporter' | import {IExportParser} from '../IExporter' | ||||
| import {AnyOptions} from 'ts-browser-helpers' | |||||
| import {GLTFWriter2} from './GLTFWriter2' | import {GLTFWriter2} from './GLTFWriter2' | ||||
| import {Object3D} from 'three' | import {Object3D} from 'three' | ||||
| import {ThreeViewer} from '../../viewer' | import {ThreeViewer} from '../../viewer' | ||||
| import {GLTFObject3DExtrasExtension} from '../gltf/GLTFObject3DExtrasExtension' | |||||
| import {GLTFLightExtrasExtension} from '../gltf/GLTFLightExtrasExtension' | |||||
| import {GLTFMaterialsBumpMapExtension} from '../gltf/GLTFMaterialsBumpMapExtension' | |||||
| import {GLTFMaterialsDisplacementMapExtension} from '../gltf/GLTFMaterialsDisplacementMapExtension' | |||||
| import {GLTFMaterialsLightMapExtension} from '../gltf/GLTFMaterialsLightMapExtension' | |||||
| import {GLTFMaterialsAlphaMapExtension} from '../gltf/GLTFMaterialsAlphaMapExtension' | |||||
| import {GLTFMaterialExtrasExtension} from '../gltf/GLTFMaterialExtrasExtension' | |||||
| import {GLTFViewerConfigExtension} from '../gltf/GLTFViewerConfigExtension' | |||||
| import { | |||||
| GLTFLightExtrasExtension, | |||||
| GLTFMaterialExtrasExtension, | |||||
| GLTFMaterialsAlphaMapExtension, | |||||
| GLTFMaterialsBumpMapExtension, | |||||
| GLTFMaterialsDisplacementMapExtension, | |||||
| GLTFMaterialsLightMapExtension, | |||||
| GLTFObject3DExtrasExtension, | |||||
| GLTFViewerConfigExtension, | |||||
| } from '../gltf' | |||||
| import {glbEncryptionProcessor} from '../gltf/gltfEncyptionHelpers' | |||||
| export type GLTFExporter2Options = GLTFExporterOptions & { | export type GLTFExporter2Options = GLTFExporterOptions & { | ||||
| /** | /** | ||||
| * embed images in glb even when remote url is available, default = false | |||||
| * embed images in glb even when remote url is available, {@default false} | |||||
| */ | */ | ||||
| embedUrlImages?: boolean, | embedUrlImages?: boolean, | ||||
| /** | |||||
| * Embed previews of images in glb, {@default false} | |||||
| */ | |||||
| embedUrlImagePreviews?: boolean, | |||||
| /** | /** | ||||
| * export viewer config (scene settings) | * export viewer config (scene settings) | ||||
| */ | */ | ||||
| */ | */ | ||||
| exportExt?: string, | exportExt?: string, | ||||
| preserveUUIDs?: boolean, | preserveUUIDs?: boolean, | ||||
| externalImagesInExtras?: boolean, // see GLTFDracoExporter and extras extension | |||||
| encodeUint16Rgbe?: boolean // see GLTFViewerExport->processViewer, default = true | |||||
| /** | |||||
| * see GLTFDracoExporter and {@link GLTFMaterialExtrasExtension} | |||||
| */ | |||||
| externalImagesInExtras?: boolean, | |||||
| /** | |||||
| * see GLTFViewerExport->processViewer, {@default false} | |||||
| */ | |||||
| encodeUint16Rgbe?: boolean | |||||
| /** | |||||
| * Number of spaces to use when exporting to json, {@default 2} | |||||
| */ | |||||
| jsonSpaces?: number, | |||||
| /** | |||||
| * Encrypt the exported file in a GLB container using {@link encryptKey}, {@default false}. Works only for glb export. | |||||
| */ | |||||
| encrypt?: boolean, | |||||
| /** | |||||
| * Encryption key, if not provided, will be prompted, {@default undefined}. Works only for glb export. | |||||
| */ | |||||
| encryptKey?: string|Uint8Array, | |||||
| [key: string]: any | |||||
| } | } | ||||
| export class GLTFExporter2 extends GLTFExporter implements IExportParser { | export class GLTFExporter2 extends GLTFExporter implements IExportParser { | ||||
| constructor() { | |||||
| super() | |||||
| this.processors.push(glbEncryptionProcessor) | |||||
| } | |||||
| register(callback: (writer: GLTFWriter2)=>GLTFExporterPlugin): this { | register(callback: (writer: GLTFWriter2)=>GLTFExporterPlugin): this { | ||||
| return super.register(callback as any) | return super.register(callback as any) | ||||
| } | } | ||||
| async parseAsync(obj: any, options: AnyOptions): Promise<Blob> { | |||||
| processors: ((obj: ArrayBuffer|any|Blob, options: GLTFExporter2Options) => Promise<ArrayBuffer|any|Blob>)[] = [] | |||||
| async parseAsync(obj: ArrayBuffer|any, options: GLTFExporter2Options): Promise<Blob> { | |||||
| if (!obj) throw new Error('No object to export') | if (!obj) throw new Error('No object to export') | ||||
| const gltf = !obj.__isGLTFOutput && (Array.isArray(obj) || obj.isObject3D) ? await new Promise((resolve, reject) => this.parse(obj, resolve, reject, options)) : obj | |||||
| let gltf = !obj.__isGLTFOutput && (Array.isArray(obj) || obj.isObject3D) ? await new Promise((resolve, reject) => this.parse(obj, resolve, reject, options)) : obj | |||||
| for (const processor of this.processors) { | |||||
| gltf = await processor(gltf, options) | |||||
| } | |||||
| if (gltf && gltf instanceof Blob) return gltf | |||||
| if (gltf && typeof gltf === 'object' && !gltf.byteLength) { // byteLength is for ArrayBuffer | if (gltf && typeof gltf === 'object' && !gltf.byteLength) { // byteLength is for ArrayBuffer | ||||
| return new Blob([JSON.stringify(gltf, (k, v)=> k.startsWith('__') ? undefined : v, options.jsonSpaces ?? 2)], {type: 'model/gltf+json'}) | return new Blob([JSON.stringify(gltf, (k, v)=> k.startsWith('__') ? undefined : v, options.jsonSpaces ?? 2)], {type: 'model/gltf+json'}) | ||||
| } else if (gltf) { | } else if (gltf) { | ||||
| onDone: (gltf: ArrayBuffer | {[key: string]: any}) => void, | onDone: (gltf: ArrayBuffer | {[key: string]: any}) => void, | ||||
| onError: (error: ErrorEvent) => void, | onError: (error: ErrorEvent) => void, | ||||
| options: GLTFExporter2Options = {}, | options: GLTFExporter2Options = {}, | ||||
| ): any { | |||||
| ): void { | |||||
| const gltfOptions: GLTFWriter2['options'] = { | const gltfOptions: GLTFWriter2['options'] = { | ||||
| // default options | // default options | ||||
| binary: false, | binary: false, |
| !this.options.exporterOptions.embedUrlImages | !this.options.exporterOptions.embedUrlImages | ||||
| && (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:')) | && (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:')) | ||||
| ) { | ) { | ||||
| map.source.data = null // handled below in GLTFWriter2.processImage | |||||
| if (map.source.data) { // handled below in GLTFWriter2.processImage | |||||
| if (!this.options.exporterOptions.embedUrlImagePreviews || (map as any).isDataTexture) map.source.data = null // todo make sure its only Texture, check for svg etc | |||||
| else map.source.data._savePreview = true | |||||
| } | |||||
| delete map.userData.mimeType // for extensions like ktx2 | delete map.userData.mimeType // for extensions like ktx2 | ||||
| } | } | ||||
| if (map.userData.rootPath && !this.options.exporterOptions.embedUrlImages | if (map.userData.rootPath && !this.options.exporterOptions.embedUrlImages | ||||
| && (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:')) | && (map.userData.rootPath.startsWith('http') || map.userData.rootPath.startsWith('data:')) | ||||
| ) { | ) { | ||||
| map.source.data = srcData | |||||
| if (map.source.data) delete map.source.data._savePreview | |||||
| else map.source.data = srcData | |||||
| map.userData.mimeType = mimeType | map.userData.mimeType = mimeType | ||||
| if (!textureDef) { | if (!textureDef) { | ||||
| console.error('textureDef is null', processed, map) | console.error('textureDef is null', processed, map) | ||||
| return processed | return processed | ||||
| } | } | ||||
| if (textureDef.source >= 0) { | if (textureDef.source >= 0) { | ||||
| console.warn('textureDef.source is already set', processed, map) | |||||
| // console.warn('textureDef.source is already set', processed, map) | |||||
| const img = this.json.images[textureDef.source] | |||||
| if (img.uri) { | |||||
| console.warn('uri already set', img.uri) | |||||
| } else { | |||||
| img.uri = map.userData.rootPath | |||||
| img.mimeType = mimeType | |||||
| if (!img.extras) img.extras = {} | |||||
| img.extras.flipY = map.flipY | |||||
| img.extras.uri = map.userData.rootPath // uri is removed by gltf-transform if bufferView is set | |||||
| } | |||||
| } else { | } else { | ||||
| textureDef.source = this.processImageUri(map.image, map.userData.rootPath, map.flipY, mimeType) | textureDef.source = this.processImageUri(map.image, map.userData.rootPath, map.flipY, mimeType) | ||||
| } | } | ||||
| // Add extra check for null images. This is set in processTexture when we have a rootPath | // Add extra check for null images. This is set in processTexture when we have a rootPath | ||||
| processImage(image: any, format: PixelFormat, flipY: boolean, mimeType = 'image/png') { | processImage(image: any, format: PixelFormat, flipY: boolean, mimeType = 'image/png') { | ||||
| if (!image) return -1 | if (!image) return -1 | ||||
| return super.processImage(image, format, flipY, mimeType) | |||||
| // @ts-expect-error todo: update in three-ts-types | |||||
| return super.processImage(image, format, flipY, mimeType, image._savePreview ? 32 : undefined, image._savePreview ? 32 : undefined) | |||||
| } | } | ||||
| /** | /** |
| import {ThreeSerialization} from '../../utils/serialization' | import {ThreeSerialization} from '../../utils/serialization' | ||||
| import {DoubleSide, Material} from 'three' | import {DoubleSide, Material} from 'three' | ||||
| import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | import type {GLTFExporterPlugin, GLTFWriter} from 'three/examples/jsm/exporters/GLTFExporter' | ||||
| import {ITexture} from '../../core' | |||||
| export class GLTFMaterialExtrasExtension { | export class GLTFMaterialExtrasExtension { | ||||
| static readonly WebGiMaterialExtrasExtension = 'WEBGI_material_extras' | static readonly WebGiMaterialExtrasExtension = 'WEBGI_material_extras' | ||||
| const resources = this.materialExternalResources[material.uuid] | const resources = this.materialExternalResources[material.uuid] | ||||
| if (resources) { | if (resources) { | ||||
| Object.entries(resources).forEach(([k, v]) => { | |||||
| Object.entries(resources).forEach(([k, v]: [string, any|ITexture]) => { | |||||
| if (k.startsWith('_')) return | if (k.startsWith('_')) return | ||||
| let setFlag = false | |||||
| if (v?.userData && v.userData.embedUrlImagePreviews === undefined) { // check ThreeSerialization texture serialization and GLTFWriter2.processTexture | |||||
| v.userData.embedUrlImagePreviews = w.options.exporterOptions?.embedUrlImagePreviews | |||||
| setFlag = true | |||||
| } | |||||
| dat[k] = ThreeSerialization.Serialize(v, this.serializedMeta) | dat[k] = ThreeSerialization.Serialize(v, this.serializedMeta) | ||||
| if (v?.userData && setFlag) delete v.userData.embedUrlImagePreviews | |||||
| }) | }) | ||||
| } | } | ||||
| if (Object.keys(dat).length > 0) { | if (Object.keys(dat).length > 0) { |
| import {ISerializedViewerConfig, ThreeViewer} from '../../viewer' | import {ISerializedViewerConfig, ThreeViewer} from '../../viewer' | ||||
| import {Group, ImageUtils} from 'three' | import {Group, ImageUtils} from 'three' | ||||
| import {RGBEPNGLoader} from '../import/RGBEPNGLoader' | import {RGBEPNGLoader} from '../import/RGBEPNGLoader' | ||||
| import {SerializationResourcesType} from '../../utils/serialization' | |||||
| import {SerializationResourcesType} from '../../utils' | |||||
| import {RootSceneImportResult} from '../IAssetImporter' | |||||
| import {halfFloatToRgbe} from '../../three' | |||||
| export class GLTFViewerConfigExtension { | export class GLTFViewerConfigExtension { | ||||
| } | } | ||||
| scene = scenes[0] | scene = scenes[0] | ||||
| } | } | ||||
| const resultScene = resultScenes[0] | |||||
| const resultScene = resultScenes.length > 0 ? resultScenes[0] : undefined | |||||
| const viewerConfig: Partial<ISerializedViewerConfig> = scene.extensions?.[this.ViewerConfigGLTFExtension] | |||||
| const viewerConfig1: Partial<ISerializedViewerConfig> = scene.extensions?.[this.ViewerConfigGLTFExtension] | |||||
| // console.log({...viewerConfig?.resources}) | // console.log({...viewerConfig?.resources}) | ||||
| if (!viewerConfig) return {} | |||||
| if (!viewerConfig1) return {} | |||||
| const viewerConfig: ISerializedViewerConfig = { | |||||
| type: 'ThreeViewer', | |||||
| version: '0', | |||||
| plugins: [], | |||||
| assetType: 'config', | |||||
| ...viewerConfig1, | |||||
| } | |||||
| if (viewerConfig.resources) { | if (viewerConfig.resources) { | ||||
| await this._parseArrayBuffers(viewerConfig.resources, parser) | await this._parseArrayBuffers(viewerConfig.resources, parser) | ||||
| viewerConfig.resources = await viewer.loadConfigResources(viewerConfig.resources || {}, extraResources) | viewerConfig.resources = await viewer.loadConfigResources(viewerConfig.resources || {}, extraResources) | ||||
| ;(resultScene as any).importedViewerConfig = viewerConfig // todo | |||||
| if (resultScene) (resultScene as RootSceneImportResult).importedViewerConfig = viewerConfig | |||||
| } | } | ||||
| return viewerConfig | return viewerConfig | ||||
| const blob = new Blob([bufferView]) | const blob = new Blob([bufferView]) | ||||
| // const blob2 = new Blob([await blob.text()], {type: img.mimeType}) | // const blob2 = new Blob([await blob.text()], {type: img.mimeType}) | ||||
| let url = URL.createObjectURL(blob) | let url = URL.createObjectURL(blob) | ||||
| if ((buff.encodingVersion || 1) < 2) { | |||||
| const encodingVersion = buff.encodingVersion || 1 | |||||
| if (encodingVersion < 2) { | |||||
| url = 'data:image/png;base64,' + btoa(await blob.text()) | url = 'data:image/png;base64,' + btoa(await blob.text()) | ||||
| } | } | ||||
| // fetch(url).then(async r=>r.blob()).then(b=>console.log(b)) | // fetch(url).then(async r=>r.blob()).then(b=>console.log(b)) | ||||
| // console.log(view2) | // console.log(view2) | ||||
| buff.data = (await new RGBEPNGLoader().parseAsync(url, undefined, true)).data | |||||
| buff.data = (await new RGBEPNGLoader().parseAsync(url, undefined, encodingVersion < 3)).data | |||||
| URL.revokeObjectURL(url) | URL.revokeObjectURL(url) | ||||
| delete buff.encoding | delete buff.encoding | ||||
| delete buff.encodingVersion | delete buff.encodingVersion | ||||
| if (encodeUint16Rgbe && buffer.type === 'Uint16Array' && buffer.width > 0 && buffer.height > 0) { // import for this is handled in gltf.ts:importViewer. | if (encodeUint16Rgbe && buffer.type === 'Uint16Array' && buffer.width > 0 && buffer.height > 0) { // import for this is handled in gltf.ts:importViewer. | ||||
| // todo: also check if this is indeed an hdr image or something else like LUT or other kind of embedded file. | // todo: also check if this is indeed an hdr image or something else like LUT or other kind of embedded file. | ||||
| const encodingVersion: any = 3 | |||||
| // todo: can we optimize this? this is too many steps | // todo: can we optimize this? this is too many steps | ||||
| const d = halfFloatToRgbe(buffer.data, 4) | |||||
| const d = encodingVersion < 3 ? halfFloatToRgbe2(buffer.data, 4) : halfFloatToRgbe(buffer.data, 4) | |||||
| const id = new ImageData(d, buffer.width, buffer.height) | const id = new ImageData(d, buffer.width, buffer.height) | ||||
| // @ts-expect-error patched three | |||||
| const b64 = ImageUtils.getDataURL(id, true).split(',')[1] | |||||
| // console.log(b64) | |||||
| const encodingVersion: any = 2 | |||||
| // @ts-expect-error patched three. todo: update the types | |||||
| const b64 = ImageUtils.getDataURL(id, true).split(',')[1] | |||||
| mime = 'image/png' | mime = 'image/png' | ||||
| if (encodingVersion === 1) { | if (encodingVersion === 1) { | ||||
| buffer.data = atob(b64) | buffer.data = atob(b64) | ||||
| } else if (encodingVersion === 2) { | |||||
| } else if (encodingVersion === 2 || encodingVersion === 3) { | |||||
| buffer.data = Uint8Array.from(atob(b64), c => c.charCodeAt(0)) | buffer.data = Uint8Array.from(atob(b64), c => c.charCodeAt(0)) | ||||
| } else { | } else { | ||||
| throw new Error('Invalid encoding version') | throw new Error('Invalid encoding version') | ||||
| } | } | ||||
| // adapted from https://github.com/enkimute/hdrpng.js/blob/3a62b3ae2940189777df9f669df5ece3e78d9c16/hdrpng.js#L235 | |||||
| // channels = 4 for RGBA data or 3 for RGB data. buffer from THREE.DataTexture | |||||
| function halfFloatToRgbe(buffer: Uint16Array, channels = 3, res?: Uint8ClampedArray): Uint8ClampedArray { | |||||
| /** | |||||
| * @deprecated old version. see {@link halfFloatToRgbe} to convert half float buffer to rgbe | |||||
| * adapted from https://github.com/enkimute/hdrpng.js/blob/3a62b3ae2940189777df9f669df5ece3e78d9c16/hdrpng.js#L235 | |||||
| * channels = 4 for RGBA data or 3 for RGB data. buffer from THREE.DataTexture | |||||
| * @param buffer | |||||
| * @param channels | |||||
| * @param res | |||||
| */ | |||||
| function halfFloatToRgbe2(buffer: Uint16Array, channels = 3, res?: Uint8ClampedArray): Uint8ClampedArray { | |||||
| let r, g, b, v, s | let r, g, b, v, s | ||||
| const l = buffer.byteLength / (channels * 2) | 0 | const l = buffer.byteLength / (channels * 2) | 0 | ||||
| res = res || new Uint8ClampedArray(l * 4) | res = res || new Uint8ClampedArray(l * 4) |
| import {aesGcmDecrypt, aesGcmEncrypt, getOrCall} from 'ts-browser-helpers' | |||||
| import {makeGLBFile} from '../../utils' | |||||
| import {GLTFExporter2Options} from '../export' | |||||
| import {GLTFBinaryExtension} from 'three/examples/jsm/loaders/GLTFLoader.js' | |||||
| import {GLTFPreparser} from '../import' | |||||
| /** | |||||
| * Sample encryption processor for {@link GLTFExporter2} that wraps the glb in a new glb with encrypted content and encryption metadata. | |||||
| * Uses AES-GCM ({@link aesGcmEncrypt}) for encryption since it is widely supported across browsers and js environments. | |||||
| * @param gltf | |||||
| * @param options | |||||
| */ | |||||
| export const glbEncryptionProcessor = async(gltf: ArrayBuffer|any, options: GLTFExporter2Options) => { | |||||
| if (!gltf || typeof gltf === 'object' || !gltf.byteLength) return gltf | |||||
| if (!options.encryptKey && window && window.prompt) { | |||||
| options.encryptKey = window.prompt('GLTFEncryption: Enter encryption key/password') || '' | |||||
| } | |||||
| if (!options.encryptKey) { | |||||
| console.warn('GLTF Export: encryption key not provided, skipping encryption') | |||||
| return gltf | |||||
| } | |||||
| const buffer = await aesGcmEncrypt(new Uint8Array(gltf as ArrayBuffer), options.encryptKey) | |||||
| return makeGLBFile(buffer, { | |||||
| asset: { | |||||
| version: '2.0', | |||||
| generator: 'ThreePipeGLBWrapper', | |||||
| encryption: { | |||||
| type: 'aesgcm', | |||||
| version: 1, | |||||
| }, | |||||
| }, | |||||
| }) | |||||
| } | |||||
| export interface IGLBEncryptionPreparser extends GLTFPreparser{ | |||||
| key: string | ((encryption: any, json: any)=>string|Promise<string>) | |||||
| } | |||||
| /** | |||||
| * Sample encryption preparser for {@link GLTFLoader2} that unwraps the glb container and decrypts the content. The encryption key can be provided in the file or set in this const is prompted from the user. | |||||
| */ | |||||
| export const glbEncryptionPreparser: IGLBEncryptionPreparser = { | |||||
| key: (encryption: any, _: any) => { | |||||
| return encryption.key || window && window.prompt && window.prompt('GLTFEncryption: Please enter the password/key for this model') || '' | |||||
| }, | |||||
| async process(dat: string | ArrayBuffer) { | |||||
| if (typeof dat === 'string') return dat | |||||
| const prefixBytes = 100 | |||||
| const prefix = new TextDecoder().decode(new Uint8Array(dat, 0, prefixBytes)) | |||||
| if (!prefix.includes('GLBWrapper')) return dat | |||||
| const binaryExtension = new GLTFBinaryExtension(dat) | |||||
| const json = JSON.parse(binaryExtension.content || '{}') | |||||
| let dat2 = binaryExtension.body || dat | |||||
| const encryption = json.asset?.encryption | |||||
| if (!encryption) return dat2 | |||||
| const type = encryption.type | |||||
| const version = encryption.version | |||||
| if (type === 'aesgcm' && version === 1) { | |||||
| const key = await getOrCall(this.key, encryption, json) || '' | |||||
| try { | |||||
| dat2 = (await aesGcmDecrypt(new Uint8Array(dat2), key)).buffer | |||||
| } catch (e) { | |||||
| throw new ErrorEvent('decryption error') | |||||
| } | |||||
| } | |||||
| return dat2 | |||||
| }, | |||||
| } |
| export {GLTFMaterialsLightMapExtension} from './GLTFMaterialsLightMapExtension' | export {GLTFMaterialsLightMapExtension} from './GLTFMaterialsLightMapExtension' | ||||
| export {GLTFObject3DExtrasExtension} from './GLTFObject3DExtrasExtension' | export {GLTFObject3DExtrasExtension} from './GLTFObject3DExtrasExtension' | ||||
| export {GLTFViewerConfigExtension} from './GLTFViewerConfigExtension' | export {GLTFViewerConfigExtension} from './GLTFViewerConfigExtension' | ||||
| export {glbEncryptionPreparser, glbEncryptionProcessor, type IGLBEncryptionPreparser} from './gltfEncyptionHelpers' |
| import {GLTFMaterialsAlphaMapExtension} from '../gltf/GLTFMaterialsAlphaMapExtension' | import {GLTFMaterialsAlphaMapExtension} from '../gltf/GLTFMaterialsAlphaMapExtension' | ||||
| import {RootSceneImportResult} from '../IAssetImporter' | import {RootSceneImportResult} from '../IAssetImporter' | ||||
| import {ILoader} from '../IImporter' | import {ILoader} from '../IImporter' | ||||
| import {glbEncryptionPreparser} from '../gltf' | |||||
| export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|undefined> { | export class GLTFLoader2 extends GLTFLoader implements ILoader<GLTF, Object3D|undefined> { | ||||
| isGLTFLoader2 = true | isGLTFLoader2 = true | ||||
| constructor(manager: LoadingManager) { | constructor(manager: LoadingManager) { | ||||
| super(manager) | super(manager) | ||||
| this.preparsers.push(glbEncryptionPreparser) | |||||
| } | } | ||||
| static ImportExtensions: ((parser: GLTFParser) => GLTFLoaderPlugin)[] = [ | static ImportExtensions: ((parser: GLTFParser) => GLTFLoaderPlugin)[] = [ | ||||
| GLTFMaterialsAlphaMapExtension.Import, | GLTFMaterialsAlphaMapExtension.Import, | ||||
| ] | ] | ||||
| /** | |||||
| * Preparsers are run on the arraybuffer/string before parsing to read the glb/gltf data | |||||
| */ | |||||
| preparsers: GLTFPreparser[] = [] | |||||
| async preparse(data: ArrayBuffer | string): Promise<ArrayBuffer | string> { | |||||
| for (const preparser of this.preparsers) { | |||||
| data = await preparser.process(data) | |||||
| } | |||||
| return data | |||||
| } | |||||
| parse(data: ArrayBuffer | string, path: string, onLoad: (gltf: GLTF) => void, onError?: (event: ErrorEvent) => void) { | |||||
| this.preparse.call(this, data) | |||||
| .then((res: ArrayBuffer | string) => res ? super.parse(res, path, onLoad, onError) : onError && onError(new ErrorEvent('no data'))) | |||||
| .catch((e: any) => { | |||||
| console.error(e) | |||||
| if (onError) onError(e ?? new ErrorEvent('unknown error')) | |||||
| }) | |||||
| } | |||||
| /** | |||||
| * This is run post parse to extract the result scene from the GLTF object | |||||
| * @param res | |||||
| * @param _ | |||||
| */ | |||||
| transform(res: GLTF, _: AnyOptions): Object3D|undefined { | transform(res: GLTF, _: AnyOptions): Object3D|undefined { | ||||
| // todo: support loading of multiple scenes? | // todo: support loading of multiple scenes? | ||||
| const scene: RootSceneImportResult|undefined = res ? res.scene || !!res.scenes && res.scenes.length > 0 && res.scenes[0] : undefined as any | const scene: RootSceneImportResult|undefined = res ? res.scene || !!res.scenes && res.scenes.length > 0 && res.scenes[0] : undefined as any | ||||
| return scene | return scene | ||||
| } | } | ||||
| register(callback: (parser: GLTFParser) => GLTFLoaderPlugin): this { | register(callback: (parser: GLTFParser) => GLTFLoaderPlugin): this { | ||||
| return super.register(callback) as this | return super.register(callback) as this | ||||
| } | } | ||||
| } | } | ||||
| export interface GLTFPreparser{ | |||||
| process(dat: string | ArrayBuffer): Promise<string | ArrayBuffer> | |||||
| [key: string]: any | |||||
| } |
| if (created) URL.revokeObjectURL(url) | if (created) URL.revokeObjectURL(url) | ||||
| let aType: any = Uint8Array | let aType: any = Uint8Array | ||||
| if (this.type === HalfFloatType) aType = Uint16Array | if (this.type === HalfFloatType) aType = Uint16Array | ||||
| else if (this.type === FloatType) aType = Uint32Array | |||||
| else if (this.type === FloatType) aType = Float32Array | |||||
| const buffer = rgbeToHalfFloat(imageData.data, 4, aType, isFloat16Data) | const buffer = rgbeToHalfFloat(imageData.data, 4, aType, isFloat16Data) | ||||
| return {data: buffer, width: imageData.width, height: imageData.height} | return {data: buffer, width: imageData.width, height: imageData.height} | ||||
| } | } |
| export {MTLLoader2, type MaterialCreator} from './MTLLoader2' | export {MTLLoader2, type MaterialCreator} from './MTLLoader2' | ||||
| export {OBJLoader2} from './OBJLoader2' | export {OBJLoader2} from './OBJLoader2' | ||||
| export {ZipLoader} from './ZipLoader' | export {ZipLoader} from './ZipLoader' | ||||
| export {GLTFLoader2} from './GLTFLoader2' | |||||
| export {GLTFLoader2, type GLTFPreparser} from './GLTFLoader2' | |||||
| export {DRACOLoader2} from './DRACOLoader2' | export {DRACOLoader2} from './DRACOLoader2' | ||||
| export {RGBEPNGLoader} from './RGBEPNGLoader' | export {RGBEPNGLoader} from './RGBEPNGLoader' |
| export interface ITextureUserData{ | export interface ITextureUserData{ | ||||
| mimeType?: string | mimeType?: string | ||||
| embedUrlImagePreviews?: boolean | |||||
| disposeOnIdle?: boolean // automatically dispose when added to a material and then not used in any material | disposeOnIdle?: boolean // automatically dispose when added to a material and then not used in any material | ||||
| __appliedMaterials?: Set<IMaterial> | __appliedMaterials?: Set<IMaterial> | ||||
| } | } |
| import {Color, DataTexture, LinearSRGBColorSpace, RGBAFormat, UnsignedByteType, Vector4} from 'three' | |||||
| import {Color, DataTexture, DataUtils, LinearSRGBColorSpace, RGBAFormat, UnsignedByteType, Vector4} from 'three' | |||||
| export function dataTextureFromColor(color: Color) { | export function dataTextureFromColor(color: Color) { | ||||
| const dataTexture = new DataTexture(new Uint8Array([Math.floor(color.r * 255), Math.floor(color.g * 255), Math.floor(color.b * 255), 255]), 1, 1, RGBAFormat, UnsignedByteType) | const dataTexture = new DataTexture(new Uint8Array([Math.floor(color.r * 255), Math.floor(color.g * 255), Math.floor(color.b * 255), 255]), 1, 1, RGBAFormat, UnsignedByteType) | ||||
| dataTexture.needsUpdate = true | dataTexture.needsUpdate = true | ||||
| return dataTexture | return dataTexture | ||||
| } | } | ||||
| /** | |||||
| * Convert half float buffer to rgbe | |||||
| * adapted from https://github.com/enkimute/hdrpng.js/blob/3a62b3ae2940189777df9f669df5ece3e78d9c16/hdrpng.js#L235 | |||||
| * channels = 4 for RGBA data or 3 for RGB data. buffer from THREE.DataTexture | |||||
| * @param buffer | |||||
| * @param channels | |||||
| * @param res | |||||
| */ | |||||
| export function halfFloatToRgbe(buffer: Uint16Array, channels = 3, res?: Uint8ClampedArray): Uint8ClampedArray { | |||||
| let r, g, b, v, s | |||||
| const l = buffer.byteLength / (channels * 2) | 0 | |||||
| res = res || new Uint8ClampedArray(l * 4) | |||||
| for (let i = 0;i < l;i++) { | |||||
| r = DataUtils.fromHalfFloat(buffer[i * channels]) * 65504 | |||||
| g = DataUtils.fromHalfFloat(buffer[i * channels + 1]) * 65504 | |||||
| b = DataUtils.fromHalfFloat(buffer[i * channels + 2]) * 65504 | |||||
| v = Math.max(Math.max(r, g), b) | |||||
| const e = Math.ceil(Math.log2(v)); s = Math.pow(2, e - 8) | |||||
| res[i * 4] = r / s | 0 | |||||
| res[i * 4 + 1] = g / s | 0 | |||||
| res[i * 4 + 2] = b / s | 0 | |||||
| res[i * 4 + 3] = e + 128 | |||||
| } | |||||
| return res | |||||
| } |
| export {computeScreenSpaceBoundingBox} from './bbox' | export {computeScreenSpaceBoundingBox} from './bbox' | ||||
| export {overrideThreeCache} from './cache' | export {overrideThreeCache} from './cache' | ||||
| export {dataTextureFromColor, dataTextureFromVec4} from './conversion' | |||||
| export {dataTextureFromColor, dataTextureFromVec4, halfFloatToRgbe} from './conversion' | |||||
| export {uniform, matDefine} from './decorators' | export {uniform, matDefine} from './decorators' | ||||
| export {getEncodingComponents, getTexelEncoding, getTexelDecoding, getTexelDecoding2, getTexelDecodingFunction, getTexelEncodingFunction, getTextureColorSpaceFromMap} from './encoding' | export {getEncodingComponents, getTexelEncoding, getTexelDecoding, getTexelDecoding2, getTexelDecodingFunction, getTexelEncodingFunction, getTextureColorSpaceFromMap} from './encoding' | ||||
| export {generateUUID, toIndexedGeometry} from './misc' | export {generateUUID, toIndexedGeometry} from './misc' | ||||
| export {getTextureDataType} from './texture' | |||||
| export {getTextureDataType, textureToCanvas, textureDataToImageData, textureToDataUrl, imageToCanvas} from './texture' | |||||
| // export {} from './constants' | // export {} from './constants' |
| import {FloatType, HalfFloatType, TextureDataType, UnsignedByteType, WebGLRenderer} from 'three' | |||||
| import { | |||||
| ColorSpace, | |||||
| DataTexture, | |||||
| DataUtils, | |||||
| FloatType, | |||||
| HalfFloatType, | |||||
| LinearSRGBColorSpace, | |||||
| Texture, | |||||
| TextureDataType, | |||||
| UnsignedByteType, | |||||
| WebGLRenderer, | |||||
| } from 'three' | |||||
| import {TextureImageData} from 'three/src/textures/types' | |||||
| import {LinearToSRGB} from 'ts-browser-helpers' | |||||
| export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType { | export function getTextureDataType(renderer?: WebGLRenderer): TextureDataType { | ||||
| if (!renderer) return UnsignedByteType | if (!renderer) return UnsignedByteType | ||||
| return halfFloatSupport ? HalfFloatType : floatSupport ? FloatType : UnsignedByteType | return halfFloatSupport ? HalfFloatType : floatSupport ? FloatType : UnsignedByteType | ||||
| } | } | ||||
| export function textureDataToImageData(imgData: TextureImageData | ImageData | {data: Float32Array|Uint16Array|Uint8Array, width: number, height: number}, colorSpace?: ColorSpace, outData?: ImageData) { | |||||
| const data = outData?.data ?? new Uint8ClampedArray(imgData.height * imgData.width * 4) | |||||
| const isFloat32 = imgData.data instanceof Float32Array | |||||
| const isUint16 = imgData.data instanceof Uint16Array | |||||
| for (let i = 0; i < data.length; i++) { | |||||
| if (isFloat32) { // Float32 | |||||
| data[i] = imgData.data[i] * 255 | |||||
| } else if (isUint16) { // Uint16 (half float) | |||||
| data[i] = DataUtils.fromHalfFloat(imgData.data[i]) * 255 | |||||
| } else { // Uint8 | |||||
| data[i] = imgData.data[i] | |||||
| } | |||||
| if (colorSpace === LinearSRGBColorSpace) { | |||||
| data[i] = LinearToSRGB(data[i] / 255.0) * 255 | |||||
| } | |||||
| // todo: rgbm? | |||||
| } | |||||
| return outData ?? new ImageData(data, imgData.width, imgData.height) | |||||
| } | |||||
| /** | |||||
| * | |||||
| * @param texture | |||||
| * @param maxWidth | |||||
| * @param flipY | |||||
| * @param canvas | |||||
| */ | |||||
| export function textureToCanvas(texture: Texture|DataTexture, maxWidth: number, flipY = false, canvas?: HTMLCanvasElement) { | |||||
| let img | |||||
| if ((texture as DataTexture).isDataTexture) img = textureDataToImageData(texture.image, texture.colorSpace) | |||||
| else img = texture.image | |||||
| return imageToCanvas(img, maxWidth, flipY, canvas) | |||||
| } | |||||
| export function imageToCanvas(image: TexImageSource, maxWidth: number, flipY = false, canvas?: HTMLCanvasElement) { | |||||
| canvas = canvas || document.createElement('canvas') | |||||
| // resize it to the size of our image | |||||
| canvas.width = Math.min(maxWidth, image.width as number) | |||||
| canvas.height = Math.floor(1.0 + canvas.width * (image.height as number) / (image.width as number)) | |||||
| const ctx = canvas.getContext('2d') | |||||
| if (!ctx) { | |||||
| console.error('textureToDataUrl: could not get canvas context') | |||||
| return canvas | |||||
| } | |||||
| if (flipY === true) { | |||||
| ctx.translate(0, canvas.height) | |||||
| ctx.scale(1, -1) | |||||
| } | |||||
| if ((image as ImageData).data !== undefined) { // THREE.DataTexture | |||||
| const imageData = image as ImageData | |||||
| if (image.width !== canvas.width || image.height !== canvas.height) { | |||||
| const tempCanvas = document.createElement('canvas') | |||||
| tempCanvas.width = image.width | |||||
| tempCanvas.height = image.height | |||||
| const tempCtx = tempCanvas.getContext('2d') | |||||
| if (!tempCtx) { | |||||
| console.error('textureToDataUrl: could not get temp canvas context') | |||||
| ctx.putImageData(imageData, 0, 0) | |||||
| } else { | |||||
| tempCtx.putImageData(imageData, 0, 0) | |||||
| ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height) | |||||
| } | |||||
| } else { | |||||
| ctx.putImageData(imageData, 0, 0) | |||||
| } | |||||
| } else { | |||||
| ctx.drawImage(image as any, 0, 0, canvas.width, canvas.height) | |||||
| } | |||||
| return canvas | |||||
| } | |||||
| export function textureToDataUrl(texture: Texture|DataTexture, maxWidth: number, flipY: boolean, mimeType?: string, quality?: number) { | |||||
| return textureToCanvas(texture, maxWidth, flipY).toDataURL(mimeType, quality) | |||||
| } |
| import type {BlobExt} from '../assetmanager' | |||||
| /** | |||||
| * Returns a buffer aligned to 4-byte boundary. | |||||
| * https://github.com/mrdoob/three.js/blob/4dbd0065f2ec29b89c250d8582f61e9f4792e077/examples/jsm/exporters/GLTFExporter.js#L381 | |||||
| * @param arrayBuffer Buffer to pad | |||||
| * @param paddingByte (Optional) | |||||
| * @returns The same buffer if it's already aligned to 4-byte boundary or a new buffer | |||||
| */ | |||||
| function getPaddedArrayBuffer(arrayBuffer: ArrayBuffer, paddingByte = 0): ArrayBuffer { | |||||
| const paddedLength = getPaddedBufferSize(arrayBuffer.byteLength) | |||||
| if (paddedLength !== arrayBuffer.byteLength) { | |||||
| const array = new Uint8Array(paddedLength) | |||||
| array.set(new Uint8Array(arrayBuffer)) | |||||
| if (paddingByte !== 0) { | |||||
| for (let i = arrayBuffer.byteLength; i < paddedLength; i++) { | |||||
| array[ i ] = paddingByte | |||||
| } | |||||
| } | |||||
| return array.buffer | |||||
| } | |||||
| return arrayBuffer | |||||
| } | |||||
| /** | |||||
| * Get the required size + padding for a buffer, rounded to the next 4-byte boundary. | |||||
| * https://github.com/KhronosGroup/glTF/tree/master/specification/2.0#data-alignment | |||||
| * | |||||
| * @param bufferSize The size the original buffer. | |||||
| * @returns new buffer size with required padding. | |||||
| * | |||||
| */ | |||||
| function getPaddedBufferSize(bufferSize: number) { | |||||
| return Math.ceil(bufferSize / 4) * 4 | |||||
| } | |||||
| // GLB constants | |||||
| // https://github.com/KhronosGroup/glTF/blob/master/specification/2.0/README.md#glb-file-format-specification | |||||
| const GLB_HEADER_BYTES = 12 | |||||
| const GLB_HEADER_MAGIC = 0x46546C67 | |||||
| const GLB_VERSION = 2 | |||||
| const GLB_CHUNK_PREFIX_BYTES = 8 | |||||
| const GLB_CHUNK_TYPE_JSON = 0x4E4F534A | |||||
| const GLB_CHUNK_TYPE_BIN = 0x004E4942 | |||||
| // https://github.com/mrdoob/three.js/blob/4dbd0065f2ec29b89c250d8582f61e9f4792e077/examples/jsm/exporters/GLTFExporter.js#L558 | |||||
| export function makeGLBFile(buffers: ArrayBuffer, json: any): BlobExt { | |||||
| // Binary chunk. | |||||
| const binaryChunk = getPaddedArrayBuffer(buffers) | |||||
| const binaryChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES)) | |||||
| binaryChunkPrefix.setUint32(0, binaryChunk.byteLength, true) | |||||
| binaryChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_BIN, true) | |||||
| // JSON chunk. | |||||
| const jsonChunk = getPaddedArrayBuffer(new TextEncoder().encode(JSON.stringify(json || {})).buffer, 0x20) | |||||
| const jsonChunkPrefix = new DataView(new ArrayBuffer(GLB_CHUNK_PREFIX_BYTES)) | |||||
| jsonChunkPrefix.setUint32(0, jsonChunk.byteLength, true) | |||||
| jsonChunkPrefix.setUint32(4, GLB_CHUNK_TYPE_JSON, true) | |||||
| // GLB header. | |||||
| const header = new ArrayBuffer(GLB_HEADER_BYTES) | |||||
| const headerView = new DataView(header) | |||||
| headerView.setUint32(0, GLB_HEADER_MAGIC, true) | |||||
| headerView.setUint32(4, GLB_VERSION, true) | |||||
| const totalByteLength = GLB_HEADER_BYTES | |||||
| + jsonChunkPrefix.byteLength + jsonChunk.byteLength | |||||
| + binaryChunkPrefix.byteLength + binaryChunk.byteLength | |||||
| headerView.setUint32(8, totalByteLength, true) | |||||
| const glbBlob: BlobExt = new Blob([ | |||||
| header, | |||||
| jsonChunkPrefix, | |||||
| jsonChunk, | |||||
| binaryChunkPrefix, | |||||
| binaryChunk, | |||||
| ], {type: 'model/gltf+binary'}) as any | |||||
| glbBlob.ext = 'glb' | |||||
| return glbBlob | |||||
| } |
| export {Dropzone, type DropFile, type ListenerCallback, type DropEventType} from './Dropzone' | export {Dropzone, type DropFile, type ListenerCallback, type DropEventType} from './Dropzone' | ||||
| export {ThreeSerialization, type SerializationMetaType, type SerializationResourcesType, MetaImporter, metaToResources, getEmptyMeta, metaFromResources, convertArrayBufferToStringsInMeta, convertStringsToArrayBuffersInMeta, copyMaterialUserData, copyObject3DUserData, copyUserData, copyTextureUserData} from './serialization' | export {ThreeSerialization, type SerializationMetaType, type SerializationResourcesType, MetaImporter, metaToResources, getEmptyMeta, metaFromResources, convertArrayBufferToStringsInMeta, convertStringsToArrayBuffersInMeta, copyMaterialUserData, copyObject3DUserData, copyUserData, copyTextureUserData} from './serialization' | ||||
| export {shaderReplaceString} from './shader-helpers' | export {shaderReplaceString} from './shader-helpers' | ||||
| export {makeGLBFile} from './gltf' | |||||
| export {serialize, serializable, Serialization} from 'ts-browser-helpers' | export {serialize, serializable, Serialization} from 'ts-browser-helpers' |
| import {ThreeViewer} from '../viewer' | import {ThreeViewer} from '../viewer' | ||||
| import {ITexture} from '../core' | import {ITexture} from '../core' | ||||
| import {IRenderTarget, RenderManager} from '../rendering' | import {IRenderTarget, RenderManager} from '../rendering' | ||||
| import {textureToCanvas} from '../three/utils/texture' | |||||
| const copier = (c: any) => (v: any, o: any) => o?.copy?.(v) ?? new c().copy(v) | const copier = (c: any) => (v: any, o: any) => o?.copy?.(v) ?? new c().copy(v) | ||||
| export class ThreeSerialization { | export class ThreeSerialization { | ||||
| serialize: (obj: any, meta?: SerializationMetaType) => { | serialize: (obj: any, meta?: SerializationMetaType) => { | ||||
| if (!obj?.isTexture) throw new Error('Expected a texture') | if (!obj?.isTexture) throw new Error('Expected a texture') | ||||
| if (obj.isRenderTargetTexture) return undefined // todo: support render targets | if (obj.isRenderTargetTexture) return undefined // todo: support render targets | ||||
| // if (obj.isRenderTargetTexture && !obj.userData?.serializableRenderTarget) return undefined | |||||
| if (meta?.textures[obj.uuid]) return {uuid: obj.uuid, resource: 'textures'} | if (meta?.textures[obj.uuid]) return {uuid: obj.uuid, resource: 'textures'} | ||||
| const imgData = obj.source.data | const imgData = obj.source.data | ||||
| if (obj.userData.rootPath) obj.source.data = null // if root-path exists we don't need to serialize the image data | |||||
| const hasRootPath = !obj.isRenderTargetTexture && obj.userData.rootPath | |||||
| if (hasRootPath) { | |||||
| if (obj.source.data) { | |||||
| if (!obj.userData.embedUrlImagePreviews) // todo make sure its only Texture, check for svg etc | |||||
| obj.source.data = null // handled in GLTFWriter2.processImage | |||||
| else { | |||||
| obj.source.data = textureToCanvas(obj, 16, obj.flipY) // todo: check flipY | |||||
| } | |||||
| } | |||||
| } | |||||
| const ud = obj.userData | const ud = obj.userData | ||||
| obj.userData = {} // toJSON will call JSON.stringify, which will serialize userData | obj.userData = {} // toJSON will call JSON.stringify, which will serialize userData | ||||
| const meta2 = {images: {} as any} // in-case meta is undefined | const meta2 = {images: {} as any} // in-case meta is undefined | ||||
| let res = obj.toJSON(meta || meta2) | let res = obj.toJSON(meta || meta2) | ||||
| if (!meta && res.image) res.image = obj.userData.rootPath ? undefined : meta2.images[res.image] | |||||
| if (!meta && res.image) res.image = hasRootPath && !obj.userData.embedUrlImagePreviews ? undefined : meta2.images[res.image] | |||||
| obj.userData = ud | obj.userData = ud | ||||
| res.userData = Serialization.Serialize(copyTextureUserData({}, ud), meta, false) | res.userData = Serialization.Serialize(copyTextureUserData({}, ud), meta, false) | ||||
| if (obj.userData.rootPath) { | |||||
| if (meta) delete meta.images[obj.source.uuid] // because its empty. uuid still stored in the texture.image | |||||
| if (hasRootPath) { | |||||
| if (meta && !obj.userData.embedUrlImagePreviews) delete meta.images[obj.source.uuid] // because its empty. uuid still stored in the texture.image | |||||
| obj.source.data = imgData | obj.source.data = imgData | ||||
| } | } | ||||
| // }) | // }) | ||||
| // } | // } | ||||
| if (Array.isArray(json.textures)) { | |||||
| console.error('TODO: check file format') | |||||
| json.textures = json.textures.reduce((acc, cur) => { | |||||
| if (!cur) return acc | |||||
| acc[cur.uuid] = cur | |||||
| return acc | |||||
| }) | |||||
| } | |||||
| await MetaImporter.LoadRootPathTextures({textures: json.textures, images: resources.images}, assetImporter) | await MetaImporter.LoadRootPathTextures({textures: json.textures, images: resources.images}, assetImporter) | ||||
| // console.log(json.textures) | // console.log(json.textures) | ||||
| } | } | ||||
| resources.textures = json.textures ? objLoader.parseTextures(textures, resources.images) : {} | resources.textures = json.textures ? objLoader.parseTextures(textures, resources.images) : {} | ||||
| // replace the source of the textures(which has preview) with the loaded images, see {@link LoadRootPathTextures} for `rootPathPromise` | |||||
| // todo: should this be moved after processRaw? | |||||
| const textures2 = {...resources.textures} | |||||
| for (const inpTexture of Object.values(json.textures)) { | |||||
| inpTexture.rootPathPromise?.then((v: Source|null) => { | |||||
| if (!v) return | |||||
| const texture = textures2[inpTexture.uuid] | |||||
| texture.dispose() | |||||
| texture.source = v | |||||
| texture.source.needsUpdate = true | |||||
| texture.needsUpdate = true | |||||
| }) | |||||
| } | |||||
| for (const entry of Object.entries(resources.textures)) { | for (const entry of Object.entries(resources.textures)) { | ||||
| entry[1] = await assetImporter.processRawSingle(entry[1], {}) | entry[1] = await assetImporter.processRawSingle(entry[1], {}) | ||||
| if (entry[1]) resources.textures[entry[0]] = entry[1] | if (entry[1]) resources.textures[entry[0]] = entry[1] | ||||
| static async LoadRootPathTextures({textures, images}: Pick<SerializationMetaType, 'textures'|'images'>, importer: IAssetImporter) { | static async LoadRootPathTextures({textures, images}: Pick<SerializationMetaType, 'textures'|'images'>, importer: IAssetImporter) { | ||||
| const pms = [] | const pms = [] | ||||
| for (const inpTexture of Object.values(textures ?? {} as any) as any as any[]) { | |||||
| const path = inpTexture?.userData?.rootPath // done separately(from parseTextures2) for hdr etc textures. | |||||
| if (path && (!inpTexture.image || !images[inpTexture.image])) { | |||||
| pms.push(importer.importSingle<ITexture>(path, {processRaw: false}).then(texture => { | |||||
| const source = texture?.source as any | |||||
| // const image = texture?.image as any | |||||
| if (!texture || !source) return | |||||
| // console.log(typeof image) | |||||
| const source2 = new Source(source.data) | |||||
| if (inpTexture.image) source2.uuid = inpTexture.image | |||||
| for (const inpTexture of Array.isArray(textures) ? textures : Object.values(textures ?? {} as any) as any as any[]) { | |||||
| const path = inpTexture?.userData?.rootPath | |||||
| const hasImage = inpTexture.image && images[inpTexture.image] // its possible to have both image and rootPath, then the image will be preview image. | |||||
| if (!path) continue | |||||
| // console.warn(path, inpTexture, images) | |||||
| const promise = importer.importSingle<ITexture>(path, {processRaw: false}).then((texture) => { | |||||
| const source = texture?.source as any | |||||
| // const image = texture?.image as any | |||||
| if (!texture || !source) return null | |||||
| // console.log(typeof image) | |||||
| const source2 = new Source(source.data) | |||||
| if (inpTexture.image) source2.uuid = inpTexture.image | |||||
| inpTexture.image = source2.uuid | |||||
| if (!hasImage) | |||||
| images[source2.uuid] = source2 | images[source2.uuid] = source2 | ||||
| inpTexture.image = source2.uuid | |||||
| texture.dispose() // todo: what happens when we reimport a cached disposed texture asset, is three.js able to recreate the webgl texture on render? | |||||
| }).catch((e)=>{ | |||||
| console.error(e) | |||||
| delete inpTexture.userData.rootPath | |||||
| })) | |||||
| } | |||||
| texture.dispose() // todo: what happens when we reimport a cached disposed texture asset, is three.js able to recreate the webgl texture on render? | |||||
| return source2 | |||||
| }).catch((e) => { | |||||
| console.error(e) | |||||
| delete inpTexture.userData.rootPath | |||||
| return null | |||||
| }) | |||||
| if (hasImage) inpTexture.rootPathPromise = promise | |||||
| else pms.push(promise) | |||||
| } | } | ||||
| await Promise.allSettled(pms) | await Promise.allSettled(pms) | ||||
| } | } | ||||