mirror of
https://github.com/lubosz/overte.git
synced 2025-04-05 09:25:59 +02:00
453 lines
20 KiB
JavaScript
453 lines
20 KiB
JavaScript
//
|
|
// walk.js
|
|
// version 1.25
|
|
//
|
|
// Created by David Wooldridge, June 2015
|
|
// Copyright © 2014 - 2015 High Fidelity, Inc.
|
|
//
|
|
// Animates an avatar using procedural animation techniques.
|
|
//
|
|
// 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
|
|
//
|
|
|
|
// animations, reach poses, reach pose parameters, transitions, transition parameters, sounds, image/s and reference files
|
|
var pathToAssets = Script.getExternalPath(Script.ExternalPaths.Assets, "procedural-animator/assets/");
|
|
|
|
Script.include([
|
|
"./libraries/walkConstants.js",
|
|
"./libraries/walkFilters.js",
|
|
"./libraries/walkApi.js",
|
|
pathToAssets + "walkAssets.js"
|
|
]);
|
|
|
|
// construct Avatar, Motion and (null) Transition
|
|
var avatar = new Avatar();
|
|
var motion = new Motion();
|
|
var nullTransition = new Transition();
|
|
motion.currentTransition = nullTransition;
|
|
|
|
// create settings (gets initial values from avatar)
|
|
Script.include("./libraries/walkSettings.js");
|
|
|
|
// Main loop
|
|
Script.update.connect(function(deltaTime) {
|
|
|
|
if (motion.isLive) {
|
|
|
|
// assess current locomotion state
|
|
motion.assess(deltaTime);
|
|
|
|
// decide which animation should be playing
|
|
selectAnimation();
|
|
|
|
// advance the animation cycle/s by the correct amount/s
|
|
advanceAnimations();
|
|
|
|
// update the progress of any live transitions
|
|
updateTransitions();
|
|
|
|
// apply translation and rotations
|
|
renderMotion();
|
|
|
|
// save this frame's parameters
|
|
motion.saveHistory();
|
|
}
|
|
});
|
|
|
|
// helper function for selectAnimation()
|
|
function setTransition(nextAnimation, playTransitionReachPoses) {
|
|
var lastTransition = motion.currentTransition;
|
|
var lastAnimation = avatar.currentAnimation;
|
|
|
|
// if already transitioning from a blended walk need to maintain the previous walk's direction
|
|
if (lastAnimation.lastDirection) {
|
|
switch(lastAnimation.lastDirection) {
|
|
|
|
case FORWARDS:
|
|
lastAnimation = avatar.selectedWalk;
|
|
break;
|
|
|
|
case BACKWARDS:
|
|
lastAnimation = avatar.selectedWalkBackwards;
|
|
break;
|
|
|
|
case LEFT:
|
|
lastAnimation = avatar.selectedSideStepLeft;
|
|
break;
|
|
|
|
case RIGHT:
|
|
lastAnimation = avatar.selectedSideStepRight;
|
|
break;
|
|
}
|
|
}
|
|
|
|
motion.currentTransition = new Transition(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses);
|
|
avatar.currentAnimation = nextAnimation;
|
|
|
|
// reset default first footstep
|
|
if (nextAnimation === avatar.selectedWalkBlend && lastTransition === nullTransition) {
|
|
avatar.nextStep = RIGHT;
|
|
}
|
|
}
|
|
|
|
// fly animation blending: smoothing / damping filters
|
|
const FLY_BLEND_DAMPING = 50;
|
|
var flyUpFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
|
var flyDownFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
|
var flyForwardFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
|
var flyBackwardFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING);
|
|
|
|
// select / blend the appropriate animation for the current state of motion
|
|
function selectAnimation() {
|
|
var playTransitionReachPoses = true;
|
|
|
|
// select appropriate animation. create transitions where appropriate
|
|
switch (motion.nextState) {
|
|
case STATIC: {
|
|
if (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD &&
|
|
avatar.currentAnimation !== avatar.selectedIdle) {
|
|
setTransition(avatar.selectedIdle, playTransitionReachPoses);
|
|
} else if (!(avatar.distanceFromSurface < ON_SURFACE_THRESHOLD) &&
|
|
avatar.currentAnimation !== avatar.selectedHover) {
|
|
setTransition(avatar.selectedHover, playTransitionReachPoses);
|
|
}
|
|
motion.state = STATIC;
|
|
avatar.selectedWalkBlend.lastDirection = NONE;
|
|
break;
|
|
}
|
|
|
|
case SURFACE_MOTION: {
|
|
// walk transition reach poses are currently only specified for starting to walk forwards
|
|
playTransitionReachPoses = (motion.direction === FORWARDS);
|
|
var isAlreadyWalking = (avatar.currentAnimation === avatar.selectedWalkBlend);
|
|
|
|
switch (motion.direction) {
|
|
case FORWARDS:
|
|
if (avatar.selectedWalkBlend.lastDirection !== FORWARDS) {
|
|
animationOperations.deepCopy(avatar.selectedWalk, avatar.selectedWalkBlend);
|
|
avatar.calibration.strideLength = avatar.selectedWalk.calibration.strideLength;
|
|
}
|
|
avatar.selectedWalkBlend.lastDirection = FORWARDS;
|
|
break;
|
|
|
|
case BACKWARDS:
|
|
if (avatar.selectedWalkBlend.lastDirection !== BACKWARDS) {
|
|
animationOperations.deepCopy(avatar.selectedWalkBackwards, avatar.selectedWalkBlend);
|
|
avatar.calibration.strideLength = avatar.selectedWalkBackwards.calibration.strideLength;
|
|
}
|
|
avatar.selectedWalkBlend.lastDirection = BACKWARDS;
|
|
break;
|
|
|
|
case LEFT:
|
|
animationOperations.deepCopy(avatar.selectedSideStepLeft, avatar.selectedWalkBlend);
|
|
avatar.selectedWalkBlend.lastDirection = LEFT;
|
|
avatar.calibration.strideLength = avatar.selectedSideStepLeft.calibration.strideLength;
|
|
break
|
|
|
|
case RIGHT:
|
|
animationOperations.deepCopy(avatar.selectedSideStepRight, avatar.selectedWalkBlend);
|
|
avatar.selectedWalkBlend.lastDirection = RIGHT;
|
|
avatar.calibration.strideLength = avatar.selectedSideStepRight.calibration.strideLength;
|
|
break;
|
|
|
|
default:
|
|
// condition occurs when the avi goes through the floor due to collision hull errors
|
|
animationOperations.deepCopy(avatar.selectedWalk, avatar.selectedWalkBlend);
|
|
avatar.selectedWalkBlend.lastDirection = FORWARDS;
|
|
avatar.calibration.strideLength = avatar.selectedWalk.calibration.strideLength;
|
|
break;
|
|
}
|
|
|
|
if (!isAlreadyWalking && !motion.isComingToHalt) {
|
|
setTransition(avatar.selectedWalkBlend, playTransitionReachPoses);
|
|
}
|
|
motion.state = SURFACE_MOTION;
|
|
break;
|
|
}
|
|
|
|
case AIR_MOTION: {
|
|
// blend the up, down, forward and backward flying animations relative to motion speed and direction
|
|
animationOperations.zeroAnimation(avatar.selectedFlyBlend);
|
|
|
|
// calculate influences based on velocity and direction
|
|
var velocityMagnitude = Vec3.length(motion.velocity);
|
|
var verticalProportion = motion.velocity.y / velocityMagnitude;
|
|
var thrustProportion = motion.velocity.z / velocityMagnitude / 2;
|
|
|
|
// directional components
|
|
var upComponent = motion.velocity.y > 0 ? verticalProportion : 0;
|
|
var downComponent = motion.velocity.y < 0 ? -verticalProportion : 0;
|
|
var forwardComponent = motion.velocity.z < 0 ? -thrustProportion : 0;
|
|
var backwardComponent = motion.velocity.z > 0 ? thrustProportion : 0;
|
|
|
|
// smooth / damp directional components to add visual 'weight'
|
|
upComponent = flyUpFilter.process(upComponent);
|
|
downComponent = flyDownFilter.process(downComponent);
|
|
forwardComponent = flyForwardFilter.process(forwardComponent);
|
|
backwardComponent = flyBackwardFilter.process(backwardComponent);
|
|
|
|
// normalise directional components
|
|
var normaliser = upComponent + downComponent + forwardComponent + backwardComponent;
|
|
upComponent = upComponent / normaliser;
|
|
downComponent = downComponent / normaliser;
|
|
forwardComponent = forwardComponent / normaliser;
|
|
backwardComponent = backwardComponent / normaliser;
|
|
|
|
// blend animations proportionally
|
|
if (upComponent > 0) {
|
|
animationOperations.blendAnimation(avatar.selectedFlyUp,
|
|
avatar.selectedFlyBlend,
|
|
upComponent);
|
|
}
|
|
if (downComponent > 0) {
|
|
animationOperations.blendAnimation(avatar.selectedFlyDown,
|
|
avatar.selectedFlyBlend,
|
|
downComponent);
|
|
}
|
|
if (forwardComponent > 0) {
|
|
animationOperations.blendAnimation(avatar.selectedFly,
|
|
avatar.selectedFlyBlend,
|
|
Math.abs(forwardComponent));
|
|
}
|
|
if (backwardComponent > 0) {
|
|
animationOperations.blendAnimation(avatar.selectedFlyBackwards,
|
|
avatar.selectedFlyBlend,
|
|
Math.abs(backwardComponent));
|
|
}
|
|
|
|
if (avatar.currentAnimation !== avatar.selectedFlyBlend) {
|
|
setTransition(avatar.selectedFlyBlend, playTransitionReachPoses);
|
|
}
|
|
motion.state = AIR_MOTION;
|
|
avatar.selectedWalkBlend.lastDirection = NONE;
|
|
break;
|
|
}
|
|
} // end switch next state of motion
|
|
}
|
|
|
|
// determine the length of stride. advance the frequency time wheels. advance frequency time wheels for any live transitions
|
|
function advanceAnimations() {
|
|
var wheelAdvance = 0;
|
|
|
|
// turn the frequency time wheel
|
|
if (avatar.currentAnimation === avatar.selectedWalkBlend) {
|
|
// 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 and circumference = 2 * stride length
|
|
var speed = Vec3.length(motion.velocity);
|
|
motion.frequencyTimeWheelRadius = avatar.calibration.strideLength / Math.PI;
|
|
var ftWheelAngularVelocity = speed / motion.frequencyTimeWheelRadius;
|
|
// calculate the degrees turned (at this angular speed) since last frame
|
|
wheelAdvance = filter.radToDeg(motion.deltaTime * ftWheelAngularVelocity);
|
|
} else {
|
|
// turn the frequency time wheel by the amount specified for this animation
|
|
wheelAdvance = filter.radToDeg(avatar.currentAnimation.calibration.frequency * motion.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.selectedWalkBlend) {
|
|
// 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(motion.deltaTime);
|
|
}
|
|
|
|
// avoid unnaturally fast walking when landing at speed - simulates skimming / skidding
|
|
if (Math.abs(wheelAdvance) > MAX_FT_WHEEL_INCREMENT) {
|
|
wheelAdvance = 0;
|
|
}
|
|
|
|
// advance the walk wheel the appropriate amount
|
|
motion.advanceFrequencyTimeWheel(wheelAdvance);
|
|
|
|
// walking? then see if it's a good time to measure the stride length (needs to be at least 97% of max walking speed)
|
|
const ALMOST_ONE = 0.97;
|
|
if (avatar.currentAnimation === avatar.selectedWalkBlend &&
|
|
(Vec3.length(motion.velocity) / MAX_WALK_SPEED > ALMOST_ONE)) {
|
|
|
|
var strideMaxAt = avatar.currentAnimation.calibration.strideMaxAt;
|
|
const TOLERANCE = 1.0;
|
|
|
|
if (motion.frequencyTimeWheelPos < (strideMaxAt + TOLERANCE) &&
|
|
motion.frequencyTimeWheelPos > (strideMaxAt - TOLERANCE) &&
|
|
motion.currentTransition === nullTransition) {
|
|
// measure and save stride length
|
|
var footRPos = MyAvatar.getJointPosition("RightFoot");
|
|
var footLPos = MyAvatar.getJointPosition("LeftFoot");
|
|
avatar.calibration.strideLength = Vec3.distance(footRPos, footLPos);
|
|
avatar.currentAnimation.calibration.strideLength = avatar.calibration.strideLength;
|
|
} else {
|
|
// use the previously saved value for stride length
|
|
avatar.calibration.strideLength = avatar.currentAnimation.calibration.strideLength;
|
|
}
|
|
} // end get walk stride length
|
|
}
|
|
|
|
// initialise a new transition. update progress of a live transition
|
|
function updateTransitions() {
|
|
|
|
if (motion.currentTransition !== nullTransition) {
|
|
// is this a new transition?
|
|
if (motion.currentTransition.progress === 0) {
|
|
// do we have 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 === avatar.currentAnimation) {
|
|
// then sync the nested transition's frequency time wheel for a smooth animation blend
|
|
motion.frequencyTimeWheelPos = motion.currentTransition.lastTransition.lastFrequencyTimeWheelPos;
|
|
}
|
|
}
|
|
}
|
|
if (motion.currentTransition.updateProgress() === TRANSITION_COMPLETE) {
|
|
motion.currentTransition = nullTransition;
|
|
}
|
|
}
|
|
}
|
|
|
|
// helper function for renderMotion(). calculate the amount to lean forwards (or backwards) based on the avi's velocity
|
|
var leanPitchSmoothingFilter = filter.createButterworthFilter();
|
|
function getLeanPitch() {
|
|
var leanProgress = 0;
|
|
|
|
if (motion.direction === DOWN ||
|
|
motion.direction === FORWARDS ||
|
|
motion.direction === BACKWARDS) {
|
|
leanProgress = -motion.velocity.z / TOP_SPEED;
|
|
}
|
|
// use filters to shape the walking acceleration response
|
|
leanProgress = leanPitchSmoothingFilter.process(leanProgress);
|
|
return PITCH_MAX * leanProgress;
|
|
}
|
|
|
|
// helper function for renderMotion(). calculate the angle at which to bank into corners whilst turning
|
|
var leanRollSmoothingFilter = filter.createButterworthFilter();
|
|
function getLeanRoll() {
|
|
var leanRollProgress = 0;
|
|
var linearContribution = 0;
|
|
const LOG_SCALER = 8;
|
|
|
|
if (Vec3.length(motion.velocity) > 0) {
|
|
linearContribution = (Math.log(Vec3.length(motion.velocity) / TOP_SPEED) + LOG_SCALER) / LOG_SCALER;
|
|
}
|
|
var angularContribution = Math.abs(motion.yawDelta) / DELTA_YAW_MAX;
|
|
leanRollProgress = linearContribution;
|
|
leanRollProgress *= angularContribution;
|
|
// shape the response curve
|
|
leanRollProgress = filter.bezier(leanRollProgress, {x: 1, y: 0}, {x: 1, y: 0});
|
|
// which way to lean?
|
|
var turnSign = (motion.yawDelta >= 0) ? 1 : -1;
|
|
|
|
if (motion.direction === BACKWARDS ||
|
|
motion.direction === LEFT) {
|
|
turnSign *= -1;
|
|
}
|
|
// filter progress
|
|
leanRollProgress = leanRollSmoothingFilter.process(turnSign * leanRollProgress);
|
|
return ROLL_MAX * leanRollProgress;
|
|
}
|
|
|
|
// animate the avatar using sine waves, geometric waveforms and harmonic generators
|
|
function renderMotion() {
|
|
// leaning in response to speed and acceleration
|
|
var leanPitch = motion.state === STATIC ? 0 : getLeanPitch();
|
|
var leanRoll = motion.state === STATIC ? 0 : getLeanRoll();
|
|
var lastDirection = motion.lastDirection;
|
|
// hips translations from currently playing animations
|
|
var hipsTranslations = {x:0, y:0, z:0};
|
|
|
|
if (motion.currentTransition !== nullTransition) {
|
|
// maintain previous direction when transitioning from a walk
|
|
if (motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
|
motion.lastDirection = motion.currentTransition.lastDirection;
|
|
}
|
|
hipsTranslations = motion.currentTransition.blendTranslations(motion.frequencyTimeWheelPos,
|
|
motion.lastDirection);
|
|
} else {
|
|
hipsTranslations = animationOperations.calculateTranslations(avatar.currentAnimation,
|
|
motion.frequencyTimeWheelPos,
|
|
motion.direction);
|
|
}
|
|
// factor any leaning into the hips offset
|
|
hipsTranslations.z += avatar.calibration.hipsToFeet * Math.sin(filter.degToRad(leanPitch));
|
|
hipsTranslations.x += avatar.calibration.hipsToFeet * Math.sin(filter.degToRad(leanRoll));
|
|
|
|
// ensure skeleton offsets are within the 1m limit
|
|
hipsTranslations.x = hipsTranslations.x > 1 ? 1 : hipsTranslations.x;
|
|
hipsTranslations.x = hipsTranslations.x < -1 ? -1 : hipsTranslations.x;
|
|
hipsTranslations.y = hipsTranslations.y > 1 ? 1 : hipsTranslations.y;
|
|
hipsTranslations.y = hipsTranslations.y < -1 ? -1 : hipsTranslations.y;
|
|
hipsTranslations.z = hipsTranslations.z > 1 ? 1 : hipsTranslations.z;
|
|
hipsTranslations.z = hipsTranslations.z < -1 ? -1 : hipsTranslations.z;
|
|
// apply translations
|
|
MyAvatar.setSkeletonOffset(hipsTranslations);
|
|
|
|
// play footfall sound?
|
|
var producingFootstepSounds = (avatar.currentAnimation === avatar.selectedWalkBlend) && avatar.makesFootStepSounds;
|
|
|
|
if (motion.currentTransition !== nullTransition && avatar.makesFootStepSounds) {
|
|
if (motion.currentTransition.nextAnimation === avatar.selectedWalkBlend ||
|
|
motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
|
producingFootstepSounds = true;
|
|
}
|
|
}
|
|
if (producingFootstepSounds) {
|
|
const QUARTER_CYCLE = 90;
|
|
const THREE_QUARTER_CYCLE = 270;
|
|
var ftWheelPosition = motion.frequencyTimeWheelPos;
|
|
|
|
if (motion.currentTransition !== nullTransition &&
|
|
motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
|
|
ftWheelPosition = motion.currentTransition.lastFrequencyTimeWheelPos;
|
|
}
|
|
if (avatar.nextStep === LEFT && ftWheelPosition > THREE_QUARTER_CYCLE) {
|
|
avatar.makeFootStepSound();
|
|
} else if (avatar.nextStep === RIGHT && (ftWheelPosition < THREE_QUARTER_CYCLE && ftWheelPosition > QUARTER_CYCLE)) {
|
|
avatar.makeFootStepSound();
|
|
}
|
|
}
|
|
|
|
// apply joint rotations
|
|
for (jointName in avatar.currentAnimation.joints) {
|
|
var joint = walkAssets.animationReference.joints[jointName];
|
|
var jointRotations = undefined;
|
|
|
|
// ignore arms / head rotations if options are selected in the settings
|
|
if (avatar.armsFree && (joint.IKChain === "LeftArm" || joint.IKChain === "RightArm")) {
|
|
continue;
|
|
}
|
|
if (avatar.headFree && joint.IKChain === "Head") {
|
|
continue;
|
|
}
|
|
|
|
// if there's a live transition, blend the rotations with the last animation's rotations
|
|
if (motion.currentTransition !== nullTransition) {
|
|
jointRotations = motion.currentTransition.blendRotations(jointName,
|
|
motion.frequencyTimeWheelPos,
|
|
motion.lastDirection);
|
|
} else {
|
|
jointRotations = animationOperations.calculateRotations(jointName,
|
|
avatar.currentAnimation,
|
|
motion.frequencyTimeWheelPos,
|
|
motion.direction);
|
|
}
|
|
|
|
// apply angular velocity and speed induced leaning
|
|
if (jointName === "Hips") {
|
|
jointRotations.x += leanPitch;
|
|
jointRotations.z += leanRoll;
|
|
}
|
|
|
|
// apply rotations
|
|
MyAvatar.setJointRotation(jointName, Quat.fromVec3Degrees(jointRotations));
|
|
}
|
|
}
|