mirror of
https://github.com/overte-org/overte.git
synced 2025-04-09 10:22:26 +02:00
852 lines
31 KiB
JavaScript
852 lines
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));
|
|
}
|
|
}
|
|
}
|