UNPKG

playcanvas

Version:

PlayCanvas WebGL game engine

255 lines (252 loc) 9.71 kB
import { AnimTargetValue } from './anim-target-value.js'; import { AnimBlend } from './anim-blend.js'; /** * @import { AnimBinder } from '../binder/anim-binder.js' * @import { AnimClip } from './anim-clip.js' */ /** * AnimEvaluator blends multiple sets of animation clips together. * * @ignore */ class AnimEvaluator { /** * Create a new animation evaluator. * * @param {AnimBinder} binder - Interface that resolves curve paths to instances of * {@link AnimTarget}. */ constructor(binder){ this._binder = binder; this._clips = []; this._inputs = []; this._outputs = []; this._targets = {}; } /** * The list of animation clips. * * @type {AnimClip[]} */ get clips() { return this._clips; } /** * Add a clip to the evaluator. * * @param {AnimClip} clip - The clip to add to the evaluator. */ addClip(clip) { const targets = this._targets; const binder = this._binder; // store list of input/output arrays const curves = clip.track.curves; const snapshot = clip.snapshot; const inputs = []; const outputs = []; for(let i = 0; i < curves.length; ++i){ const curve = curves[i]; const paths = curve.paths; for(let j = 0; j < paths.length; ++j){ const path = paths[j]; const resolved = binder.resolve(path); let target = targets[resolved && resolved.targetPath || null]; // create new target if it doesn't exist yet if (!target && resolved) { target = { target: resolved, value: [], curves: 0, blendCounter: 0 // per-frame number of blends (used to identify first blend) }; for(let k = 0; k < target.target.components; ++k){ target.value.push(0); } targets[resolved.targetPath] = target; if (binder.animComponent) { if (!binder.animComponent.targets[resolved.targetPath]) { let type; if (resolved.targetPath.substring(resolved.targetPath.length - 13) === 'localRotation') { type = AnimTargetValue.TYPE_QUAT; } else { type = AnimTargetValue.TYPE_VEC3; } binder.animComponent.targets[resolved.targetPath] = new AnimTargetValue(binder.animComponent, type); } binder.animComponent.targets[resolved.targetPath].layerCounter++; binder.animComponent.targets[resolved.targetPath].setMask(binder.layerIndex, 1); } } // binding may have failed // TODO: it may be worth storing quaternions and vector targets in separate // lists. this way the update code won't be forced to check target type before // setting/blending each target. if (target) { target.curves++; inputs.push(snapshot._results[i]); outputs.push(target); } } } this._clips.push(clip); this._inputs.push(inputs); this._outputs.push(outputs); } /** * Remove a clip from the evaluator. * * @param {number} index - Index of the clip to remove. */ removeClip(index) { const targets = this._targets; const binder = this._binder; const clips = this._clips; const clip = clips[index]; const curves = clip.track.curves; for(let i = 0; i < curves.length; ++i){ const curve = curves[i]; const paths = curve.paths; for(let j = 0; j < paths.length; ++j){ const path = paths[j]; const target = this._binder.resolve(path); if (target) { target.curves--; if (target.curves === 0) { binder.unresolve(path); delete targets[target.targetPath]; if (binder.animComponent) { binder.animComponent.targets[target.targetPath].layerCounter--; } } } } } clips.splice(index, 1); this._inputs.splice(index, 1); this._outputs.splice(index, 1); } /** * Remove all clips from the evaluator. */ removeClips() { while(this._clips.length > 0){ this.removeClip(0); } } updateClipTrack(name, animTrack) { this._clips.forEach((clip)=>{ if (clip.name.includes(name)) { clip.track = animTrack; } }); this.rebind(); } /** * Returns the first clip which matches the given name, or null if no such clip was found. * * @param {string} name - Name of the clip to find. * @returns {AnimClip|null} - The clip with the given name or null if no such clip was found. */ findClip(name) { const clips = this._clips; for(let i = 0; i < clips.length; ++i){ const clip = clips[i]; if (clip.name === name) { return clip; } } return null; } rebind() { this._binder.rebind(); this._targets = {}; const clips = [ ...this.clips ]; this.removeClips(); clips.forEach((clip)=>{ this.addClip(clip); }); } assignMask(mask) { return this._binder.assignMask(mask); } /** * Evaluator frame update function. All the attached {@link AnimClip}s are evaluated, blended * and the results set on the {@link AnimTarget}. * * @param {number} deltaTime - The amount of time that has passed since the last update, in * seconds. * @param {boolean} [outputAnimation] - Whether the evaluator should output the results of the * update to the bound animation targets. */ update(deltaTime, outputAnimation = true) { // copy clips const clips = this._clips; // stable sort order const order = clips.map((c, i)=>{ return i; }); AnimBlend.stableSort(order, (a, b)=>{ return clips[a].blendOrder < clips[b].blendOrder; }); for(let i = 0; i < order.length; ++i){ const index = order[i]; const clip = clips[index]; const inputs = this._inputs[index]; const outputs = this._outputs[index]; const blendWeight = clip.blendWeight; // update clip if (blendWeight > 0.0) { clip._update(deltaTime); } if (!outputAnimation) break; let input; let output; let value; if (blendWeight >= 1.0) { for(let j = 0; j < inputs.length; ++j){ input = inputs[j]; output = outputs[j]; value = output.value; AnimBlend.set(value, input, output.target.type); output.blendCounter++; } } else if (blendWeight > 0.0) { for(let j = 0; j < inputs.length; ++j){ input = inputs[j]; output = outputs[j]; value = output.value; if (output.blendCounter === 0) { AnimBlend.set(value, input, output.target.type); } else { AnimBlend.blend(value, input, blendWeight, output.target.type); } output.blendCounter++; } } } // apply result to anim targets const targets = this._targets; const binder = this._binder; for(const path in targets){ if (targets.hasOwnProperty(path)) { const target = targets[path]; // if this evaluator is associated with an anim component then we should blend the result of this evaluator with all other anim layer's evaluators if (binder.animComponent && target.target.usesLayerBlending) { const animTarget = binder.animComponent.targets[path]; if (animTarget.counter === animTarget.layerCounter) { animTarget.counter = 0; } if (!animTarget.path) { animTarget.path = path; animTarget.baseValue = target.target.get(); animTarget.setter = target.target.set; } // Add this layer's value onto the target value animTarget.updateValue(binder.layerIndex, target.value); animTarget.counter++; } else { target.target.set(target.value); } target.blendCounter = 0; } } // give the binder an opportunity to update itself // TODO: is this even necessary? binder could know when to update // itself without our help. this._binder.update(deltaTime); } } export { AnimEvaluator };