Needs a lot of cleanup. Data has been de-duplicated, and where identical copies existed, one of them has been replaced with a symlink. Some files have been excluded, such as binaries, installers and debug dumps. Some of that may still be present.
852 lines
No EOL
31 KiB
JavaScript
852 lines
No EOL
31 KiB
JavaScript
//
|
|
// walk.js
|
|
//
|
|
// version 1.12
|
|
//
|
|
// Created by David Wooldridge, Autumn 2014
|
|
//
|
|
// Animates an avatar using procedural animation techniques
|
|
//
|
|
// Distributed under the Apache License, Version 2.0.
|
|
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
|
|
//
|
|
|
|
// constants
|
|
var MALE = 1;
|
|
var FEMALE = 2;
|
|
var MAX_WALK_SPEED = 2.5;
|
|
var TAKE_FLIGHT_SPEED = 4.55;
|
|
var TOP_SPEED = 300;
|
|
var UP = 1;
|
|
var DOWN = 2;
|
|
var LEFT = 4;
|
|
var RIGHT = 8;
|
|
var FORWARDS = 16;
|
|
var BACKWARDS = 32;
|
|
var PITCH = 64;
|
|
var YAW = 128;
|
|
var ROLL = 256;
|
|
var SAWTOOTH = 1;
|
|
var TRIANGLE = 2;
|
|
var SQUARE = 4;
|
|
var VERY_LONG_TIME = 1000000.0;
|
|
var VERY_SHORT_TIME = 0.000001;
|
|
|
|
// ovelay images location
|
|
var pathToAssets = 'http://s3.amazonaws.com/hifi-public/procedural-animator/';
|
|
|
|
// load the UI
|
|
Script.include("./libraries/walkInterface.js");
|
|
|
|
// load filters (Bezier, Butterworth, add harmonics, averaging)
|
|
Script.include("./libraries/walkFilters.js");
|
|
|
|
// load objects, constructors and assets (state, Motion, Transition, walkAssets)
|
|
Script.include("./libraries/walkApi.js");
|
|
|
|
// initialise the motion properties and history object
|
|
var motion = new Motion();
|
|
|
|
// initialise Transitions
|
|
var nullTransition = new Transition();
|
|
motion.currentTransition = nullTransition;
|
|
|
|
// initialise the UI
|
|
walkInterface.initialise(state, motion, walkAssets);
|
|
|
|
// Begin by setting the STANDING internal state
|
|
state.setInternalState(state.STANDING);
|
|
|
|
// smoothing filters
|
|
var leanPitchFilter = filter.createButterworthFilter1();
|
|
var leanPitchSmoothingFilter = filter.createAveragingFilter(10);
|
|
var leanRollSmoothingFilter = filter.createAveragingFilter(10);
|
|
var flyUpFilter = filter.createAveragingFilter(30);
|
|
var flyFilter = filter.createAveragingFilter(30);
|
|
|
|
// Main loop
|
|
Script.update.connect(function(deltaTime) {
|
|
|
|
if (state.powerOn) {
|
|
|
|
// calculate avi linear speed
|
|
var speed = Vec3.length(MyAvatar.getVelocity());
|
|
|
|
// decide which animation should be playing
|
|
selectAnimation(deltaTime, speed);
|
|
|
|
// turn the frequency time wheels
|
|
turnFrequencyTimeWheels(deltaTime, speed);
|
|
|
|
// calculate (or fetch pre-calculated) stride length for this avi
|
|
setStrideLength(speed);
|
|
|
|
// update the progress of any live transition
|
|
updateTransition();
|
|
|
|
// apply translation and rotations
|
|
animateAvatar(deltaTime, speed);
|
|
}
|
|
});
|
|
|
|
// helper function for selectAnimation()
|
|
function determineLocomotionMode(speed) {
|
|
|
|
var locomotionMode = undefined;
|
|
|
|
if (speed < 0.1 && motion.currentAnimation !== motion.selectedFlyBlend) {
|
|
|
|
locomotionMode = state.STANDING;
|
|
|
|
} else if (speed === 0 && motion.currentAnimation === motion.selectedFlyBlend) {
|
|
|
|
locomotionMode = state.STANDING;
|
|
|
|
} else if (speed < TAKE_FLIGHT_SPEED) {
|
|
|
|
locomotionMode = state.WALKING;
|
|
|
|
} else if (speed >= TAKE_FLIGHT_SPEED) {
|
|
|
|
locomotionMode = state.FLYING;
|
|
}
|
|
|
|
// maybe travelling at walking speed, but sideways?
|
|
if (locomotionMode === state.WALKING &&
|
|
(motion.direction === LEFT ||
|
|
motion.direction === RIGHT)) {
|
|
|
|
locomotionMode = state.SIDE_STEP;
|
|
}
|
|
|
|
// maybe completing a final step during a walking to standing transition?
|
|
if (locomotionMode === state.WALKING &&
|
|
motion.currentTransition.percentToMove > 0) {
|
|
|
|
locomotionMode = state.STANDING;
|
|
}
|
|
|
|
// maybe starting to fly up or down?
|
|
if (locomotionMode === state.WALKING &&
|
|
motion.direction === UP || motion.direction === DOWN) {
|
|
|
|
locomotionMode = state.FLYING;
|
|
}
|
|
|
|
// are we on a voxel surface?
|
|
if(spatialInformation.distanceToVoxels() > 0.2 && speed > 0.1) {
|
|
|
|
locomotionMode = state.FLYING;
|
|
}
|
|
|
|
return locomotionMode;
|
|
}
|
|
|
|
// helper function for selectAnimation()
|
|
function determineDirection(localVelocity) {
|
|
|
|
if (Math.abs(localVelocity.x) > Math.abs(localVelocity.y) &&
|
|
Math.abs(localVelocity.x) > Math.abs(localVelocity.z)) {
|
|
|
|
if (localVelocity.x < 0) {
|
|
return LEFT;
|
|
} else {
|
|
return RIGHT;
|
|
}
|
|
|
|
} else if (Math.abs(localVelocity.y) > Math.abs(localVelocity.x) &&
|
|
Math.abs(localVelocity.y) > Math.abs(localVelocity.z)) {
|
|
|
|
if (localVelocity.y > 0) {
|
|
return UP;
|
|
} else {
|
|
return DOWN;
|
|
}
|
|
|
|
} else if (Math.abs(localVelocity.z) > Math.abs(localVelocity.x) &&
|
|
Math.abs(localVelocity.z) > Math.abs(localVelocity.y)) {
|
|
|
|
if (localVelocity.z < 0) {
|
|
return FORWARDS;
|
|
} else {
|
|
return BACKWARDS;
|
|
}
|
|
}
|
|
}
|
|
|
|
// advance the animation's frequency time wheels. advance frequency time wheels for any live transitions also
|
|
function turnFrequencyTimeWheels(deltaTime, speed) {
|
|
|
|
var wheelAdvance = 0;
|
|
|
|
// turn the frequency time wheel
|
|
if (motion.currentAnimation === motion.selectedWalk ||
|
|
motion.currentAnimation === motion.selectedSideStepLeft ||
|
|
motion.currentAnimation === motion.selectedSideStepRight) {
|
|
|
|
// Using technique described here: http://www.gdcvault.com/play/1020583/Animation-Bootcamp-An-Indie-Approach
|
|
// wrap the stride length around a 'surveyor's wheel' twice and calculate the angular speed at the given (linear) speed
|
|
// omega = v / r , where r = circumference / 2 PI , where circumference = 2 * stride length
|
|
motion.frequencyTimeWheelRadius = motion.strideLength / Math.PI;
|
|
var angularVelocity = speed / motion.frequencyTimeWheelRadius;
|
|
|
|
// calculate the degrees turned (at this angular speed) since last frame
|
|
wheelAdvance = filter.radToDeg(deltaTime * angularVelocity);
|
|
|
|
} else {
|
|
|
|
// turn the frequency time wheel by the amount specified for this animation
|
|
wheelAdvance = filter.radToDeg(motion.currentAnimation.calibration.frequency * deltaTime);
|
|
}
|
|
|
|
if(motion.currentTransition !== nullTransition) {
|
|
|
|
// the last animation is still playing so we turn it's frequency time wheel to maintain the animation
|
|
if (motion.currentTransition.lastAnimation === motion.selectedWalk) {
|
|
|
|
// if at a stop angle (i.e. feet now under the avi) hold the wheel position for remainder of transition
|
|
var tolerance = motion.currentTransition.lastFrequencyTimeIncrement + 0.1;
|
|
if ((motion.currentTransition.lastFrequencyTimeWheelPos >
|
|
(motion.currentTransition.stopAngle - tolerance)) &&
|
|
(motion.currentTransition.lastFrequencyTimeWheelPos <
|
|
(motion.currentTransition.stopAngle + tolerance))) {
|
|
|
|
motion.currentTransition.lastFrequencyTimeIncrement = 0;
|
|
}
|
|
}
|
|
motion.currentTransition.advancePreviousFrequencyTimeWheel();
|
|
}
|
|
|
|
// advance the walk wheel the appropriate amount
|
|
motion.advanceFrequencyTimeWheel(wheelAdvance);
|
|
}
|
|
|
|
// helper function for selectAnimation()
|
|
function setTransition(nextAnimation) {
|
|
|
|
var lastTransition = motion.currentTransition;
|
|
motion.currentTransition = new Transition(nextAnimation,
|
|
motion.currentAnimation,
|
|
motion.currentTransition);
|
|
|
|
if(motion.currentTransition.recursionDepth > 5) {
|
|
|
|
delete motion.currentTransition;
|
|
motion.currentTransition = lastTransition;
|
|
}
|
|
}
|
|
|
|
// select which animation should be played based on speed, mode of locomotion and direction of travel
|
|
function selectAnimation(deltaTime, speed) {
|
|
|
|
var localVelocity = {x: 0, y: 0, z: 0};
|
|
if (speed > 0) {
|
|
localVelocity = Vec3.multiplyQbyV(Quat.inverse(MyAvatar.orientation), MyAvatar.getVelocity());
|
|
}
|
|
|
|
// determine principle direction of movement
|
|
motion.direction = determineDirection(localVelocity);
|
|
|
|
// determine mode of locomotion
|
|
var locomotionMode = determineLocomotionMode(speed);
|
|
|
|
// select appropriate animation. If changing animation, initiate a new transition
|
|
// note: The transitions are work in progress: https://worklist.net/20186
|
|
switch (locomotionMode) {
|
|
|
|
case state.STANDING: {
|
|
|
|
var onVoxelSurface = spatialInformation.distanceToVoxels() < 0.3 ? true : false;
|
|
|
|
if (state.currentState !== state.STANDING) {
|
|
|
|
if (onVoxelSurface) {
|
|
|
|
setTransition(motion.selectedStand);
|
|
|
|
} else {
|
|
|
|
setTransition(motion.selectedHovering);
|
|
}
|
|
state.setInternalState(state.STANDING);
|
|
}
|
|
if (onVoxelSurface) {
|
|
|
|
motion.currentAnimation = motion.selectedStand;
|
|
|
|
} else {
|
|
|
|
motion.currentAnimation = motion.selectedHovering;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case state.WALKING: {
|
|
|
|
if (state.currentState !== state.WALKING) {
|
|
|
|
setTransition(motion.selectedWalk);
|
|
// set the walk wheel to it's starting point for this animation
|
|
if (motion.direction === BACKWARDS) {
|
|
|
|
motion.frequencyTimeWheelPos = motion.selectedWalk.calibration.startAngleBackwards;
|
|
|
|
} else {
|
|
|
|
motion.frequencyTimeWheelPos = motion.selectedWalk.calibration.startAngleForwards;
|
|
}
|
|
state.setInternalState(state.WALKING);
|
|
}
|
|
motion.currentAnimation = motion.selectedWalk;
|
|
break;
|
|
}
|
|
|
|
case state.SIDE_STEP: {
|
|
|
|
var selSideStep = undefined;
|
|
|
|
if (state.currentState !== state.SIDE_STEP) {
|
|
|
|
if (motion.direction === LEFT) {
|
|
// set the walk wheel to it's starting point for this animation
|
|
motion.frequencyTimeWheelPos = motion.selectedSideStepLeft.calibration.startAngle;
|
|
selSideStep = motion.selectedSideStepLeft;
|
|
|
|
} else {
|
|
// set the walk wheel to it's starting point for this animation
|
|
motion.frequencyTimeWheelPos = motion.selectedSideStepRight.calibration.startAngle;
|
|
selSideStep = motion.selectedSideStepRight;
|
|
}
|
|
setTransition(selSideStep);
|
|
state.setInternalState(state.SIDE_STEP);
|
|
|
|
} else if (motion.direction === LEFT) {
|
|
|
|
if (motion.lastDirection !== LEFT) {
|
|
// set the walk wheel to it's starting point for this animation
|
|
motion.frequencyTimeWheelPos = motion.selectedSideStepLeft.calibration.startAngle;
|
|
setTransition(motion.selectedSideStepLeft);
|
|
}
|
|
selSideStep = motion.selectedSideStepLeft;
|
|
|
|
} else {
|
|
|
|
if (motion.lastDirection !== RIGHT) {
|
|
// set the walk wheel to it's starting point for this animation
|
|
motion.frequencyTimeWheelPos = motion.selectedSideStepRight.calibration.startAngle;
|
|
setTransition(motion.selectedSideStepRight);
|
|
}
|
|
selSideStep = motion.selectedSideStepRight;
|
|
}
|
|
motion.currentAnimation = selSideStep;
|
|
break;
|
|
}
|
|
|
|
case state.FLYING: {
|
|
|
|
// blend the up, down and forward flying animations relative to motion direction
|
|
animation.zeroAnimation(motion.selectedFlyBlend);
|
|
|
|
var upVector = 0;
|
|
var forwardVector = 0;
|
|
|
|
if (speed > 0) {
|
|
|
|
// calculate directionals
|
|
upVector = localVelocity.y / speed;
|
|
forwardVector = -localVelocity.z / speed;
|
|
|
|
// add damping
|
|
upVector = flyUpFilter.process(upVector);
|
|
forwardVector = flyFilter.process(forwardVector);
|
|
|
|
// normalise
|
|
var denominator = Math.abs(upVector) + Math.abs(forwardVector);
|
|
upVector = upVector / denominator;
|
|
forwardVector = forwardVector / denominator;
|
|
}
|
|
|
|
if (upVector > 0) {
|
|
|
|
animation.blendAnimation(motion.selectedFlyUp,
|
|
motion.selectedFlyBlend,
|
|
upVector);
|
|
|
|
} else if (upVector < 0) {
|
|
|
|
animation.blendAnimation(motion.selectedFlyDown,
|
|
motion.selectedFlyBlend,
|
|
-upVector);
|
|
} else {
|
|
|
|
forwardVector = 1;
|
|
}
|
|
animation.blendAnimation(motion.selectedFly,
|
|
motion.selectedFlyBlend,
|
|
Math.abs(forwardVector));
|
|
|
|
if (state.currentState !== state.FLYING) {
|
|
|
|
state.setInternalState(state.FLYING);
|
|
setTransition(motion.selectedFlyBlend);
|
|
}
|
|
motion.currentAnimation = motion.selectedFlyBlend;
|
|
break;
|
|
}
|
|
|
|
} // end switch(locomotionMode)
|
|
|
|
// record frame details for future reference
|
|
motion.lastDirection = motion.direction;
|
|
motion.lastSpeed = localVelocity;
|
|
motion.lastDistanceToVoxels = motion.calibration.distanceToVoxels;
|
|
|
|
return;
|
|
}
|
|
|
|
// if the timing's right, recalculate the stride length. if not, fetch the previously calculated value
|
|
function setStrideLength(speed) {
|
|
|
|
// if not at full speed the calculation could be wrong, as we may still be transitioning
|
|
var atMaxSpeed = speed / MAX_WALK_SPEED < 0.97 ? false : true;
|
|
|
|
// walking? then get the stride length
|
|
if (motion.currentAnimation === motion.selectedWalk) {
|
|
|
|
var strideMaxAt = motion.currentAnimation.calibration.forwardStrideMaxAt;
|
|
if (motion.direction === BACKWARDS) {
|
|
strideMaxAt = motion.currentAnimation.calibration.backwardsStrideMaxAt;
|
|
}
|
|
|
|
var tolerance = 1.0;
|
|
if (motion.frequencyTimeWheelPos < (strideMaxAt + tolerance) &&
|
|
motion.frequencyTimeWheelPos > (strideMaxAt - tolerance) &&
|
|
atMaxSpeed && motion.currentTransition === nullTransition) {
|
|
|
|
// measure and save stride length
|
|
var footRPos = MyAvatar.getJointPosition("RightFoot");
|
|
var footLPos = MyAvatar.getJointPosition("LeftFoot");
|
|
motion.strideLength = Vec3.distance(footRPos, footLPos);
|
|
|
|
if (motion.direction === FORWARDS) {
|
|
motion.currentAnimation.calibration.strideLengthForwards = motion.strideLength;
|
|
} else if (motion.direction === BACKWARDS) {
|
|
motion.currentAnimation.calibration.strideLengthBackwards = motion.strideLength;
|
|
}
|
|
|
|
} else {
|
|
|
|
// use the previously saved value for stride length
|
|
if (motion.direction === FORWARDS) {
|
|
motion.strideLength = motion.currentAnimation.calibration.strideLengthForwards;
|
|
} else if (motion.direction === BACKWARDS) {
|
|
motion.strideLength = motion.currentAnimation.calibration.strideLengthBackwards;
|
|
}
|
|
}
|
|
} // end get walk stride length
|
|
|
|
// sidestepping? get the stride length
|
|
if (motion.currentAnimation === motion.selectedSideStepLeft ||
|
|
motion.currentAnimation === motion.selectedSideStepRight) {
|
|
|
|
// if the timing's right, take a snapshot of the stride max and recalibrate the stride length
|
|
var tolerance = 1.0;
|
|
if (motion.direction === LEFT) {
|
|
|
|
if (motion.frequencyTimeWheelPos < motion.currentAnimation.calibration.strideMaxAt + tolerance &&
|
|
motion.frequencyTimeWheelPos > motion.currentAnimation.calibration.strideMaxAt - tolerance &&
|
|
atMaxSpeed && motion.currentTransition === nullTransition) {
|
|
|
|
var footRPos = MyAvatar.getJointPosition("RightFoot");
|
|
var footLPos = MyAvatar.getJointPosition("LeftFoot");
|
|
motion.strideLength = Vec3.distance(footRPos, footLPos);
|
|
motion.currentAnimation.calibration.strideLength = motion.strideLength;
|
|
|
|
} else {
|
|
|
|
motion.strideLength = motion.selectedSideStepLeft.calibration.strideLength;
|
|
}
|
|
|
|
} else if (motion.direction === RIGHT) {
|
|
|
|
if (motion.frequencyTimeWheelPos < motion.currentAnimation.calibration.strideMaxAt + tolerance &&
|
|
motion.frequencyTimeWheelPos > motion.currentAnimation.calibration.strideMaxAt - tolerance &&
|
|
atMaxSpeed && motion.currentTransition === nullTransition) {
|
|
|
|
var footRPos = MyAvatar.getJointPosition("RightFoot");
|
|
var footLPos = MyAvatar.getJointPosition("LeftFoot");
|
|
motion.strideLength = Vec3.distance(footRPos, footLPos);
|
|
motion.currentAnimation.calibration.strideLength = motion.strideLength;
|
|
|
|
} else {
|
|
|
|
motion.strideLength = motion.selectedSideStepRight.calibration.strideLength;
|
|
}
|
|
}
|
|
} // end get sidestep stride length
|
|
}
|
|
|
|
// initialise a new transition. update progress of a live transition
|
|
function updateTransition() {
|
|
|
|
if (motion.currentTransition !== nullTransition) {
|
|
|
|
// new transition?
|
|
if (motion.currentTransition.progress === 0) {
|
|
|
|
// overlapping transitions?
|
|
if (motion.currentTransition.lastTransition !== nullTransition) {
|
|
|
|
// is the last animation for the nested transition the same as the new animation?
|
|
if (motion.currentTransition.lastTransition.lastAnimation === motion.currentAnimation) {
|
|
|
|
// sync the nested transitions's frequency time wheel for a smooth animation blend
|
|
motion.frequencyTimeWheelPos = motion.currentTransition.lastTransition.lastFrequencyTimeWheelPos;
|
|
}
|
|
}
|
|
|
|
if (motion.currentTransition.lastAnimation === motion.selectedWalk) {
|
|
|
|
// decide at which angle we should stop the frequency time wheel
|
|
var stopAngle = motion.selectedWalk.calibration.stopAngleForwards;
|
|
var percentToMove = 0;
|
|
var lastFrequencyTimeWheelPos = motion.currentTransition.lastFrequencyTimeWheelPos;
|
|
var lastElapsedFTDegrees = motion.currentTransition.lastElapsedFTDegrees;
|
|
|
|
// set the stop angle depending on which quadrant of the walk cycle we are currently in
|
|
// and decide whether we need to take an extra step to complete the walk cycle or not
|
|
// - currently work in progress
|
|
if(lastFrequencyTimeWheelPos <= stopAngle && lastElapsedFTDegrees < 180) {
|
|
|
|
// we have not taken a complete step yet, so we do need to do so before stopping
|
|
percentToMove = 100;
|
|
stopAngle += 180;
|
|
|
|
} else if(lastFrequencyTimeWheelPos > stopAngle && lastFrequencyTimeWheelPos <= stopAngle + 90) {
|
|
|
|
// take an extra step to complete the walk cycle and stop at the second stop angle
|
|
percentToMove = 100;
|
|
stopAngle += 180;
|
|
|
|
} else if(lastFrequencyTimeWheelPos > stopAngle + 90 && lastFrequencyTimeWheelPos <= stopAngle + 180) {
|
|
|
|
// stop on the other foot at the second stop angle for this walk cycle
|
|
percentToMove = 0;
|
|
if (motion.currentTransition.lastDirection === BACKWARDS) {
|
|
|
|
percentToMove = 100;
|
|
|
|
} else {
|
|
|
|
stopAngle += 180;
|
|
}
|
|
|
|
} else if(lastFrequencyTimeWheelPos > stopAngle + 180 && lastFrequencyTimeWheelPos <= stopAngle + 270) {
|
|
|
|
// take an extra step to complete the walk cycle and stop at the first stop angle
|
|
percentToMove = 100;
|
|
}
|
|
|
|
// set it all in motion
|
|
motion.currentTransition.stopAngle = stopAngle;
|
|
motion.currentTransition.percentToMove = percentToMove;
|
|
}
|
|
|
|
} // end if new transition
|
|
|
|
// update the Transition progress
|
|
if (motion.currentTransition.updateProgress() >= 1) {
|
|
|
|
// it's time to kill off this transition
|
|
delete motion.currentTransition;
|
|
motion.currentTransition = nullTransition;
|
|
}
|
|
}
|
|
}
|
|
|
|
// helper function for animateAvatar(). calculate the amount to lean forwards (or backwards) based on the avi's acceleration
|
|
function getLeanPitch(speed) {
|
|
|
|
var leanProgress = undefined;
|
|
|
|
if (motion.direction === LEFT ||
|
|
motion.direction === RIGHT ||
|
|
motion.direction === UP) {
|
|
|
|
leanProgress = 0;
|
|
|
|
} else {
|
|
|
|
// boost lean for flying (leaning whilst walking is work in progress)
|
|
var signedSpeed = -Vec3.multiplyQbyV(Quat.inverse(MyAvatar.orientation), MyAvatar.getVelocity()).z;
|
|
var reverseMod = 1;
|
|
var BOOST = 6;
|
|
|
|
if (signedSpeed < 0) {
|
|
|
|
reverseMod = -1.9;
|
|
if (motion.direction === DOWN) {
|
|
|
|
reverseMod *= -1;
|
|
}
|
|
}
|
|
leanProgress = reverseMod * BOOST * speed / TOP_SPEED;
|
|
}
|
|
|
|
// use filters to shape the walking acceleration response
|
|
leanProgress = leanPitchFilter.process(leanProgress);
|
|
leanProgress = leanPitchSmoothingFilter.process(leanProgress);
|
|
return motion.calibration.pitchMax * leanProgress;
|
|
}
|
|
|
|
// helper function for animateAvatar(). calculate the angle at which to bank into corners whilst turning
|
|
function getLeanRoll(deltaTime, speed) {
|
|
|
|
var leanRollProgress = 0;
|
|
var angularVelocity = filter.radToDeg(MyAvatar.getAngularVelocity().y);
|
|
|
|
leanRollProgress = speed / TOP_SPEED;
|
|
leanRollProgress *= Math.abs(angularVelocity) / motion.calibration.angularVelocityMax;
|
|
|
|
// shape the response curve
|
|
leanRollProgress = filter.bezier((1 - leanRollProgress),
|
|
{x: 0, y: 0}, {x: 0, y: 1.3}, {x: 0.25, y: 0.5}, {x: 1, y: 1}).y;
|
|
|
|
// which way to lean?
|
|
var turnSign = -1;
|
|
if (angularVelocity < 0.001) {
|
|
|
|
turnSign = 1;
|
|
}
|
|
if (motion.direction === BACKWARDS ||
|
|
motion.direction === LEFT) {
|
|
|
|
turnSign *= -1;
|
|
}
|
|
|
|
// add damping with averaging filter
|
|
leanRollProgress = leanRollSmoothingFilter.process(turnSign * leanRollProgress);
|
|
return motion.calibration.rollMax * leanRollProgress;
|
|
}
|
|
|
|
// check for existence of object property (e.g. animation waveform synthesis filters)
|
|
function isDefined(value) {
|
|
|
|
try {
|
|
if (typeof value != 'undefined') return true;
|
|
} catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// helper function for animateAvatar(). calculate joint translations based on animation file settings and frequency * time
|
|
function calculateTranslations(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 JointModifiers(joint, direction);
|
|
|
|
// calculate translations. Use synthesis filters where specified by the animation 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(ft + joint.swayPhase)) +
|
|
joint.swayOffset;
|
|
|
|
} else {
|
|
|
|
jointTranslations.x = joint.sway *
|
|
Math.sin(filter.degToRad(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 + modifiers.bobReverseModifier)) +
|
|
joint.bobOffset;
|
|
|
|
} else {
|
|
|
|
jointTranslations.y = joint.bob *
|
|
Math.sin(filter.degToRad(modifiers.bobFrequencyMultiplier * ft +
|
|
joint.bobPhase + modifiers.bobReverseModifier)) +
|
|
joint.bobOffset;
|
|
|
|
// check for specified low pass filter for this joint (currently Hips bob only)
|
|
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 animateAvatar(). calculate joint rotations based on animation file settings and frequency * time
|
|
function calculateRotations(jointName, animation, ft, direction) {
|
|
|
|
var joint = animation.joints[jointName];
|
|
var jointRotations = {x:0, y:0, z:0};
|
|
|
|
// gather modifiers and multipliers for this joint
|
|
modifiers = new JointModifiers(joint, direction);
|
|
|
|
// calculate rotations. Use synthesis filters where specified by the animation file.
|
|
|
|
// calculate pitch
|
|
if(animation.filters.hasOwnProperty(jointName) &&
|
|
'pitchFilter' in animation.filters[jointName]) {
|
|
|
|
jointRotations.x = modifiers.pitchReverseInvert * (modifiers.pitchSign * joint.pitch *
|
|
animation.filters[jointName].pitchFilter.calculate
|
|
(filter.degToRad(ft * modifiers.pitchFrequencyMultiplier +
|
|
joint.pitchPhase + modifiers.pitchPhaseModifier + modifiers.pitchReverseModifier)) +
|
|
modifiers.pitchOffsetSign * joint.pitchOffset);
|
|
|
|
} else {
|
|
|
|
jointRotations.x = modifiers.pitchReverseInvert * (modifiers.pitchSign * joint.pitch *
|
|
Math.sin(filter.degToRad(ft * modifiers.pitchFrequencyMultiplier +
|
|
joint.pitchPhase + modifiers.pitchPhaseModifier + modifiers.pitchReverseModifier)) +
|
|
modifiers.pitchOffsetSign * joint.pitchOffset);
|
|
}
|
|
|
|
// calculate yaw
|
|
if(animation.filters.hasOwnProperty(jointName) &&
|
|
'yawFilter' in animation.filters[jointName]) {
|
|
|
|
jointRotations.y = modifiers.yawSign * joint.yaw *
|
|
animation.filters[jointName].yawFilter.calculate
|
|
(filter.degToRad(ft + joint.yawPhase + modifiers.yawReverseModifier)) +
|
|
modifiers.yawOffsetSign * joint.yawOffset;
|
|
|
|
} else {
|
|
|
|
jointRotations.y = modifiers.yawSign * joint.yaw *
|
|
Math.sin(filter.degToRad(ft + joint.yawPhase + modifiers.yawReverseModifier)) +
|
|
modifiers.yawOffsetSign * joint.yawOffset;
|
|
}
|
|
|
|
// calculate roll
|
|
if(animation.filters.hasOwnProperty(jointName) &&
|
|
'rollFilter' in animation.filters[jointName]) {
|
|
|
|
jointRotations.z = modifiers.rollSign * joint.roll *
|
|
animation.filters[jointName].rollFilter.calculate
|
|
(filter.degToRad(ft + joint.rollPhase + modifiers.rollReverseModifier)) +
|
|
modifiers.rollOffsetSign * joint.rollOffset;
|
|
|
|
} else {
|
|
|
|
jointRotations.z = modifiers.rollSign * joint.roll *
|
|
Math.sin(filter.degToRad(ft + joint.rollPhase + modifiers.rollReverseModifier)) +
|
|
modifiers.rollOffsetSign * joint.rollOffset;
|
|
}
|
|
return jointRotations;
|
|
}
|
|
|
|
// animate the avatar using sine waves, geometric waveforms and harmonic generators
|
|
function animateAvatar(deltaTime, speed) {
|
|
|
|
// adjust leaning direction for flying
|
|
var flyingModifier = 1;
|
|
if (state.currentState.FLYING) {
|
|
|
|
flyingModifier = -1;
|
|
}
|
|
|
|
// leaning in response to speed and acceleration (affects Hips pitch and z-axis acceleration driven offset)
|
|
var leanPitch = getLeanPitch(speed);
|
|
|
|
// hips translations
|
|
var hipsTranslations = undefined;
|
|
if (motion.currentTransition !== nullTransition) {
|
|
|
|
hipsTranslations = motion.currentTransition.blendTranslations(motion.frequencyTimeWheelPos, motion.direction);
|
|
|
|
} else {
|
|
|
|
hipsTranslations = calculateTranslations(motion.currentAnimation,
|
|
motion.frequencyTimeWheelPos,
|
|
motion.direction);
|
|
}
|
|
|
|
// factor in any acceleration driven leaning
|
|
hipsTranslations.z += motion.calibration.hipsToFeet * Math.sin(filter.degToRad(leanPitch));
|
|
|
|
// apply translations
|
|
MyAvatar.setSkeletonOffset(hipsTranslations);
|
|
|
|
// joint rotations
|
|
for (jointName in walkAssets.animationReference.joints) {
|
|
|
|
// ignore arms rotations if 'arms free' is selected for Leap / Hydra use
|
|
if((walkAssets.animationReference.joints[jointName].IKChain === "LeftArm" ||
|
|
walkAssets.animationReference.joints[jointName].IKChain === "RightArm") &&
|
|
motion.armsFree) {
|
|
|
|
continue;
|
|
}
|
|
|
|
if (isDefined(motion.currentAnimation.joints[jointName])) {
|
|
|
|
var jointRotations = undefined;
|
|
|
|
// if there's a live transition, blend the rotations with the last animation's rotations for this joint
|
|
if (motion.currentTransition !== nullTransition) {
|
|
|
|
jointRotations = motion.currentTransition.blendRotations(jointName,
|
|
motion.frequencyTimeWheelPos,
|
|
motion.direction);
|
|
} else {
|
|
|
|
jointRotations = calculateRotations(jointName,
|
|
motion.currentAnimation,
|
|
motion.frequencyTimeWheelPos,
|
|
motion.direction);
|
|
}
|
|
|
|
// apply lean
|
|
if (jointName === "Hips") {
|
|
|
|
jointRotations.x += leanPitch;
|
|
jointRotations.z += getLeanRoll(deltaTime, speed);
|
|
}
|
|
|
|
// apply rotation
|
|
MyAvatar.setJointData(jointName, Quat.fromVec3Degrees(jointRotations));
|
|
}
|
|
}
|
|
} |