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.

290 lines (261 loc) 11.1 kB
import {GLTFExporter, GLTFExporterPlugin} from 'three/examples/jsm/exporters/GLTFExporter.js' import {IExportWriter} from '../IExporter' import {GLTFWriter2} from './GLTFWriter2' import {AnimationClip, Object3D} from 'three' import {ThreeViewer} from '../../viewer' import { glbEncryptionProcessor, GLTFLightExtrasExtension, GLTFMaterialExtrasExtension, GLTFMaterialsAlphaMapExtension, GLTFMaterialsDisplacementMapExtension, GLTFMaterialsLightMapExtension, GLTFObject3DExtrasExtension, GLTFViewerConfigExtension, } from '../gltf' import {GLTF} from 'three/examples/jsm/loaders/GLTFLoader.js' import {metaToResources, SerializationMetaType} from '../../utils' import {GLTFLoader2} from '../import' export interface GLTFExporter2Options { /** * embed images in glb even when remote url is available * @default false */ embedUrlImages?: boolean, /** * Embed previews of images in glb * @default false */ embedUrlImagePreviews?: boolean, /** * export viewer config (scene settings) */ viewerConfig?: boolean, /** * Extension to export to, default for object/scene = glb */ exportExt?: string, preserveUUIDs?: boolean, /** * 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, // From GLTFExporter /** * Export position, rotation and scale instead of matrix per node. * Default is false */ trs?: boolean; /** * Export only visible objects. * Default is false. */ onlyVisible?: boolean; /** * Export just the attributes within the drawRange, if defined, instead of exporting the whole array. * Default is true. */ truncateDrawRange?: boolean; /** * Restricts the image maximum size (both width and height) to the given value. This option works only if embedImages is true. * Default is Infinity. */ maxTextureSize?: number; /** * List of animations to be included in the export. */ animations?: AnimationClip[]; /** * Generate indices for non-index geometry and export with them. * Default is false. */ forceIndices?: boolean; /** * Export custom glTF extensions defined on an object's userData.gltfExtensions property. * Default is true. */ includeCustomExtensions?: boolean; // wip - replace this base path from embedded resources url. this is expected to be path/resourcePath in GLTFLoader when this exported asset is loaded the next time. _basePath?: string [key: string]: any } /** * GLTFExporter2 is an improved version of the three.js GLTFExporter with support for: * - glb encryption * - embedding image previews * - saving external image references in extras * - saving viewer config (scene settings) * - various optimizations and bug fixes */ export class GLTFExporter2 extends GLTFExporter implements IExportWriter { constructor() { super() this.processors.push(glbEncryptionProcessor) } register(callback: (writer: GLTFWriter2)=>GLTFExporterPlugin): this { return super.register(callback as any) } 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') 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 return new Blob([JSON.stringify(gltf, (k, v)=> k.startsWith('__') ? undefined : v, options.jsonSpaces ?? 2)], {type: 'model/gltf+json'}) } else if (gltf) { return new Blob([gltf as ArrayBuffer], {type: 'model/gltf+binary'}) } else { throw new Error('GLTFExporter2.parse() failed') } } parse( input: Object3D | Object3D[], onDone: (gltf: ArrayBuffer | {[key: string]: any}) => void, onError: (error: ErrorEvent) => void, options: GLTFExporter2Options = {}, ): void { const gltfOptions = { // default options binary: false as boolean, trs: options.trs ?? false, onlyVisible: options.onlyVisible ?? false, truncateDrawRange: options.truncateDrawRange ?? true, externalImagesInExtras: !options.embedUrlImages && options.externalImagesInExtras || false, // this is handled in gltfMaterialExtrasWriter, also see GLTFDracoExporter maxTextureSize: options.maxTextureSize ?? Infinity, animations: options.animations ?? [], includeCustomExtensions: options.includeCustomExtensions ?? true, forceIndices: options.forceIndices ?? false, // todo implement exporterOptions: options, ignoreInvalidMorphTargetTracks: options.ignoreInvalidMorphTargetTracks, ignoreEmptyTextures: options.ignoreEmptyTextures, } satisfies GLTFWriter2['options'] if (options.exportExt === 'glb') { gltfOptions.binary = true } const onDone1 = (o: GLTF)=> { // eslint-disable-next-line @typescript-eslint/naming-convention onDone(Object.assign(o, {__isGLTFOutput: true})) } return super.parse(input, onDone1, onError, gltfOptions, new GLTFWriter2()) } static ExportExtensions: ((writer: GLTFWriter2) => GLTFExporterPlugin)[] = [ GLTFMaterialExtrasExtension.Export, GLTFObject3DExtrasExtension.Export, GLTFLightExtrasExtension.Export, // GLTFMaterialsBumpMapExtension.Export, // deprecated GLTFMaterialsDisplacementMapExtension.Export, GLTFMaterialsLightMapExtension.Export, GLTFMaterialsAlphaMapExtension.Export, (w)=>{ // Extension to write any gltfExtras that is imported with GLTFLoader back into the gltf json. return { name: '_AssetPropsWriter', beforeParse: (input: any)=>{ const inputs = Array.isArray(input) ? input : [input] for (const obj of inputs) { // remove from userData so its not serialized inside the node/scene if (obj.userData.gltfExtras) { obj.__gltfExtras = obj.userData.gltfExtras delete obj.userData.gltfExtras } if (obj.userData.gltfAsset) { obj.__gltfAsset = obj.userData.gltfAsset delete obj.userData.gltfAsset } } }, afterParse: (input: any)=>{ const extras = (w.json as any).extras || {} const inputs = Array.isArray(input) ? input : [input] for (const obj of inputs) { // ignoring .gltfAsset right now, is that fine? any copyright info would have been copied to userData.license during import. if (obj.__gltfAsset) { obj.userData.gltfAsset = obj.__gltfAsset delete obj.__gltfAsset } if (obj.__gltfExtras) { obj.userData.gltfExtras = obj.__gltfExtras Object.assign(extras, obj.userData.gltfExtras) delete obj.__gltfExtras } } if (Object.keys(extras).length > 0) { (w.json as any).extras = extras } }, } }, // (w)=>new GLTFMeshGpuInstancingExporter(w), // added to threejs ] setup(viewer: ThreeViewer, extraExtensions?: ((writer: GLTFWriter2) => GLTFExporterPlugin)[]): this { for (const ext of GLTFExporter2.ExportExtensions) this.register(ext) if (extraExtensions) for (const ext of extraExtensions) this.register(ext) // should be last this.register(this.gltfViewerWriter(viewer)) return this } // BundledResources or viewer config writer gltfViewerWriter(viewer: ThreeViewer): (writer: GLTFWriter2) => GLTFExporterPlugin { return (writer: GLTFWriter2) => ({ afterParse: (input: any)=>{ input = Array.isArray(input) ? input[0] : input let resources: Partial<SerializationMetaType>|undefined = undefined if (!input?.userData?.rootSceneModelRoot || writer.options?.exporterOptions?.viewerConfig === false || input?.userData?.__exportViewerConfig === false ) { resources = metaToResources(writer.serializationMeta) GLTFViewerConfigExtension.BundleExtraResources(writer.json, resources) GLTFViewerConfigExtension.BundleArrayBuffers(resources, writer) } else { // resources will be bundled in the viewer config extension. // note this has to be absolutely at the end of parse. writer.serializationMeta is used and converted to resources inside this resources = GLTFViewerConfigExtension.ExportViewerConfig(viewer, writer) } if (!resources) return const itemCount = Object.values(resources).reduce((sum, arr) => sum + Object.keys(arr).length, 0) if (itemCount === 0) return // no resources to bundle const extras = (writer.json as any).extras || {} extras[GLTFLoader2.BundledResourcesKey] = resources ;(writer.json as any).extras = extras }, }) } } declare module 'three'{ interface AnimationClip { /** * Whether to export this animation in glTF format. * @default true */ __gltfExport?: boolean; userData: { clipActions?: Record<string, any[]> [key: string]: any } } }