UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

266 lines (230 loc) • 9.92 kB
import { Object3D, Vector3 } from "three"; import { AnimationClip } from "three"; import { GLTFExporter, type GLTFExporterOptions } from 'three/examples/jsm/exporters/GLTFExporter.js'; import { SerializationContext } from "../../../engine/engine_serialization_core.js"; import { serializable } from "../../../engine/engine_serialization_decorator.js"; import { getWorldPosition } from "../../../engine/engine_three_utils.js"; import { getParam } from "../../../engine/engine_utils.js"; import { RenderTextureWriter } from "../../../engine/export/gltf/Writers.js"; import { shouldExport_HideFlags } from "../../../engine/export/utils.js"; import { registerExportExtensions } from "../../../engine/extensions/index.js"; import { NEEDLE_components } from "../../../engine/extensions/NEEDLE_components.js"; import GLTFMeshGPUInstancingExtension from '../../../include/three/EXT_mesh_gpu_instancing_exporter.js'; import { BoxHelperComponent } from "../../BoxHelperComponent.js"; import { Behaviour, GameObject } from "../../Component.js"; import { Renderer } from "../../Renderer.js"; const debugExport = getParam("debuggltfexport"); declare type ExportOptions = GLTFExporterOptions & { pivot?: Vector3, needleComponents?: boolean, } export const componentsArrayExportKey = "$___Export_Components"; // @generate-component export class GltfExportBox extends BoxHelperComponent { sceneRoot?: Object3D; } /** * @category Asset Management * @group Components */ export class GltfExport extends Behaviour { @serializable() binary: boolean = true; @serializable(Object3D) objects: Object3D[] = []; private ext?: NEEDLE_components; async exportNow(name: string, opts?: ExportOptions) { if (debugExport) console.log("Exporting objects as glTF", this.objects); if (!name) name = "scene"; if (!this.objects || this.objects.length <= 0) this.objects = [this.context.scene]; const options = { binary: this.binary, pivot: GltfExport.calculateCenter(this.objects), ...opts }; const res = await this.export(this.objects, options).catch(err => { console.error(err); return false; }) if (res === false) return false; if (!this.binary) { if (!name.endsWith(".gltf")) name += ".gltf"; } else if (!name.endsWith(".glb")) name += ".glb"; if (this.binary) GltfExport.saveArrayBuffer(res, name); else GltfExport.saveJson(res, name); return true; } async export(objectsToExport: Object3D[], opts?: ExportOptions): Promise<any> { // ----------------------- // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // TODO: refactor this to use ../engine/export/index exportAsGLTF function // TODO add filtering / tags for what to export and what not < this is implemented in exportAsGLTF, see TODO above if (!objectsToExport || objectsToExport.length <= 0) { console.warn("No objects set to export"); return; } // Instantiate a exporter const exporter = new GLTFExporter(); exporter.register(writer => new GLTFMeshGPUInstancingExtension(writer)); exporter.register(writer => new RenderTextureWriter(writer)); registerExportExtensions(exporter, this.context); GltfExport.filterTopmostParent(objectsToExport); // https://threejs.org/docs/#examples/en/exporters/GLTFExporter const options = { trs: false, onlyVisible: true, truncateDrawRange: false, binary: true, maxTextureSize: Infinity, // To prevent NaN value, embedImages: true, includeCustomExtensions: true, animations: opts?.animations || GltfExport.collectAnimations(objectsToExport), ...opts }; const undo = new Array<() => void>(); const exportScene = new Object3D(); // set the pivot position if (opts?.pivot) exportScene.position.sub(opts.pivot); // console.log(exportScene.position); // add objects for export if (debugExport) console.log("EXPORT", objectsToExport); objectsToExport.forEach(obj => { if (obj && shouldExport_HideFlags(obj)) { // adding directly does not require us to change parents and mess with the hierarchy actually exportScene.children.push(obj); // TODO: we should probably be doing this before writing nodes?? apply world scale, position, rotation etc for export only obj.matrixAutoUpdate = false; obj.matrix.copy(obj.matrixWorld); // disable instancing GameObject.getComponentsInChildren(obj, Renderer).forEach(r => { if (GameObject.isActiveInHierarchy(r.gameObject)) r.setInstancingEnabled(false) }); obj.traverse(o => { if (!shouldExport_HideFlags(o)) { const parent = o.parent; o.removeFromParent(); undo.push(() => { if (parent) parent.add(o); }); } }) } }); const serializationContext = new SerializationContext(exportScene); if (opts?.needleComponents) { this.ext = new NEEDLE_components(); } if (this.ext) { this.ext.registerExport(exporter); this.ext.context = serializationContext; } return new Promise((resolve, reject) => { if (debugExport) console.log("Starting glTF export.") try { // Parse the input and generate the glTF output exporter?.parse( exportScene, // called when the gltf has been generated res => { cleanup(); resolve(res); }, // called when there is an error in the generation err => { cleanup(); reject(err); }, options ); } catch (err) { console.error(err); reject(err); } finally { undo.forEach(u => u()); if (debugExport) console.log("Finished glTF export."); } }); function cleanup() { objectsToExport.forEach(obj => { if (!obj) return; obj.matrixAutoUpdate = true; GameObject.getComponentsInChildren(obj, Renderer).forEach(r => { if (GameObject.isActiveInHierarchy(r.gameObject)) r.setInstancingEnabled(false) }); }); } }; private static saveArrayBuffer(buffer, filename) { this.save(new Blob([buffer], { type: 'application/octet-stream' }), filename); } private static saveJson(json, filename) { this.save("data: text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(json)), filename); } private static save(blob, filename) { const link = document.createElement('a'); link.style.display = 'none'; document.body.appendChild(link); // Firefox workaround, see #6594 if (typeof blob === "string") link.href = blob; else link.href = URL.createObjectURL(blob); link.download = filename; link.click(); link.remove(); // console.log(link.href); // URL.revokeObjectURL( url ); breaks Firefox... } private static collectAnimations(objs: Object3D[], target?: Array<AnimationClip>): Array<AnimationClip> { target = target || []; for (const obj of objs) { if (!obj) continue; obj.traverseVisible(o => { if (o.animations && o.animations.length > 0) target!.push(...o.animations); }); } return target; } private static calculateCenter(objs: Object3D[], target?: Vector3): Vector3 { const center = target || new Vector3(); center.set(0, 0, 0); objs.forEach(obj => { center.add(getWorldPosition(obj)); }); center.divideScalar(objs.length); return center; } private static filterTopmostParent(objs: Object3D[]) { if (objs.length <= 0) return; for (let index = 0; index < objs.length; index++) { let obj = objs[index]; if (!obj) { objs.splice(index, 1); index--; continue; } // loop hierarchy up and kick object if any of its parents is already in this list // because then this object will already be exported (and we dont want to export it) while (obj.parent) { if (objs.includes(obj.parent)) { // console.log("FILTER", objs[index]); objs.splice(index, 1); index--; break; } obj = obj.parent; } } } }