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.

404 lines (351 loc) • 18.4 kB
import "./codegen/register_types.js"; import { Object3D } from "three"; import { type GLTF } from "three/examples/jsm/loaders/GLTFLoader.js"; import { LogType, showBalloonMessage } from "./debug/index.js"; import { addNewComponent } from "./engine_components.js"; import { builtinComponentKeyName, editorGuidKeyName } from "./engine_constants.js"; import { debugExtension } from "./engine_default_parameters.js"; import { InstantiateIdProvider } from "./engine_networking_instantiate.js" import { isLocalNetwork } from "./engine_networking_utils.js"; import { deserializeObject, serializeObject } from "./engine_serialization.js"; import { assign, ImplementationInformation, type ISerializable, SerializationContext } from "./engine_serialization_core.js"; import { Context } from "./engine_setup.js"; import type { GuidsMap, ICamera, ICollider, IComponent, IGameObject, IRigidbody, SourceIdentifier, UIDProvider } from "./engine_types.js"; import { TypeStore } from "./engine_typestore.js"; import { getParam } from "./engine_utils.js"; import { NEEDLE_components } from "./extensions/NEEDLE_components.js"; const debug = debugExtension; const debugTypeStore = getParam("debugtypestore"); if (debugTypeStore) console.log(TypeStore); export function writeBuiltinComponentData(comp: IComponent, context: SerializationContext): object | null { // const fn = (comp as unknown as ISerializable)?.onBeforeSerialize; // if (fn) { // const res = fn?.call(comp); // if (res !== undefined) { // res["name"] = comp.constructor.name; // return res; // } // } const serializable = comp as unknown as ISerializable; const data = serializeObject(serializable, context); // console.log(data); if (data !== undefined) return data; return null; } const typeImplementationInformation = new ImplementationInformation(); const $context_deserialize_queue = Symbol("deserialize-queue"); export async function createBuiltinComponents(context: Context, gltfId: SourceIdentifier, gltf: GLTF & { children?: Array<Object3D> }, seed: number | null | UIDProvider = null, extension?: NEEDLE_components) { if (!gltf) { console.debug("Can not create component instances: gltf is null"); return; } const lateResolve: Array<(gltf: Object3D) => {}> = []; let idProvider: UIDProvider | null = seed as UIDProvider; if (typeof idProvider === "number") { idProvider = new InstantiateIdProvider(seed as number); } const idEnd = gltfId.indexOf("?"); gltfId = idEnd === -1 ? gltfId : gltfId.substring(0, idEnd); const serializationContext = new SerializationContext(gltf.scene); serializationContext.gltfId = gltfId; serializationContext.context = context; serializationContext.gltf = gltf; serializationContext.nodeToObject = extension?.nodeToObjectMap; serializationContext.implementationInformation = typeImplementationInformation; // If we're loading multiple gltf files in one scene we need to make sure we deserialize all of them in one go // for that we collect them in one list per context let deserializeQueue = context[$context_deserialize_queue]; if (!deserializeQueue) deserializeQueue = context[$context_deserialize_queue] = []; if (gltf.scenes) { for (const scene of gltf.scenes) { await onCreateBuiltinComponents(serializationContext, scene, deserializeQueue, lateResolve); } } // TODO: when is the gltf here an object3d? if (gltf.children) { for (const ch of gltf.children) { await onCreateBuiltinComponents(serializationContext, ch, deserializeQueue, lateResolve); } } context.new_scripts_pre_setup_callbacks.push(() => { // First deserialize ALL components that were loaded before pre setup // Down below they get new guids assigned so we have to do all of them first // E.g. in cases where we load multiple glb files on startup from one scene // and they might have cross-glb references const queue = context[$context_deserialize_queue]; if (queue) { for (const des of queue) { handleDeserialization(des, serializationContext); } queue.length = 0; } // when dropping the same file multiple times we need to generate new guids // e.g. SyncedTransform sends its own guid to the server to know about ownership // so it requires a unique guid for a new instance // doing it here at the end of resolving of references should ensure that // and this should run before awake and onenable of newly created components if (idProvider) { // TODO: should we do this after setup callbacks now? const guidsMap: GuidsMap = {}; const resolveGuids: IHasResolveGuids[] = []; // TODO: when is the gltf here an object3d? recursiveCreateGuids(gltf as any, idProvider, guidsMap, resolveGuids); for (const scene of gltf.scenes) recursiveCreateGuids(scene, idProvider, guidsMap, resolveGuids); // make sure to resolve all guids AFTER new guids have been assigned for (const res of resolveGuids) { res.resolveGuids(guidsMap); } } }); } declare type IHasResolveGuids = { resolveGuids: (guidsMap: GuidsMap) => void; } const originalComponentNameKey = Symbol("original-component-name"); /** * We want to create one id provider per component * If a component is used multiple times we want to create a new id provider for each instance * That way the order of components in the scene doesnt affect the result GUID */ // TODO: clear this when re-loading the context const idProviderCache = new Map<string, InstantiateIdProvider>(); function recursiveCreateGuids(obj: Object3D, idProvider: UIDProvider | null, guidsMap: GuidsMap, resolveGuids: IHasResolveGuids[]) { if (idProvider === null) return; if (!obj) return; const prev = obj.guid; // we also want to use the idproviderCache for objects because they might be removed or re-ordered in the scene hierarchy // in which case we dont want to e.g. change the syncedInstantiate objects that get created because suddenly another object has that guid const idProviderKey = obj.guid; if (idProviderKey?.length) { if (!idProviderCache.has(idProviderKey)) { if (debug) console.log("Creating InstanceIdProvider with key \"" + idProviderKey + "\" for object " + obj.name); idProviderCache.set(idProviderKey, new InstantiateIdProvider(idProviderKey)); } } const objectIdProvider = idProviderKey && idProviderCache.get(idProviderKey) || idProvider; obj.guid = objectIdProvider.generateUUID(); if (prev && prev !== "invalid") guidsMap[prev] = obj.guid; // console.log(obj); if (obj && obj.userData && obj.userData.components) { for (const comp of obj.userData.components) { if (comp === null) continue; // by default we use the component guid as a key - order of the components in the scene doesnt matter with this approach // this is to prevent cases where multiple GLBs are loaded with the same component guid const idProviderKey = comp.guid; if (idProviderKey) { if (!idProviderCache.has(idProviderKey)) { if (debug) console.log("Creating InstanceIdProvider with key \"" + idProviderKey + "\" for component " + comp[originalComponentNameKey]); idProviderCache.set(idProviderKey, new InstantiateIdProvider(idProviderKey)); } } else if (debug) console.warn("Can not create IdProvider: component " + comp[originalComponentNameKey] + " has no guid", comp.guid); const componentIdProvider = idProviderCache.get(idProviderKey) || idProvider const prev = comp.guid; comp.guid = componentIdProvider.generateUUID(); if (prev && prev !== "invalid") guidsMap[prev] = comp.guid; if (comp.resolveGuids) resolveGuids.push(comp); } } if (obj.children) { for (const child of obj.children) { recursiveCreateGuids(child as IGameObject, idProvider, guidsMap, resolveGuids); } } } declare interface IGltfbuiltinComponent { name: string; } declare interface IGltfBuiltinComponentData { [builtinComponentKeyName]: IGltfbuiltinComponent[]; } declare class DeserializeData { instance: any; compData: IGltfbuiltinComponent; obj: Object3D; } declare type LateResolveCallback = (gltf: Object3D) => void; const unknownComponentsBuffer: Array<string> = []; async function onCreateBuiltinComponents(context: SerializationContext, obj: Object3D, deserialize: DeserializeData[], lateResolve: LateResolveCallback[]) { if (!obj) return; // iterate injected data const data = obj.userData as IGltfBuiltinComponentData; if (data) { const components = data.builtin_components; if (components && components.length > 0) { // console.log(obj); for (const compData of components) { try { if (compData === null) continue; const type = TypeStore.get(compData.name); // console.log(compData, compData.name, type, TypeStore); if (type !== undefined && type !== null) { const instance: IComponent = new type() as IComponent; instance.sourceId = context.gltfId; // assign basic fields assign(instance, compData, context.implementationInformation); // make sure we assign the Needle Engine Context because Context.Current is unreliable when loading multiple <needle-engine> elements at the same time due to async processes instance.context = context.context; // assign the guid of the original instance if ("guid" in compData) instance[editorGuidKeyName] = compData.guid; // we store the original component name per component which will later be used to get or initialize the InstanceIdProvider instance[originalComponentNameKey] = compData.name; // Object.assign(instance, compData); // dont call awake here because some references might not be resolved yet and components that access those fields in awake will throw // for example Duplicatable reference to object might still be { node: id } const callAwake = false; addNewComponent(obj, instance, callAwake); deserialize.push({ instance, compData, obj }); // if the component instance is a camera and we dont have a main camera yet // we want to assign it to the context BEFORE any component becomes active (ensuring that in awake and onEnable the mainCamera is assigned) // alternatively we could try to search for the mainCamera in the getter of the context when creating the engine for the first time if ((instance as ICamera).isCamera && context.context) { if (context.context.mainCamera === null && (instance.tag === "MainCamera")) context.context.setCurrentCamera(instance as ICamera); } // if the component is a rigidbody or collider and the physics engine is not initialized yet // initialize the physics engine right away if (context.context?.physics?.engine?.isInitialized === false && ((instance as ICollider).isCollider || (instance as IRigidbody).isRigidbody)) { context.context?.physics.engine?.initialize(); } } else { if (debug) console.debug("unknown component: " + compData.name); if (!unknownComponentsBuffer.includes(compData.name)) unknownComponentsBuffer.push(compData.name); } } catch (err: any) { console.error(compData.name + " - " + err.message, err); } } // console.debug("finished adding gltf builtin components", obj); } if (unknownComponentsBuffer.length > 0) { const unknown = unknownComponentsBuffer.join(", "); console.warn("unknown components: " + unknown); unknownComponentsBuffer.length = 0; if (isLocalNetwork()) showBalloonMessage(`<strong>Unknown components in scene</strong>:\n\n${unknown}\n\nThis could mean you forgot to add a npmdef to your ExportInfo\n<a href="https://engine.needle.tools/docs/project_structure.html#creating-and-installing-a-npmdef" target="_blank">documentation</a>`, LogType.Warn); } } if (obj.children) { for (const ch of obj.children) { await onCreateBuiltinComponents(context, ch, deserialize, lateResolve); } } } function handleDeserialization(data: DeserializeData, context: SerializationContext) { const { instance, compData, obj } = data; context.object = obj; context.target = instance; // const beforeFn = (instance as ISerializationCallbackReceiver)?.onBeforeDeserialize; // console.log(beforeFn, instance); // if (beforeFn) beforeFn.call(instance, data.compData); let deserialized: boolean = true; // console.log(instance, compData); // TODO: first build components and then deserialize data? // currently a component referencing another component can not find it if the referenced component hasnt been added // we should split this up in two steps then. deserialized = deserializeObject(instance, compData, context) === true; // if (!deserialized) { // // now loop through data again and search for special reference types // for (const key in compData) { // const entry = compData[key]; // if (!entry) { // instance[key] = null; // continue; // } // const fn = (instance as ISerializationCallbackReceiver)?.onDeserialize; // if (fn) { // const res = fn.call(instance, key, entry); // if (res !== undefined) { // instance[key] = res; // continue; // } // } // // if (!resolve(instance, key, entry, lateResolve)) { // // } // } // } // console.log(instance); // const afterFn = (instance as ISerializationCallbackReceiver)?.onAfterDeserialize; // if (afterFn) afterFn.call(instance); if (debug) console.debug("add " + compData.name, compData, instance); } // // TODO: THIS should be legacy once we update unity builtin component exports // function resolve(instance, key: string, entry, lateResolve: LateResolveCallback[]): boolean { // switch (entry["$type"]) { // default: // const type = entry["$type"]; // if (type !== undefined) { // const res = tryResolveType(type, entry); // if (res !== undefined) // instance[key] = res; // return res !== undefined; // } // break; // // the thing is a reference // case "reference": // // we expect some identifier entry to use for finding the reference // const guid = entry["guid"]; // lateResolve.push(async (gltf) => { // instance[key] = findInGltf(guid, gltf); // }); // return true; // } // if (Array.isArray(entry)) { // // the thing is an array // const targetArray = instance[key]; // for (const index in entry) { // const val = entry[index]; // if (val === null) { // targetArray[index] = null; // continue; // } // switch (val["$type"]) { // default: // const type = val["$type"]; // if (type !== undefined) { // const res = tryResolveType(type, entry); // if (res !== undefined) targetArray[index] = res; // } // break; // case "reference": // // this entry is a reference // const guid = val["guid"]; // lateResolve.push(async (gltf) => { // targetArray[index] = findInGltf(guid, gltf); // }); // break; // } // } // return true; // } // return false; // } // function findInGltf(guid: string, gltf) { // let res = tools.tryFindScript(guid); // if (!res) res = tools.tryFindObject(guid, gltf, true); // return res; // } // function tryResolveType(type, entry): any | undefined { // switch (type) { // case "Vector2": // return new Vector2(entry.x, entry.y); // case "Vector3": // return new Vector3(entry.x, entry.y, entry.z); // case "Vector4": // return new Vector4(entry.x, entry.y, entry.z, entry.w); // case "Quaternion": // return new Quaternion(entry.x, entry.y, entry.z, entry.w); // } // return undefined; // }