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.
203 lines (178 loc) • 8.58 kB
text/typescript
import {EventDispatcher, WebGLRenderTarget} from 'three'
import {IMaterial, IObject3D, ITexture} from '../core'
import {BlobExt, ExportFileOptions, IAssetExporter, IExporter, IExportWriter} from './IExporter'
import {assetExportHook, AssetExportHooks, EXRExporter2, SimpleJSONExporter, SimpleTextExporter} from './export'
import {IRenderTarget} from '../rendering'
import {Zippable, zipSync} from 'three/examples/jsm/libs/fflate.module.js'
export interface AssetExporterEventMap {
exporterCreate: {exporter: IExporter, parser: IExportWriter} // todo rename parser to writer
exportFile: {
obj: IObject3D|IMaterial|ITexture|IRenderTarget,
state: 'processing'|'exporting'|'done'|'error',
progress?: number, // between 0 and 1
error?: any,
exportOptions: ExportFileOptions
}
}
/**
* Asset Exporter
*
* Utility class to export objects, materials, textures, render targets, etc.
* Used in {@link AssetManager} to export assets.
* @category Asset Manager
*/
export class AssetExporter extends EventDispatcher<AssetExporterEventMap> implements IAssetExporter {
readonly exporters: IExporter[] = [
{ctor: ()=>new SimpleJSONExporter(), ext: ['json']},
{ctor: ()=>new SimpleTextExporter(), ext: ['txt', 'text']},
{ctor: ()=>new EXRExporter2(), ext: ['exr']},
// {ctor: ()=>new EXRExporter2(), ext: ['png', 'jpeg', 'webp']}, // todo
// {ctor: ()=>new GLTFDracoExporter(), ext: ['gltf', 'glb']},
]
addExporter(...exporters: IExporter[]) {
for (const exporter of exporters) {
if (this.exporters.includes(exporter)) {
console.warn('Exporter already added', exporter)
return
}
this.exporters.push(exporter)
}
}
removeExporter(...exporters: IExporter[]) {
for (const exporter of exporters) {
const i = this.exporters.indexOf(exporter)
if (i >= 0) this.exporters.splice(i, 1)
}
}
getExporter(...ext: string[]): IExporter|undefined {
return this.exporters.find(e=>e.ext.some(e1=>ext.includes(e1)))
}
// this can be set from outside to add custom processing during export, apart from the exportFile event
exportHooks: AssetExportHooks = {}
constructor() {
super()
this.addEventListener('exportFile', (e)=>assetExportHook(e, this.exportHooks))
}
public async exportObject(obj?: IObject3D|IMaterial|ITexture|IRenderTarget, options: ExportFileOptions = {}): Promise<BlobExt|undefined> {
if (!obj?.assetType) {
console.error('Object has no asset type')
return undefined
}
const excluded: IObject3D[] = []
if (obj.assetType === 'model') {
obj.traverse((o)=>{
// todo this wont work when we are exporting invisible objects as well
if (o.userData.excludeFromExport && o.visible) {
o.visible = false
excluded.push(o)
}
})
}
const blob = await this._exportFile(obj, options)
if (obj.assetType === 'model') {
excluded.forEach((o: any)=>o.visible = true)
}
if ((obj as any)?.userData?.rootSceneModelRoot && options.viewerConfig === false) {
delete (obj as any)!.userData!.__exportViewerConfig
}
return blob
}
// export to blob
private async _exportFile(obj: IObject3D|IMaterial|ITexture|IRenderTarget, options: ExportFileOptions = {}): Promise<BlobExt|undefined> {
// if ((file as any)?.__imported) return (file as any).__imported // todo: cache exports?
let res: BlobExt
try {
this.dispatchEvent({type: 'exportFile', obj, state:'processing', exportOptions: options})
const processed = await this.processBeforeExport(obj, options)
const ext = processed?.typeExt || processed?.ext
if (!processed || !ext) {
console.error(processed, options, obj)
throw new Error(`AssetExporter - Unable to preprocess before export ${ext}`)
}
if (processed.blob) res = processed.blob
else {
const writer = this._getWriter(ext)
this.dispatchEvent({type: 'exportFile', obj, state:'exporting', exportOptions: options})
res = await writer.parseAsync(processed.obj, {exportExt: processed.ext ?? ext, ...options}) as BlobExt
res.ext = processed.ext
}
res = await this.processAfterExport(res, obj, options)
this.dispatchEvent({type: 'exportFile', obj, state: 'done', exportOptions: options})
} catch (e) {
console.error('AssetExporter: Unable to Export file', obj)
// console.error(e)
this.dispatchEvent({type: 'exportFile', obj, state: 'error', error: e, exportOptions: options})
throw e
return undefined
}
// if (file) (file as any).__imported = res
return res
}
private _createParser(ext: string): IExportWriter {
const exporter = this.exporters.find(e => e.ext.includes(ext))
if (!exporter)
throw new Error(`No exporter found for extension ${ext}`)
const writer = exporter?.ctor(this, exporter)
if (!writer) throw new Error(`Unable to create writer for extension ${ext}`)
this._cachedWriters.push({ext: exporter.ext, parser: writer})
this.dispatchEvent({type: 'exporterCreate', exporter, parser: writer})
return writer
}
private _cachedWriters: {parser: IExportWriter, ext: string[]}[] = []
private _getWriter(ext: string): IExportWriter {
return this._cachedWriters.find(e => e.ext.includes(ext))?.parser ?? this._createParser(ext)
}
public async processBeforeExport(obj: IObject3D|IMaterial|ITexture|IRenderTarget, options: ExportFileOptions = {}): Promise<{obj:any, ext:string, typeExt?:string, blob?: BlobExt}|undefined> {
// if (obj.assetExporterProcessed && !options.forceExporterReprocess) return obj //todo;;;
switch (obj.assetType) {
case 'light':
console.error('AssetExporter: light export not implemented')
return undefined
case 'model':
return {obj, ext: options.exportExt ?? 'glb'}
// return {obj, ext: 'gltf'}
case 'material':
return {obj: matToJson(obj as IMaterial), ext: options.exportExt || (obj as IMaterial).constructor?.TypeSlug, typeExt: 'json'}
case 'texture':
return options.exportExt ? {obj, ext: options.exportExt} : {obj: (obj as ITexture).toJSON(), ext: 'json'}
case 'renderTarget':
if (!obj.renderManager) return {obj, ext: 'exr'}
else {
const mime = (options.exportExt || '' !== '') && options.exportExt !== 'auto' ?
options.exportExt === 'exr' ? 'image/x-exr' : 'image/' + options.exportExt : 'auto'
let blob
if (obj.textures.length > 1) {
const zippa: Zippable = {}
for (let i = 0; i < obj.textures.length; i++) {
const expBlob = obj.renderManager!.exportRenderTarget(obj as WebGLRenderTarget, mime, i)
// zippa[(f as File).name] = new Uint8Array(await (f as File).arrayBuffer())
zippa[`texture_${i}.${expBlob.ext}`] = new Uint8Array(expBlob.__buffer || await expBlob.arrayBuffer())
}
const zipped = zipSync(zippa)
blob = new Blob([zipped], {type: 'application/zip'}) as any as BlobExt
blob.ext = 'zip'
blob.__buffer = zipped.buffer
} else {
blob = obj.renderManager.exportRenderTarget(obj as WebGLRenderTarget, mime)
}
return {
obj, ext: blob.ext, blob,
}
}
break
default:
console.error('AssetExporter: unknown asset type', obj.assetType)
}
return undefined
}
public async processAfterExport(blob: BlobExt, _obj: IObject3D|IMaterial|ITexture|IRenderTarget, _options: ExportFileOptions = {}): Promise<BlobExt> {
return blob
}
dispose(): void {
// todo
}
}
export function matToJson(mat: IMaterial) {
const json = mat.toJSON()
return json
}