UNPKG

threepipe

Version:

A modern 3D viewer framework built on top of three.js, written in TypeScript, designed to make creating high-quality, modular, and extensible 3D experiences on the web simple and enjoyable.

297 lines (228 loc) 10.6 kB
import {GLTFExporter, GLTFExporterOptions} from 'three/examples/jsm/exporters/GLTFExporter.js' import {AnimationClip, BufferGeometry, Material, MeshStandardMaterial, Object3D, PixelFormat, Texture} from 'three' import {blobToDataURL} from 'ts-browser-helpers' import type {GLTFExporter2Options} from './GLTFExporter2' import {getEmptyMeta, isNonRelativeUrl, ThreeSerialization} from '../../utils' import {IMaterial} from '../../core' export class GLTFWriter2 extends GLTFExporter.Utils.GLTFWriter { readonly TPAssetVersion = 1 serializationMeta = getEmptyMeta() constructor() { super() this.json.asset.subversion = this.TPAssetVersion } declare options: GLTFExporterOptions & { externalImagesInExtras: boolean, exporterOptions: GLTFExporter2Options } serializeUserData(object: Object3D | Material | BufferGeometry | AnimationClip | Texture, objectDef: any): void { const userData = object.userData const temp: any = {} if (userData.__disposed) { console.error('Serializing a disposed object', object) } Object.entries(userData).forEach(([key, value]: any) => { if (!value || typeof value === 'function' || value.isObject3D || value.isTexture || value.isMaterial || value.assetType != null || key.startsWith('_') // private data. todo remove private values inside userdata.ecs... ) { temp[key] = value delete userData[key] } }) const ud2 = ThreeSerialization.Serialize(userData, this.serializationMeta) Object.entries(temp).forEach(([key, value]) => { userData[key] = value delete temp[key] }) object.userData = ud2 super.serializeUserData(object as any, objectDef) object.userData = userData } processObjects(objects: Object3D[]) { if (objects.length === 1 && objects[0]?.userData.rootSceneModelRoot) { // objects[0].isScene = true this.processScene(objects[0]) // delete objects[0].isScene } else super.processObjects(objects) } protected _defaultMaterial = new MeshStandardMaterial() /** * Checks for shader material and does the same thing... * @param material */ processMaterial(material: Material): number|null { if (this.cache.materials.has(material)) return this.cache.materials.get(material)! let mat = material as any // set default material when material is null. shader material is processed further below for custom extensions like diamonds. if (!mat || mat.isShaderMaterial) mat = this._defaultMaterial const defIndex = super.processMaterial(mat) if (defIndex === null) { console.error('GLTFWriter2: Unexpected error: Failed to process material', material) return null } // when not a shader material if (!material || mat === material) return defIndex // when shader material const defaultDef = JSON.stringify(this.json.materials[defIndex]) const materialDef = JSON.parse(defaultDef) // for deep clone // console.log(defIndex, defaultDef, materialDef) const color = (material as IMaterial).color?.isColor ? (material as IMaterial).color!.toArray().concat([material.opacity]) : null if (color && !color.every((c) => c === 1) && materialDef.pbrMetallicRoughness) { materialDef.pbrMetallicRoughness.baseColorFactor = color } this.serializeUserData(material, materialDef) this._invokeAll((ext)=>{ ext.writeMaterial && ext.writeMaterial(material, materialDef) }) // todo: test remove this // if (JSON.stringify(materialDef) === defaultDef) { // return defIndex // } const index = this.json.materials.push(materialDef) - 1 this.cache.materials.set(material, index) return index } /** * Same as processImage but for image blobs * @param blob * @param texture */ processImageBlob(blob: Blob, texture: Texture) { if (!blob) return -1 const cache = this.cache const options = this.options const pending = this.pending const json = this.json const image = texture.image if (!cache.images.has(image)) cache.images.set(image, {}) const cachedImages = cache.images.get(image) const key = blob.type + ':flipY/' + texture.flipY.toString() if (cachedImages[ key ] !== undefined) return cachedImages[ key ] if (!json.images) json.images = [] const imageDef: any = {mimeType: blob.type} if (options.binary === true) { pending.push(new Promise<void>((resolve)=>{ this.processBufferViewImage(blob).then((bufferViewIndex: number)=>{ imageDef.bufferView = bufferViewIndex resolve() }) })) } else { pending.push(blobToDataURL(blob).then((dataURL: string)=>{ imageDef.uri = dataURL })) } const index = json.images.push(imageDef) - 1 cachedImages[ key ] = index return index } processSampler(map: Texture) { const samplerIndex = super.processSampler(map) // const samplerDef = this.json.samplers[samplerIndex] // if (!samplerDef) return samplerIndex // this.serializeUserData(map, samplerDef) // todo check when serializeUserData added to three.js core // todo: uncomment when sampler extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645 // if (!samplerDef.extras) samplerDef.extras = {} // samplerDef.extras.uuid = map.uuid return samplerIndex } processTexture(map: Texture) { const cache = this.cache const json = this.json if (cache.textures.has(map)) return cache.textures.get(map)! const srcData = map.source.data const mimeType = map.userData.mimeType const hasRootPath = !map.isRenderTargetTexture && map.userData.rootPath && typeof map.userData.rootPath === 'string' && isNonRelativeUrl(map.userData.rootPath) if (hasRootPath && !this.options.exporterOptions.embedUrlImages) { 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 } const processed = super.processTexture(map) const textureDef = json.textures[processed] if (!textureDef) { console.error('No texture def', processed, map) return processed } // if (!textureDef.extras) textureDef.extras = {} if (hasRootPath && !this.options.exporterOptions.embedUrlImages) { if (map.source.data) delete map.source.data._savePreview else map.source.data = srcData map.userData.mimeType = mimeType if (!textureDef) { console.error('textureDef is null', processed, map) return processed } let uri = map.userData.rootPath const basePath = this.options.exporterOptions._basePath if (basePath && typeof uri === 'string' && uri.startsWith(basePath)) { uri = uri.slice(basePath.length) } if (textureDef.source >= 0) { // 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 = uri img.mimeType = mimeType if (!img.extras) img.extras = {} img.extras.flipY = map.flipY img.extras.uri = uri // uri is removed by gltf-transform if bufferView is set } } else { textureDef.source = this.processImageUri(map.image, uri, map.flipY, mimeType) } } if (textureDef.source < 0) { console.error('textureDef.source cannot be saved', textureDef, map) delete textureDef.source // gltf spec allows undefined, not -1 } else { const imageDef = json.images ? json.images[textureDef.source] : null if (imageDef) { if (!imageDef.extras) imageDef.extras = {} if (map.source) imageDef.extras.uuid = map.source.uuid imageDef.extras.t_uuid = map.uuid // todo: remove when extras supported by gltf-transform: https://github.com/donmccurdy/glTF-Transform/issues/645 } } // map uuid, extras saved in processSampler. return processed } // 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') { if (!image) return -1 return super.processImage(image, format, flipY, mimeType, image._savePreview ? 32 : undefined, image._savePreview ? 32 : undefined) } /** * Used in GLTFWriter2.processTexture for rootPath. Note that this does not check for options.exporterOptions.embedUrlImages, it must be done separately. * @param image * @param uri * @param flipY * @param mimeType */ processImageUri(image: any, uri: string, flipY: boolean, mimeType = 'image/png') { const cache = this.cache const json = this.json if (!cache.images.has(image)) cache.images.set(image, {}) const cachedImages = cache.images.get(image) const key = mimeType + ':flipY/' + flipY.toString() if (cachedImages[ key ] !== undefined) return cachedImages[ key ] if (!json.images) json.images = [] const imageDef: any = { mimeType, uri, extras: {flipY}, } const index = json.images.push(imageDef) - 1 cachedImages[ key ] = index return index } }