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.

187 lines • 8.29 kB
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