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 • 19.4 kB
import { Color, CompressedTexture, Euler, LinearSRGBColorSpace, Object3D, RGBAFormat, Texture, WebGLRenderTarget } from "three"; import { isDevEnvironment, showBalloonWarning } from "../engine/debug/index.js"; import { Behaviour, Component, GameObject } from "../engine-components/Component.js"; import { CallInfo, EventList } from "../engine-components/EventList.js"; import { AssetReference } from "./engine_addressables.js"; import { debugExtension } from "./engine_default_parameters.js"; import { TypeSerializer } from "./engine_serialization_core.js"; import { RenderTexture } from "./engine_texture.js"; import { resolveUrl } from "./engine_utils.js"; import { RGBAColor } from "./js-extensions/index.js"; // export class SourcePath { // src?:string // }; // class SourcePathSerializer extends TypeSerializer{ // constructor(){ // super(SourcePath); // } // onDeserialize(data: any, _context: SerializationContext) { // if(data.src && typeof data.src === "string"){ // return data.src; // } // } // onSerialize(_data: any, _context: SerializationContext) { // } // } // new SourcePathSerializer(); class ColorSerializer extends TypeSerializer { constructor() { super([Color, RGBAColor], "ColorSerializer"); } onDeserialize(data) { if (data === undefined || data === null) return; if (data.a !== undefined) { return new RGBAColor(data.r, data.g, data.b, data.a); } else if (data.alpha !== undefined) { return new RGBAColor(data.r, data.g, data.b, data.alpha); } return new Color(data.r, data.g, data.b); } onSerialize(data) { if (data === undefined || data === null) return; if (data.a !== undefined) return { r: data.r, g: data.g, b: data.b, a: data.a }; else return { r: data.r, g: data.g, b: data.b }; } } export const colorSerializer = new ColorSerializer(); class EulerSerializer extends TypeSerializer { constructor() { super([Euler], "EulerSerializer"); } onDeserialize(data, _context) { if (data === undefined || data === null) return undefined; if (data.order) { return new Euler(data.x, data.y, data.z, data.order); } else if (data.x != undefined) { return new Euler(data.x, data.y, data.z); } return undefined; } onSerialize(data, _context) { return { x: data.x, y: data.y, z: data.z, order: data.order }; } } export const euler = new EulerSerializer(); class ObjectSerializer extends TypeSerializer { constructor() { super(Object3D, "ObjectSerializer"); } onSerialize(data, context) { if (context.objectToNode !== undefined && data.uuid) { const node = context.objectToNode[data.uuid]; if (debugExtension) console.log(node, data.name, data.uuid); return { node: node }; } return undefined; } onDeserialize(data, context) { if (typeof data === "string") { if (data.endsWith(".glb") || data.endsWith(".gltf")) { // If the @serializable([Object3D, AssetReference]) looks like this we don't need to warn here. This is the case e.g. with SyncedCamera referencing a scene if (context.serializable instanceof Array) { if (context.serializable.includes(AssetReference)) return undefined; } if (isDevEnvironment()) showBalloonWarning("Detected wrong usage of @serializable with Object3D or GameObject. Instead you should use AssetReference here! Please see the console for details."); const scriptname = context.target?.constructor?.name; console.warn(`Wrong usage of @serializable detected in your script \"${scriptname}\"\n\nIt looks like you used @serializable(Object3D) or @serializable(GameObject) for a prefab or scene reference which is exported to a separate glTF file.\n\nTo fix this please change your code to:\n\n@serializable(AssetReference)\n${context.path}! : AssetReference;\n\0`); } // ACTUALLY: this is already handled by the extension_utils where we resolve json pointers recursively // if(data.startsWith("/nodes/")){ // const node = parseInt(data.substring("/nodes/".length)); // if (context.nodeToObject) { // const res = context.nodeToObject[node]; // if (debugExtension) // console.log("Deserialized object reference?", data, res, context?.nodeToObject); // if (!res) console.warn("Did not find node: " + data, context.nodeToObject, context.object); // return res; // } // } return undefined; } if (data) { if (data.node !== undefined && context.nodeToObject) { const res = context.nodeToObject[data.node]; if (debugExtension) console.log("Deserialized object reference?", data, res, context?.nodeToObject); if (!res) console.warn("Did not find node: " + data.node, context.nodeToObject, context.object); return res; } else if (data.guid) { if (!context.context) { console.error("Missing context"); return undefined; } // it is possible that the object is not yet loaded // e.g. if we have a scene with multiple gltf files and the first gltf references something in the second gltf // we need a way to wait for all components to be created before we can resolve those references // independent of order of loading let res = undefined; // first try to search in the current gltf scene (if any) const gltfScene = context.gltf?.scene; if (gltfScene) { res = GameObject.findByGuid(data.guid, gltfScene); } // if not found, search in the whole scene if (!res) { res = GameObject.findByGuid(data.guid, context.context.scene); } if (!res) { if (isDevEnvironment() || debugExtension) console.warn("Could not resolve object reference", context.path, data, context.target, context.context.scene); data["could_not_resolve"] = true; } else { if (res && res.isComponent === true) { if (debugExtension) console.warn("Deserialized object reference is a component"); res = res.gameObject; } if (debugExtension) console.log("Deserialized object reference?", data, res, context?.nodeToObject); } return res; } } return undefined; } } export const objectSerializer = new ObjectSerializer(); class ComponentSerializer extends TypeSerializer { constructor() { super([Component, Behaviour], "ComponentSerializer"); } onSerialize(data, _context) { if (data?.guid) { return { guid: data.guid }; } return undefined; } onDeserialize(data, context) { if (data?.guid) { // it's a workaround for VolumeParameter having a guid as well. Generally we will probably never have to resolve a component in the scene when it's coming from the persistent asset extension (like in the case for postprocessing volume parameters) if (data.___persistentAsset) { if (debugExtension) console.log("Skipping component deserialization because it's a persistent asset", data); return undefined; } const currentPath = context.path; // TODO: need to serialize some identifier for referenced components as well, maybe just guid? // because here the components are created but dont have their former guid assigned // and will later in the stack just get a newly generated guid if (debugExtension) console.log(data.guid, context.root, context.object, context.target); // first search within the gltf (e.g. necessary when using AssetReference and loading a gltf without adding it to the scene) // if we would search JUST the scene those references would NEVER be resolved let res = this.findObjectForGuid(data.guid, context.root); if (res) { return res; } if (context.context) { // if not found within the gltf use the provided context scene // to find references outside res = this.findObjectForGuid(data.guid, context.context?.scene); if (res) return res; } if (isDevEnvironment() || debugExtension) { console.warn("Could not resolve component reference: \"" + currentPath + "\" using guid " + data.guid, context.target); } data["could_not_resolve"] = true; return undefined; } // if (data?.node !== undefined && context.nodeToObject) { // return context.nodeToObject[data.node]; // } return undefined; } findObjectForGuid(guid, root) { // recursively search root // need to check the root object too if (root["guid"] === guid) return root; const res = GameObject.foreachComponent(root, (c) => { if (c.guid === guid) return c; return undefined; }, false); if (res !== undefined) return res; // if not found, search in children for (let i = 0; i < root.children.length; i++) { const child = root.children[i]; const res = this.findObjectForGuid(guid, child); if (res) return res; } } } export const componentSerializer = new ComponentSerializer(); const $eventListDebugInfo = Symbol("eventListDebugInfo"); class EventListSerializer extends TypeSerializer { constructor() { super([EventList]); } onSerialize(_data, _context) { console.log("TODO: SERIALIZE EVENT"); return undefined; } onDeserialize(data, context) { // TODO: check that we dont accidentally deserialize methods to EventList objects. This is here to make is easy for react-three-fiber to just add props as { () => { } } if (typeof data === "function") { const evtList = new EventList([new CallInfo(data, null, [], true)]); return evtList; } else if (data && data.type === "EventList") { if (debugExtension) console.log("DESERIALIZE EVENT", data); const fns = new Array(); if (data.calls && Array.isArray(data.calls)) { for (const call of data.calls) { if (debugExtension) console.log(call); let target = componentSerializer.findObjectForGuid(call.target, context.root); // if the object is not found in the current glb try find it in the whole scene if (!target && context.context?.scene) { target = componentSerializer.findObjectForGuid(call.target, context.context?.scene); } const hasMethod = call.method?.length > 0; if (target && hasMethod) { const printWarningMethodNotFound = () => { const uppercaseMethodName = call.method[0].toUpperCase() + call.method.slice(1); if (typeof target[uppercaseMethodName] === "function") { console.warn(`EventList method:\nCould not find method ${call.method} on object ${target.name}. Please rename ${call.method} to ${uppercaseMethodName}?\n`, target[uppercaseMethodName], "\n in script: ", target); showBalloonWarning("EventList methods must start with lowercase letter, see console for details"); return; } else { console.warn(`EventList method:\nCould not find method ${call.method} on object ${target.name}`, target, typeof target[call.method]); } }; const method = target[call.method]; if (typeof method !== "function") { let foundMethod = false; let currentPrototype = target; // test if the target method is actually a property setter while (currentPrototype) { const desc = Object.getOwnPropertyDescriptor(currentPrototype, call.method); if (desc && (desc.writable === true || desc.set)) { foundMethod = true; break; } currentPrototype = Object.getPrototypeOf(currentPrototype); } if (!foundMethod && (isDevEnvironment() || debugExtension)) printWarningMethodNotFound(); } } function deserializeArgument(arg) { if (typeof arg === "object") { // Try to deserialize the call argument to either a object or a component reference let argRes = objectSerializer.onDeserialize(arg, context); if (!argRes) argRes = componentSerializer.onDeserialize(arg, context); if (argRes) return argRes; } return arg; } if (target) { let args = call.argument; if (args !== undefined) { args = deserializeArgument(args); } else if (call.arguments !== undefined) { args = call.arguments.map(deserializeArgument); } const method = target[call.method]; if (!method) { console.warn(`EventList method not found: \"${call.method}\" on ${target?.name}`); } else { if (args !== undefined && !Array.isArray(args)) { args = [args]; } // This is the final method we pass to the call info (or undefined if the method couldnt be resolved) // const eventMethod = hasMethod ? this.createEventMethod(target, call.method, args) : undefined; const fn = new CallInfo(target, call.method, args, call.enabled); fns.push(fn); } } else if (isDevEnvironment()) { console.warn("[Debug] EventList: Could not find event listener in scene", call, context.object, data); } } } const evt = new EventList(fns); if (debugExtension) console.log(evt); const eventListOwner = context.target; if (eventListOwner !== undefined && context.path !== undefined) { evt.setEventTarget(context.path, eventListOwner); } return evt; } return undefined; } } export const eventListSerializer = new EventListSerializer(); /** Map<Clone, Original> texture. This is used for compressed textures (or when the GLTFLoader is cloning RenderTextures) * It's a weak map so we don't have to worry about memory leaks */ const cloneOriginalMap = new WeakMap(); const textureClone = Texture.prototype.clone; Texture.prototype.clone = function () { const clone = textureClone.call(this); if (!cloneOriginalMap.has(clone)) { cloneOriginalMap.set(clone, this); } return clone; }; export class RenderTextureSerializer extends TypeSerializer { constructor() { super([RenderTexture, WebGLRenderTarget]); } onSerialize(_data, _context) { } onDeserialize(data, context) { if (data instanceof Texture && context.type === RenderTexture) { let tex = data; // If this is a cloned render texture we want to map it back to the original texture // See https://linear.app/needle/issue/NE-5530 if (cloneOriginalMap.has(tex)) { const original = cloneOriginalMap.get(tex); tex = original; } tex.isRenderTargetTexture = true; tex.flipY = true; tex.offset.y = 1; tex.repeat.y = -1; tex.needsUpdate = true; // when we have a compressed texture using mipmaps causes error in threejs because the bindframebuffer call will then try to set an array of framebuffers https://linear.app/needle/issue/NE-4294 tex.mipmaps = []; if (tex instanceof CompressedTexture) { //@ts-ignore tex["isCompressedTexture"] = false; //@ts-ignore tex.format = RGBAFormat; } const rt = new RenderTexture(tex.image.width, tex.image.height, { colorSpace: LinearSRGBColorSpace, }); rt.texture = tex; return rt; } return undefined; } } new RenderTextureSerializer(); export class UriSerializer extends TypeSerializer { constructor() { super([URL]); } onSerialize(_data, _context) { return null; } onDeserialize(data, _context) { if (typeof data === "string" && data.length > 0) { return resolveUrl(_context.gltfId, data); } return undefined; } } new UriSerializer(); //# sourceMappingURL=engine_serialization_builtin_serializer.js.map