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.

159 lines (131 loc) • 5.44 kB
import { AnimationClip, Object3D } from "three"; import { GLTFExporter, GLTFExporterOptions } from "three/examples/jsm/exporters/GLTFExporter.js"; import GLTFMeshGPUInstancingExtension from "../../../include/three/EXT_mesh_gpu_instancing_exporter.js"; import { AnimationUtils } from "../../engine_animation.js"; import type { Context } from "../../engine_setup.js"; import { registerExportExtensions } from "../../extensions/index.js"; import { __isExporting } from "../state.js"; import { shouldExport_HideFlags } from "../utils.js"; import { GizmoWriter as GLTFGizmoWriter, RenderTextureWriter as GLTFRenderTextureWriter } from "./Writers.js"; declare type ExportOptions = { context: Context, scene?: Object3D | Array<Object3D>, binary?: boolean, animations?: boolean, downloadAs?: string, } const DEFAULT_OPTIONS: Omit<ExportOptions, "context" | "scene"> = { binary: true, animations: true, } export async function exportAsGLTF(_opts: ExportOptions): Promise<ArrayBuffer | Record<string, any>> { if (!_opts.context) { throw new Error("No context provided to exportAsGLTF"); } if (!_opts.scene) { _opts.scene = _opts.context.scene; } const opts = { ...DEFAULT_OPTIONS, ..._opts } as Required<ExportOptions>; const { context } = opts; const exporter = new GLTFExporter(); exporter.register(writer => new GLTFMeshGPUInstancingExtension(writer)); exporter.register(writer => new GLTFGizmoWriter(writer)); exporter.register(writer => new GLTFRenderTextureWriter(writer)); registerExportExtensions(exporter, opts.context); const exporterOptions: GLTFExporterOptions = { binary: opts.binary, animations: collectAnimations(context, opts.scene, []), } const state = new ExporterState(); console.debug("Exporting GLTF", exporterOptions); state.onBeforeExport(opts); __isExporting(true); const res = await exporter.parseAsync(opts.scene, exporterOptions).catch((e) => { console.error(e); return null; }); __isExporting(false); state.onAfterExport(opts); if (!res) { throw new Error("Failed to export GLTF"); } if (opts.downloadAs != undefined) { let blob: Blob | null = null; if (res instanceof ArrayBuffer) { blob = new Blob([res], { type: "application/octet-stream" }); } else { console.error("Can not download GLTF as a blob", res); } if (blob) { const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; let name = opts.downloadAs; if (!name.endsWith(".glb") && !name.endsWith(".gltf")) { name += opts.binary ? ".glb" : ".gltf"; } a.download = name; a.click(); } } return res; } const ACTIONS_WEIGHT_KEY = Symbol("needle:weight"); class ExporterState { private readonly _undo: Array<() => void> = []; onBeforeExport(opts: Required<ExportOptions>) { opts.context.animations.mixers.forEach(mixer => { const actions = AnimationUtils.tryGetActionsFromMixer(mixer); if (actions) { for (let i = 0; i < actions.length; i++) { const action = actions[i]; action[ACTIONS_WEIGHT_KEY] = action.weight; action.weight = 0; this._undo.push(() => { action.weight = action[ACTIONS_WEIGHT_KEY]; }); } } mixer.update(0); }); opts.context.scene.traverse(obj => { if(!shouldExport_HideFlags(obj)) { const parent = obj.parent; if(parent) { obj.removeFromParent(); this._undo.push(() => parent.add(obj)); } } }); } onAfterExport(_opts: Required<ExportOptions>) { this._undo.forEach(fn => fn()); this._undo.length = 0; } } function collectAnimations(context: Context, scene: Object3D | Array<Object3D>, clips: Array<AnimationClip>): Array<AnimationClip> { // Get all animations that are used by any mixer in the scene // technically we might also collect animations here that aren't used by any object in the scene because they're part of another scene // But that's a problem for later... context.animations.mixers.forEach(mixer => { const actions = AnimationUtils.tryGetActionsFromMixer(mixer); if (actions) { for (let i = 0; i < actions.length; i++) { const action = actions[i]; const clip = action.getClip(); // TODO: might need to check if the clip is part of the scene that we want to export clips.push(clip); } } }); // Get all animations that are directly assigned to objects in the scene if (!Array.isArray(scene)) scene = [scene]; for (const obj of scene) { AnimationUtils.tryGetAnimationClipsFromObjectHierarchy(obj, clips); } // ensure we only have unique clips const uniqueClips = new Set(clips); return Array.from(uniqueClips); }