@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.
269 lines (233 loc) • 10.4 kB
text/typescript
import { Object3D } from "three";
import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter.js';
import { type GLTF, type GLTFLoaderPlugin, GLTFParser } from "three/examples/jsm/loaders/GLTFLoader.js";
import { builtinComponentKeyName } from "../engine_constants.js";
import { debugExtension } from "../engine_default_parameters.js";
import { getLoader } from "../engine_gltf.js";
import { type NodeToObjectMap, type ObjectToNodeMap, SerializationContext } from "../engine_serialization_core.js";
import { apply } from "../js-extensions/index.js";
import { maskGltfAssociation,resolveReferences } from "./extension_utils.js";
export const debug = debugExtension
const componentsArrayExportKey = "$___Export_Components";
export const EXTENSION_NAME = "NEEDLE_components";
class ExtensionData {
[builtinComponentKeyName]?: Array<object | null>
}
class ExportData {
node: Object3D;
nodeIndex: number;
nodeDef: any;
constructor(node: Object3D, nodeIndex: number, nodeDef: any) {
this.node = node;
this.nodeIndex = nodeIndex;
this.nodeDef = nodeDef;
}
}
export class NEEDLE_components implements GLTFLoaderPlugin {
get name(): string {
return EXTENSION_NAME;
}
// import
parser?: GLTFParser;
nodeToObjectMap: NodeToObjectMap = {};
/** The loaded gltf */
gltf: GLTF | null = null;
// export
exportContext!: { [nodeIndex: number]: ExportData };
objectToNodeMap: ObjectToNodeMap = {};
context!: SerializationContext;
writer?: any;
registerExport(exp: GLTFExporter) {
//@ts-ignore
exp.register(writer => {
// we want to hook into BEFORE user data is written
// because we want to remove the components list (circular references)
// and replace them with the serialized data
// the write node callback is called after user data is serialized
// we could also traverse everything before export and remove components
// but doing it like that we avoid traversing multiple times
if ("serializeUserData" in writer) {
//@ts-ignore
const originalFunction = writer.serializeUserData.bind(writer);
this.writer = writer;
//@ts-ignore
writer.serializeUserData = (o, def) => {
try {
const hadUserData = this.serializeUserData(o, def);
if (hadUserData)
//@ts-ignore
writer.extensionsUsed[this.name] = true;
originalFunction(o, def);
}
finally {
this.afterSerializeUserData(o, def);
}
}
}
return this;
});
}
beforeParse() {
this.exportContext = {};
this.objectToNodeMap = {};
}
// https://github.com/mrdoob/three.js/blob/efbfc67edc7f65cfcc61a389ffc5fd43ea702bc6/examples/jsm/exporters/GLTFExporter.js#L532
serializeUserData(node: Object3D, _nodeDef: any): boolean {
const components = node.userData?.components;
if (!components || components.length <= 0) return false;
// delete components before serializing user data to avoid circular references
delete node.userData.components;
node[componentsArrayExportKey] = components;
return true;
}
afterSerializeUserData(node: Object3D, _nodeDef) {
if (node.type === "Scene") {
if (debug)
console.log("DONE", JSON.stringify(_nodeDef));
}
// reset userdata
if (node[componentsArrayExportKey] === undefined) return;
const components = node[componentsArrayExportKey];
delete node[componentsArrayExportKey];
if (components !== null) {
node.userData.components = components;
}
// console.log(_nodeDef, _nodeDef.mesh);
}
writeNode(node: Object3D, nodeDef) {
const nodeIndex = this.writer.json.nodes.length;
if (debug)
console.log(node.name, nodeIndex, node.uuid);
const context = new ExportData(node, nodeIndex, nodeDef);
this.exportContext[nodeIndex] = context;
this.objectToNodeMap[node.uuid] = nodeIndex;
};
afterParse(input) {
if (debug)
console.log("AFTER", input);
for (const i in this.exportContext) {
const context = this.exportContext[i];
const node = context.node;
const nodeDef = context.nodeDef;
const nodeIndex = context.nodeIndex;
const components = node.userData?.components;
if (!components || components.length <= 0) continue;
// create data container
const data: ExtensionData = new ExtensionData();
nodeDef.extensions = nodeDef.extensions || {};
nodeDef.extensions[this.name] = data;
this.context.object = node;
this.context.nodeId = nodeIndex;
this.context.objectToNode = this.objectToNodeMap;
const serializedComponentData: Array<object | null> = [];
for (const comp of components) {
this.context.target = comp;
const res = getLoader().writeBuiltinComponentData(comp, this.context);
if (res !== null) {
serializedComponentData.push(res);
// (comp as unknown as ISerializationCallbackReceiver)?.onAfterSerialize?.call(comp);
}
}
if (serializedComponentData.length > 0) {
data[builtinComponentKeyName] = serializedComponentData;
if (debug)
console.log("DID WRITE", node, "nodeIndex", nodeIndex, serializedComponentData);
}
}
}
// -------------------------------------
// LOADING
// called by GLTFLoader
beforeRoot() {
if (debug)
console.log("BEGIN LOAD");
this.nodeToObjectMap = {};
return null;
}
// called by GLTFLoader
async afterRoot(result: GLTF): Promise<void> {
this.gltf = result;
const parser = result.parser;
const ext = parser?.extensions;
if (!ext) return;
const hasExtension = ext[this.name];
if (debug)
console.log("After root", result, this.parser, ext);
const loadComponents: Array<Promise<void>> = [];
if (hasExtension === true) {
const nodes = parser.json.nodes;
if (nodes) {
for (let i = 0; i < nodes.length; i++) {
const obj = await parser.getDependency('node', i);
this.nodeToObjectMap[i] = obj;
}
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i];
const index = i;// node.mesh;
const ext = node.extensions;
if (!ext) continue;
const data = ext[this.name];
if (!data) continue;
if (debug)
console.log("NODE", node);
const obj = this.nodeToObjectMap[index];
if (!obj) {
console.error("Could not find object for node index: " + index, node, parser);
continue;
}
apply(obj);
loadComponents.push(this.createComponents(obj, data));
}
}
}
await Promise.all(loadComponents);
for (const instance of parser.associations.keys()) {
const value = parser.associations.get(instance);
if (value?.materials != undefined) {
const key = "/materials/" + value.materials;
maskGltfAssociation(instance, key);
}
}
}
private async createComponents(obj: Object3D, data: ExtensionData) {
if (!data) return;
const componentData = data[builtinComponentKeyName];
if (componentData) {
const tasks = new Array<Promise<any>>();
if (debug)
console.log(obj.name, componentData);
for (const i in componentData) {
const serializedData = componentData[i];
if (debug)
console.log("Serialized data", JSON.parse(JSON.stringify(serializedData)));
if (serializedData && this.parser) {
tasks.push(
resolveReferences(this.parser, serializedData)
.catch(e => console.error(`Error while resolving references (see console for details)\n`, e, obj, serializedData))
);
}
obj.userData = obj.userData || {};
obj.userData[builtinComponentKeyName] = obj.userData[builtinComponentKeyName] || [];
obj.userData[builtinComponentKeyName].push(serializedData);
}
await Promise.all(tasks).catch((e) => {
console.error("Error while loading components", e);
});
}
}
// parse function https://github.com/mrdoob/three.js/blob/efbfc67edc7f65cfcc61a389ffc5fd43ea702bc6/examples/jsm/loaders/GLTFLoader.js#L2290
// createNodeAttachment(nodeIndex: number): null {
// // if(!this.parser){
// // console.error("Parser not set, call registerLoad with on this");
// // return null;
// // }
// // const node = this.parser.json.nodes[nodeIndex];
// // const extenstions = node.extensions;
// // const data = extenstions && extenstions[this.name];
// // if (!data) return null;
// // const components = data[builtinComponentKeyName];
// // if (!components) return null;
// // console.log(components);
// return null;
// }
}