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