@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.
187 lines • 8.29 kB
JavaScript
import { AnimationClip, KeyframeTrack, PropertyBinding } from "three";
import { isDevEnvironment } from "./debug/index.js";
import { TypeStore } from "./engine_typestore.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 {
context;
mixers = [];
constructor(context) {
this.context = context;
}
/** @hidden @internal */
onDestroy() {
this.mixers.forEach(mixer => mixer.stopAllAction());
this.mixers.length = 0;
}
/**
* Register an animation mixer instance.
*/
registerAnimationMixer(mixer) {
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) {
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 {
/**
* Tests if the root object of an AnimationAction can be animated. Objects where matrixAutoUpdate or matrixWorldAutoUpdate is set to false may not animate correctly.
* @param action The AnimationAction to test
* @param allowLog Whether to allow logging warnings. Default is false, which only allows logging in development environments.
* @returns True if the root object can be animated, false otherwise
*/
static testIfRootCanAnimate(action, allowLog) {
const root = action.getRoot();
if (root && (root.userData.static || root.matrixAutoUpdate === false || root.matrixWorldAutoUpdate === false)) {
if (allowLog === true || (allowLog === undefined && isDevEnvironment()))
console.warn(`AnimationUtils: The root object (${root.name || root.type}) of this AnimationAction has matrixAutoUpdate or matrixWorldAutoUpdate set to false. This may prevent the animation from working correctly. If the object is marked as static, try to change it to dynamic.`, { static: root.userData.static, name: root.userData.name, tag: root.userData.tag, matrixAutoUpdate: root.matrixAutoUpdate, matrixWorldAutoUpdate: root.matrixWorldAutoUpdate });
return false;
}
return true;
}
/**
* 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) {
const actions = mixer["_actions"];
if (!actions)
return null;
return actions;
}
static tryGetAnimationClipsFromObjectHierarchy(obj, target) {
if (!target)
target = new Array();
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 autoplayAnimations(file) {
if (!file || !file.animations) {
console.debug("No animations found in file");
return null;
}
const scene = "scene" in file ? file.scene : file;
const animationComponents = new Array();
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(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 = scene.getObjectByProperty('uuid', objectName);
if (!obj) {
// console.warn("could not find " + objectName, animation, gltf.scene);
continue;
}
}
let animationComponent = findAnimationComponentInParent(obj) || findAnimationComponentInParent(scene);
if (!animationComponent) {
const anim = TypeStore.get("Animation");
animationComponent = scene.addComponent(anim);
if (!animationComponent) {
console.error("Failed creating Animation component: No 'Animation' component found in TypeStore");
break;
}
}
animationComponents.push(animationComponent);
if (animationComponent.addClip) {
animationComponent.addClip(animation);
}
}
}
return animationComponents;
function findAnimationComponentInParent(obj) {
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];
if (component.isAnimationComponent === true) {
return component;
}
}
}
return findAnimationComponentInParent(obj.parent);
}
}
static emptyClip() {
return new AnimationClip("empty", 0, []);
}
static createScaleClip(options) {
const duration = options?.duration ?? 0.3;
let baseScale = { x: 1, y: 1, z: 1 };
if (options?.scale !== undefined) {
if (typeof options.scale === "number") {
baseScale = { x: options.scale, y: options.scale, z: options.scale };
}
else {
baseScale = options.scale;
}
}
const type = options?.type ?? "linear";
const scale = options?.scaleFactor ?? 1.2;
const times = new Array();
const values = new Array();
switch (type) {
case "linear":
times.push(0, duration);
values.push(baseScale.x, baseScale.y, baseScale.z, baseScale.x * scale, baseScale.y * scale, baseScale.z * scale);
break;
case "spring":
times.push(0, duration * 0.3, duration * 0.5, duration * 0.7, duration * 0.9, duration);
values.push(baseScale.x, baseScale.y, baseScale.z, baseScale.x * scale, baseScale.y * scale, baseScale.z * scale, baseScale.x * 0.9, baseScale.y * 0.9, baseScale.z * 0.9, baseScale.x * 1.05, baseScale.y * 1.05, baseScale.z * 1.05, baseScale.x * 0.98, baseScale.y * 0.98, baseScale.z * 0.98, baseScale.x, baseScale.y, baseScale.z);
break;
}
const track = new KeyframeTrack(".scale", times, values);
return new AnimationClip("scale", times[times.length - 1], [track]);
}
}
//# sourceMappingURL=engine_animation.js.map