playcanvas
Version:
PlayCanvas WebGL game engine
257 lines (254 loc) • 10.2 kB
JavaScript
import { Debug } from '../../core/debug.js';
import { Quat } from '../../core/math/quat.js';
import { Vec3 } from '../../core/math/vec3.js';
/**
* @import { Animation } from './animation.js'
* @import { GraphNode } from '../graph-node.js'
*/ class InterpolatedKey {
getTarget() {
return this._targetNode;
}
setTarget(node) {
this._targetNode = node;
}
constructor(){
this._written = false;
this._name = '';
this._keyFrames = [];
// Result of interpolation
this._quat = new Quat();
this._pos = new Vec3();
this._scale = new Vec3();
// Optional destination for interpolated keyframe
this._targetNode = null;
}
}
/**
* Represents a skeleton used to play animations.
*
* @category Animation
*/ class Skeleton {
/**
* Sets the animation on the skeleton.
*
* @type {Animation}
*/ set animation(value) {
this._animation = value;
this.currentTime = 0;
}
/**
* Gets the animation on the skeleton.
*
* @type {Animation}
*/ get animation() {
return this._animation;
}
/**
* Sets the current time of the currently active animation in seconds. This value is between
* zero and the duration of the animation.
*
* @type {number}
*/ set currentTime(value) {
this._time = value;
var numNodes = this._interpolatedKeys.length;
for(var i = 0; i < numNodes; i++){
var node = this._interpolatedKeys[i];
var nodeName = node._name;
this._currKeyIndices[nodeName] = 0;
}
this.addTime(0);
this.updateGraph();
}
/**
* Gets the current time of the currently active animation in seconds.
*
* @type {number}
*/ get currentTime() {
return this._time;
}
/**
* Gets the number of nodes in the skeleton.
*
* @type {number}
*/ get numNodes() {
return this._interpolatedKeys.length;
}
/**
* Progresses the animation assigned to the specified skeleton by the supplied time delta. If
* the delta takes the animation passed its end point, if the skeleton is set to loop, the
* animation will continue from the beginning. Otherwise, the animation's current time will
* remain at its duration (i.e. the end).
*
* @param {number} delta - The time in seconds to progress the skeleton's animation.
*/ addTime(delta) {
if (this._animation !== null) {
var nodes = this._animation._nodes;
var duration = this._animation.duration;
// Check if we can early out
if (this._time === duration && !this.looping) {
return;
}
// Step the current time and work out if we need to jump ahead, clamp or wrap around
this._time += delta;
if (this._time > duration) {
this._time = this.looping ? 0.0 : duration;
for(var i = 0; i < nodes.length; i++){
var node = nodes[i];
var nodeName = node._name;
this._currKeyIndices[nodeName] = 0;
}
} else if (this._time < 0) {
this._time = this.looping ? duration : 0.0;
for(var i1 = 0; i1 < nodes.length; i1++){
var node1 = nodes[i1];
var nodeName1 = node1._name;
this._currKeyIndices[nodeName1] = node1._keys.length - 2;
}
}
// For each animated node...
// keys index offset
var offset = delta >= 0 ? 1 : -1;
for(var i2 = 0; i2 < nodes.length; i2++){
var node2 = nodes[i2];
var nodeName2 = node2._name;
var keys = node2._keys;
// Determine the interpolated keyframe for this animated node
var interpKey = this._interpolatedKeyDict[nodeName2];
if (interpKey === undefined) {
Debug.warn("Unknown skeleton node name: " + nodeName2);
continue;
}
// If there's only a single key, just copy the key to the interpolated key...
var foundKey = false;
if (keys.length !== 1) {
// Otherwise, find the keyframe pair for this node
for(var currKeyIndex = this._currKeyIndices[nodeName2]; currKeyIndex < keys.length - 1 && currKeyIndex >= 0; currKeyIndex += offset){
var k1 = keys[currKeyIndex];
var k2 = keys[currKeyIndex + 1];
if (k1.time <= this._time && k2.time >= this._time) {
var alpha = (this._time - k1.time) / (k2.time - k1.time);
interpKey._pos.lerp(k1.position, k2.position, alpha);
interpKey._quat.slerp(k1.rotation, k2.rotation, alpha);
interpKey._scale.lerp(k1.scale, k2.scale, alpha);
interpKey._written = true;
this._currKeyIndices[nodeName2] = currKeyIndex;
foundKey = true;
break;
}
}
}
if (keys.length === 1 || !foundKey && this._time === 0.0 && this.looping) {
interpKey._pos.copy(keys[0].position);
interpKey._quat.copy(keys[0].rotation);
interpKey._scale.copy(keys[0].scale);
interpKey._written = true;
}
}
}
}
/**
* Blends two skeletons together.
*
* @param {Skeleton} skel1 - Skeleton holding the first pose to be blended.
* @param {Skeleton} skel2 - Skeleton holding the second pose to be blended.
* @param {number} alpha - The value controlling the interpolation in relation to the two input
* skeletons. The value is in the range 0 to 1, 0 generating skel1, 1 generating skel2 and
* anything in between generating a spherical interpolation between the two.
*/ blend(skel1, skel2, alpha) {
var numNodes = this._interpolatedKeys.length;
for(var i = 0; i < numNodes; i++){
var key1 = skel1._interpolatedKeys[i];
var key2 = skel2._interpolatedKeys[i];
var dstKey = this._interpolatedKeys[i];
if (key1._written && key2._written) {
dstKey._quat.slerp(key1._quat, skel2._interpolatedKeys[i]._quat, alpha);
dstKey._pos.lerp(key1._pos, skel2._interpolatedKeys[i]._pos, alpha);
dstKey._scale.lerp(key1._scale, key2._scale, alpha);
dstKey._written = true;
} else if (key1._written) {
dstKey._quat.copy(key1._quat);
dstKey._pos.copy(key1._pos);
dstKey._scale.copy(key1._scale);
dstKey._written = true;
} else if (key2._written) {
dstKey._quat.copy(key2._quat);
dstKey._pos.copy(key2._pos);
dstKey._scale.copy(key2._scale);
dstKey._written = true;
}
}
}
/**
* Links a skeleton to a node hierarchy. The nodes animated skeleton are then subsequently used
* to drive the local transformation matrices of the node hierarchy.
*
* @param {GraphNode} graph - The root node of the graph that the skeleton is to drive.
*/ setGraph(graph) {
this.graph = graph;
if (graph) {
for(var i = 0; i < this._interpolatedKeys.length; i++){
var interpKey = this._interpolatedKeys[i];
var graphNode = graph.findByName(interpKey._name);
this._interpolatedKeys[i].setTarget(graphNode);
}
} else {
for(var i1 = 0; i1 < this._interpolatedKeys.length; i1++){
this._interpolatedKeys[i1].setTarget(null);
}
}
}
/**
* Synchronizes the currently linked node hierarchy with the current state of the skeleton.
* Internally, this function converts the interpolated keyframe at each node in the skeleton
* into the local transformation matrix at each corresponding node in the linked node
* hierarchy.
*/ updateGraph() {
if (this.graph) {
for(var i = 0; i < this._interpolatedKeys.length; i++){
var interpKey = this._interpolatedKeys[i];
if (interpKey._written) {
var transform = interpKey.getTarget();
transform.localPosition.copy(interpKey._pos);
transform.localRotation.copy(interpKey._quat);
transform.localScale.copy(interpKey._scale);
if (!transform._dirtyLocal) {
transform._dirtifyLocal();
}
interpKey._written = false;
}
}
}
}
/**
* Create a new Skeleton instance.
*
* @param {GraphNode} graph - The root {@link GraphNode} of the skeleton.
*/ constructor(graph){
/**
* Determines whether skeleton is looping its animation.
*
* @type {boolean}
*/ this.looping = true;
/**
* @type {Animation}
* @private
*/ this._animation = null;
this._time = 0;
this._interpolatedKeys = [];
this._interpolatedKeyDict = {};
this._currKeyIndices = {};
this.graph = null;
var addInterpolatedKeys = (node)=>{
var interpKey = new InterpolatedKey();
interpKey._name = node.name;
this._interpolatedKeys.push(interpKey);
this._interpolatedKeyDict[node.name] = interpKey;
this._currKeyIndices[node.name] = 0;
for(var i = 0; i < node._children.length; i++){
addInterpolatedKeys(node._children[i]);
}
};
addInterpolatedKeys(graph);
}
}
export { Skeleton };