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.

713 lines (633 loc) • 28.1 kB
import { Bone, Object3D, Quaternion, SkinnedMesh, Vector3 } from "three"; import { $shadowDomOwner } from "../engine-components/ui/Symbols.js"; import { type AssetReference } from "./engine_addressables.js"; import { __internalNotifyObjectDestroyed as __internalRemoveReferences, disposeObjectResources } from "./engine_assetdatabase.js"; import { ComponentLifecycleEvents } from "./engine_components_internal.js"; import { activeInHierarchyFieldName } from "./engine_constants.js"; import { editorGuidKeyName } from "./engine_constants.js"; import { $isUsingInstancing, InstancingUtil } from "./engine_instancing.js"; import { processNewScripts } from "./engine_mainloop_utils.js"; import { InstantiateIdProvider } from "./engine_networking_instantiate.js"; import { assign, ISerializable } from "./engine_serialization_core.js"; import { Context, registerComponent } from "./engine_setup.js"; import { logHierarchy, setWorldPosition, setWorldQuaternion } from "./engine_three_utils.js"; import { type Constructor, type GuidsMap, type IComponent as Component, type IComponent, IEventList, type IGameObject as GameObject, type UIDProvider } from "./engine_types.js"; import { deepClone, getParam, tryFindObject } from "./engine_utils.js"; import { apply } from "./js-extensions/index.js"; const debug = getParam("debuggetcomponent"); const debugInstantiate = getParam("debuginstantiate"); export type IInstantiateOptions = { idProvider?: UIDProvider; //** parent guid or object */ parent?: string | Object3D; /** position in local space. Set `keepWorldPosition` to true if this is world space */ position?: Vector3; /** for duplicatable parenting */ keepWorldPosition?: boolean; /** rotation in local space. Set `keepWorldPosition` to true if this is world space */ rotation?: Quaternion; scale?: Vector3; /** if the instantiated object should be visible */ visible?: boolean; context?: Context; /** If true the components will be cloned as well * @default true */ components?: boolean; } /** * Instantiation options for {@link syncInstantiate} */ export class InstantiateOptions implements IInstantiateOptions { idProvider?: UIDProvider | undefined; parent?: string | undefined | Object3D; keepWorldPosition?: boolean position?: Vector3 | undefined; rotation?: Quaternion | undefined; scale?: Vector3 | undefined; visible?: boolean | undefined; context?: Context | undefined; components?: boolean | undefined; clone() { const clone = new InstantiateOptions(); clone.idProvider = this.idProvider; clone.parent = this.parent; clone.keepWorldPosition = this.keepWorldPosition; clone.position = this.position?.clone(); clone.rotation = this.rotation?.clone(); clone.scale = this.scale?.clone(); clone.visible = this.visible; clone.context = this.context; clone.components = this.components; return clone; } /** Copy fields from another object, clone field references */ cloneAssign(other: InstantiateOptions | IInstantiateOptions) { this.idProvider = other.idProvider; this.parent = other.parent; this.keepWorldPosition = other.keepWorldPosition; this.position = other.position?.clone(); this.rotation = other.rotation?.clone(); this.scale = other.scale?.clone(); this.visible = other.visible; this.context = other.context; this.components = other.components; } } // export function setActive(go: Object3D, active: boolean, processStart: boolean = true) { // if (!go) return; // go.visible = active; // main.updateActiveInHierarchyWithoutEventCall(go); // if (active && processStart) // main.processStart(Context.Current, go); // } // Object.defineProperty(Object3D.prototype, "visible", { // get: function () { // return this._visible; // }, // set: function (val) { // // const changed = val !== this._visible; // this._visible = val; // // if (changed) { // // setActive(this, val); // // } // } // }); const $isActive = Symbol("isActive"); export function isActiveSelf(go: Object3D): boolean { // if (go[$isActive] === undefined) { // go[$isActive] = true; // } return go.visible; } export function setActive(go: Object3D, active: boolean | number): boolean { if (typeof active === "number") active = active > .5; // go[$isActive] = active; go.visible = active; return go.visible;// go[$isActive]; } export function isActiveInHierarchy(go: Object3D): boolean { return go[activeInHierarchyFieldName] || isUsingInstancing(go); } export function markAsInstancedRendered(go: Object3D, instanced: boolean) { go[$isUsingInstancing] = instanced; } export function isUsingInstancing(instance: Object3D): boolean { return InstancingUtil.isUsingInstancing(instance); } export function findByGuid(guid: string, hierarchy: Object3D): GameObject | IComponent | null | undefined { return tryFindObject(guid, hierarchy, true, true); } const $isDestroyed = Symbol("isDestroyed"); export function isDestroyed(go: Object3D): boolean { return go[$isDestroyed]; } export function setDestroyed(go: Object3D, value: boolean) { go[$isDestroyed] = value; } const $isDontDestroy = Symbol("isDontDestroy"); /** Mark an Object3D or component as not destroyable * @param instance the object to be marked as not destroyable * @param value true if the object should not be destroyed in `destroy` */ export function setDontDestroy(instance: Object3D | Component, value: boolean = true) { instance[$isDontDestroy] = value; } const destroyed_components: Array<IComponent> = []; const destroyed_objects: Array<Object3D> = []; export function destroy(instance: Object3D | Component, recursive: boolean = true, dispose: boolean = false) { destroyed_components.length = 0; destroyed_objects.length = 0; internalDestroy(instance, recursive, true); for (const comp of destroyed_components) { comp.gameObject = null!; //@ts-ignore comp.context = null; } // dipose resources and remove references for (const obj of destroyed_objects) { setDestroyed(obj, true); if (dispose) { disposeObjectResources(obj); } // This needs to be called after disposing because it removes the references to resources __internalRemoveReferences(obj); } destroyed_objects.length = 0; destroyed_components.length = 0; } function internalDestroy(instance: Object3D | Component, recursive: boolean = true, isRoot: boolean = true) { if (instance === null || instance === undefined) return; const comp = instance as Component; if (comp.isComponent) { // Handle Component if (comp[$isDontDestroy]) return; destroyed_components.push(comp); const go = comp.gameObject; comp.__internalDisable(); comp.__internalDestroy(); comp.gameObject = go; return; } // handle Object3D if (instance[$isDontDestroy]) return; const obj = instance as GameObject; if (debug) console.log(obj); destroyed_objects.push(obj); // first disable and call onDestroy on components const components = obj.userData?.components; if (components != null && Array.isArray(components)) { let lastLength = components.length; for (let i = 0; i < components.length; i++) { const comp: Component = components[i]; internalDestroy(comp, recursive, false); // components will be removed from componentlist in destroy if (components.length < lastLength) { lastLength = components.length; i--; } } } // then continue in children of the passed in object if (recursive && obj.children) { for (const ch of obj.children) { internalDestroy(ch, recursive, false); } } if (isRoot) obj.removeFromParent(); } declare type ForEachComponentCallback = (comp: Component) => any; export function foreachComponent(instance: Object3D, cb: ForEachComponentCallback, recursive: boolean = true): any { return internalForEachComponent(instance, cb, recursive); } export function* foreachComponentEnumerator<T extends IComponent>(instance: Object3D, type?: Constructor<T>, includeChildren: boolean = false, maxLevel: number = 999, _currentLevel: number = 0): Generator<T> { if (!instance?.userData.components) return; if (_currentLevel > maxLevel) return; for (const comp of instance.userData.components) { if (type && comp?.isComponent === true && comp instanceof type) { yield comp; } else { yield comp; } } if (includeChildren === true) { for (const ch of instance.children) { yield* foreachComponentEnumerator(ch, type, true, maxLevel, _currentLevel + 1); } } } function internalForEachComponent(instance: Object3D, cb: ForEachComponentCallback, recursive: boolean, level: number = 0): any { if (!instance) return; if (!instance.isObject3D) { new Error("Expected Object3D but got " + instance); } if (level > 1000) { console.warn("Failed to iterate components: too many levels"); return; } if (instance.userData?.components) { for (let i = 0; i < instance.userData.components.length; i++) { const comp = instance.userData.components[i]; if (comp?.isComponent === true) { const res = cb(comp); if (res !== undefined) return res; } } } if (recursive && instance.children) { // childArrayBuffer.length = 0; // childArrayBuffer.push(...instance.children); const nextLevel = level + 1; for (let i = 0; i < instance.children.length; i++) { const child = instance.children[i]; if (!child) continue; const res = internalForEachComponent(child, cb, recursive, nextLevel); if (res !== undefined) return res; } // childArrayBuffer.length = 0; } } declare type ObjectCloneReference = { readonly original: object; readonly clone: object; } declare type InstantiateReferenceMap = Record<string, ObjectCloneReference>; /** * Provides access to the instantiated object and its clone */ export declare type InstantiateContext = Readonly<InstantiateReferenceMap>; export function instantiate(instance: AssetReference, opts?: IInstantiateOptions | null): Promise<Object3D | null> export function instantiate(instance: GameObject | Object3D, opts?: IInstantiateOptions | null): GameObject export function instantiate(instance: AssetReference | GameObject | Object3D, opts?: IInstantiateOptions | null | undefined): GameObject | Promise<Object3D | null> { if ("isAssetReference" in instance) { return instance.instantiate(opts ?? undefined); } let options: InstantiateOptions | null = null; if (opts !== null && opts !== undefined) { // if x is defined assume this is a vec3 - this is just to not break everything at once and stay a little bit backwards compatible if (opts["x"] !== undefined) { options = new InstantiateOptions(); options.position = opts as unknown as Vector3; } else { // if (opts instanceof InstantiateOptions) options = opts as InstantiateOptions; } } let context = Context.Current; if (options?.context) context = options.context; if (debug && context.alias) console.log("context", context.alias); // we need to create the id provider before calling internal instantiate because cloned gameobjects also create new guids if (options && !options.idProvider) { options.idProvider = new InstantiateIdProvider(Date.now()); } const components: Array<Component> = []; const goMapping: InstantiateReferenceMap = {}; // used to resolve references on components to components on other gameobjects to their new counterpart const skinnedMeshes: InstantiateReferenceMap = {}; // used to resolve skinned mesh bones const clone = internalInstantiate(context, instance, options, components, goMapping, skinnedMeshes); if (clone) { resolveReferences(goMapping); resolveAndBindSkinnedMeshBones(skinnedMeshes, goMapping); } if (debug) { logHierarchy(instance, true); logHierarchy(clone, true); } const guidsMap: GuidsMap = {}; if (options?.components !== false) { for (const i in components) { const copy = components[i]; const oldGuid = copy.guid; if (options && options.idProvider) { copy.guid = options.idProvider.generateUUID(); guidsMap[oldGuid] = copy.guid; if (debug) console.log(copy.name, copy.guid) } registerComponent(copy, context); if (copy.__internalNewInstanceCreated) copy.__internalNewInstanceCreated(); } for (const i in components) { const copy = components[i]; if (copy.resolveGuids) copy.resolveGuids(guidsMap); if (copy.enabled === false) continue; else copy.enabled = true; } processNewScripts(context); } return clone as GameObject; } function internalInstantiate( context: Context, instance: GameObject | Object3D, opts: IInstantiateOptions | InstantiateOptions | null, componentsList: Array<Component>, objectsMap: InstantiateReferenceMap, skinnedMeshesMap: InstantiateReferenceMap ) : GameObject | Object3D | null { if (!instance) return null; // Don't clone UI shadow objects if (instance[$shadowDomOwner]) { return null; } // prepare, remove things that dont work out of the box // e.g. user data we want to manually clone // also children throw errors (e.g. recursive toJson with nested meshes) const userData = instance.userData; instance.userData = {}; const children = instance.children; instance.children = []; const clone: Object3D | GameObject = instance.clone(false); apply(clone); // if(instance[$originalGuid]) // clone[$originalGuid] = instance[$originalGuid]; instance.userData = userData; instance.children = children; // make reference from old id to new object objectsMap[instance.uuid] = { original: instance, clone: clone }; if (debugInstantiate) console.log("ADD", instance, clone) if (instance.type === "SkinnedMesh") { skinnedMeshesMap[instance.uuid] = { original: instance, clone: clone }; } // DO NOT EVER RENAME BECAUSE IT BREAKS / MIGHT BREAK ANIMATIONS // clone.name += " (Clone)"; if (opts?.visible !== undefined) clone.visible = opts.visible; if (opts?.idProvider) { clone.uuid = opts.idProvider.generateUUID(); const cloneGo: GameObject = clone as GameObject; if (cloneGo) cloneGo.guid = clone.uuid; } if (instance.animations && instance.animations.length > 0) { clone.animations = [...instance.animations]; } const parent = instance.parent; if (parent) { parent.add(clone); } // apply transform if (opts?.position) { setWorldPosition(clone, opts.position); } else clone.position.copy(instance.position); if (opts?.rotation) { setWorldQuaternion(clone, opts.rotation); } else clone.quaternion.copy(instance.quaternion); if (opts?.scale) { clone.scale.copy(opts.scale); // TODO MAJOR: replace with worldscale // clone.worldScale = opts.scale; } else clone.scale.copy(instance.scale); if (opts?.parent && opts.parent !== "scene") { let requestedParent: Object3D | null = null; if (typeof opts.parent === "string") { requestedParent = tryFindObject(opts.parent, context.scene, true); } else { requestedParent = opts.parent; } if (requestedParent) { const func = opts.keepWorldPosition === true ? requestedParent.attach : requestedParent.add; if (!func) console.error("Invalid parent object", requestedParent, "received when instantiating:", instance); else func.call(requestedParent, clone); } else console.warn("could not find parent:", opts.parent); } for (const [key, value] of Object.entries(instance.userData)) { if (key === "components") continue; clone.userData[key] = value; } if (instance.userData?.components) { const components = instance.userData.components; const newComponents: Component[] = []; clone.userData.components = newComponents; for (let i = 0; i < components.length; i++) { const comp = components[i]; const copy = new comp.constructor(); assign(copy, comp, undefined, { // onAssign: (source, key, value) => { // if (typeof value === "object") { // const serializable = source as ISerializable; // if (serializable?.$serializedTypes?.[key]) { // console.debug("TODO CLONE", key, value); // } // } // return value; // } }); // make sure the original guid stays intact if (comp[editorGuidKeyName] !== undefined) copy[editorGuidKeyName] = comp[editorGuidKeyName]; newComponents.push(copy); copy.gameObject = clone; // copy.transform = clone; componentsList.push(copy); objectsMap[comp.guid] = { original: comp, clone: copy }; ComponentLifecycleEvents.dispatchComponentLifecycleEvent("component-added", copy); } } // children should just clone the original transform if (opts) { opts.position = undefined; opts.rotation = undefined; opts.scale = undefined; opts.parent = undefined; opts.visible = undefined; } for (const ch in instance.children) { const child = instance.children[ch]; const newChild = internalInstantiate(context, child as GameObject, opts, componentsList, objectsMap, skinnedMeshesMap); if (newChild) { objectsMap[newChild.uuid] = { original: child, clone: newChild }; clone.add(newChild); } } return clone; } function resolveAndBindSkinnedMeshBones( skinnedMeshes: { [key: string]: ObjectCloneReference }, newObjectsMap: { [key: string]: ObjectCloneReference } ) { for (const key in skinnedMeshes) { const val = skinnedMeshes[key]; const original = val.original as SkinnedMesh; const originalSkeleton = original.skeleton; const clone = val.clone as SkinnedMesh; // clone.updateWorldMatrix(true, true); if (!originalSkeleton) { console.warn("Skinned mesh has no skeleton?", val); continue; } const originalBones = originalSkeleton.bones; const clonedSkeleton = clone.skeleton.clone(); clone.skeleton = clonedSkeleton; clone.bindMatrix.clone().copy(original.bindMatrix); // console.log(clone.bindMatrix) clone.bindMatrixInverse.copy(original.bindMatrixInverse); // clone.bindMatrix.multiplyScalar(.025); // console.assert(originalSkeleton.uuid !== clonedSkeleton.uuid); // console.assert(originalBones.length === clonedSkeleton.bones.length); const bones: Array<Bone> = []; clonedSkeleton.bones = bones; for (let i = 0; i < originalBones.length; i++) { const bone = originalBones[i]; const newBoneInfo = newObjectsMap[bone.uuid]; const clonedBone = newBoneInfo.clone as Bone; // console.log("NEW BONE: ", clonedBone, "BEFORE", newBoneInfo.original); bones.push(clonedBone); } // clone.skeleton = new Skeleton(bones); // clone.skeleton.update(); // clone.pose(); // clone.scale.set(1,1,1); // clone.position.y += .1; // console.log("ORIG", original, "CLONE", clone); } for (const key in skinnedMeshes) { const clone = skinnedMeshes[key].clone as SkinnedMesh; clone.skeleton.update(); // clone.skeleton.calculateInverses(); clone.bind(clone.skeleton, clone.bindMatrix); clone.updateMatrixWorld(true); // clone.pose(); } } // private static bindNewSkinnedMeshBones(source, clone) { // const sourceLookup = new Map(); // const cloneLookup = new Map(); // // const clone = source.clone(false); // function parallelTraverse(a, b, callback) { // callback(a, b); // for (let i = 0; i < a.children.length; i++) { // parallelTraverse(a.children[i], b.children[i], callback); // } // } // parallelTraverse(source, clone, function (sourceNode, clonedNode) { // sourceLookup.set(clonedNode, sourceNode); // cloneLookup.set(sourceNode, clonedNode); // }); // clone.traverse(function (node) { // if (!node.isSkinnedMesh) return; // const clonedMesh = node; // const sourceMesh = sourceLookup.get(node); // const sourceBones = sourceMesh.skeleton.bones; // clonedMesh.skeleton = sourceMesh.skeleton.clone(); // clonedMesh.bindMatrix.copy(sourceMesh.bindMatrix); // clonedMesh.skeleton.bones = sourceBones.map(function (bone) { // return cloneLookup.get(bone); // }); // clonedMesh.bind(clonedMesh.skeleton, clonedMesh.bindMatrix); // }); // return clone; // } function resolveReferences(newObjectsMap: InstantiateReferenceMap) { // for every object that is newly created we want to update references to their newly created counterparts // e.g. a collider instance referencing a rigidbody instance should be updated so that // the cloned collider does not reference the cloned rigidbody (instead of the original rigidbody) for (const key in newObjectsMap) { const val = newObjectsMap[key]; const clone = val.clone as Object3D; // resolve references if (clone?.isObject3D && clone?.userData?.components) { for (let i = 0; i < clone.userData.components.length; i++) { const copy = clone.userData.components[i]; // find referenced within a cloned gameobject const entries = Object.entries(copy); // console.log(copy, entries); for (const [key, value] of entries) { if (Array.isArray(value)) { const clonedArray: Array<any> = []; copy[key] = clonedArray; // console.log(copy, key, value, copy[key]); for (let i = 0; i < value.length; i++) { const entry = value[i]; // push value types into new array if (typeof entry !== "object") { clonedArray.push(entry); continue; } const res: any = postProcessNewInstance(copy, key, entry, newObjectsMap); if (res !== undefined) { if (debugInstantiate) console.log("Found new instance for", key, entry, "->", res); clonedArray.push(res); } else { if (debugInstantiate) console.warn("Could not find new instance for", key, entry); clonedArray.push(entry); } } // console.log(copy[key]) } else if (typeof value === "object") { const res = postProcessNewInstance(copy, key, value as IComponent | Object3D, newObjectsMap); if (res !== undefined) { copy[key] = res; } else { if (debugInstantiate) console.warn("Could not find new instance for", key, value); } } } } } } } function postProcessNewInstance(copy: Object3D, key: string, value: IComponent | Object3D | any, newObjectsMap: InstantiateReferenceMap) { if (value === null || value === undefined) return; if ((value as IComponent).isComponent === true) { const originalGameObjectReference = value["gameObject"]; // console.log(key, value, originalGameObjectReference); if (originalGameObjectReference) { const id = originalGameObjectReference.uuid; const newGameObject = newObjectsMap[id]?.clone; if (!newGameObject) { // reference has not changed! if (debugInstantiate) console.log("reference did not change", key, copy, value); return; } const index = originalGameObjectReference.userData.components.indexOf(value); if (index >= 0 && (newGameObject as Object3D).isObject3D) { if (debugInstantiate) console.log(key, id); const found = (newGameObject as Object3D).userData.components[index]; return found; } else { console.warn("could not find component", key, value); } } } else if ((value as Object3D).isObject3D === true) { // console.log(value); if (key === "gameObject") return; const originalGameObjectReference = value as Object3D; if (originalGameObjectReference) { const id = originalGameObjectReference.uuid; const newGameObject = newObjectsMap[id]?.clone; if (newGameObject) { if (debugInstantiate) console.log(key, "old", value, "new", newGameObject); return newGameObject; } } } else { // create new instances for some types that we know should usually not be shared and can safely be cloned if (value.isVector4 || value.isVector3 || value.isVector2 || value.isQuaternion || value.isEuler) { return value.clone(); } else if (value.isColor === true) { return value.clone(); } else if ((value as IEventList).isEventList === true) { // create a new instance of the object const copy = (value as IEventList).__internalOnInstantiate(newObjectsMap); return copy; } } }