@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.
146 lines (130 loc) • 5.78 kB
text/typescript
import { AnimationAction, AnimationClip, AnimationMixer, Object3D, PropertyBinding } from "three";
import type { Context } from "./engine_context.js";
import { GLTF, IAnimationComponent, Model } from "./engine_types.js";
/**
* Registry for animation related data. Use {@link registerAnimationMixer} to register an animation mixer instance.
* Can be accessed from {@link Context.animations} and is used internally e.g. when exporting GLTF files.
* @category Animation
*/
export class AnimationsRegistry {
readonly context: Context
readonly mixers: AnimationMixer[] = []
constructor(context: Context) {
this.context = context;
}
/** @hidden @internal */
onDestroy() {
this.mixers.forEach(mixer => mixer.stopAllAction());
this.mixers.length = 0;
}
/**
* Register an animation mixer instance.
*/
registerAnimationMixer(mixer: AnimationMixer): void {
if (!mixer) {
console.warn("AnimationsRegistry.registerAnimationMixer called with null or undefined mixer")
return;
}
if (this.mixers.includes(mixer)) return;
this.mixers.push(mixer);
}
/**
* Unregister an animation mixer instance.
*/
unregisterAnimationMixer(mixer: AnimationMixer | null | undefined): void {
if (!mixer) {
console.warn("AnimationsRegistry.unregisterAnimationMixer called with null or undefined mixer")
return;
}
const index = this.mixers.indexOf(mixer);
if (index === -1) return;
this.mixers.splice(index, 1);
}
}
/**
* Utility class for working with animations.
*/
export class AnimationUtils {
/**
* Tries to get the animation actions from an animation mixer.
* @param mixer The animation mixer to get the actions from
* @returns The actions or null if the mixer is invalid
*/
static tryGetActionsFromMixer(mixer: AnimationMixer): Array<AnimationAction> | null {
const actions = mixer["_actions"] as Array<AnimationAction>;
if (!actions) return null;
return actions;
}
static tryGetAnimationClipsFromObjectHierarchy(obj: Object3D, target?: Array<AnimationClip>): Array<AnimationClip> {
if (!target) target = new Array<AnimationClip>();
if (!obj) {
return target;
}
else if (obj.animations) {
target.push(...obj.animations);
}
if (obj.children) {
for (const child of obj.children) {
this.tryGetAnimationClipsFromObjectHierarchy(child, target);
}
}
return target;
}
/**
* Assigns animations from a GLTF file to the objects in the scene.
* This method will look for objects in the scene that have animations and assign them to the correct objects.
* @param file The GLTF file to assign the animations from
*/
static assignAnimationsFromFile(file: Pick<Model, "animations" | "scene">, opts?: { createAnimationComponent(obj: Object3D, animation: AnimationClip): IAnimationComponent }) {
if (!file || !file.animations) {
console.debug("No animations found in file");
return;
}
for (let i = 0; i < file.animations.length; i++) {
const animation = file.animations[i];
if (!animation.tracks || animation.tracks.length <= 0) {
console.warn("Animation has no tracks");
continue;
}
for (const t in animation.tracks) {
const track = animation.tracks[t];
const parsedPath = PropertyBinding.parseTrackName(track.name);
let obj = PropertyBinding.findNode(file.scene, parsedPath.nodeName);
if (!obj) {
const objectName = track["__objectName"] ?? track.name.substring(0, track.name.indexOf("."));
// let obj = gltf.scene.getObjectByName(objectName);
// this finds unnamed objects that still have tracks targeting them
obj = file.scene.getObjectByProperty('uuid', objectName);
if (!obj) {
// console.warn("could not find " + objectName, animation, gltf.scene);
continue;
}
}
let animationComponent = findAnimationGameObjectInParent(obj);
if (!animationComponent) {
if (!opts?.createAnimationComponent) {
console.warn("No AnimationComponent found in parent hierarchy of object and no 'createAnimationComponent' callback was provided in options.")
continue;
}
animationComponent = opts.createAnimationComponent(file.scene, animation)
}
if (animationComponent.addClip) {
animationComponent.addClip(animation);
}
}
}
function findAnimationGameObjectInParent(obj): IAnimationComponent | null {
if (!obj) return null;
const components = obj.userData?.components;
if (components && components.length > 0) {
for (let i = 0; i < components.length; i++) {
const component = components[i] as IAnimationComponent;
if (component.isAnimationComponent === true) {
return obj;
}
}
}
return findAnimationGameObjectInParent(obj.parent);
}
}
}