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
text/typescript
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
}
}
}