@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
text/typescript
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;
}
}
}