Update to walk.js v 1.1

Revised code structure (see /libraries for new files), walk animation
completely overhauled (now using geometric wave generators, Fourier
synthesis and Butterworth LP filtering for selected motion curve
construction), redesigned sidestepping animations, tweaked flying
animations, decreased maximum walk speed in Interface to more natural
human value, improved Hydra support, added support for Leap motion,
animation file sizes optimised, cleared out code used to work around now
fixed bugs, lots of minor tweaks and improvements. walk.js is now under
100,000 characters, but I had to use tabs instead of 4 spaces to get it
below the threshold.
This commit is contained in:
DaveDubUK 2014-11-05 20:11:11 +00:00
parent 8c013a84ff
commit 7a1f7bd418
5 changed files with 5761 additions and 3698 deletions

View file

@ -0,0 +1,409 @@
//
// walkObjects.js
//
// version 1.000
//
// Created by David Wooldridge, Autumn 2014
//
// Motion, state and Transition objects for use by the walk.js script v1.1
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
// constructor for the Motion object
Motion = function() {
this.setGender = function(gender) {
this.avatarGender = gender;
switch(this.avatarGender) {
case MALE:
this.selWalk = walkAssets.maleStandardWalk;
this.selStand = walkAssets.maleStandOne;
this.selFlyUp = walkAssets.maleFlyingUp;
this.selFly = walkAssets.maleFlying;
this.selFlyDown = walkAssets.maleFlyingDown;
this.selSideStepLeft = walkAssets.maleSideStepLeft;
this.selSideStepRight = walkAssets.maleSideStepRight;
this.curAnim = this.selStand;
return;
case FEMALE:
this.selWalk = walkAssets.femaleStandardWalk;
this.selStand = walkAssets.femaleStandOne;
this.selFlyUp = walkAssets.femaleFlyingUp;
this.selFly = walkAssets.femaleFlying;
this.selFlyDown = walkAssets.femaleFlyingDown;
this.selSideStepLeft = walkAssets.femaleSideStepLeft;
this.selSideStepRight = walkAssets.femaleSideStepRight;
this.curAnim = this.selStand;
return;
}
}
this.hydraCheck = function() {
// function courtesy of Thijs Wenker, frisbee.js
var numberOfButtons = Controller.getNumberOfButtons();
var numberOfTriggers = Controller.getNumberOfTriggers();
var numberOfSpatialControls = Controller.getNumberOfSpatialControls();
var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers;
hydrasConnected = (numberOfButtons == 12 && numberOfTriggers == 2 && controllersPerTrigger == 2);
return hydrasConnected;
}
// settings
this.armsFree = this.hydraCheck(); // automatically sets true for Hydra support - temporary fix
this.makesFootStepSounds = true;
this.avatarGender = MALE;
this.motionPitchMax = 60;
this.motionRollMax = 40;
// timing
this.frameStartTime = 0; // used for measuring frame execution times
this.frameExecutionTimeMax = 0; // keep track of the longest frame execution time
this.cumulativeTime = 0.0;
this.lastWalkStartTime = 0;
// selected animations
this.selWalk = walkAssets.maleStandardWalk;
this.selStand = walkAssets.maleStandOne;
this.selFlyUp = walkAssets.maleFlyingUp;
this.selFly = walkAssets.maleFlying;
this.selFlyDown = walkAssets.maleFlyingDown;
this.selSideStepLeft = walkAssets.maleSideStepLeft;
this.selSideStepRight = walkAssets.maleSideStepRight;
// the currently selected animation, joint and transition
this.curAnim = this.selStand;
this.curJointIndex = 0;
this.curTransition = null;
// zero out avi's joints, curl the fingers nicely then take some measurements
this.avatarJointNames = MyAvatar.getJointNames();
if (!this.armsFree) {
for (var i = 0; i < this.avatarJointNames.length; i++) {
if (i > 17 || i < 34)
// left hand fingers
MyAvatar.setJointData(this.avatarJointNames[i], Quat.fromPitchYawRollDegrees(16, 0, 0));
else if (i > 33 || i < 38)
// left hand thumb
MyAvatar.setJointData(this.avatarJointNames[i], Quat.fromPitchYawRollDegrees(4, 0, 0));
else if (i > 41 || i < 58)
// right hand fingers
MyAvatar.setJointData(this.avatarJointNames[i], Quat.fromPitchYawRollDegrees(16, 0, 0));
else if (i > 57 || i < 62)
// right hand thumb
MyAvatar.setJointData(this.avatarJointNames[i], Quat.fromPitchYawRollDegrees(4, 0, 0));
else
// zero out the remaining joints
MyAvatar.clearJointData(this.avatarJointNames[i]);
}
}
this.footRPos = MyAvatar.getJointPosition("RightFoot");
this.hipsToFeet = MyAvatar.getJointPosition("Hips").y - this.footRPos.y;
// walkwheel (foot / ground speed matching)
this.direction = FORWARDS;
this.nextStep = RIGHT;
this.nFrames = 0;
this.strideLength = this.selWalk.calibration.strideLengthForwards;
this.walkWheelPos = 0;
this.advanceWalkWheel = function(angle){
this.walkWheelPos += angle;
if (motion.walkWheelPos >= 360)
this.walkWheelPos = this.walkWheelPos % 360;
}
// last frame history
this.lastDirection = 0;
this.lastVelocity = 0;
}; // end Motion constructor
// finite state machine
state = (function () {
return {
// the finite list of states
STANDING: 1,
WALKING: 2,
SIDE_STEP: 3,
FLYING: 4,
EDIT_WALK_STYLES: 5,
EDIT_WALK_TWEAKS: 6,
EDIT_WALK_JOINTS: 7,
EDIT_STANDING: 8,
EDIT_FLYING: 9,
EDIT_FLYING_UP: 10,
EDIT_FLYING_DOWN: 11,
EDIT_SIDESTEP_LEFT: 12,
EDIT_SIDESTEP_RIGHT: 14,
currentState: this.STANDING,
// status vars
powerOn: true,
minimised: true,
editing:false,
editingTranslation: false,
setInternalState: function(newInternalState) {
switch (newInternalState) {
case this.WALKING:
this.currentState = this.WALKING;
this.editing = false;
motion.lastWalkStartTime = new Date().getTime();
walkInterface.updateMenu();
return;
case this.FLYING:
this.currentState = this.FLYING;
this.editing = false;
motion.lastWalkStartTime = 0;
walkInterface.updateMenu();
return;
case this.SIDE_STEP:
this.currentState = this.SIDE_STEP;
this.editing = false;
motion.lastWalkStartTime = new Date().getTime();
walkInterface.updateMenu();
return;
case this.EDIT_WALK_STYLES:
this.currentState = this.EDIT_WALK_STYLES;
this.editing = true;
motion.lastWalkStartTime = new Date().getTime();
motion.curAnim = motion.selWalk;
walkInterface.updateMenu();
return;
case this.EDIT_WALK_TWEAKS:
this.currentState = this.EDIT_WALK_TWEAKS;
this.editing = true;
motion.lastWalkStartTime = new Date().getTime();
motion.curAnim = motion.selWalk;
walkInterface.updateMenu();
return;
case this.EDIT_WALK_JOINTS:
this.currentState = this.EDIT_WALK_JOINTS;
this.editing = true;
motion.lastWalkStartTime = new Date().getTime();
motion.curAnim = motion.selWalk;
walkInterface.updateMenu();
return;
case this.EDIT_STANDING:
this.currentState = this.EDIT_STANDING;
this.editing = true;
motion.lastWalkStartTime = 0;
motion.curAnim = motion.selStand;
walkInterface.updateMenu();
return;
case this.EDIT_SIDESTEP_LEFT:
this.currentState = this.EDIT_SIDESTEP_LEFT;
this.editing = true;
motion.lastWalkStartTime = new Date().getTime();
motion.curAnim = motion.selSideStepLeft;
walkInterface.updateMenu();
return;
case this.EDIT_SIDESTEP_RIGHT:
this.currentState = this.EDIT_SIDESTEP_RIGHT;
this.editing = true;
motion.lastWalkStartTime = new Date().getTime();
motion.curAnim = motion.selSideStepRight;
walkInterface.updateMenu();
return;
case this.EDIT_FLYING:
this.currentState = this.EDIT_FLYING;
this.editing = true;
motion.lastWalkStartTime = 0;
motion.curAnim = motion.selFly;
walkInterface.updateMenu();
return;
case this.EDIT_FLYING_UP:
this.currentState = this.EDIT_FLYING_UP;
this.editing = true;
motion.lastWalkStartTime = 0;
motion.curAnim = motion.selFlyUp;
walkInterface.updateMenu();
return;
case this.EDIT_FLYING_DOWN:
this.currentState = this.EDIT_FLYING_DOWN;
this.editing = true;
motion.lastWalkStartTime = 0;
motion.curAnim = motion.selFlyDown;
walkInterface.updateMenu();
return;
case this.STANDING:
default:
this.currentState = this.STANDING;
this.editing = false;
motion.lastWalkStartTime = 0;
motion.curAnim = motion.selStand;
walkInterface.updateMenu();
// initialisation - runs at script startup only
if (motion.strideLength === 0) {
motion.setGender(MALE);
if (motion.direction === BACKWARDS)
motion.strideLength = motion.selWalk.calibration.strideLengthBackwards;
else
motion.strideLength = motion.selWalk.calibration.strideLengthForwards;
}
return;
}
}
}
})(); // end state object literal
// constructor for animation Transition
Transition = function(lastAnimation, nextAnimation, reachPoses, transitionDuration, easingLower, easingUpper) {
this.lastAnim = lastAnimation; // name of last animation
if (lastAnimation === motion.selWalk ||
nextAnimation === motion.selSideStepLeft ||
nextAnimation === motion.selSideStepRight)
this.walkingAtStart = true; // boolean - is the last animation a walking animation?
else
this.walkingAtStart = false; // boolean - is the last animation a walking animation?
this.nextAnimation = nextAnimation; // name of next animation
if (nextAnimation === motion.selWalk ||
nextAnimation === motion.selSideStepLeft ||
nextAnimation === motion.selSideStepRight)
this.walkingAtEnd = true; // boolean - is the next animation a walking animation?
else
this.walkingAtEnd = false; // boolean - is the next animation a walking animation?
this.reachPoses = reachPoses; // placeholder / stub: array of reach poses for squash and stretch techniques
this.transitionDuration = transitionDuration; // length of transition (seconds)
this.easingLower = easingLower; // Bezier curve handle (normalised)
this.easingUpper = easingUpper; // Bezier curve handle (normalised)
this.startTime = new Date().getTime(); // Starting timestamp (seconds)
this.progress = 0; // how far are we through the transition?
this.walkWheelIncrement = 6; // how much to turn the walkwheel each frame when transitioning to / from walking
this.walkWheelAdvance = 0; // how many degrees the walk wheel has been advanced during the transition
this.walkStopAngle = 0; // what angle should we stop the walk cycle?
}; // end Transition constructor
walkAssets = (function () {
// path to the sounds used for the footsteps
var _pathToSounds = 'https://s3.amazonaws.com/hifi-public/sounds/Footsteps/';
// read in the sounds
var _footsteps = [];
_footsteps.push(new Sound(_pathToSounds+"FootstepW2Left-12db.wav"));
_footsteps.push(new Sound(_pathToSounds+"FootstepW2Right-12db.wav"));
_footsteps.push(new Sound(_pathToSounds+"FootstepW3Left-12db.wav"));
_footsteps.push(new Sound(_pathToSounds+"FootstepW3Right-12db.wav"));
_footsteps.push(new Sound(_pathToSounds+"FootstepW5Left-12db.wav"));
_footsteps.push(new Sound(_pathToSounds+"FootstepW5Right-12db.wav"));
// load the animation datafiles
Script.include(pathToAssets+"animations/dd-female-standard-walk-animation.js");
Script.include(pathToAssets+"animations/dd-female-flying-up-animation.js");
Script.include(pathToAssets+"animations/dd-female-flying-animation.js");
Script.include(pathToAssets+"animations/dd-female-flying-down-animation.js");
Script.include(pathToAssets+"animations/dd-female-standing-one-animation.js");
Script.include(pathToAssets+"animations/dd-female-sidestep-left-animation.js");
Script.include(pathToAssets+"animations/dd-female-sidestep-right-animation.js");
Script.include(pathToAssets+"animations/dd-male-standard-walk-animation.js");
Script.include(pathToAssets+"animations/dd-male-flying-up-animation.js");
Script.include(pathToAssets+"animations/dd-male-flying-animation.js");
Script.include(pathToAssets+"animations/dd-male-flying-down-animation.js");
Script.include(pathToAssets+"animations/dd-male-standing-one-animation.js");
Script.include(pathToAssets+"animations/dd-male-sidestep-left-animation.js");
Script.include(pathToAssets+"animations/dd-male-sidestep-right-animation.js");
// read in the animation files
var _FemaleStandardWalkFile = new FemaleStandardWalk();
var _femaleStandardWalk = _FemaleStandardWalkFile.loadAnimation();
var _FemaleFlyingUpFile = new FemaleFlyingUp();
var _femaleFlyingUp = _FemaleFlyingUpFile.loadAnimation();
var _FemaleFlyingFile = new FemaleFlying();
var _femaleFlying = _FemaleFlyingFile.loadAnimation();
var _FemaleFlyingDownFile = new FemaleFlyingDown();
var _femaleFlyingDown = _FemaleFlyingDownFile.loadAnimation();
var _FemaleStandOneFile = new FemaleStandingOne();
var _femaleStandOne = _FemaleStandOneFile.loadAnimation();
var _FemaleSideStepLeftFile = new FemaleSideStepLeft();
var _femaleSideStepLeft = _FemaleSideStepLeftFile.loadAnimation();
var _FemaleSideStepRightFile = new FemaleSideStepRight();
var _femaleSideStepRight = _FemaleSideStepRightFile.loadAnimation();
var _MaleStandardWalkFile = new MaleStandardWalk(filter);
var _maleStandardWalk = _MaleStandardWalkFile.loadAnimation();
var _MaleFlyingUpFile = new MaleFlyingUp();
var _maleFlyingUp = _MaleFlyingUpFile.loadAnimation();
var _MaleFlyingFile = new MaleFlying();
var _maleFlying = _MaleFlyingFile.loadAnimation();
var _MaleFlyingDownFile = new MaleFlyingDown();
var _maleFlyingDown = _MaleFlyingDownFile.loadAnimation();
var _MaleStandOneFile = new MaleStandingOne();
var _maleStandOne = _MaleStandOneFile.loadAnimation();
var _MaleSideStepLeftFile = new MaleSideStepLeft();
var _maleSideStepLeft = _MaleSideStepLeftFile.loadAnimation();
var _MaleSideStepRightFile = new MaleSideStepRight();
var _maleSideStepRight = _MaleSideStepRightFile.loadAnimation();
return {
// expose the sound assets
footsteps: _footsteps,
// expose the animation assets
femaleStandardWalk: _femaleStandardWalk,
femaleFlyingUp: _femaleFlyingUp,
femaleFlying: _femaleFlying,
femaleFlyingDown: _femaleFlyingDown,
femaleStandOne: _femaleStandOne,
femaleSideStepLeft: _femaleSideStepLeft,
femaleSideStepRight: _femaleSideStepRight,
maleStandardWalk: _maleStandardWalk,
maleFlyingUp: _maleFlyingUp,
maleFlying: _maleFlying,
maleFlyingDown: _maleFlyingDown,
maleStandOne: _maleStandOne,
maleSideStepLeft: _maleSideStepLeft,
maleSideStepRight: _maleSideStepRight,
}
})();

