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.

269 lines (233 loc) • 10.4 kB
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; // } }