skeletal-animation-system
Version:
A standalone, stateless, dual quaternion based skeletal animation system built with interactive applications in mind
171 lines (156 loc) • 8.11 kB
JavaScript
var blendDualQuaternions = require('./blend-dual-quaternions.js')
module.exports = {
interpolateJoints: interpolateJoints
}
// TODO: Finishing adding comments
// TODO: Benchmark performance and optimize
function interpolateJoints (opts) {
// Get the amount of time that the current animation has been running.
// We use this when interpolating the current animation. Depending on
// how long the animation has been running we'll sample from different
// keyframe times
var currentAnimElapsedTime = opts.currentTime - opts.currentAnimation.startTime
// Sort all of our animations keyframe times numerically so that
// they're next to each other when we're sampling them.
// ex: {1: [...], '6.5': [...], 2: [...]} becomes [1, 2, 6.5]
// All keyframe times are in seconds
var currentKeyframeTimes = Object.keys(opts.currentAnimation.keyframes)
.sort(function (a, b) {
// NOTE: This breaks if you have the same keyframe twice. But you
// really shouldn't have the same keyframe twice. In the future
// we might have a separate package for linting your model
return Number(a) > Number(b) ? 1 : -1
})
var previousKeyframeTimes
if (opts.previousAnimation) {
previousKeyframeTimes = Object.keys(opts.previousAnimation.keyframes)
.sort(function (a, b) {
return Number(a) > Number(b) ? 1 : -1
})
}
// Get the current animation's time relative to the first possible time.
// For example, if our keyframe times are [1, 2, 6.5] and the current animation's
// elapsed time is `3`, then our time relative to our first time is `1 + 3` or `4`
var timeRelativeToFirst = Number(currentKeyframeTimes[0]) + Number(currentAnimElapsedTime)
// Our duration is the number of seconds from the first keyframe time to the last
// in our current animation. So for a current animation of [1, 2, 6.5] our duration is 4.5
var duration = currentKeyframeTimes[currentKeyframeTimes.length - 1] - currentKeyframeTimes[0]
if (currentAnimElapsedTime > duration) {
// If we are NOT LOOPING then we set our upper bound of elapsed time to the duration of the animation
if (opts.currentAnimation.noLoop) {
currentAnimElapsedTime = Math.min(currentAnimElapsedTime, duration)
} else {
// If we ARE LOOPING then we use the modulus of the animation duration to
// always re-start from the beginning
currentAnimElapsedTime = currentAnimElapsedTime % duration
}
timeRelativeToFirst = currentAnimElapsedTime + Number(currentKeyframeTimes[0])
}
var currentAnimLowerKeyframe
var currentAnimUpperKeyframe
if (currentKeyframeTimes.length === 1) {
currentAnimLowerKeyframe = currentAnimUpperKeyframe = currentKeyframeTimes[0]
} else {
// Get the surrounding keyframes for our current animation
currentKeyframeTimes.forEach(function (keyframeTime) {
if (currentAnimLowerKeyframe && currentAnimUpperKeyframe) { return }
if (timeRelativeToFirst > keyframeTime) {
currentAnimLowerKeyframe = keyframeTime
} else if (timeRelativeToFirst < keyframeTime) {
currentAnimUpperKeyframe = keyframeTime
} else if (timeRelativeToFirst === Number(keyframeTime)) {
// TODO: Perform fewer calculations in places that we already know
// that the keyframe time doesn't need to be blended against an upper
// and lower keyframe. For now we don't handle this special case
currentAnimLowerKeyframe = currentAnimUpperKeyframe = keyframeTime
}
})
}
// Set the elapsed time relative to our current lower bound keyframe instead of our lowest out of all keyframes
currentAnimElapsedTime = timeRelativeToFirst - currentAnimLowerKeyframe
var previousAnimLowerKeyframe
var previousAnimUpperKeyframe
var prevAnimElapsedTime
if (opts.previousAnimation) {
var previousKeyframeData = require('./get-previous-animation-data.js')(opts, previousKeyframeTimes)
previousAnimLowerKeyframe = previousKeyframeData.lower
previousAnimUpperKeyframe = previousKeyframeData.upper
prevAnimElapsedTime = previousKeyframeData.elapsedTime
}
// Calculate the interpolated joint matrices for our consumer's animation
// TODO: acc is a bad variable name. Renaame it
var interpolatedJoints = opts.jointNums.reduce(function (acc, jointName) {
// If there is a previous animation
var blend = (opts.blendFunction || defaultBlend)(opts.currentTime - opts.currentAnimation.startTime)
if (opts.previousAnimation && blend < 1) {
var previousAnimJointDualQuat
var currentAnimJointDualQuat
if (previousAnimLowerKeyframe === previousAnimUpperKeyframe) {
// If our current frame happens to be one of our defined keyframes we use the existing frame
previousAnimJointDualQuat = opts.previousAnimation.keyframes[previousAnimLowerKeyframe][jointName]
} else {
// Blend the dual quaternions for our previous animation that we are about to blend out
previousAnimJointDualQuat = blendDualQuaternions(
[],
opts.previousAnimation.keyframes[previousAnimLowerKeyframe][jointName],
opts.previousAnimation.keyframes[previousAnimUpperKeyframe][jointName],
prevAnimElapsedTime / (previousAnimUpperKeyframe - previousAnimLowerKeyframe)
)
}
if (currentAnimLowerKeyframe === currentAnimUpperKeyframe) {
// If our current frame happens to be one of our defined keyframes we use the existing frame
currentAnimJointDualQuat = opts.currentAnimation.keyframes[currentAnimLowerKeyframe][jointName]
} else {
currentAnimJointDualQuat = blendDualQuaternions(
[],
opts.currentAnimation.keyframes[currentAnimLowerKeyframe][jointName],
opts.currentAnimation.keyframes[currentAnimUpperKeyframe][jointName],
currentAnimElapsedTime / (currentAnimUpperKeyframe - currentAnimLowerKeyframe)
)
}
acc[jointName] = blendDualQuaternions([], previousAnimJointDualQuat, currentAnimJointDualQuat, blend)
} else {
// If we are on an exact, pre-defined keyframe there is no need to blend
if (currentAnimUpperKeyframe === currentAnimLowerKeyframe) {
acc[jointName] = opts.currentAnimation.keyframes[currentAnimLowerKeyframe][jointName]
} else {
// Blend the two dual quaternions based on where we are in the current keyframe
acc[jointName] = blendDualQuaternions(
[],
// The defined keyframe right below our current frame
opts.currentAnimation.keyframes[currentAnimLowerKeyframe][jointName],
// The defined keyframe right above our current frame
opts.currentAnimation.keyframes[currentAnimUpperKeyframe][jointName],
currentAnimElapsedTime / (currentAnimUpperKeyframe - currentAnimLowerKeyframe)
)
}
}
return acc
}, {})
// Calculate the keyframe number of our upper and lower keyframe
// TODO: Handle this while we do other stuff so we don't need to loop through again
// this is a minor perf optimization that we can implement when we benchmark
var currentAnimLowerKeyframeNumber
var currentAnimUpperKeyframeNumber
currentKeyframeTimes.forEach(function (keyTime, keyframeNumber) {
currentAnimLowerKeyframeNumber = currentAnimLowerKeyframe === keyTime ? keyframeNumber : currentAnimLowerKeyframeNumber
currentAnimUpperKeyframeNumber = currentAnimUpperKeyframe === keyTime ? keyframeNumber : currentAnimUpperKeyframeNumber
})
// Return the freshly interpolated dual quaternions for each of the joints that were passed in
return {
joints: interpolatedJoints,
currentAnimationInfo: {
lowerKeyframeNumber: currentAnimLowerKeyframeNumber,
upperKeyframeNumber: currentAnimUpperKeyframeNumber
}
}
}
// Give then number of seconds elapsed between the previous animation
// and the current animation we return a blend factor between
// zero and one
function defaultBlend (dt) {
// If zero time has elapsed we avoid dividing by 0
if (!dt) { return 0 }
// Blender linearly over 0.5s
return 2 * dt
}