View file

@ -0,0 +1,230 @@
//
// walkFilters.js
//
// version 1.000
//
// Created by David Wooldridge, Autumn 2014
//
// Provides a variety of filters for use by the walk.js script v1.1
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
AveragingFilter = function(length) {
//this.name = name;
this.pastValues = [];
for(var i = 0; i < length; i++) {
this.pastValues.push(0);
}
// single arg is the nextInputValue
this.process = function() {
if (this.pastValues.length === 0 && arguments[0]) {
return arguments[0];
}
else if (arguments[0]) {
// apply quick and simple LP filtering
this.pastValues.push(arguments[0]);
this.pastValues.shift();
var nextOutputValue = 0;
for (var ea in this.pastValues) nextOutputValue += this.pastValues[ea];
return nextOutputValue / this.pastValues.length;
}
else return 0;
};
};
// 2nd order Butterworth LP filter - calculate coeffs here: http://www-users.cs.york.ac.uk/~fisher/mkfilter/trad.html
// provides LP filtering with a more stable frequency / phase response
ButterworthFilter = function(cutOff) {
switch(cutOff) {
case 5:
this.gain = 20.20612010;
this.coeffOne = -0.4775922501;
this.coeffTwo = 1.2796324250;
break;
}
// initialise the arrays
this.xv = [];
this.yv = [];
for(var i = 0; i < 3; i++) {
this.xv.push(0);
this.yv.push(0);
}
// process values
this.process = function(nextInputValue) {
this.xv[0] = this.xv[1];
this.xv[1] = this.xv[2];
this.xv[2] = nextInputValue / this.gain;
this.yv[0] = this.yv[1];
this.yv[1] = this.yv[2];
this.yv[2] = (this.xv[0] + this.xv[2]) +
2 * this.xv[1] +
(this.coeffOne * this.yv[0]) +
(this.coeffTwo * this.yv[1]);
return this.yv[2];
};
}; // end Butterworth filter contructor
// Add harmonics to a given sine wave to form square, sawtooth or triangle waves
// Geometric wave synthesis fundamentals taken from: http://hyperphysics.phy-astr.gsu.edu/hbase/audio/geowv.html
WaveSynth = function(waveShape, numHarmonics, smoothing) {
this.numHarmonics = numHarmonics;
this.waveShape = waveShape;
this.averagingFilter = new AveragingFilter(smoothing);
// NB: frequency in radians
this.shapeWave = function(frequency) {
// make some shapes
var harmonics = 0;
var multiplier = 0;
var iterations = this.numHarmonics * 2 + 2;
if (this.waveShape === TRIANGLE) iterations++;
for(var n = 2; n < iterations; n++) {
switch(this.waveShape) {
case SAWTOOTH:
multiplier = 1 / n;
harmonics += multiplier * Math.sin(n * frequency);
break;
case TRIANGLE:
if (n % 2 === 1) {
var mulitplier = 1 / (n * n);
// multiply every (4n-1)th harmonic by -1
if (n === 3 || n === 7 || n === 11 || n === 15)
mulitplier *= -1;
harmonics += mulitplier * Math.sin(n * frequency);
}
break;
case SQUARE:
if (n % 2 === 1) {
multiplier = 1 / n;
harmonics += multiplier * Math.sin(n * frequency);
}
break;
}
}
// smooth the result and return
return this.averagingFilter.process(harmonics);
};
};
// Create a wave shape by summing pre-calcualted sinusoidal harmonics
HarmonicsFilter = function(magnitudes, phaseAngles) {
this.magnitudes = magnitudes;
this.phaseAngles = phaseAngles;
this.calculate = function(twoPiFT) {
var harmonics = 0;
var numHarmonics = magnitudes.length;
for(var n = 0; n < numHarmonics; n++)
harmonics += this.magnitudes[n] * Math.cos(n * twoPiFT - this.phaseAngles[n]);
return harmonics;
};
};
// the main filter object literal
filter = (function() {
// Bezier private functions
function _B1(t) { return t * t * t };
function _B2(t) { return 3 * t * t * (1 - t) };
function _B3(t) { return 3 * t * (1 - t) * (1 - t) };
function _B4(t) { return (1 - t) * (1 - t) * (1 - t) };
return {
// helper methods
degToRad: function(degrees) {
var convertedValue = degrees * Math.PI / 180;
return convertedValue;
},
radToDeg: function(radians) {
var convertedValue = radians * 180 / Math.PI;
return convertedValue;
},
// these filters need instantiating, as they hold arrays of previous values
createAveragingFilter: function(length) {
var newAveragingFilter = new AveragingFilter(length);
return newAveragingFilter;
},
createButterworthFilter: function(cutoff) {
var newButterworthFilter = new ButterworthFilter(cutoff);
return newButterworthFilter;
},
createWaveSynth: function(waveShape, numHarmonics, smoothing) {
var newWaveSynth = new WaveSynth(waveShape, numHarmonics, smoothing);
return newWaveSynth;
},
createHarmonicsFilter: function(magnitudes, phaseAngles) {
var newHarmonicsFilter = new HarmonicsFilter(magnitudes, phaseAngles);
return newHarmonicsFilter;
},
// the following filters do not need separate instances, as they hold no previous values
bezier: function(percent, C1, C2, C3, C4) {
// Bezier functions for more natural transitions
// based on script by Dan Pupius (www.pupius.net) http://13thparallel.com/archive/bezier-curves/
var pos = {x: 0, y: 0};
pos.x = C1.x * _B1(percent) + C2.x * _B2(percent) + C3.x * _B3(percent) + C4.x * _B4(percent);
pos.y = C1.y * _B1(percent) + C2.y * _B2(percent) + C3.y * _B3(percent) + C4.y * _B4(percent);
return pos;
},
// simple clipping filter (faster way to make square waveforms)
clipTrough: function(inputValue, peak, strength) {
var outputValue = inputValue * strength;
if (outputValue < -peak)
outputValue = -peak;
return outputValue;
}
}
})();

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -49,7 +49,7 @@ const float PITCH_SPEED = 100.0f; // degrees/sec
const float COLLISION_RADIUS_SCALAR = 1.2f; // pertains to avatar-to-avatar collisions
const float COLLISION_RADIUS_SCALE = 0.125f;
const float MAX_WALKING_SPEED = 4.5f;
const float MAX_WALKING_SPEED = 2.5f; // human walking speed
const float MAX_BOOST_SPEED = 0.5f * MAX_WALKING_SPEED; // keyboard motor gets additive boost below this speed
const float MIN_AVATAR_SPEED = 0.05f; // speed is set to zero below this