overte/script-archive/libraries/walkApi.js
2016-04-26 11:18:22 -07:00

927 lines
46 KiB
JavaScript

//
// walkApi.js
// version 1.3
//
// Created by David Wooldridge, June 2015
// Copyright © 2014 - 2015 High Fidelity, Inc.
//
// Exposes API for use by walk.js version 1.2+.
//
// Editing tools for animation data files available here: https://github.com/DaveDubUK/walkTools
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
// included here to ensure walkApi.js can be used as an API, separate from walk.js
Script.include("walkConstants.js");
Avatar = function() {
// if Hydras are connected, the only way to enable use is to never set any arm joint rotation
this.hydraCheck = function () {
return Controller.Hardware.Hydra !== undefined;
}
// settings
this.headFree = true;
this.armsFree = this.hydraCheck(); // automatically sets true to enable Hydra support - temporary fix
this.makesFootStepSounds = false;
this.blenderPreRotations = false; // temporary fix
this.animationSet = undefined; // currently just one animation set
this.setAnimationSet = function(animationSet) {
this.animationSet = animationSet;
switch (animationSet) {
case 'standardMale':
this.selectedIdle = walkAssets.getAnimationDataFile("MaleIdle");
this.selectedWalk = walkAssets.getAnimationDataFile("MaleWalk");
this.selectedWalkBackwards = walkAssets.getAnimationDataFile("MaleWalkBackwards");
this.selectedSideStepLeft = walkAssets.getAnimationDataFile("MaleSideStepLeft");
this.selectedSideStepRight = walkAssets.getAnimationDataFile("MaleSideStepRight");
this.selectedWalkBlend = walkAssets.getAnimationDataFile("WalkBlend");
this.selectedHover = walkAssets.getAnimationDataFile("MaleHover");
this.selectedFly = walkAssets.getAnimationDataFile("MaleFly");
this.selectedFlyBackwards = walkAssets.getAnimationDataFile("MaleFlyBackwards");
this.selectedFlyDown = walkAssets.getAnimationDataFile("MaleFlyDown");
this.selectedFlyUp = walkAssets.getAnimationDataFile("MaleFlyUp");
this.selectedFlyBlend = walkAssets.getAnimationDataFile("FlyBlend");
this.currentAnimation = this.selectedIdle;
return;
}
}
this.setAnimationSet('standardMale');
// calibration
this.calibration = {
hipsToFeet: 1,
strideLength: this.selectedWalk.calibration.strideLength
}
this.distanceFromSurface = 0;
this.calibrate = function() {
// Triple check: measurements are taken three times to ensure accuracy - the first result is often too large
const MAX_ATTEMPTS = 3;
var attempts = MAX_ATTEMPTS;
var extraAttempts = 0;
do {
for (joint in walkAssets.animationReference.joints) {
var IKChain = walkAssets.animationReference.joints[joint].IKChain;
// only need to zero right leg IK chain and hips
if (IKChain === "RightLeg" || joint === "Hips" ) {
MyAvatar.setJointRotation(joint, Quat.fromPitchYawRollDegrees(0, 0, 0));
}
}
this.calibration.hipsToFeet = MyAvatar.getJointPosition("Hips").y - MyAvatar.getJointPosition("RightToeBase").y;
// maybe measuring before Blender pre-rotations have been applied?
if (this.calibration.hipsToFeet < 0 && this.blenderPreRotations) {
this.calibration.hipsToFeet *= -1;
}
if (this.calibration.hipsToFeet === 0 && extraAttempts < 100) {
attempts++;
extraAttempts++;// Interface can sometimes report zero for hips to feet. if so, we try again.
}
} while (attempts-- > 1)
// just in case
if (this.calibration.hipsToFeet <= 0 || isNaN(this.calibration.hipsToFeet)) {
this.calibration.hipsToFeet = 1;
print('walk.js error: Unable to get a non-zero measurement for the avatar hips to feet measure. Hips to feet set to default value ('+
this.calibration.hipsToFeet.toFixed(3)+'m). This will cause some foot sliding. If your avatar has only just appeared, it is recommended that you re-load the walk script.');
} else {
print('walk.js info: Hips to feet calibrated to '+this.calibration.hipsToFeet.toFixed(3)+'m');
}
}
// pose the fingers
this.poseFingers = function() {
for (knuckle in walkAssets.animationReference.leftHand) {
if (walkAssets.animationReference.leftHand[knuckle].IKChain === "LeftHandThumb") {
MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(0, 0, -4));
} else {
MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(16, 0, 5));
}
}
for (knuckle in walkAssets.animationReference.rightHand) {
if (walkAssets.animationReference.rightHand[knuckle].IKChain === "RightHandThumb") {
MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(0, 0, 4));
} else {
MyAvatar.setJointRotation(knuckle, Quat.fromPitchYawRollDegrees(16, 0, -5));
}
}
};
this.calibrate();
this.poseFingers();
// footsteps
this.nextStep = RIGHT; // the first step is right, because the waveforms say so
this.leftAudioInjector = null;
this.rightAudioInjector = null;
this.makeFootStepSound = function() {
// correlate footstep volume with avatar speed. place the audio source at the feet, not the hips
const SPEED_THRESHOLD = 0.4;
const VOLUME_ATTENUATION = 0.8;
const MIN_VOLUME = 0.5;
var volume = Vec3.length(motion.velocity) > SPEED_THRESHOLD ?
VOLUME_ATTENUATION * Vec3.length(motion.velocity) / MAX_WALK_SPEED : MIN_VOLUME;
volume = volume > 1 ? 1 : volume; // occurs when landing at speed - can walk faster than max walking speed
var options = {
position: Vec3.sum(MyAvatar.position, {x:0, y: -this.calibration.hipsToFeet, z:0}),
volume: volume
};
if (this.nextStep === RIGHT) {
if (this.rightAudioInjector === null) {
this.rightAudioInjector = Audio.playSound(walkAssets.footsteps[0], options);
} else {
this.rightAudioInjector.setOptions(options);
this.rightAudioInjector.restart();
}
this.nextStep = LEFT;
} else if (this.nextStep === LEFT) {
if (this.leftAudioInjector === null) {
this.leftAudioInjector = Audio.playSound(walkAssets.footsteps[1], options);
} else {
this.leftAudioInjector.setOptions(options);
this.leftAudioInjector.restart();
}
this.nextStep = RIGHT;
}
}
};
// constructor for the Motion object
Motion = function() {
this.isLive = true;
// locomotion status
this.state = STATIC;
this.nextState = STATIC;
this.isMoving = false;
this.isWalkingSpeed = false;
this.isFlyingSpeed = false;
this.isAccelerating = false;
this.isDecelerating = false;
this.isDeceleratingFast = false;
this.isComingToHalt = false;
this.directedAcceleration = 0;
// used to make sure at least one step has been taken when transitioning from a walk cycle
this.elapsedFTDegrees = 0;
// the current transition (any layered transitions are nested within this transition)
this.currentTransition = null;
// orientation, locomotion and timing
this.velocity = {x:0, y:0, z:0};
this.acceleration = {x:0, y:0, z:0};
this.yaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
this.yawDelta = 0;
this.yawDeltaAcceleration = 0;
this.direction = FORWARDS;
this.deltaTime = 0;
// historical orientation, locomotion and timing
this.lastDirection = FORWARDS;
this.lastVelocity = {x:0, y:0, z:0};
this.lastYaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
this.lastYawDelta = 0;
this.lastYawDeltaAcceleration = 0;
// Quat.safeEulerAngles(MyAvatar.orientation).y tends to repeat values between frames, so values are filtered
var YAW_SMOOTHING = 22;
this.yawFilter = filter.createAveragingFilter(YAW_SMOOTHING);
this.deltaTimeFilter = filter.createAveragingFilter(YAW_SMOOTHING);
this.yawDeltaAccelerationFilter = filter.createAveragingFilter(YAW_SMOOTHING);
// assess locomotion state
this.assess = function(deltaTime) {
// calculate avatar frame speed, velocity and acceleration
this.deltaTime = deltaTime;
this.velocity = Vec3.multiplyQbyV(Quat.inverse(MyAvatar.orientation), MyAvatar.getVelocity());
var lateralVelocity = Math.sqrt(Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.z, 2));
// MyAvatar.getAcceleration() currently not working. bug report submitted: https://worklist.net/20527
var acceleration = {x:0, y:0, z:0};
this.acceleration.x = (this.velocity.x - this.lastVelocity.x) / deltaTime;
this.acceleration.y = (this.velocity.y - this.lastVelocity.y) / deltaTime;
this.acceleration.z = (this.velocity.z - this.lastVelocity.z) / deltaTime;
// MyAvatar.getAngularVelocity and MyAvatar.getAngularAcceleration currently not working. bug report submitted
this.yaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
if (this.lastYaw < 0 && this.yaw > 0 || this.lastYaw > 0 && this.yaw < 0) {
this.lastYaw *= -1;
}
var timeDelta = this.deltaTimeFilter.process(deltaTime);
this.yawDelta = filter.degToRad(this.yawFilter.process(this.lastYaw - this.yaw)) / timeDelta;
this.yawDeltaAcceleration = this.yawDeltaAccelerationFilter.process(this.lastYawDelta - this.yawDelta) / timeDelta;
// how far above the surface is the avatar? (for testing / validation purposes)
var pickRay = {origin: MyAvatar.position, direction: {x:0, y:-1, z:0}};
var distanceFromSurface = Entities.findRayIntersectionBlocking(pickRay).distance;
avatar.distanceFromSurface = distanceFromSurface - avatar.calibration.hipsToFeet;
// determine principle direction of locomotion
var FWD_BACK_BIAS = 100; // helps prevent false sidestep condition detection when banking hard
if (Math.abs(this.velocity.x) > Math.abs(this.velocity.y) &&
Math.abs(this.velocity.x) > FWD_BACK_BIAS * Math.abs(this.velocity.z)) {
if (this.velocity.x < 0) {
this.directedAcceleration = -this.acceleration.x;
this.direction = LEFT;
} else if (this.velocity.x > 0){
this.directedAcceleration = this.acceleration.x;
this.direction = RIGHT;
}
} else if (Math.abs(this.velocity.y) > Math.abs(this.velocity.x) &&
Math.abs(this.velocity.y) > Math.abs(this.velocity.z)) {
if (this.velocity.y > 0) {
this.directedAcceleration = this.acceleration.y;
this.direction = UP;
} else if (this.velocity.y < 0) {
this.directedAcceleration = -this.acceleration.y;
this.direction = DOWN;
}
} else if (FWD_BACK_BIAS * Math.abs(this.velocity.z) > Math.abs(this.velocity.x) &&
Math.abs(this.velocity.z) > Math.abs(this.velocity.y)) {
if (this.velocity.z < 0) {
this.direction = FORWARDS;
this.directedAcceleration = -this.acceleration.z;
} else if (this.velocity.z > 0) {
this.directedAcceleration = this.acceleration.z;
this.direction = BACKWARDS;
}
} else {
this.direction = NONE;
this.directedAcceleration = 0;
}
// set speed flags
if (Vec3.length(this.velocity) < MOVE_THRESHOLD) {
this.isMoving = false;
this.isWalkingSpeed = false;
this.isFlyingSpeed = false;
this.isComingToHalt = false;
} else if (Vec3.length(this.velocity) < MAX_WALK_SPEED) {
this.isMoving = true;
this.isWalkingSpeed = true;
this.isFlyingSpeed = false;
} else {
this.isMoving = true;
this.isWalkingSpeed = false;
this.isFlyingSpeed = true;
}
// set acceleration flags
if (this.directedAcceleration > ACCELERATION_THRESHOLD) {
this.isAccelerating = true;
this.isDecelerating = false;
this.isDeceleratingFast = false;
this.isComingToHalt = false;
} else if (this.directedAcceleration < DECELERATION_THRESHOLD) {
this.isAccelerating = false;
this.isDecelerating = true;
this.isDeceleratingFast = (this.directedAcceleration < FAST_DECELERATION_THRESHOLD);
} else {
this.isAccelerating = false;
this.isDecelerating = false;
this.isDeceleratingFast = false;
}
// use the gathered information to build up some spatial awareness
var isOnSurface = (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD);
var isUnderGravity = (avatar.distanceFromSurface < GRAVITY_THRESHOLD);
var isTakingOff = (isUnderGravity && this.velocity.y > OVERCOME_GRAVITY_SPEED);
var isComingInToLand = (isUnderGravity && this.velocity.y < -OVERCOME_GRAVITY_SPEED);
var aboutToLand = isComingInToLand && avatar.distanceFromSurface < LANDING_THRESHOLD;
var surfaceMotion = isOnSurface && this.isMoving;
var acceleratingAndAirborne = this.isAccelerating && !isOnSurface;
var goingTooFastToWalk = !this.isDecelerating && this.isFlyingSpeed;
var movingDirectlyUpOrDown = (this.direction === UP || this.direction === DOWN)
var maybeBouncing = Math.abs(this.acceleration.y > BOUNCE_ACCELERATION_THRESHOLD) ? true : false;
// we now have enough information to set the appropriate locomotion mode
switch (this.state) {
case STATIC:
var staticToAirMotion = this.isMoving && (acceleratingAndAirborne || goingTooFastToWalk ||
(movingDirectlyUpOrDown && !isOnSurface));
var staticToSurfaceMotion = surfaceMotion && !motion.isComingToHalt && !movingDirectlyUpOrDown &&
!this.isDecelerating && lateralVelocity > MOVE_THRESHOLD;
if (staticToAirMotion) {
this.nextState = AIR_MOTION;
} else if (staticToSurfaceMotion) {
this.nextState = SURFACE_MOTION;
} else {
this.nextState = STATIC;
}
break;
case SURFACE_MOTION:
var surfaceMotionToStatic = !this.isMoving ||
(this.isDecelerating && motion.lastDirection !== DOWN && surfaceMotion &&
!maybeBouncing && Vec3.length(this.velocity) < MAX_WALK_SPEED);
var surfaceMotionToAirMotion = (acceleratingAndAirborne || goingTooFastToWalk || movingDirectlyUpOrDown) &&
(!surfaceMotion && isTakingOff) ||
(!surfaceMotion && this.isMoving && !isComingInToLand);
if (surfaceMotionToStatic) {
// working on the assumption that stopping is now inevitable
if (!motion.isComingToHalt && isOnSurface) {
motion.isComingToHalt = true;
}
this.nextState = STATIC;
} else if (surfaceMotionToAirMotion) {
this.nextState = AIR_MOTION;
} else {
this.nextState = SURFACE_MOTION;
}
break;
case AIR_MOTION:
var airMotionToSurfaceMotion = (surfaceMotion || aboutToLand) && !movingDirectlyUpOrDown;
var airMotionToStatic = !this.isMoving && this.direction === this.lastDirection;
if (airMotionToSurfaceMotion){
this.nextState = SURFACE_MOTION;
} else if (airMotionToStatic) {
this.nextState = STATIC;
} else {
this.nextState = AIR_MOTION;
}
break;
}
}
// frequency time wheel (foot / ground speed matching)
const DEFAULT_HIPS_TO_FEET = 1;
this.frequencyTimeWheelPos = 0;
this.frequencyTimeWheelRadius = DEFAULT_HIPS_TO_FEET / 2;
this.recentFrequencyTimeIncrements = [];
const FT_WHEEL_HISTORY_LENGTH = 8;
for (var i = 0; i < FT_WHEEL_HISTORY_LENGTH; i++) {
this.recentFrequencyTimeIncrements.push(0);
}
this.averageFrequencyTimeIncrement = 0;
this.advanceFrequencyTimeWheel = function(angle){
this.elapsedFTDegrees += angle;
// keep a running average of increments for use in transitions (used during transitioning)
this.recentFrequencyTimeIncrements.push(angle);
this.recentFrequencyTimeIncrements.shift();
for (increment in this.recentFrequencyTimeIncrements) {
this.averageFrequencyTimeIncrement += this.recentFrequencyTimeIncrements[increment];
}
this.averageFrequencyTimeIncrement /= this.recentFrequencyTimeIncrements.length;
this.frequencyTimeWheelPos += angle;
const FULL_CIRCLE = 360;
if (this.frequencyTimeWheelPos >= FULL_CIRCLE) {
this.frequencyTimeWheelPos = this.frequencyTimeWheelPos % FULL_CIRCLE;
}
}
this.saveHistory = function() {
this.lastDirection = this.direction;
this.lastVelocity = this.velocity;
this.lastYaw = this.yaw;
this.lastYawDelta = this.yawDelta;
this.lastYawDeltaAcceleration = this.yawDeltaAcceleration;
}
}; // end Motion constructor
// animation manipulation object
animationOperations = (function() {
return {
// helper function for renderMotion(). calculate joint translations based on animation file settings and frequency * time
calculateTranslations: function(animation, ft, direction) {
var jointName = "Hips";
var joint = animation.joints[jointName];
var jointTranslations = {x:0, y:0, z:0};
// gather modifiers and multipliers
modifiers = new FrequencyMultipliers(joint, direction);
// calculate translations. Use synthesis filters where specified by the animation data file.
// sway (oscillation on the x-axis)
if (animation.filters.hasOwnProperty(jointName) && 'swayFilter' in animation.filters[jointName]) {
jointTranslations.x = joint.sway * animation.filters[jointName].swayFilter.calculate
(filter.degToRad(modifiers.swayFrequencyMultiplier * ft + joint.swayPhase)) + joint.swayOffset;
} else {
jointTranslations.x = joint.sway * Math.sin
(filter.degToRad(modifiers.swayFrequencyMultiplier * ft + joint.swayPhase)) + joint.swayOffset;
}
// bob (oscillation on the y-axis)
if (animation.filters.hasOwnProperty(jointName) && 'bobFilter' in animation.filters[jointName]) {
jointTranslations.y = joint.bob * animation.filters[jointName].bobFilter.calculate
(filter.degToRad(modifiers.bobFrequencyMultiplier * ft + joint.bobPhase)) + joint.bobOffset;
} else {
jointTranslations.y = joint.bob * Math.sin
(filter.degToRad(modifiers.bobFrequencyMultiplier * ft + joint.bobPhase)) + joint.bobOffset;
if (animation.filters.hasOwnProperty(jointName) && 'bobLPFilter' in animation.filters[jointName]) {
jointTranslations.y = filter.clipTrough(jointTranslations.y, joint, 2);
jointTranslations.y = animation.filters[jointName].bobLPFilter.process(jointTranslations.y);
}
}
// thrust (oscillation on the z-axis)
if (animation.filters.hasOwnProperty(jointName) && 'thrustFilter' in animation.filters[jointName]) {
jointTranslations.z = joint.thrust * animation.filters[jointName].thrustFilter.calculate
(filter.degToRad(modifiers.thrustFrequencyMultiplier * ft + joint.thrustPhase)) + joint.thrustOffset;
} else {
jointTranslations.z = joint.thrust * Math.sin
(filter.degToRad(modifiers.thrustFrequencyMultiplier * ft + joint.thrustPhase)) + joint.thrustOffset;
}
return jointTranslations;
},
// helper function for renderMotion(). calculate joint rotations based on animation file settings and frequency * time
calculateRotations: function(jointName, animation, ft, direction) {
var joint = animation.joints[jointName];
var jointRotations = {x:0, y:0, z:0};
if (avatar.blenderPreRotations) {
jointRotations = Vec3.sum(jointRotations, walkAssets.blenderPreRotations.joints[jointName]);
}
// gather frequency multipliers for this joint
modifiers = new FrequencyMultipliers(joint, direction);
// calculate rotations. Use synthesis filters where specified by the animation data file.
// calculate pitch
if (animation.filters.hasOwnProperty(jointName) &&
'pitchFilter' in animation.filters[jointName]) {
jointRotations.x += joint.pitch * animation.filters[jointName].pitchFilter.calculate
(filter.degToRad(ft * modifiers.pitchFrequencyMultiplier + joint.pitchPhase)) + joint.pitchOffset;
} else {
jointRotations.x += joint.pitch * Math.sin
(filter.degToRad(ft * modifiers.pitchFrequencyMultiplier + joint.pitchPhase)) + joint.pitchOffset;
}
// calculate yaw
if (animation.filters.hasOwnProperty(jointName) &&
'yawFilter' in animation.filters[jointName]) {
jointRotations.y += joint.yaw * animation.filters[jointName].yawFilter.calculate
(filter.degToRad(ft * modifiers.yawFrequencyMultiplier + joint.yawPhase)) + joint.yawOffset;
} else {
jointRotations.y += joint.yaw * Math.sin
(filter.degToRad(ft * modifiers.yawFrequencyMultiplier + joint.yawPhase)) + joint.yawOffset;
}
// calculate roll
if (animation.filters.hasOwnProperty(jointName) &&
'rollFilter' in animation.filters[jointName]) {
jointRotations.z += joint.roll * animation.filters[jointName].rollFilter.calculate
(filter.degToRad(ft * modifiers.rollFrequencyMultiplier + joint.rollPhase)) + joint.rollOffset;
} else {
jointRotations.z += joint.roll * Math.sin
(filter.degToRad(ft * modifiers.rollFrequencyMultiplier + joint.rollPhase)) + joint.rollOffset;
}
return jointRotations;
},
zeroAnimation: function(animation) {
for (i in animation.joints) {
for (j in animation.joints[i]) {
animation.joints[i][j] = 0;
}
}
},
blendAnimation: function(sourceAnimation, targetAnimation, percent) {
for (i in targetAnimation.joints) {
targetAnimation.joints[i].pitch += percent * sourceAnimation.joints[i].pitch;
targetAnimation.joints[i].yaw += percent * sourceAnimation.joints[i].yaw;
targetAnimation.joints[i].roll += percent * sourceAnimation.joints[i].roll;
targetAnimation.joints[i].pitchPhase += percent * sourceAnimation.joints[i].pitchPhase;
targetAnimation.joints[i].yawPhase += percent * sourceAnimation.joints[i].yawPhase;
targetAnimation.joints[i].rollPhase += percent * sourceAnimation.joints[i].rollPhase;
targetAnimation.joints[i].pitchOffset += percent * sourceAnimation.joints[i].pitchOffset;
targetAnimation.joints[i].yawOffset += percent * sourceAnimation.joints[i].yawOffset;
targetAnimation.joints[i].rollOffset += percent * sourceAnimation.joints[i].rollOffset;
if (i === "Hips") {
// Hips only
targetAnimation.joints[i].thrust += percent * sourceAnimation.joints[i].thrust;
targetAnimation.joints[i].sway += percent * sourceAnimation.joints[i].sway;
targetAnimation.joints[i].bob += percent * sourceAnimation.joints[i].bob;
targetAnimation.joints[i].thrustPhase += percent * sourceAnimation.joints[i].thrustPhase;
targetAnimation.joints[i].swayPhase += percent * sourceAnimation.joints[i].swayPhase;
targetAnimation.joints[i].bobPhase += percent * sourceAnimation.joints[i].bobPhase;
targetAnimation.joints[i].thrustOffset += percent * sourceAnimation.joints[i].thrustOffset;
targetAnimation.joints[i].swayOffset += percent * sourceAnimation.joints[i].swayOffset;
targetAnimation.joints[i].bobOffset += percent * sourceAnimation.joints[i].bobOffset;
}
}
},
deepCopy: function(sourceAnimation, targetAnimation) {
// calibration
targetAnimation.calibration = JSON.parse(JSON.stringify(sourceAnimation.calibration));
// harmonics
targetAnimation.harmonics = {};
if (sourceAnimation.harmonics) {
targetAnimation.harmonics = JSON.parse(JSON.stringify(sourceAnimation.harmonics));
}
// filters
targetAnimation.filters = {};
for (i in sourceAnimation.filters) {
// are any filters specified for this joint?
if (sourceAnimation.filters[i]) {
targetAnimation.filters[i] = sourceAnimation.filters[i];
// wave shapers
if (sourceAnimation.filters[i].pitchFilter) {
targetAnimation.filters[i].pitchFilter = sourceAnimation.filters[i].pitchFilter;
}
if (sourceAnimation.filters[i].yawFilter) {
targetAnimation.filters[i].yawFilter = sourceAnimation.filters[i].yawFilter;
}
if (sourceAnimation.filters[i].rollFilter) {
targetAnimation.filters[i].rollFilter = sourceAnimation.filters[i].rollFilter;
}
// LP filters
if (sourceAnimation.filters[i].swayLPFilter) {
targetAnimation.filters[i].swayLPFilter = sourceAnimation.filters[i].swayLPFilter;
}
if (sourceAnimation.filters[i].bobLPFilter) {
targetAnimation.filters[i].bobLPFilter = sourceAnimation.filters[i].bobLPFilter;
}
if (sourceAnimation.filters[i].thrustLPFilter) {
targetAnimation.filters[i].thrustLPFilter = sourceAnimation.filters[i].thrustLPFilter;
}
}
}
// joints
targetAnimation.joints = JSON.parse(JSON.stringify(sourceAnimation.joints));
}
}
})(); // end animation object literal
// ReachPose datafile wrapper object
ReachPose = function(reachPoseName) {
this.name = reachPoseName;
this.reachPoseParameters = walkAssets.getReachPoseParameters(reachPoseName);
this.reachPoseDataFile = walkAssets.getReachPoseDataFile(reachPoseName);
this.progress = 0;
this.smoothingFilter = filter.createAveragingFilter(this.reachPoseParameters.smoothing);
this.currentStrength = function() {
// apply optionally smoothed (D)ASDR envelope to reach pose's strength / influence whilst active
var segmentProgress = undefined; // progress through chosen segment
var segmentTimeDelta = undefined; // total change in time over chosen segment
var segmentStrengthDelta = undefined; // total change in strength over chosen segment
var lastStrength = undefined; // the last value the previous segment held
var currentStrength = undefined; // return value
// select parameters based on segment (a segment being one of (D),A,S,D or R)
if (this.progress >= this.reachPoseParameters.sustain.timing) {
// release segment
segmentProgress = this.progress - this.reachPoseParameters.sustain.timing;
segmentTimeDelta = this.reachPoseParameters.release.timing - this.reachPoseParameters.sustain.timing;
segmentStrengthDelta = this.reachPoseParameters.release.strength - this.reachPoseParameters.sustain.strength;
lastStrength = this.reachPoseParameters.sustain.strength;
} else if (this.progress >= this.reachPoseParameters.decay.timing) {
// sustain phase
segmentProgress = this.progress - this.reachPoseParameters.decay.timing;
segmentTimeDelta = this.reachPoseParameters.sustain.timing - this.reachPoseParameters.decay.timing;
segmentStrengthDelta = this.reachPoseParameters.sustain.strength - this.reachPoseParameters.decay.strength;
lastStrength = this.reachPoseParameters.decay.strength;
} else if (this.progress >= this.reachPoseParameters.attack.timing) {
// decay phase
segmentProgress = this.progress - this.reachPoseParameters.attack.timing;
segmentTimeDelta = this.reachPoseParameters.decay.timing - this.reachPoseParameters.attack.timing;
segmentStrengthDelta = this.reachPoseParameters.decay.strength - this.reachPoseParameters.attack.strength;
lastStrength = this.reachPoseParameters.attack.strength;
} else if (this.progress >= this.reachPoseParameters.delay.timing) {
// attack phase
segmentProgress = this.progress - this.reachPoseParameters.delay.timing;
segmentTimeDelta = this.reachPoseParameters.attack.timing - this.reachPoseParameters.delay.timing;
segmentStrengthDelta = this.reachPoseParameters.attack.strength - this.reachPoseParameters.delay.strength;
lastStrength = 0; //this.delay.strength;
} else {
// delay phase
segmentProgress = this.progress;
segmentTimeDelta = this.reachPoseParameters.delay.timing;
segmentStrengthDelta = this.reachPoseParameters.delay.strength;
lastStrength = 0;
}
currentStrength = segmentTimeDelta > 0 ? lastStrength + segmentStrengthDelta * segmentProgress / segmentTimeDelta
: lastStrength;
// smooth off the response curve
currentStrength = this.smoothingFilter.process(currentStrength);
return currentStrength;
}
};
// constructor with default parameters
TransitionParameters = function() {
this.duration = 0.5;
this.easingLower = {x:0.25, y:0.75};
this.easingUpper = {x:0.75, y:0.25};
this.reachPoses = [];
}
const QUARTER_CYCLE = 90;
const HALF_CYCLE = 180;
const THREE_QUARTER_CYCLE = 270;
const FULL_CYCLE = 360;
// constructor for animation Transition
Transition = function(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses) {
if (playTransitionReachPoses === undefined) {
playTransitionReachPoses = true;
}
// record the current state of animation
this.nextAnimation = nextAnimation;
this.lastAnimation = lastAnimation;
this.lastTransition = lastTransition;
// collect information about the currently playing animation
this.direction = motion.direction;
this.lastDirection = motion.lastDirection;
this.lastFrequencyTimeWheelPos = motion.frequencyTimeWheelPos;
this.lastFrequencyTimeIncrement = motion.averageFrequencyTimeIncrement;
this.lastFrequencyTimeWheelRadius = motion.frequencyTimeWheelRadius;
this.degreesToTurn = 0; // total degrees to turn the ft wheel before the avatar stops (walk only)
this.degreesRemaining = 0; // remaining degrees to turn the ft wheel before the avatar stops (walk only)
this.lastElapsedFTDegrees = motion.elapsedFTDegrees; // degrees elapsed since last transition start
motion.elapsedFTDegrees = 0; // reset ready for the next transition
motion.frequencyTimeWheelPos = 0; // start the next animation's frequency time wheel from zero
// set parameters for the transition
this.parameters = new TransitionParameters();
this.liveReachPoses = [];
if (walkAssets && lastAnimation && nextAnimation) {
// overwrite this.parameters with any transition parameters specified for this particular transition
walkAssets.getTransitionParameters(lastAnimation, nextAnimation, this.parameters);
// fire up any reach poses for this transition
if (playTransitionReachPoses) {
for (poseName in this.parameters.reachPoses) {
this.liveReachPoses.push(new ReachPose(this.parameters.reachPoses[poseName]));
}
}
}
this.startTime = new Date().getTime(); // Starting timestamp (seconds)
this.progress = 0; // how far are we through the transition?
this.filteredProgress = 0;
// coming to a halt whilst walking? if so, will need a clean stopping point defined
if (motion.isComingToHalt) {
const FULL_CYCLE_THRESHOLD = 320;
const HALF_CYCLE_THRESHOLD = 140;
const CYCLE_COMMIT_THRESHOLD = 5;
// how many degrees do we need to turn the walk wheel to finish walking with both feet on the ground?
if (this.lastElapsedFTDegrees < CYCLE_COMMIT_THRESHOLD) {
// just stop the walk cycle right here and blend to idle
this.degreesToTurn = 0;
} else if (this.lastElapsedFTDegrees < HALF_CYCLE) {
// we have not taken a complete step yet, so we advance to the second stop angle
this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos;
} else if (this.lastFrequencyTimeWheelPos > 0 && this.lastFrequencyTimeWheelPos <= HALF_CYCLE_THRESHOLD) {
// complete the step and stop at 180
this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos;
} else if (this.lastFrequencyTimeWheelPos > HALF_CYCLE_THRESHOLD && this.lastFrequencyTimeWheelPos <= HALF_CYCLE) {
// complete the step and next then stop at 0
this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos + HALF_CYCLE;
} else if (this.lastFrequencyTimeWheelPos > HALF_CYCLE && this.lastFrequencyTimeWheelPos <= FULL_CYCLE_THRESHOLD) {
// complete the step and stop at 0
this.degreesToTurn = FULL_CYCLE - this.lastFrequencyTimeWheelPos;
} else {
// complete the step and the next then stop at 180
this.degreesToTurn = FULL_CYCLE - this.lastFrequencyTimeWheelPos + HALF_CYCLE;
}
// transition length in this case should be directly proportional to the remaining degrees to turn
var MIN_FT_INCREMENT = 5.0; // degrees per frame
var MIN_TRANSITION_DURATION = 0.4;
const TWO_THIRDS = 0.6667;
this.lastFrequencyTimeIncrement *= TWO_THIRDS; // help ease the transition
var lastFrequencyTimeIncrement = this.lastFrequencyTimeIncrement > MIN_FT_INCREMENT ?
this.lastFrequencyTimeIncrement : MIN_FT_INCREMENT;
var timeToFinish = Math.max(motion.deltaTime * this.degreesToTurn / lastFrequencyTimeIncrement,
MIN_TRANSITION_DURATION);
this.parameters.duration = timeToFinish;
this.degreesRemaining = this.degreesToTurn;
}
// deal with transition recursion (overlapping transitions)
this.recursionDepth = 0;
this.incrementRecursion = function() {
this.recursionDepth += 1;
// cancel any continued motion
this.degreesToTurn = 0;
// limit the number of layered / nested transitions
if (this.lastTransition !== nullTransition) {
this.lastTransition.incrementRecursion();
if (this.lastTransition.recursionDepth > MAX_TRANSITION_RECURSION) {
this.lastTransition = nullTransition;
}
}
};
if (this.lastTransition !== nullTransition) {
this.lastTransition.incrementRecursion();
}
// end of transition initialisation. begin Transition public methods
// keep up the pace for the frequency time wheel for the last animation
this.advancePreviousFrequencyTimeWheel = function(deltaTime) {
var wheelAdvance = undefined;
if (this.lastAnimation === avatar.selectedWalkBlend &&
this.nextAnimation === avatar.selectedIdle) {
if (this.degreesRemaining <= 0) {
// stop continued motion
wheelAdvance = 0;
if (motion.isComingToHalt) {
if (this.lastFrequencyTimeWheelPos < QUARTER_CYCLE) {
this.lastFrequencyTimeWheelPos = 0;
} else {
this.lastFrequencyTimeWheelPos = HALF_CYCLE;
}
}
} else {
wheelAdvance = this.lastFrequencyTimeIncrement;
var distanceToTravel = avatar.calibration.strideLength * wheelAdvance / HALF_CYCLE;
if (this.degreesRemaining <= 0) {
distanceToTravel = 0;
this.degreesRemaining = 0;
} else {
this.degreesRemaining -= wheelAdvance;
}
}
} else {
wheelAdvance = this.lastFrequencyTimeIncrement;
}
// advance the ft wheel
this.lastFrequencyTimeWheelPos += wheelAdvance;
if (this.lastFrequencyTimeWheelPos >= FULL_CYCLE) {
this.lastFrequencyTimeWheelPos = this.lastFrequencyTimeWheelPos % FULL_CYCLE;
}
// advance ft wheel for the nested (previous) Transition
if (this.lastTransition !== nullTransition) {
this.lastTransition.advancePreviousFrequencyTimeWheel(deltaTime);
}
// update the lastElapsedFTDegrees for short stepping
this.lastElapsedFTDegrees += wheelAdvance;
this.degreesTurned += wheelAdvance;
};
this.updateProgress = function() {
const MILLISECONDS_CONVERT = 1000;
const ACCURACY_INCREASER = 1000;
var elapasedTime = (new Date().getTime() - this.startTime) / MILLISECONDS_CONVERT;
this.progress = elapasedTime / this.parameters.duration;
this.progress = Math.round(this.progress * ACCURACY_INCREASER) / ACCURACY_INCREASER;
// updated nested transition/s
if (this.lastTransition !== nullTransition) {
if (this.lastTransition.updateProgress() === TRANSITION_COMPLETE) {
// the previous transition is now complete
this.lastTransition = nullTransition;
}
}
// update any reachPoses
for (pose in this.liveReachPoses) {
// use independent timing for reachPoses
this.liveReachPoses[pose].progress += (motion.deltaTime / this.liveReachPoses[pose].reachPoseParameters.duration);
if (this.liveReachPoses[pose].progress >= 1) {
// time to kill off this reach pose
this.liveReachPoses.splice(pose, 1);
}
}
// update transition progress
this.filteredProgress = filter.bezier(this.progress, this.parameters.easingLower, this.parameters.easingUpper);
return this.progress >= 1 ? TRANSITION_COMPLETE : false;
};
this.blendTranslations = function(frequencyTimeWheelPos, direction) {
var lastTranslations = {x:0, y:0, z:0};
var nextTranslations = animationOperations.calculateTranslations(this.nextAnimation,
frequencyTimeWheelPos,
direction);
// are we blending with a previous, still live transition?
if (this.lastTransition !== nullTransition) {
lastTranslations = this.lastTransition.blendTranslations(this.lastFrequencyTimeWheelPos,
this.lastDirection);
} else {
lastTranslations = animationOperations.calculateTranslations(this.lastAnimation,
this.lastFrequencyTimeWheelPos,
this.lastDirection);
}
// blend last / next translations
nextTranslations = Vec3.multiply(this.filteredProgress, nextTranslations);
lastTranslations = Vec3.multiply((1 - this.filteredProgress), lastTranslations);
nextTranslations = Vec3.sum(nextTranslations, lastTranslations);
if (this.liveReachPoses.length > 0) {
for (pose in this.liveReachPoses) {
var reachPoseStrength = this.liveReachPoses[pose].currentStrength();
var poseTranslations = animationOperations.calculateTranslations(
this.liveReachPoses[pose].reachPoseDataFile,
frequencyTimeWheelPos,
direction);
// can't use Vec3 operations here, as if x,y or z is zero, the reachPose should have no influence at all
if (Math.abs(poseTranslations.x) > 0) {
nextTranslations.x = reachPoseStrength * poseTranslations.x + (1 - reachPoseStrength) * nextTranslations.x;
}
if (Math.abs(poseTranslations.y) > 0) {
nextTranslations.y = reachPoseStrength * poseTranslations.y + (1 - reachPoseStrength) * nextTranslations.y;
}
if (Math.abs(poseTranslations.z) > 0) {
nextTranslations.z = reachPoseStrength * poseTranslations.z + (1 - reachPoseStrength) * nextTranslations.z;
}
}
}
return nextTranslations;
};
this.blendRotations = function(jointName, frequencyTimeWheelPos, direction) {
var lastRotations = {x:0, y:0, z:0};
var nextRotations = animationOperations.calculateRotations(jointName,
this.nextAnimation,
frequencyTimeWheelPos,
direction);
// are we blending with a previous, still live transition?
if (this.lastTransition !== nullTransition) {
lastRotations = this.lastTransition.blendRotations(jointName,
this.lastFrequencyTimeWheelPos,
this.lastDirection);
} else {
lastRotations = animationOperations.calculateRotations(jointName,
this.lastAnimation,
this.lastFrequencyTimeWheelPos,
this.lastDirection);
}
// blend last / next translations
nextRotations = Vec3.multiply(this.filteredProgress, nextRotations);
lastRotations = Vec3.multiply((1 - this.filteredProgress), lastRotations);
nextRotations = Vec3.sum(nextRotations, lastRotations);
// are there reachPoses defined for this transition?
if (this.liveReachPoses.length > 0) {
for (pose in this.liveReachPoses) {
var reachPoseStrength = this.liveReachPoses[pose].currentStrength();
var poseRotations = animationOperations.calculateRotations(jointName,
this.liveReachPoses[pose].reachPoseDataFile,
frequencyTimeWheelPos,
direction);
// don't use Vec3 operations here, as if x,y or z is zero, the reach pose should have no influence at all
if (Math.abs(poseRotations.x) > 0) {
nextRotations.x = reachPoseStrength * poseRotations.x + (1 - reachPoseStrength) * nextRotations.x;
}
if (Math.abs(poseRotations.y) > 0) {
nextRotations.y = reachPoseStrength * poseRotations.y + (1 - reachPoseStrength) * nextRotations.y;
}
if (Math.abs(poseRotations.z) > 0) {
nextRotations.z = reachPoseStrength * poseRotations.z + (1 - reachPoseStrength) * nextRotations.z;
}
}
}
return nextRotations;
};
}; // end Transition constructor
// individual joint modifiers
FrequencyMultipliers = function(joint, direction) {
// gather multipliers
this.pitchFrequencyMultiplier = 1;
this.yawFrequencyMultiplier = 1;
this.rollFrequencyMultiplier = 1;
this.swayFrequencyMultiplier = 1;
this.bobFrequencyMultiplier = 1;
this.thrustFrequencyMultiplier = 1;
if (joint) {
if (joint.pitchFrequencyMultiplier) {
this.pitchFrequencyMultiplier = joint.pitchFrequencyMultiplier;
}
if (joint.yawFrequencyMultiplier) {
this.yawFrequencyMultiplier = joint.yawFrequencyMultiplier;
}
if (joint.rollFrequencyMultiplier) {
this.rollFrequencyMultiplier = joint.rollFrequencyMultiplier;
}
if (joint.swayFrequencyMultiplier) {
this.swayFrequencyMultiplier = joint.swayFrequencyMultiplier;
}
if (joint.bobFrequencyMultiplier) {
this.bobFrequencyMultiplier = joint.bobFrequencyMultiplier;
}
if (joint.thrustFrequencyMultiplier) {
this.thrustFrequencyMultiplier = joint.thrustFrequencyMultiplier;
}
}
};