UNPKG

playcanvas

Version:

Open-source WebGL/WebGPU 3D engine for the web

593 lines (592 loc) 17.5 kB
var __defProp = Object.defineProperty; var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); import { Debug } from "../../../core/debug.js"; import { AnimClip } from "../../anim/evaluator/anim-clip.js"; import { AnimEvaluator } from "../../anim/evaluator/anim-evaluator.js"; import { AnimTrack } from "../../anim/evaluator/anim-track.js"; import { DefaultAnimBinder } from "../../anim/binder/default-anim-binder.js"; import { Skeleton } from "../../../scene/animation/skeleton.js"; import { Asset } from "../../asset/asset.js"; import { Component } from "../component.js"; class AnimationComponent extends Component { constructor() { super(...arguments); /** * @type {Object<string, Animation>} * @private */ __publicField(this, "_animations", {}); /** * @type {Array.<number|Asset>} * @private */ __publicField(this, "_assets", []); /** @private */ __publicField(this, "_loop", true); /** * @type {AnimEvaluator|null} * @ignore */ __publicField(this, "animEvaluator", null); /** * @type {Model|null} * @ignore */ __publicField(this, "model", null); /** * Get the skeleton for the current model. If the model is loaded from glTF/glb, then the * skeleton is null. * * @type {Skeleton|null} */ __publicField(this, "skeleton", null); /** * @type {Skeleton|null} * @ignore */ __publicField(this, "fromSkel", null); /** * @type {Skeleton|null} * @ignore */ __publicField(this, "toSkel", null); /** * @type {Object<string, string>} * @ignore */ __publicField(this, "animationsIndex", {}); /** * @type {string|null} * @private */ __publicField(this, "prevAnim", null); /** * @type {string|null} * @private */ __publicField(this, "currAnim", null); /** @private */ __publicField(this, "blend", 0); /** @private */ __publicField(this, "blending", false); /** @private */ __publicField(this, "blendSpeed", 0); /** * If true, the first animation asset will begin playing when the scene is loaded. */ __publicField(this, "activate", true); /** * Speed multiplier for animation play back. 1 is playback at normal speed and 0 pauses the * animation. */ __publicField(this, "speed", 1); } /** * Sets the dictionary of animations by name. * * @type {Object<string, Animation>} */ set animations(value) { this._animations = value; this.onSetAnimations(); } /** * Gets the dictionary of animations by name. * * @type {Object<string, Animation>} */ get animations() { return this._animations; } /** * Sets the array of animation assets or asset ids. * * @type {Array.<number|Asset>} */ set assets(value) { const assets = this._assets; if (assets && assets.length) { for (let i = 0; i < assets.length; i++) { if (assets[i]) { const asset = this.system.app.assets.get(assets[i]); if (asset) { asset.off("change", this.onAssetChanged, this); asset.off("remove", this.onAssetRemoved, this); const animName = this.animationsIndex[asset.id]; if (this.currAnim === animName) { this._stopCurrentAnimation(); } delete this.animations[animName]; delete this.animationsIndex[asset.id]; } } } } this._assets = value; const assetIds = value.map((value2) => { return value2 instanceof Asset ? value2.id : value2; }); this.loadAnimationAssets(assetIds); } /** * Gets the array of animation assets or asset ids. * * @type {Array.<number|Asset>} */ get assets() { return this._assets; } /** * Sets the current time position (in seconds) of the animation. * * @type {number} */ set currentTime(currentTime) { if (this.skeleton) { this.skeleton.currentTime = currentTime; this.skeleton.addTime(0); this.skeleton.updateGraph(); } if (this.animEvaluator) { const clips = this.animEvaluator.clips; for (let i = 0; i < clips.length; ++i) { clips[i].time = currentTime; } } } /** * Gets the current time position (in seconds) of the animation. * * @type {number} */ get currentTime() { if (this.skeleton) { return this.skeleton._time; } if (this.animEvaluator) { const clips = this.animEvaluator.clips; if (clips.length > 0) { return clips[clips.length - 1].time; } } return 0; } /** * Gets the duration in seconds of the current animation. Returns 0 if no animation is playing. * * @type {number} */ get duration() { if (this.currAnim) { return this.animations[this.currAnim].duration; } Debug.warn("No animation is playing to get a duration. Returning 0."); return 0; } /** * Sets whether the animation will restart from the beginning when it reaches the end. * * @type {boolean} */ set loop(value) { this._loop = value; if (this.skeleton) { this.skeleton.looping = value; } if (this.animEvaluator) { for (let i = 0; i < this.animEvaluator.clips.length; ++i) { this.animEvaluator.clips[i].loop = value; } } } /** * Gets whether the animation will restart from the beginning when it reaches the end. * * @type {boolean} */ get loop() { return this._loop; } /** * Start playing an animation. * * @param {string} name - The name of the animation asset to begin playing. * @param {number} [blendTime] - The time in seconds to blend from the current * animation state to the start of the animation being set. Defaults to 0. */ play(name, blendTime = 0) { if (!this.enabled || !this.entity.enabled) { return; } if (!this.animations[name]) { Debug.error(`Trying to play animation '${name}' which doesn't exist`); return; } this.prevAnim = this.currAnim; this.currAnim = name; if (this.model) { if (!this.skeleton && !this.animEvaluator) { this._createAnimationController(); } const prevAnim = this.animations[this.prevAnim]; const currAnim = this.animations[this.currAnim]; this.blending = blendTime > 0 && !!this.prevAnim; if (this.blending) { this.blend = 0; this.blendSpeed = 1 / blendTime; } if (this.skeleton) { if (this.blending) { this.fromSkel.animation = prevAnim; this.fromSkel.addTime(this.skeleton._time); this.toSkel.animation = currAnim; } else { this.skeleton.animation = currAnim; } } if (this.animEvaluator) { const animEvaluator = this.animEvaluator; if (this.blending) { while (animEvaluator.clips.length > 1) { animEvaluator.removeClip(0); } } else { this.animEvaluator.removeClips(); } const clip = new AnimClip(this.animations[this.currAnim], 0, 1, true, this.loop); clip.name = this.currAnim; clip.blendWeight = this.blending ? 0 : 1; clip.reset(); this.animEvaluator.addClip(clip); } } this.playing = true; } /** * Return an animation. * * @param {string} name - The name of the animation asset. * @returns {Animation} An Animation. */ getAnimation(name) { return this.animations[name]; } /** * Set the model driven by this animation component. * * @param {Model} model - The model to set. * @ignore */ setModel(model) { if (model !== this.model) { this._resetAnimationController(); this.model = model; if (this.animations && this.currAnim && this.animations[this.currAnim]) { this.play(this.currAnim); } } } onSetAnimations() { const modelComponent = this.entity.model; if (modelComponent) { const m = modelComponent.model; if (m && m !== this.model) { this.setModel(m); } } if (!this.currAnim && this.activate && this.enabled && this.entity.enabled) { const animationNames = Object.keys(this._animations); if (animationNames.length > 0) { this.play(animationNames[0]); } } } /** @private */ _resetAnimationController() { this.skeleton = null; this.fromSkel = null; this.toSkel = null; this.animEvaluator = null; } /** @private */ _createAnimationController() { const model = this.model; const animations = this.animations; let hasJson = false; let hasGlb = false; for (const animation in animations) { if (animations.hasOwnProperty(animation)) { const anim = animations[animation]; if (anim.constructor === AnimTrack) { hasGlb = true; } else { hasJson = true; } } } const graph = model.getGraph(); if (hasJson) { this.fromSkel = new Skeleton(graph); this.toSkel = new Skeleton(graph); this.skeleton = new Skeleton(graph); this.skeleton.looping = this.loop; this.skeleton.setGraph(graph); } else if (hasGlb) { this.animEvaluator = new AnimEvaluator(new DefaultAnimBinder(this.entity)); } } /** * @param {number[]} ids - Array of animation asset ids. * @private */ loadAnimationAssets(ids) { if (!ids || !ids.length) { return; } const assets = this.system.app.assets; const onAssetReady = (asset) => { if (asset.resources.length > 1) { for (let i = 0; i < asset.resources.length; i++) { this.animations[asset.resources[i].name] = asset.resources[i]; this.animationsIndex[asset.id] = asset.resources[i].name; } } else { this.animations[asset.name] = asset.resource; this.animationsIndex[asset.id] = asset.name; } this.animations = this.animations; }; const onAssetAdd = (asset) => { asset.off("change", this.onAssetChanged, this); asset.on("change", this.onAssetChanged, this); asset.off("remove", this.onAssetRemoved, this); asset.on("remove", this.onAssetRemoved, this); if (asset.resource) { onAssetReady(asset); } else { asset.once("load", onAssetReady, this); if (this.enabled && this.entity.enabled) { assets.load(asset); } } }; for (let i = 0, l = ids.length; i < l; i++) { const asset = assets.get(ids[i]); if (asset) { onAssetAdd(asset); } else { assets.on(`add:${ids[i]}`, onAssetAdd); } } } /** * Handle asset change events. * * @param {Asset} asset - The asset that changed. * @param {string} attribute - The name of the asset attribute that changed. Can be 'data', * 'file', 'resource' or 'resources'. * @param {*} newValue - The new value of the specified asset property. * @param {*} oldValue - The old value of the specified asset property. * @private */ onAssetChanged(asset, attribute, newValue, oldValue) { if (attribute === "resource" || attribute === "resources") { if (attribute === "resources" && newValue && newValue.length === 0) { newValue = null; } if (newValue) { let restarted = false; if (newValue.length > 1) { if (oldValue && oldValue.length > 1) { for (let i = 0; i < oldValue.length; i++) { delete this.animations[oldValue[i].name]; } } else { delete this.animations[asset.name]; } restarted = false; for (let i = 0; i < newValue.length; i++) { this.animations[newValue[i].name] = newValue[i]; if (!restarted && this.currAnim === newValue[i].name) { if (this.playing && this.enabled && this.entity.enabled) { restarted = true; this.play(newValue[i].name); } } } if (!restarted) { this._stopCurrentAnimation(); this.onSetAnimations(); } } else { if (oldValue && oldValue.length > 1) { for (let i = 0; i < oldValue.length; i++) { delete this.animations[oldValue[i].name]; } } this.animations[asset.name] = newValue[0] || newValue; restarted = false; if (this.currAnim === asset.name) { if (this.playing && this.enabled && this.entity.enabled) { restarted = true; this.play(asset.name); } } if (!restarted) { this._stopCurrentAnimation(); this.onSetAnimations(); } } this.animationsIndex[asset.id] = asset.name; } else { if (oldValue.length > 1) { for (let i = 0; i < oldValue.length; i++) { delete this.animations[oldValue[i].name]; if (this.currAnim === oldValue[i].name) { this._stopCurrentAnimation(); } } } else { delete this.animations[asset.name]; if (this.currAnim === asset.name) { this._stopCurrentAnimation(); } } delete this.animationsIndex[asset.id]; } } } /** * @param {Asset} asset - The asset that was removed. * @private */ onAssetRemoved(asset) { asset.off("remove", this.onAssetRemoved, this); if (this.animations) { if (asset.resources.length > 1) { for (let i = 0; i < asset.resources.length; i++) { delete this.animations[asset.resources[i].name]; if (this.currAnim === asset.resources[i].name) { this._stopCurrentAnimation(); } } } else { delete this.animations[asset.name]; if (this.currAnim === asset.name) { this._stopCurrentAnimation(); } } delete this.animationsIndex[asset.id]; } } /** @private */ _stopCurrentAnimation() { this.currAnim = null; this.playing = false; if (this.skeleton) { this.skeleton.currentTime = 0; this.skeleton.animation = null; } if (this.animEvaluator) { for (let i = 0; i < this.animEvaluator.clips.length; ++i) { this.animEvaluator.clips[i].stop(); } this.animEvaluator.update(0); this.animEvaluator.removeClips(); } } onEnable() { super.onEnable(); const assets = this.assets; const registry = this.system.app.assets; if (assets) { for (let i = 0, len = assets.length; i < len; i++) { let asset = assets[i]; if (!(asset instanceof Asset)) { asset = registry.get(asset); } if (asset && !asset.resource) { registry.load(asset); } } } if (this.activate && !this.currAnim) { const animationNames = Object.keys(this.animations); if (animationNames.length > 0) { this.play(animationNames[0]); } } } onBeforeRemove() { for (let i = 0; i < this.assets.length; i++) { let asset = this.assets[i]; if (typeof asset === "number") { asset = this.system.app.assets.get(asset); } if (!asset) continue; asset.off("change", this.onAssetChanged, this); asset.off("remove", this.onAssetRemoved, this); } this.skeleton = null; this.fromSkel = null; this.toSkel = null; this.animEvaluator = null; } /** * Update the state of the component. * * @param {number} dt - The time delta. * @ignore */ update(dt) { if (this.blending) { this.blend += dt * this.blendSpeed; if (this.blend >= 1) { this.blend = 1; } } if (this.playing) { const skeleton = this.skeleton; if (skeleton !== null && this.model !== null) { if (this.blending) { skeleton.blend(this.fromSkel, this.toSkel, this.blend); } else { const delta = dt * this.speed; skeleton.addTime(delta); if (this.speed > 0 && skeleton._time === skeleton.animation.duration && !this.loop) { this.playing = false; } else if (this.speed < 0 && skeleton._time === 0 && !this.loop) { this.playing = false; } } if (this.blending && this.blend === 1) { skeleton.animation = this.toSkel.animation; } skeleton.updateGraph(); } } const animEvaluator = this.animEvaluator; if (animEvaluator) { for (let i = 0; i < animEvaluator.clips.length; ++i) { const clip = animEvaluator.clips[i]; clip.speed = this.speed; if (!this.playing) { clip.pause(); } else { clip.resume(); } } if (this.blending && animEvaluator.clips.length > 1) { animEvaluator.clips[1].blendWeight = this.blend; } animEvaluator.update(dt); } if (this.blending && this.blend === 1) { this.blending = false; } } } export { AnimationComponent };