@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
text/typescript
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 {
()
binary: boolean = true;
(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;
}
}
}
}