diff --git a/examples/html/walkSettings.html b/examples/html/walkSettings.html
new file mode 100644
index 0000000000..6c0eaff2cc
--- /dev/null
+++ b/examples/html/walkSettings.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/libraries/walkApi.js b/examples/libraries/walkApi.js
index f800682af5..2a64f8abed 100644
--- a/examples/libraries/walkApi.js
+++ b/examples/libraries/walkApi.js
@@ -1,229 +1,492 @@
//
-// walkObjects.js
+// walkApi.js
+// version 1.3
//
-// version 1.003
+// Created by David Wooldridge, June 2015
+// Copyright © 2014 - 2015 High Fidelity, Inc.
//
-// Created by David Wooldridge, Autumn 2014
+// Exposes API for use by walk.js version 1.2+.
//
-// Motion, state and Transition objects for use by the walk.js script v1.12
+// 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
//
-// constructor for the Motion object
-Motion = function() {
-
- // selected animations
- this.selectedWalk = undefined;
- this.selectedStand = undefined;
- this.selectedFly = undefined;
- this.selectedFlyUp = undefined;
- this.selectedFlyDown = undefined;
- this.selectedFlyBlend = undefined;
- this.selectedHovering = undefined;
- this.selectedSideStepLeft = undefined;
- this.selectedSideStepRight = undefined;
-
+Avatar = function() {
// if Hydras are connected, the only way to enable use is by never setting any rotations on the arm joints
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;
+ if (numberOfButtons == 12 && numberOfTriggers == 2 && controllersPerTrigger == 2) {
+ print('walk.js info: Razer Hydra detected. Setting arms free (not controlled by script)');
+ return true;
+ } else {
+ print('walk.js info: Razer Hydra not detected. Arms will be controlled by script.');
+ return false;
+ }
}
-
// settings
- this.armsFree = this.hydraCheck(); // automatically sets true for Hydra support - temporary fix
- this.makesFootStepSounds = true;
- this.avatarGender = undefined;
- this.setGender = function(gender) {
-
- this.avatarGender = gender;
-
- switch(this.avatarGender) {
-
- case MALE:
-
- this.selectedWalk = walkAssets.maleStandardWalk;
- this.selectedStand = walkAssets.maleStandOne;
- this.selectedFlyUp = walkAssets.maleFlyingUp;
- this.selectedFly = walkAssets.maleFlying;
- this.selectedFlyDown = walkAssets.maleFlyingDown;
- this.selectedFlyBlend = walkAssets.maleFlyingBlend;
- this.selectedHovering = walkAssets.maleHovering;
- this.selectedSideStepLeft = walkAssets.maleSideStepLeft;
- this.selectedSideStepRight = walkAssets.maleSideStepRight;
- this.currentAnimation = this.selectedStand;
- return;
-
- case FEMALE:
-
- this.selectedWalk = walkAssets.femaleStandardWalk;
- this.selectedStand = walkAssets.femaleStandOne;
- this.selectedFlyUp = walkAssets.femaleFlyingUp;
- this.selectedFly = walkAssets.femaleFlying;
- this.selectedFlyDown = walkAssets.femaleFlyingDown;
- this.selectedFlyBlend = walkAssets.femaleFlyingBlend;
- this.selectedHovering = walkAssets.femaleHovering;
- this.selectedSideStepLeft = walkAssets.femaleSideStepLeft;
- this.selectedSideStepRight = walkAssets.femaleSideStepRight;
- this.currentAnimation = this.selectedStand;
+ this.headFree = true;
+ this.armsFree = this.hydraCheck(); // automatically sets true to enable Hydra support - temporary fix
+ this.makesFootStepSounds = false; // true ? still inexplicably glitchy : fine
+ this.isBlenderExport = false; // temporary fix
+ this.animationSet = undefined; // currently just one animation set
+ this.setAnimationSet = function(animationSet) {
+ this.animationSet = animationSet;
+ switch (animationSet) {
+ case 'standardMale':
+ this.selectedIdle = walkAssets.getAnimationDataFile("MaleIdle");
+ this.selectedWalk = walkAssets.getAnimationDataFile("MaleWalk");
+ this.selectedWalkBackwards = walkAssets.getAnimationDataFile("MaleWalkBackwards");
+ this.selectedSideStepLeft = walkAssets.getAnimationDataFile("MaleSideStepLeft");
+ this.selectedSideStepRight = walkAssets.getAnimationDataFile("MaleSideStepRight");
+ this.selectedWalkBlend = walkAssets.getAnimationDataFile("WalkBlend");
+ this.selectedTurnLeft = walkAssets.getAnimationDataFile("MaleTurnLeft");
+ this.selectedTurnRight = walkAssets.getAnimationDataFile("MaleTurnRight");
+ this.selectedHover = walkAssets.getAnimationDataFile("MaleHover");
+ this.selectedFly = walkAssets.getAnimationDataFile("MaleFly");
+ this.selectedFlyBackwards = walkAssets.getAnimationDataFile("MaleFlyBackwards");
+ this.selectedFlyDown = walkAssets.getAnimationDataFile("MaleFlyDown");
+ this.selectedFlyUp = walkAssets.getAnimationDataFile("MaleFlyUp");
+ this.selectedFlyBlend = walkAssets.getAnimationDataFile("FlyBlend");
+ this.currentAnimation = this.selectedIdle;
return;
}
}
- this.setGender(MALE);
+ this.setAnimationSet('standardMale');
+
+ this.startTime = new Date().getTime();
// calibration
this.calibration = {
+ hipsToFeet: 1.011,
+ strideLength: this.selectedWalk.calibration.strideLength
+ }
+ this.distanceFromSurface = 0;
+ this.calibrate = function() {
+ // Triple check: measurements are taken three times to ensure accuracy - the first result is often too large
+ var attempts = 3;
+ var extraAttempts = 0;
+ do {
+ for (joint in walkAssets.animationReference.joints) {
+ var IKChain = walkAssets.animationReference.joints[joint].IKChain;
- pitchMax: 8,
- maxWalkAcceleration: 5,
- maxWalkDeceleration: 25,
- rollMax: 80,
- angularVelocityMax: 70,
- hipsToFeet: MyAvatar.getJointPosition("Hips").y - MyAvatar.getJointPosition("RightFoot").y,
+ // only need to zero right leg IK chain and hips
+ if (IKChain === "RightLeg" || joint === "Hips" ) {
+ MyAvatar.setJointData(joint, Quat.fromPitchYawRollDegrees(0, 0, 0));
+ }
+ }
+ this.calibration.hipsToFeet = MyAvatar.getJointPosition("Hips").y - MyAvatar.getJointPosition("RightToeBase").y;
+
+ // maybe measuring before Blender pre-rotations have been applied?
+ if (this.calibration.hipsToFeet < 0 && this.isBlenderExport) {
+ this.calibration.hipsToFeet *= -1;
+ }
+
+ if (this.calibration.hipsToFeet === 0 && extraAttempts < 100) {
+ attempts++;
+ extraAttempts++;// Interface can sometimes report zero for hips to feet. if so, we try again.
+ }
+ } while (attempts-- > 1)
+
+ // just in case
+ if (this.calibration.hipsToFeet <= 0 || isNaN(this.calibration.hipsToFeet)) {
+ this.calibration.hipsToFeet = 1.0;
+ print('walk.js error: Unable to get a non-zero measurement for the avatar hips to feet measure. Hips to feet set to default value ('+
+ this.calibration.hipsToFeet.toFixed(3)+'m). This will cause some foot sliding. If your avatar has only just appeared, it is recommended that you re-load the walk script.');
+ } else {
+ print('walk.js info: Hips to feet calibrated to '+this.calibration.hipsToFeet.toFixed(3)+'m');
+ }
+ }
+
+ // pose the fingers
+ this.poseFingers = function() {
+ for (knuckle in walkAssets.animationReference.leftHand) {
+ if (walkAssets.animationReference.leftHand[knuckle].IKChain === "LeftHandThumb") {
+ MyAvatar.setJointData(knuckle, Quat.fromPitchYawRollDegrees(0, 0, -4));
+ } else {
+ MyAvatar.setJointData(knuckle, Quat.fromPitchYawRollDegrees(16, 0, 5));
+ }
+ }
+ for (knuckle in walkAssets.animationReference.rightHand) {
+ if (walkAssets.animationReference.rightHand[knuckle].IKChain === "RightHandThumb") {
+ MyAvatar.setJointData(knuckle, Quat.fromPitchYawRollDegrees(0, 0, 4));
+ } else {
+ MyAvatar.setJointData(knuckle, Quat.fromPitchYawRollDegrees(16, 0, -5));
+ }
+ }
+ };
+ this.calibrate();
+ this.poseFingers();
+
+ // footsteps
+ this.nextStep = RIGHT; // the first step is right, because the waveforms say so
+ this.makeFootStepSound = function() {
+ // correlate footstep volume with avatar speed. place the audio source at the feet, not the hips
+ var SPEED_THRESHOLD = 0.4;
+ var VOLUME_ATTENUATION = 0.8;
+ var MIN_VOLUME = 0.5;
+ var options = {
+ position: Vec3.sum(MyAvatar.position, {x:0, y: -this.calibration.hipsToFeet, z:0}),
+ volume: Vec3.length(motion.velocity) > SPEED_THRESHOLD ?
+ VOLUME_ATTENUATION * Vec3.length(motion.velocity) / MAX_WALK_SPEED : MIN_VOLUME
+ };
+
+ if (this.nextStep === RIGHT) {
+ Audio.playSound(walkAssets.footsteps[0], options);
+ this.nextStep = LEFT;
+ } else if (this.nextStep === LEFT) {
+ Audio.playSound(walkAssets.footsteps[1], options);
+ this.nextStep = RIGHT;
+ }
+ }
+};
+
+// constructor for the Motion object
+Motion = function() {
+ // locomotion status
+ this.state = STATIC;
+ this.nextState = STATIC;
+ this.isMoving = false;
+ this.isWalkingSpeed = false;
+ this.isFlyingSpeed = false;
+ this.isAccelerating = false;
+ this.isDecelerating = false;
+ this.isDeceleratingFast = false;
+ this.isComingToHalt = false;
+ this.directedAcceleration = 0;
+
+ // settings
+ this.isLive = true;
+ this.calibration = {
+ PITCH_MAX: 60,
+ ROLL_MAX: 80,
+ DELTA_YAW_MAX: 1.7
}
// used to make sure at least one step has been taken when transitioning from a walk cycle
this.elapsedFTDegrees = 0;
- // the current animation and transition
- this.currentAnimation = this.selectedStand;
+ // the current transition (any layered transitions are nested within this transition)
this.currentTransition = null;
- // zero out avi's joints and pose the fingers
- this.avatarJointNames = MyAvatar.getJointNames();
- this.poseFingers = function() {
+ // orientation, locomotion and timing
+ this.velocity = {x:0, y:0, z:0};
+ this.acceleration = {x:0, y:0, z:0};
+ this.yaw = Quat.safeEulerAngles(MyAvatar.orientation).y; // MyAvatar.orientation.y; //
+ this.yawDelta = 0;
+ this.yawDeltaAcceleration = 0;
+ this.direction = FORWARDS;
+ this.deltaTime = 0;
- for (var i = 0; i < this.avatarJointNames.length; i++) {
+ // historical orientation, locomotion and timing
+ this.lastDirection = FORWARDS;
+ this.lastVelocity = {x:0, y:0, z:0};
+ this.lastYaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
+ this.lastYawDelta = 0;
+ this.lastYawDeltaAcceleration = 0;
+
+ // Quat.safeEulerAngles(MyAvatar.orientation).y tends to repeat values between frames, so values are filtered
+ var YAW_SMOOTHING = 22;
+ this.yawFilter = filter.createAveragingFilter(YAW_SMOOTHING); //createButterworthFilter(); //
+ this.deltaTimeFilter = filter.createAveragingFilter(YAW_SMOOTHING); //createButterworthFilter(); //
+ this.yawDeltaAccelerationFilter = filter.createAveragingFilter(YAW_SMOOTHING); //createButterworthFilter(); //
- 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));
- }
+ // assess locomotion state
+ this.assess = function(deltaTime) {
+ // calculate avatar frame speed, velocity and acceleration
+ this.deltaTime = deltaTime;
+ this.velocity = Vec3.multiplyQbyV(Quat.inverse(MyAvatar.orientation), MyAvatar.getVelocity());
+ var lateralVelocity = Math.sqrt(Math.pow(this.velocity.x, 2) + Math.pow(this.velocity.z, 2));
+
+ // MyAvatar.getAcceleration() currently not working. bug report submitted: https://worklist.net/20527
+ var acceleration = {x:0, y:0, z:0};
+ this.acceleration.x = (this.velocity.x - this.lastVelocity.x) / deltaTime;
+ this.acceleration.y = (this.velocity.y - this.lastVelocity.y) / deltaTime;
+ this.acceleration.z = (this.velocity.z - this.lastVelocity.z) / deltaTime;
+
+ // MyAvatar.getAngularVelocity and MyAvatar.getAngularAcceleration currently not working. bug report submitted
+ this.yaw = Quat.safeEulerAngles(MyAvatar.orientation).y;
+ if (this.lastYaw < 0 && this.yaw > 0 || this.lastYaw > 0 && this.yaw < 0) {
+ this.lastYaw *= -1;
+ }
+ var timeDelta = this.deltaTimeFilter.process(deltaTime);
+ this.yawDelta = filter.degToRad(this.yawFilter.process(this.lastYaw - this.yaw)) / timeDelta;
+ this.yawDeltaAcceleration = this.yawDeltaAccelerationFilter.process(this.lastYawDelta - this.yawDelta) / timeDelta;
+
+ // how far above the surface is the avatar? (for testing / validation purposes)
+ var pickRay = {origin: MyAvatar.position, direction: {x:0, y:-1, z:0}};
+ var distanceFromSurface = Entities.findRayIntersectionBlocking(pickRay).distance;
+ avatar.distanceFromSurface = distanceFromSurface - avatar.calibration.hipsToFeet;
+
+ // determine principle direction of locomotion
+ var FWD_BACK_BIAS = 100; // helps prevent false sidestep condition detection when banking hard
+ if (Math.abs(this.velocity.x) > Math.abs(this.velocity.y) &&
+ Math.abs(this.velocity.x) > FWD_BACK_BIAS * Math.abs(this.velocity.z)) {
+ if (this.velocity.x < 0) {
+ this.directedAcceleration = -this.acceleration.x;
+ this.direction = LEFT;
+ } else if (this.velocity.x > 0){
+ this.directedAcceleration = this.acceleration.x;
+ this.direction = RIGHT;
+ }
+ } else if (Math.abs(this.velocity.y) > Math.abs(this.velocity.x) &&
+ Math.abs(this.velocity.y) > Math.abs(this.velocity.z)) {
+ if (this.velocity.y > 0) {
+ this.directedAcceleration = this.acceleration.y;
+ this.direction = UP;
+ } else if (this.velocity.y < 0) {
+ this.directedAcceleration = -this.acceleration.y;
+ this.direction = DOWN;
+ }
+ } else if (FWD_BACK_BIAS * Math.abs(this.velocity.z) > Math.abs(this.velocity.x) &&
+ Math.abs(this.velocity.z) > Math.abs(this.velocity.y)) {
+ if (this.velocity.z < 0) {
+ this.direction = FORWARDS;
+ this.directedAcceleration = -this.acceleration.z;
+ } else if (this.velocity.z > 0) {
+ this.directedAcceleration = this.acceleration.z;
+ this.direction = BACKWARDS;
+ }
+ } else {
+ this.direction = NONE;
+ this.directedAcceleration = 0;
+ }
+
+ // set speed flags
+ if (Vec3.length(this.velocity) < MOVE_THRESHOLD) {
+ this.isMoving = false;
+ this.isWalkingSpeed = false;
+ this.isFlyingSpeed = false;
+ this.isComingToHalt = false;
+ } else if (Vec3.length(this.velocity) < MAX_WALK_SPEED) {
+ this.isMoving = true;
+ this.isWalkingSpeed = true;
+ this.isFlyingSpeed = false;
+ } else {
+ this.isMoving = true;
+ this.isWalkingSpeed = false;
+ this.isFlyingSpeed = true;
+ }
+
+ // set acceleration flags
+ if (this.directedAcceleration > ACCELERATION_THRESHOLD) {
+ this.isAccelerating = true;
+ this.isDecelerating = false;
+ this.isDeceleratingFast = false;
+ this.isComingToHalt = false;
+ } else if (this.directedAcceleration < DECELERATION_THRESHOLD) {
+ this.isAccelerating = false;
+ this.isDecelerating = true;
+ this.isDeceleratingFast = (this.directedAcceleration < FAST_DECELERATION_THRESHOLD);
+ } else {
+ this.isAccelerating = false;
+ this.isDecelerating = false;
+ this.isDeceleratingFast = false;
+ }
+
+ // use the gathered information to build up some spatial awareness
+ var isOnSurface = (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD);
+ var isUnderGravity = (avatar.distanceFromSurface < GRAVITY_THRESHOLD);
+ var isTakingOff = (isUnderGravity && this.velocity.y > OVERCOME_GRAVITY_SPEED);
+ var isComingInToLand = (isUnderGravity && this.velocity.y < -OVERCOME_GRAVITY_SPEED);
+ var aboutToLand = isComingInToLand && avatar.distanceFromSurface < LANDING_THRESHOLD;
+ var surfaceMotion = isOnSurface && this.isMoving;
+ var acceleratingAndAirborne = this.isAccelerating && !isOnSurface;
+ var goingTooFastToWalk = !this.isDecelerating && this.isFlyingSpeed;
+ var movingDirectlyUpOrDown = (this.direction === UP || this.direction === DOWN) // && lateralVelocity < MOVE_THRESHOLD;
+ var maybeBouncing = Math.abs(this.acceleration.y > BOUNCE_ACCELERATION_THRESHOLD) ? true : false;
+
+ // we now have enough information to set the appropriate locomotion mode
+ switch (this.state) {
+ case STATIC:
+ var staticToAirMotion = this.isMoving && (acceleratingAndAirborne || goingTooFastToWalk ||
+ (movingDirectlyUpOrDown && !isOnSurface));
+ var staticToSurfaceMotion = surfaceMotion && !motion.isComingToHalt && !movingDirectlyUpOrDown &&
+ !this.isDecelerating && lateralVelocity > MOVE_THRESHOLD;
+ if (staticToAirMotion) {
+ this.nextState = AIR_MOTION;
+ } else if (staticToSurfaceMotion) {
+ this.nextState = SURFACE_MOTION;
+ } else {
+ this.nextState = STATIC;
+ }
+ break;
+
+ case SURFACE_MOTION:
+ var surfaceMotionToStatic = !this.isMoving ||
+ (this.isDecelerating && motion.lastDirection !== DOWN && surfaceMotion &&
+ !maybeBouncing && Vec3.length(this.velocity) < MAX_WALK_SPEED);
+ var surfaceMotionToAirMotion = (acceleratingAndAirborne || goingTooFastToWalk || movingDirectlyUpOrDown) &&
+ (!surfaceMotion && isTakingOff) ||
+ (!surfaceMotion && this.isMoving && !isComingInToLand);
+ if (surfaceMotionToStatic) {
+ // working on the assumption that stopping is now inevitable
+ if (!motion.isComingToHalt && isOnSurface) {
+ motion.isComingToHalt = true;
+ }
+ this.nextState = STATIC;
+ } else if (surfaceMotionToAirMotion) {
+ this.nextState = AIR_MOTION;
+ } else {
+ this.nextState = SURFACE_MOTION;
+ }
+ break;
+
+ case AIR_MOTION:
+ var airMotionToSurfaceMotion = (surfaceMotion || aboutToLand) && !movingDirectlyUpOrDown;
+ var airMotionToStatic = !this.isMoving && this.direction === this.lastDirection; //||
+ //this.isDeceleratingFast || isOnSurface;
+ if (airMotionToSurfaceMotion){
+ this.nextState = SURFACE_MOTION;
+ } else if (airMotionToStatic) {
+ this.nextState = STATIC;
+ } else {
+ this.nextState = AIR_MOTION;
+ }
+ break;
}
- };
- if (!this.armsFree) {
- this.poseFingers();
}
// frequency time wheel (foot / ground speed matching)
- this.direction = FORWARDS;
- this.strideLength = this.selectedWalk.calibration.strideLengthForwards;
this.frequencyTimeWheelPos = 0;
this.frequencyTimeWheelRadius = 0.5;
this.recentFrequencyTimeIncrements = [];
- for(var i = 0; i < 8; i++) {
-
+ for (var i = 0; i < 8; i++) {
this.recentFrequencyTimeIncrements.push(0);
}
this.averageFrequencyTimeIncrement = 0;
this.advanceFrequencyTimeWheel = function(angle){
-
this.elapsedFTDegrees += angle;
-
+ // keep a running average of increments for use in transitions (used during transitioning)
this.recentFrequencyTimeIncrements.push(angle);
this.recentFrequencyTimeIncrements.shift();
- for(increment in this.recentFrequencyTimeIncrements) {
+ for (increment in this.recentFrequencyTimeIncrements) {
this.averageFrequencyTimeIncrement += this.recentFrequencyTimeIncrements[increment];
}
this.averageFrequencyTimeIncrement /= this.recentFrequencyTimeIncrements.length;
-
this.frequencyTimeWheelPos += angle;
if (this.frequencyTimeWheelPos >= 360) {
this.frequencyTimeWheelPos = this.frequencyTimeWheelPos % 360;
}
}
- // currently disabled due to breaking changes in js audio native objects
- this.playFootStepSound = function(side) {
-
- //var options = new AudioInjectionOptions();
- //options.position = Camera.getPosition();
- //options.volume = 0.3;
- //var soundNumber = 2; // 0 to 2
- //if (side === RIGHT && motion.makesFootStepSounds) {
- // Audio.playS ound(walkAssets.footsteps[soundNumber + 1], options);
- //} else if (side === LEFT && motion.makesFootStepSounds) {
- // Audio.playSound(walkAssets.footsteps[soundNumber], options);
- //}
+ this.saveHistory = function() {
+ this.lastDirection = this.direction;
+ this.lastVelocity = this.velocity;
+ this.lastYaw = this.yaw;
+ this.lastYawDelta = this.yawDelta;
+ this.lastYawDeltaAcceleration = this.yawDeltaAcceleration;
}
-
- // history
- this.lastDirection = 0;
- this.lastSpeed = 0;
- this.transitionCount = 0;
- this.lastDistanceToVoxels = 0;
-
}; // end Motion constructor
-
-spatialInformation = (function() {
+// animation manipulation object
+animationOperations = (function() {
return {
- distanceToVoxels: function() {
+ // helper function for renderMotion(). calculate joint translations based on animation file settings and frequency * time
+ calculateTranslations: function(animation, ft, direction) {
+ var jointName = "Hips";
+ var joint = animation.joints[jointName];
+ var jointTranslations = {x:0, y:0, z:0};
- // use the blocking version of findRayIntersection to avoid errors
- var pickRay = {origin: MyAvatar.position, direction: {x:0, y:-1, z:0}};
-
- // TODO: change this to use entities?
- return false; // Voxels.findRayIntersectionBlocking(pickRay).distance - motion.calibration.hipsToFeet;;
- }
- }
+ // gather modifiers and multipliers
+ modifiers = new FrequencyMultipliers(joint, direction);
-})(); // end spatialInformation object literal
+ // calculate translations. Use synthesis filters where specified by the animation data file.
+ // sway (oscillation on the x-axis)
+ if (animation.filters.hasOwnProperty(jointName) && 'swayFilter' in animation.filters[jointName]) {
+ jointTranslations.x = joint.sway * animation.filters[jointName].swayFilter.calculate
+ (filter.degToRad(modifiers.swayFrequencyMultiplier * ft + joint.swayPhase)) + joint.swayOffset;
+ } else {
+ jointTranslations.x = joint.sway * Math.sin
+ (filter.degToRad(modifiers.swayFrequencyMultiplier * ft + joint.swayPhase)) + joint.swayOffset;
+ }
+ // bob (oscillation on the y-axis)
+ if (animation.filters.hasOwnProperty(jointName) && 'bobFilter' in animation.filters[jointName]) {
+ jointTranslations.y = joint.bob * animation.filters[jointName].bobFilter.calculate
+ (filter.degToRad(modifiers.bobFrequencyMultiplier * ft + joint.bobPhase)) + joint.bobOffset;
+ } else {
+ jointTranslations.y = joint.bob * Math.sin
+ (filter.degToRad(modifiers.bobFrequencyMultiplier * ft + joint.bobPhase)) + joint.bobOffset;
-// animation file manipulation
-animation = (function() {
+ 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;
+ },
- return {
+ // helper function for renderMotion(). calculate joint rotations based on animation file settings and frequency * time
+ calculateRotations: function(jointName, animation, ft, direction) {
+ var joint = animation.joints[jointName];
+ var jointRotations = {x:0, y:0, z:0};
+
+ if (avatar.isBlenderExport) {
+ jointRotations = Vec3.sum(jointRotations, walkAssets.blenderPreRotations.joints[jointName]);
+ }
+
+ // gather frequency multipliers for this joint
+ modifiers = new FrequencyMultipliers(joint, direction);
+
+ // calculate rotations. Use synthesis filters where specified by the animation data file.
+
+ // calculate pitch
+ if (animation.filters.hasOwnProperty(jointName) &&
+ 'pitchFilter' in animation.filters[jointName]) {
+ jointRotations.x += joint.pitch * animation.filters[jointName].pitchFilter.calculate
+ (filter.degToRad(ft * modifiers.pitchFrequencyMultiplier + joint.pitchPhase)) + joint.pitchOffset;
+ } else {
+ jointRotations.x += joint.pitch * Math.sin
+ (filter.degToRad(ft * modifiers.pitchFrequencyMultiplier + joint.pitchPhase)) + joint.pitchOffset;
+ }
+ // calculate yaw
+ if (animation.filters.hasOwnProperty(jointName) &&
+ 'yawFilter' in animation.filters[jointName]) {
+ jointRotations.y += joint.yaw * animation.filters[jointName].yawFilter.calculate
+ (filter.degToRad(ft * modifiers.yawFrequencyMultiplier + joint.yawPhase)) + joint.yawOffset;
+ } else {
+ jointRotations.y += joint.yaw * Math.sin
+ (filter.degToRad(ft * modifiers.yawFrequencyMultiplier + joint.yawPhase)) + joint.yawOffset;
+ }
+ // calculate roll
+ if (animation.filters.hasOwnProperty(jointName) &&
+ 'rollFilter' in animation.filters[jointName]) {
+ jointRotations.z += joint.roll * animation.filters[jointName].rollFilter.calculate
+ (filter.degToRad(ft * modifiers.rollFrequencyMultiplier + joint.rollPhase)) + joint.rollOffset;
+ } else {
+ jointRotations.z += joint.roll * Math.sin
+ (filter.degToRad(ft * modifiers.rollFrequencyMultiplier + joint.rollPhase)) + joint.rollOffset;
+ }
+ return jointRotations;
+ },
zeroAnimation: function(animation) {
-
for (i in animation.joints) {
-
- animation.joints[i].pitch = 0;
- animation.joints[i].yaw = 0;
- animation.joints[i].roll = 0;
- animation.joints[i].pitchPhase = 0;
- animation.joints[i].yawPhase = 0;
- animation.joints[i].rollPhase = 0;
- animation.joints[i].pitchOffset = 0;
- animation.joints[i].yawOffset = 0;
- animation.joints[i].rollOffset = 0;
- if (i === 0) {
- // Hips only
- animation.joints[i].thrust = 0;
- animation.joints[i].sway = 0;
- animation.joints[i].bob = 0;
- animation.joints[i].thrustPhase = 0;
- animation.joints[i].swayPhase = 0;
- animation.joints[i].bobPhase = 0;
+ for (j in animation.joints[i]) {
+ animation.joints[i][j] = 0;
}
}
},
blendAnimation: function(sourceAnimation, targetAnimation, percent) {
-
for (i in targetAnimation.joints) {
-
targetAnimation.joints[i].pitch += percent * sourceAnimation.joints[i].pitch;
targetAnimation.joints[i].yaw += percent * sourceAnimation.joints[i].yaw;
targetAnimation.joints[i].roll += percent * sourceAnimation.joints[i].roll;
@@ -233,7 +496,7 @@ animation = (function() {
targetAnimation.joints[i].pitchOffset += percent * sourceAnimation.joints[i].pitchOffset;
targetAnimation.joints[i].yawOffset += percent * sourceAnimation.joints[i].yawOffset;
targetAnimation.joints[i].rollOffset += percent * sourceAnimation.joints[i].rollOffset;
- if (i === 0) {
+ if (i === "Hips") {
// Hips only
targetAnimation.joints[i].thrust += percent * sourceAnimation.joints[i].thrust;
targetAnimation.joints[i].sway += percent * sourceAnimation.joints[i].sway;
@@ -241,368 +504,419 @@ animation = (function() {
targetAnimation.joints[i].thrustPhase += percent * sourceAnimation.joints[i].thrustPhase;
targetAnimation.joints[i].swayPhase += percent * sourceAnimation.joints[i].swayPhase;
targetAnimation.joints[i].bobPhase += percent * sourceAnimation.joints[i].bobPhase;
+ targetAnimation.joints[i].thrustOffset += percent * sourceAnimation.joints[i].thrustOffset;
+ targetAnimation.joints[i].swayOffset += percent * sourceAnimation.joints[i].swayOffset;
+ targetAnimation.joints[i].bobOffset += percent * sourceAnimation.joints[i].bobOffset;
}
}
+ },
+
+ deepCopy: function(sourceAnimation, targetAnimation) {
+ // calibration
+ targetAnimation.calibration = JSON.parse(JSON.stringify(sourceAnimation.calibration));
+
+ // harmonics
+ targetAnimation.harmonics = {};
+ if (isDefined(sourceAnimation.harmonics)) {
+ targetAnimation.harmonics = JSON.parse(JSON.stringify(sourceAnimation.harmonics));
+ }
+
+ // filters
+ targetAnimation.filters = {};
+ for (i in sourceAnimation.filters) {
+ // are any filters specified for this joint?
+ if (isDefined(sourceAnimation.filters[i])) {
+ targetAnimation.filters[i] = sourceAnimation.filters[i];
+ // wave shapers
+ if (isDefined(sourceAnimation.filters[i].pitchFilter)) {
+ targetAnimation.filters[i].pitchFilter = sourceAnimation.filters[i].pitchFilter;
+ }
+ if (isDefined(sourceAnimation.filters[i].yawFilter)) {
+ targetAnimation.filters[i].yawFilter = sourceAnimation.filters[i].yawFilter;
+ }
+ if (isDefined(sourceAnimation.filters[i].rollFilter)) {
+ targetAnimation.filters[i].rollFilter = sourceAnimation.filters[i].rollFilter;
+ }
+ // LP filters
+ if (isDefined(sourceAnimation.filters[i].swayLPFilter)) {
+ targetAnimation.filters[i].swayLPFilter = sourceAnimation.filters[i].swayLPFilter;
+ }
+ if (isDefined(sourceAnimation.filters[i].bobLPFilter)) {
+ targetAnimation.filters[i].bobLPFilter = sourceAnimation.filters[i].bobLPFilter;
+ }
+ if (isDefined(sourceAnimation.filters[i].thrustLPFilter)) {
+ targetAnimation.filters[i].thrustLPFilter = sourceAnimation.filters[i].thrustLPFilter;
+ }
+ }
+ }
+ // joints
+ targetAnimation.joints = JSON.parse(JSON.stringify(sourceAnimation.joints));
}
}
+
})(); // end animation object literal
-// finite state machine. Avatar locomotion modes represent states in the FSM
-state = (function () {
+// ReachPose datafile wrapper object
+ReachPose = function(reachPoseName) {
+ this.name = reachPoseName;
+ this.reachPoseParameters = walkAssets.getReachPoseParameters(reachPoseName);
+ this.reachPoseDataFile = walkAssets.getReachPoseDataFile(reachPoseName);
+ this.progress = 0;
+ this.smoothingFilter = filter.createAveragingFilter(this.reachPoseParameters.smoothing);
+ this.currentStrength = function() {
+ // apply optionally smoothed (D)ASDR envelope to reach pose's strength / influence whilst active
+ var segmentProgress = undefined; // progress through chosen segment
+ var segmentTimeDelta = undefined; // total change in time over chosen segment
+ var segmentStrengthDelta = undefined; // total change in strength over chosen segment
+ var lastStrength = undefined; // the last value the previous segment held
+ var currentStrength = undefined; // return value
- return {
-
- // the finite list of states
- STANDING: 1,
- WALKING: 2,
- SIDE_STEP: 3,
- FLYING: 4,
- currentState: this.STANDING,
-
- // status vars
- powerOn: true,
-
- setInternalState: function(newInternalState) {
-
- motion.elapsedFTDegrees = 0;
-
- switch (newInternalState) {
-
- case this.WALKING:
-
- this.currentState = this.WALKING;
- return;
-
- case this.FLYING:
-
- this.currentState = this.FLYING;
- return;
-
- case this.SIDE_STEP:
-
- this.currentState = this.SIDE_STEP;
- return;
-
- case this.STANDING:
- default:
-
- this.currentState = this.STANDING;
- return;
- }
+ // select parameters based on segment (a segment being one of (D),A,S,D or R)
+ if (this.progress >= this.reachPoseParameters.sustain.timing) {
+ // release segment
+ segmentProgress = this.progress - this.reachPoseParameters.sustain.timing;
+ segmentTimeDelta = this.reachPoseParameters.release.timing - this.reachPoseParameters.sustain.timing;
+ segmentStrengthDelta = this.reachPoseParameters.release.strength - this.reachPoseParameters.sustain.strength;
+ lastStrength = this.reachPoseParameters.sustain.strength;
+ } else if (this.progress >= this.reachPoseParameters.decay.timing) {
+ // sustain phase
+ segmentProgress = this.progress - this.reachPoseParameters.decay.timing;
+ segmentTimeDelta = this.reachPoseParameters.sustain.timing - this.reachPoseParameters.decay.timing;
+ segmentStrengthDelta = this.reachPoseParameters.sustain.strength - this.reachPoseParameters.decay.strength;
+ lastStrength = this.reachPoseParameters.decay.strength;
+ } else if (this.progress >= this.reachPoseParameters.attack.timing) {
+ // decay phase
+ segmentProgress = this.progress - this.reachPoseParameters.attack.timing;
+ segmentTimeDelta = this.reachPoseParameters.decay.timing - this.reachPoseParameters.attack.timing;
+ segmentStrengthDelta = this.reachPoseParameters.decay.strength - this.reachPoseParameters.attack.strength;
+ lastStrength = this.reachPoseParameters.attack.strength;
+ } else if (this.progress >= this.reachPoseParameters.delay.timing) {
+ // attack phase
+ segmentProgress = this.progress - this.reachPoseParameters.delay.timing;
+ segmentTimeDelta = this.reachPoseParameters.attack.timing - this.reachPoseParameters.delay.timing;
+ segmentStrengthDelta = this.reachPoseParameters.attack.strength - this.reachPoseParameters.delay.strength;
+ lastStrength = 0; //this.delay.strength;
+ } else {
+ // delay phase
+ segmentProgress = this.progress;
+ segmentTimeDelta = this.reachPoseParameters.delay.timing;
+ segmentStrengthDelta = this.reachPoseParameters.delay.strength;
+ lastStrength = 0;
}
+ currentStrength = segmentTimeDelta > 0 ? lastStrength + segmentStrengthDelta * segmentProgress / segmentTimeDelta
+ : lastStrength;
+ // smooth off the response curve
+ currentStrength = this.smoothingFilter.process(currentStrength);
+ return currentStrength;
}
-})(); // end state object literal
+};
+
+// constructor with default parameters
+TransitionParameters = function() {
+ this.duration = 0.5;
+ this.easingLower = {x:0.25, y:0.75};
+ this.easingUpper = {x:0.75, y:0.25};
+ this.reachPoseNames = [];
+}
// constructor for animation Transition
-Transition = function(nextAnimation, lastAnimation, lastTransition) {
+Transition = function(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses) {
+//if (isDefined(lastAnimation) && isDefined(nextAnimation)) walkTools.toLog(lastAnimation.name + ' to '+ nextAnimation.name + ': started');
+ if (playTransitionReachPoses === undefined) {
+ playTransitionReachPoses = true;
+ }
// record the current state of animation
this.nextAnimation = nextAnimation;
this.lastAnimation = lastAnimation;
this.lastTransition = lastTransition;
- // deal with transition recursion (overlapping transitions)
- this.id = motion.transitionCount++; // serial number for this transition
- this.recursionDepth = 0;
- this.incrementRecursion = function() {
-
- this.recursionDepth += 1;
-
- if(this.lastTransition !== nullTransition) {
-
- this.lastTransition.incrementRecursion();
- if(this.lastTransition.recursionDepth > 5) {
-
- delete this.lastTransition;
- this.lastTransition = nullTransition;
- }
- }
- }
- if(lastTransition !== nullTransition) {
-
- this.lastTransition.incrementRecursion();
- }
-
- this.reachPoses = []; // placeholder / stub: work in progress - array of reach poses for squash and stretch techniques
- this.transitionDuration = 0.5; // length of transition (seconds)
- this.easingLower = {x:0.60, y:0.03}; // Bezier curve handle
- this.easingUpper = {x:0.87, y:1.35}; // Bezier curve handle
- this.startTime = new Date().getTime(); // Starting timestamp (seconds)
- this.progress = 0; // how far are we through the transition?
- this.lastDirection = motion.lastDirection;
-
// collect information about the currently playing animation
+ this.direction = motion.direction;
this.lastDirection = motion.lastDirection;
this.lastFrequencyTimeWheelPos = motion.frequencyTimeWheelPos;
this.lastFrequencyTimeIncrement = motion.averageFrequencyTimeIncrement;
this.lastFrequencyTimeWheelRadius = motion.frequencyTimeWheelRadius;
- this.stopAngle = 0; // what angle should we stop turning this frequency time wheel?
- this.percentToMove = 0; // if we need to keep moving to complete a step, this will be set to 100
- this.lastElapsedFTDegrees = motion.elapsedFTDegrees;
- this.advancePreviousFrequencyTimeWheel = function() {
+ this.degreesToTurn = 0; // total degrees to turn the ft wheel before the avatar stops (walk only)
+ this.degreesRemaining = 0; // remaining degrees to turn the ft wheel before the avatar stops (walk only)
+ this.lastElapsedFTDegrees = motion.elapsedFTDegrees; // degrees elapsed since last transition start
+ motion.elapsedFTDegrees = 0; // reset ready for the next transition
+ motion.frequencyTimeWheelPos = 0; // start the next animation's frequency time wheel from zero
- this.lastFrequencyTimeWheelPos += this.lastFrequencyTimeIncrement;
+ // set parameters for the transition
+ this.parameters = new TransitionParameters();
+ this.liveReachPoses = [];
+ if (walkAssets && isDefined(lastAnimation) && isDefined(nextAnimation)) {
+ // overwrite this.parameters with any transition parameters specified for this particular transition
+ walkAssets.getTransitionParameters(lastAnimation, nextAnimation, this.parameters);
+ // fire up any reach poses for this transition
+ if (playTransitionReachPoses) {
+ for (poseName in this.parameters.reachPoseNames) {
+ this.liveReachPoses.push(new ReachPose(this.parameters.reachPoseNames[poseName]));
+ }
+ }
+ }
+ this.startTime = new Date().getTime(); // Starting timestamp (seconds)
+ this.progress = 0; // how far are we through the transition?
+ this.filteredProgress = 0;
- if (this.lastFrequencyTimeWheelPos >= 360) {
+ // coming to a halt whilst walking? if so, will need a clean stopping point defined
+ if (motion.isComingToHalt) {
+ var FULL_CYCLE = 360;
+ var FULL_CYCLE_THRESHOLD = 320;
+ var HALF_CYCLE = 180;
+ var HALF_CYCLE_THRESHOLD = 140;
+ var CYCLE_COMMIT_THRESHOLD = 5;
- this.lastFrequencyTimeWheelPos = this.lastFrequencyTimeWheelPos % 360;
+ // how many degrees do we need to turn the walk wheel to finish walking with both feet on the ground?
+ if (this.lastElapsedFTDegrees < CYCLE_COMMIT_THRESHOLD) {
+ // just stop the walk cycle right here and blend to idle
+ this.degreesToTurn = 0;
+ } else if (this.lastElapsedFTDegrees < HALF_CYCLE) {
+ // we have not taken a complete step yet, so we advance to the second stop angle
+ this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos;
+ } else if (this.lastFrequencyTimeWheelPos > 0 && this.lastFrequencyTimeWheelPos <= HALF_CYCLE_THRESHOLD) {
+ // complete the step and stop at 180
+ this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos;
+ } else if (this.lastFrequencyTimeWheelPos > HALF_CYCLE_THRESHOLD && this.lastFrequencyTimeWheelPos <= HALF_CYCLE) {
+ // complete the step and next then stop at 0
+ this.degreesToTurn = HALF_CYCLE - this.lastFrequencyTimeWheelPos + HALF_CYCLE;
+ } else if (this.lastFrequencyTimeWheelPos > HALF_CYCLE && this.lastFrequencyTimeWheelPos <= FULL_CYCLE_THRESHOLD) {
+ // complete the step and stop at 0
+ this.degreesToTurn = FULL_CYCLE - this.lastFrequencyTimeWheelPos;
+ } else {
+ // complete the step and the next then stop at 180
+ this.degreesToTurn = FULL_CYCLE - this.lastFrequencyTimeWheelPos + HALF_CYCLE;
}
- if(this.lastTransition !== nullTransition) {
+ // transition length in this case should be directly proportional to the remaining degrees to turn
+ var MIN_FT_INCREMENT = 5.0; // degrees per frame
+ var MIN_TRANSITION_DURATION = 0.4;
+ this.lastFrequencyTimeIncrement *= 0.66; // help ease the transition
+ var lastFrequencyTimeIncrement = this.lastFrequencyTimeIncrement > MIN_FT_INCREMENT ?
+ this.lastFrequencyTimeIncrement : MIN_FT_INCREMENT;
+ var timeToFinish = Math.max(motion.deltaTime * this.degreesToTurn / lastFrequencyTimeIncrement,
+ MIN_TRANSITION_DURATION);
+ this.parameters.duration = timeToFinish;
+ this.degreesRemaining = this.degreesToTurn;
+ }
- this.lastTransition.advancePreviousFrequencyTimeWheel();
- }
- };
+ // deal with transition recursion (overlapping transitions)
+ this.recursionDepth = 0;
+ this.incrementRecursion = function() {
+ this.recursionDepth += 1;
- this.updateProgress = function() {
+ // cancel any continued motion
+ this.degreesToTurn = 0;
- var elapasedTime = (new Date().getTime() - this.startTime) / 1000;
- this.progress = elapasedTime / this.transitionDuration;
-
- this.progress = filter.bezier((1 - this.progress),
- {x: 0, y: 0},
- this.easingLower,
- this.easingUpper,
- {x: 1, y: 1}).y;
-
- // ensure there is at least some progress
- if(this.progress <= 0) {
-
- this.progress = VERY_SHORT_TIME;
- }
- if(this.lastTransition !== nullTransition) {
-
- if(this.lastTransition.updateProgress() >= 1) {
-
- // the previous transition is now complete
- delete this.lastTransition;
+ // limit the number of layered / nested transitions
+ if (this.lastTransition !== nullTransition) {
+ this.lastTransition.incrementRecursion();
+ if (this.lastTransition.recursionDepth > MAX_TRANSITION_RECURSION) {
this.lastTransition = nullTransition;
}
}
- return this.progress;
+ };
+ if (this.lastTransition !== nullTransition) {
+ this.lastTransition.incrementRecursion();
+ }
+
+
+ // end of transition initialisation. begin Transition public methods
+
+ // keep up the pace for the frequency time wheel for the last animation
+ this.advancePreviousFrequencyTimeWheel = function(deltaTime) {
+ var wheelAdvance = undefined;
+
+ if (this.lastAnimation === avatar.selectedWalkBlend &&
+ this.nextAnimation === avatar.selectedIdle) {
+ if (this.degreesRemaining <= 0) {
+ // stop continued motion
+ wheelAdvance = 0;
+ if (motion.isComingToHalt) {
+ if (this.lastFrequencyTimeWheelPos < 90) {
+ this.lastFrequencyTimeWheelPos = 0;
+ } else {
+ this.lastFrequencyTimeWheelPos = 180;
+ }
+ }
+ } else {
+ wheelAdvance = this.lastFrequencyTimeIncrement;
+ var distanceToTravel = avatar.calibration.strideLength * wheelAdvance / 180;
+ if (this.degreesRemaining <= 0) {
+ distanceToTravel = 0;
+ this.degreesRemaining = 0;
+ } else {
+ this.degreesRemaining -= wheelAdvance;
+ }
+ }
+ } else {
+ wheelAdvance = this.lastFrequencyTimeIncrement;
+ }
+
+ // advance the ft wheel
+ this.lastFrequencyTimeWheelPos += wheelAdvance;
+ if (this.lastFrequencyTimeWheelPos >= 360) {
+ this.lastFrequencyTimeWheelPos = this.lastFrequencyTimeWheelPos % 360;
+ }
+
+ // advance ft wheel for the nested (previous) Transition
+ if (this.lastTransition !== nullTransition) {
+ this.lastTransition.advancePreviousFrequencyTimeWheel(deltaTime);
+ }
+ // update the lastElapsedFTDegrees for short stepping
+ this.lastElapsedFTDegrees += wheelAdvance;
+ this.degreesTurned += wheelAdvance;
+ };
+
+ this.updateProgress = function() {
+ var elapasedTime = (new Date().getTime() - this.startTime) / 1000;
+ this.progress = elapasedTime / this.parameters.duration;
+ this.progress = Math.round(this.progress * 1000) / 1000;
+
+ // updated nested transition/s
+ if (this.lastTransition !== nullTransition) {
+ if (this.lastTransition.updateProgress() === TRANSITION_COMPLETE) {
+ // the previous transition is now complete
+ this.lastTransition = nullTransition;
+ }
+ }
+
+ // update any reachPoses
+ for (pose in this.liveReachPoses) {
+ // use independent timing for reachPoses
+ this.liveReachPoses[pose].progress += (motion.deltaTime / this.liveReachPoses[pose].reachPoseParameters.duration);
+ if (this.liveReachPoses[pose].progress >= 1) {
+ // time to kill off this reach pose
+ this.liveReachPoses.splice(pose, 1);
+ }
+ }
+
+ // update transition progress
+ this.filteredProgress = filter.bezier(this.progress, this.parameters.easingLower, this.parameters.easingUpper);
+ //if (this.progress >= 1) walkTools.toLog(this.lastAnimation.name + ' to '+ this.nextAnimation.name + ': done');
+ return this.progress >= 1 ? TRANSITION_COMPLETE : false;
};
this.blendTranslations = function(frequencyTimeWheelPos, direction) {
-
var lastTranslations = {x:0, y:0, z:0};
- if(!isDefined(this.nextAnimation)) {
-
- return lastTranslations;
- }
- var nextTranslations = calculateTranslations(this.nextAnimation,
- frequencyTimeWheelPos,
- direction);
-
+ var nextTranslations = animationOperations.calculateTranslations(this.nextAnimation,
+ frequencyTimeWheelPos,
+ direction);
// are we blending with a previous, still live transition?
- if(this.lastTransition !== nullTransition) {
-
+ if (this.lastTransition !== nullTransition) {
lastTranslations = this.lastTransition.blendTranslations(this.lastFrequencyTimeWheelPos,
this.lastDirection);
-
} else {
-
- lastTranslations = calculateTranslations(this.lastAnimation,
+ lastTranslations = animationOperations.calculateTranslations(this.lastAnimation,
this.lastFrequencyTimeWheelPos,
this.lastDirection);
}
- nextTranslations.x = this.progress * nextTranslations.x +
- (1 - this.progress) * lastTranslations.x;
+ // blend last / next translations
+ nextTranslations = Vec3.multiply(this.filteredProgress, nextTranslations);
+ lastTranslations = Vec3.multiply((1 - this.filteredProgress), lastTranslations);
+ nextTranslations = Vec3.sum(nextTranslations, lastTranslations);
- nextTranslations.y = this.progress * nextTranslations.y +
- (1 - this.progress) * lastTranslations.y;
-
- nextTranslations.z = this.progress * nextTranslations.z +
- (1 - this.progress) * lastTranslations.z;
+ if (this.liveReachPoses.length > 0) {
+ for (pose in this.liveReachPoses) {
+ var reachPoseStrength = this.liveReachPoses[pose].currentStrength();
+ var poseTranslations = animationOperations.calculateTranslations(
+ this.liveReachPoses[pose].reachPoseDataFile,
+ frequencyTimeWheelPos,
+ direction);
+ // can't use Vec3 operations here, as if x,y or z is zero, the reachPose should have no influence at all
+ if (Math.abs(poseTranslations.x) > 0) {
+ nextTranslations.x = reachPoseStrength * poseTranslations.x + (1 - reachPoseStrength) * nextTranslations.x;
+ }
+ if (Math.abs(poseTranslations.y) > 0) {
+ nextTranslations.y = reachPoseStrength * poseTranslations.y + (1 - reachPoseStrength) * nextTranslations.y;
+ }
+ if (Math.abs(poseTranslations.z) > 0) {
+ nextTranslations.z = reachPoseStrength * poseTranslations.z + (1 - reachPoseStrength) * nextTranslations.z;
+ }
+ }
+ }
return nextTranslations;
};
this.blendRotations = function(jointName, frequencyTimeWheelPos, direction) {
-
var lastRotations = {x:0, y:0, z:0};
- var nextRotations = calculateRotations(jointName,
+ var nextRotations = animationOperations.calculateRotations(jointName,
this.nextAnimation,
frequencyTimeWheelPos,
direction);
// are we blending with a previous, still live transition?
- if(this.lastTransition !== nullTransition) {
-
+ if (this.lastTransition !== nullTransition) {
lastRotations = this.lastTransition.blendRotations(jointName,
this.lastFrequencyTimeWheelPos,
this.lastDirection);
} else {
-
- lastRotations = calculateRotations(jointName,
+ lastRotations = animationOperations.calculateRotations(jointName,
this.lastAnimation,
this.lastFrequencyTimeWheelPos,
this.lastDirection);
}
+ // blend last / next translations
+ nextRotations = Vec3.multiply(this.filteredProgress, nextRotations);
+ lastRotations = Vec3.multiply((1 - this.filteredProgress), lastRotations);
+ nextRotations = Vec3.sum(nextRotations, lastRotations);
- nextRotations.x = this.progress * nextRotations.x +
- (1 - this.progress) * lastRotations.x;
-
- nextRotations.y = this.progress * nextRotations.y +
- (1 - this.progress) * lastRotations.y;
-
- nextRotations.z = this.progress * nextRotations.z +
- (1 - this.progress) * lastRotations.z;
+ // are there reachPoses defined for this transition?
+ if (this.liveReachPoses.length > 0) {
+ for (pose in this.liveReachPoses) {
+ var reachPoseStrength = this.liveReachPoses[pose].currentStrength();
+ var poseRotations = animationOperations.calculateRotations(jointName,
+ this.liveReachPoses[pose].reachPoseDataFile,
+ frequencyTimeWheelPos,
+ direction);
+ // don't use Vec3 operations here, as if x,y or z is zero, the reach pose should have no influence at all
+ if (Math.abs(poseRotations.x) > 0) {
+ nextRotations.x = reachPoseStrength * poseRotations.x + (1 - reachPoseStrength) * nextRotations.x;
+ }
+ if (Math.abs(poseRotations.y) > 0) {
+ nextRotations.y = reachPoseStrength * poseRotations.y + (1 - reachPoseStrength) * nextRotations.y;
+ }
+ if (Math.abs(poseRotations.z) > 0) {
+ nextRotations.z = reachPoseStrength * poseRotations.z + (1 - reachPoseStrength) * nextRotations.z;
+ }
+ }
+ }
return nextRotations;
};
-
}; // end Transition constructor
-
-// individual joint modiers (mostly used to provide symmetry between left and right limbs)
-JointModifiers = function(joint, direction) {
-
- // gather modifiers and multipliers
+// individual joint modifiers
+FrequencyMultipliers = function(joint, direction) {
+ // gather multipliers
this.pitchFrequencyMultiplier = 1;
- this.pitchPhaseModifier = 0;
- this.pitchReverseModifier = 0;
- this.yawReverseModifier = 0;
- this.rollReverseModifier = 0;
- this.pitchSign = 1; // for sidestepping and incorrectly rigged Ron ToeBases
- this.yawSign = 1;
- this.rollSign = 1;
- this.pitchReverseInvert = 1;
- this.pitchOffsetSign = 1;
- this.yawOffsetSign = 1;
- this.rollOffsetSign = 1;
- this.bobReverseModifier = 0;
+ this.yawFrequencyMultiplier = 1;
+ this.rollFrequencyMultiplier = 1;
+ this.swayFrequencyMultiplier = 1;
this.bobFrequencyMultiplier = 1;
this.thrustFrequencyMultiplier = 1;
- if (isDefined(joint.pitchFrequencyMultiplier)) {
- this.pitchFrequencyMultiplier = joint.pitchFrequencyMultiplier;
+ if (isDefined(joint)) {
+ if (isDefined(joint.pitchFrequencyMultiplier)) {
+ this.pitchFrequencyMultiplier = joint.pitchFrequencyMultiplier;
+ }
+ if (isDefined(joint.yawFrequencyMultiplier)) {
+ this.yawFrequencyMultiplier = joint.yawFrequencyMultiplier;
+ }
+ if (isDefined(joint.rollFrequencyMultiplier)) {
+ this.rollFrequencyMultiplier = joint.rollFrequencyMultiplier;
+ }
+ if (isDefined(joint.swayFrequencyMultiplier)) {
+ this.swayFrequencyMultiplier = joint.swayFrequencyMultiplier;
+ }
+ if (isDefined(joint.bobFrequencyMultiplier)) {
+ this.bobFrequencyMultiplier = joint.bobFrequencyMultiplier;
+ }
+ if (isDefined(joint.thrustFrequencyMultiplier)) {
+ this.thrustFrequencyMultiplier = joint.thrustFrequencyMultiplier;
+ }
}
- if (isDefined(joint.pitchPhaseModifier)) {
- this.pitchPhaseModifier = joint.pitchPhaseModifier;
- }
- if (isDefined(joint.pitchSign)) {
- this.pitchSign = joint.pitchSign;
- }
- if (isDefined(joint.yawSign)) {
- this.yawSign = joint.yawSign;
- }
- if (isDefined(joint.rollSign)) {
- this.rollSign = joint.rollSign;
- }
- if (isDefined(joint.pitchReverseInvert) && direction === BACKWARDS) {
- this.pitchReverseInvert = joint.pitchReverseInvert;
- }
- if (isDefined(joint.pitchReverseModifier) && direction === BACKWARDS) {
- this.pitchReverseModifier = joint.pitchReverseModifier;
- }
- if (isDefined(joint.yawReverseModifier) && direction === BACKWARDS) {
- this.yawReverseModifier = joint.yawReverseModifier;
- }
- if (isDefined(joint.rollReverseModifier) && direction === BACKWARDS) {
- this.rollReverseModifier = joint.rollReverseModifier;
- }
- if (isDefined(joint.pitchOffsetSign)) {
- this.pitchOffsetSign = joint.pitchOffsetSign;
- }
- if (isDefined(joint.yawOffsetSign)) {
- this.yawOffsetSign = joint.yawOffsetSign;
- }
- if (isDefined(joint.rollOffsetSign)) {
- this.rollOffsetSign = joint.rollOffsetSign;
- }
- if (isDefined(joint.bobReverseModifier) && direction === BACKWARDS) {
- this.bobReverseModifier = joint.bobReverseModifier;
- }
- if (isDefined(joint.bobFrequencyMultiplier)) {
- this.bobFrequencyMultiplier = joint.bobFrequencyMultiplier;
- }
- if (isDefined(joint.thrustFrequencyMultiplier)) {
- this.thrustFrequencyMultiplier = joint.thrustFrequencyMultiplier;
- }
-};
-
-walkAssets = (function () {
-
- // load the sounds - currently disabled due to breaking changes in js audio native objects
- //var _pathToSounds = 'https://s3.amazonaws.com/hifi-public/sounds/Footsteps/';
-
- //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-standing-one-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-flying-blend-animation.js");
- Script.include(pathToAssets+"animations/dd-female-hovering-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-standing-one-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-flying-blend-animation.js");
- Script.include(pathToAssets+"animations/dd-male-hovering-animation.js");
- Script.include(pathToAssets+"animations/dd-male-sidestep-left-animation.js");
- Script.include(pathToAssets+"animations/dd-male-sidestep-right-animation.js");
- Script.include(pathToAssets+"animations/dd-animation-reference.js");
-
- var _femaleStandardWalk = new FemaleStandardWalk();
- var _femaleFlyingUp = new FemaleFlyingUp();
- var _femaleFlying = new FemaleFlying();
- var _femaleFlyingDown = new FemaleFlyingDown();
- var _femaleStandOne = new FemaleStandingOne();
- var _femaleSideStepLeft = new FemaleSideStepLeft();
- var _femaleSideStepRight = new FemaleSideStepRight();
- var _femaleFlyingBlend = new FemaleFlyingBlend();
- var _femaleHovering = new FemaleHovering();
-
- var _maleStandardWalk = new MaleStandardWalk(filter);
- var _maleStandOne = new MaleStandingOne();
- var _maleSideStepLeft = new MaleSideStepLeft();
- var _maleSideStepRight = new MaleSideStepRight();
- var _maleFlying = new MaleFlying();
- var _maleFlyingDown = new MaleFlyingDown();
- var _maleFlyingUp = new MaleFlyingUp();
- var _maleFlyingBlend = new MaleFlyingBlend();
- var _maleHovering = new MaleHovering();
-
- var _animationReference = new AnimationReference();
-
- return {
-
- // expose the sound assets
- //footsteps: _footsteps,
-
- // expose the animation assets
- femaleStandardWalk: _femaleStandardWalk,
- femaleFlyingUp: _femaleFlyingUp,
- femaleFlying: _femaleFlying,
- femaleFlyingDown: _femaleFlyingDown,
- femaleFlyingBlend: _femaleFlyingBlend,
- femaleHovering: _femaleHovering,
- femaleStandOne: _femaleStandOne,
- femaleSideStepLeft: _femaleSideStepLeft,
- femaleSideStepRight: _femaleSideStepRight,
- maleStandardWalk: _maleStandardWalk,
- maleFlyingUp: _maleFlyingUp,
- maleFlying: _maleFlying,
- maleFlyingDown: _maleFlyingDown,
- maleFlyingBlend: _maleFlyingBlend,
- maleHovering: _maleHovering,
- maleStandOne: _maleStandOne,
- maleSideStepLeft: _maleSideStepLeft,
- maleSideStepRight: _maleSideStepRight,
- animationReference: _animationReference,
- }
-
-})();
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/examples/libraries/walkFilters.js b/examples/libraries/walkFilters.js
index 4e77fe6ec7..d4474f5dcf 100644
--- a/examples/libraries/walkFilters.js
+++ b/examples/libraries/walkFilters.js
@@ -1,134 +1,78 @@
//
// walkFilters.js
+// version 1.1
//
-// version 1.002
+// Created by David Wooldridge, June 2015
+// Copyright © 2014 - 2015 High Fidelity, Inc.
//
-// Created by David Wooldridge, Autumn 2014
-//
-// Provides a variety of filters for use by the walk.js script v1.12
+// Provides a variety of filters for use by the walk.js script v1.2+
//
// Distributed under the Apache License, Version 2.0.
// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
//
+// simple averaging (LP) filter for damping / smoothing
AveragingFilter = function(length) {
-
- //this.name = name;
+ // initialise the array of past values
this.pastValues = [];
-
- for(var i = 0; i < length; i++) {
+ 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] !== null) {
-
- // 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];
+ for (var value in this.pastValues) nextOutputValue += this.pastValues[value];
return nextOutputValue / this.pastValues.length;
-
} else {
-
return 0;
}
};
};
-
-// 1st order Butterworth 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 (-3 dB @ 3 Hz)
-ButterworthFilter1 = function() {
-
- this.gain = 7.313751515;
- this.coeff = 0.7265425280;
+// 2nd order 2Hz Butterworth LP filter
+ButterworthFilter = function() {
+ // coefficients calculated at: http://www-users.cs.york.ac.uk/~fisher/mkfilter/trad.html
+ this.gain = 104.9784742;
+ this.coeffOne = -0.7436551950;
+ this.coeffTwo = 1.7055521455;
// initialise the arrays
this.xv = [];
this.yv = [];
-
- for(var i = 0; i < 2; i++) {
-
+ 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] = nextInputValue / this.gain;
-
- this.yv[0] = this.yv[1];
- this.yv[1] = this.xv[0] + this.xv[1] + this.coeff * this.yv[0];
-
- return this.yv[1];
- };
-
-}; // end Butterworth filter constructor
-
-// 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
-ButterworthFilter2 = function(cutOff) {
-
- switch(cutOff) {
-
- case 5:
- default:
-
- 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 constructor
-
// 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.smoothingFilter = new AveragingFilter(smoothing);
// NB: frequency in radians
this.calculate = function(frequency) {
-
// make some shapes
var harmonics = 0;
var multiplier = 0;
@@ -136,20 +80,15 @@ WaveSynth = function(waveShape, numHarmonics, smoothing) {
if (this.waveShape === TRIANGLE) {
iterations++;
}
-
- for(var n = 1; n < iterations; n++) {
-
- switch(this.waveShape) {
-
+ for (var n = 1; 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 (4n-1)th harmonics by -1
@@ -162,7 +101,6 @@ WaveSynth = function(waveShape, numHarmonics, smoothing) {
}
case SQUARE: {
-
if (n % 2 === 1) {
multiplier = 1 / n;
harmonics += multiplier * Math.sin(n * frequency);
@@ -171,33 +109,31 @@ WaveSynth = function(waveShape, numHarmonics, smoothing) {
}
}
}
-
// smooth the result and return
return this.smoothingFilter.process(harmonics);
};
};
-// Create a motion wave by summing pre-calcualted sinusoidal harmonics
+// Create a motion wave by summing pre-calculated harmonics (Fourier synthesis)
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++) {
+ for (var n = 0; n < numHarmonics; n++) {
harmonics += this.magnitudes[n] * Math.cos(n * twoPiFT - this.phaseAngles[n]);
}
return harmonics;
};
};
-
-// the main filter object
+// the main filter object literal
filter = (function() {
+ // Bezier private variables
+ var _C1 = {x:0, y:0};
+ var _C4 = {x:1, y:1};
// Bezier private functions
function _B1(t) { return t * t * t };
@@ -209,63 +145,52 @@ filter = (function() {
// 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
+
+ // simple averaging (LP) filter for damping / smoothing
createAveragingFilter: function(length) {
-
var newAveragingFilter = new AveragingFilter(length);
return newAveragingFilter;
},
- createButterworthFilter1: function() {
-
- var newButterworthFilter = new ButterworthFilter1();
- return newButterworthFilter;
- },
-
- createButterworthFilter2: function(cutoff) {
-
- var newButterworthFilter = new ButterworthFilter2(cutoff);
+ // provides LP filtering with improved frequency / phase response
+ createButterworthFilter: function() {
+ var newButterworthFilter = new ButterworthFilter();
return newButterworthFilter;
},
+ // generates sawtooth, triangle or square waves using harmonics
createWaveSynth: function(waveShape, numHarmonics, smoothing) {
-
var newWaveSynth = new WaveSynth(waveShape, numHarmonics, smoothing);
return newWaveSynth;
},
+ // generates arbitrary waveforms using pre-calculated harmonics
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
+
+ // Bezier response curve shaping for more natural transitions
+ bezier: function(input, C2, C3) {
// 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;
+ input = 1 - input;
+ return _C1.y * _B1(input) + C2.y * _B2(input) + C3.y * _B3(input) + _C4.y * _B4(input);
},
- // simple clipping filter (clips bottom of wave only)
+ // simple clipping filter (special case for hips y-axis skeleton offset for walk animation)
clipTrough: function(inputValue, peak, strength) {
-
var outputValue = inputValue * strength;
if (outputValue < -peak) {
outputValue = -peak;
@@ -273,5 +198,4 @@ filter = (function() {
return outputValue;
}
}
-
})();
\ No newline at end of file
diff --git a/examples/libraries/walkInterface.js b/examples/libraries/walkInterface.js
deleted file mode 100644
index 0375fb7bea..0000000000
--- a/examples/libraries/walkInterface.js
+++ /dev/null
@@ -1,340 +0,0 @@
-//
-// walkInterface.js
-//
-// version 2.0
-//
-// Created by David Wooldridge, Autumn 2014
-//
-// Presents the UI for the walk.js script v1.12
-//
-// Distributed under the Apache License, Version 2.0.
-// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html
-//
-
-walkInterface = (function() {
-
- // references to walk.js objects
- var _motion = null;
- var _walkAssets = null;
-
- // controller UI element positions and dimensions
- var _backgroundWidth = 350;
- var _backgroundHeight = 700;
- var _backgroundX = Window.innerWidth - _backgroundWidth - 58;
- var _backgroundY = Window.innerHeight / 2 - _backgroundHeight / 2;
- var _bigButtonsY = 348;
-
- // Load up the overlays
- var _buttonOverlays = [];
-
- // ui minimised tab
- var _controlsMinimisedTab = Overlays.addOverlay("image", {
- x: Window.innerWidth - 58,
- y: Window.innerHeight - 145,
- width: 50, height: 50,
- imageURL: pathToAssets + 'overlay-images/minimised-tab.png',
- visible: true, alpha: 0.9
- });
-
- // ui background
- var _controlsBackground = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX,
- y: _backgroundY,
- width: _backgroundWidth,
- height: _backgroundHeight
- },
- imageURL: pathToAssets + "overlay-images/background.png",
- alpha: 1, visible: false
- });
-
- // button overlays
- var _controlsMinimiseButton = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth - 62,
- y: _backgroundY + 40,
- width: 25, height: 25
- },
- imageURL: pathToAssets + "overlay-images/minimise-button.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_controlsMinimiseButton);
-
- var _onButton = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/power-button-selected.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_onButton);
-
- var _offButton = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/power-button.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_offButton);
-
- var _femaleButton = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 60,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/female-button.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_femaleButton);
-
- var _femaleButtonSelected = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 60,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/female-button-selected.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_femaleButtonSelected);
-
- var _maleButton = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 120,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/male-button.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_maleButton);
-
- var _maleButtonSelected = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 120,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/male-button-selected.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_maleButtonSelected);
-
- var _armsFreeButton = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 180,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/arms-free-button.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_armsFreeButton);
-
- var _armsFreeButtonSelected = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 180,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/arms-free-button-selected.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_armsFreeButtonSelected);
-
- var _footstepsButton = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 240,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/footstep-sounds-button.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_footstepsButton);
-
- var _footstepsButtonSelected = Overlays.addOverlay("image", {
- bounds: {
- x: _backgroundX + _backgroundWidth / 2 - 115,
- y: _backgroundY + _bigButtonsY + 240,
- width: 230, height: 36
- },
- imageURL: pathToAssets + "overlay-images/footstep-sounds-button-selected.png",
- alpha: 1, visible: false
- });
- _buttonOverlays.push(_footstepsButtonSelected);
-
-
- function minimiseDialog(minimise) {
-
- Overlays.editOverlay(_controlsBackground, {visible: !minimise});
- Overlays.editOverlay(_controlsMinimisedTab, {visible: minimise});
- Overlays.editOverlay(_controlsMinimiseButton, {visible: !minimise});
-
- if(_state.powerOn) {
-
- Overlays.editOverlay(_onButton, {visible: !minimise});
- Overlays.editOverlay(_offButton, {visible: false});
-
- } else {
-
- Overlays.editOverlay(_onButton, {visible: false});
- Overlays.editOverlay(_offButton, {visible: !minimise});
-
- }
- if (_motion.avatarGender === FEMALE) {
-
- Overlays.editOverlay(_femaleButtonSelected, {visible: !minimise});
- Overlays.editOverlay(_femaleButton, {visible: false});
- Overlays.editOverlay(_maleButtonSelected, {visible: false});
- Overlays.editOverlay(_maleButton, {visible: !minimise});
-
- } else {
-
- Overlays.editOverlay(_femaleButtonSelected, {visible: false});
- Overlays.editOverlay(_femaleButton, {visible: !minimise});
- Overlays.editOverlay(_maleButtonSelected, {visible: !minimise});
- Overlays.editOverlay(_maleButton, {visible: false});
- }
- if (_motion.armsFree) {
-
- Overlays.editOverlay(_armsFreeButtonSelected, {visible: !minimise});
- Overlays.editOverlay(_armsFreeButton, {visible: false});
-
- } else {
-
- Overlays.editOverlay(_armsFreeButtonSelected, {visible: false});
- Overlays.editOverlay(_armsFreeButton, {visible: !minimise});
- }
- if (_motion.makesFootStepSounds) {
-
- Overlays.editOverlay(_footstepsButtonSelected, {visible: !minimise});
- Overlays.editOverlay(_footstepsButton, {visible: false});
-
- } else {
-
- Overlays.editOverlay(_footstepsButtonSelected, {visible: false});
- Overlays.editOverlay(_footstepsButton, {visible: !minimise});
- }
- };
-
- // mouse event handler
- function mousePressEvent(event) {
-
- var clickedOverlay = Overlays.getOverlayAtPoint({x: event.x, y: event.y});
-
- switch (clickedOverlay) {
-
- case _controlsMinimiseButton:
-
- minimiseDialog(true);
- _state.setInternalState(_state.STANDING);
- return;
-
- case _controlsMinimisedTab:
-
- minimiseDialog(false);
- _state.setInternalState(_state.STANDING);
- return;
-
- case _onButton:
-
- _state.powerOn = false;
- Overlays.editOverlay(_offButton, {visible: true});
- Overlays.editOverlay(_onButton, {visible: false});
- _state.setInternalState(state.STANDING);
- return;
-
- case _offButton:
-
- _state.powerOn = true;
- Overlays.editOverlay(_offButton, {visible: false});
- Overlays.editOverlay(_onButton, {visible: true});
- _state.setInternalState(state.STANDING);
- return;
-
-
- case _footstepsButton:
-
- _motion.makesFootStepSounds = true;
- Overlays.editOverlay(_footstepsButtonSelected, {visible: true});
- Overlays.editOverlay(_footstepsButton, {visible: false});
- return;
-
- case _footstepsButtonSelected:
-
- _motion.makesFootStepSounds = false;
- Overlays.editOverlay(_footstepsButton, {visible: true});
- Overlays.editOverlay(_footstepsButtonSelected, {visible: false});
- return;
-
- case _femaleButton:
- case _maleButtonSelected:
-
- _motion.setGender(FEMALE);
- Overlays.editOverlay(_femaleButtonSelected, {visible: true});
- Overlays.editOverlay(_femaleButton, {visible: false});
- Overlays.editOverlay(_maleButton, {visible: true});
- Overlays.editOverlay(_maleButtonSelected, {visible: false});
- return;
-
- case _maleButton:
- case _femaleButtonSelected:
-
- _motion.setGender(MALE);
- Overlays.editOverlay(_femaleButton, {visible: true});
- Overlays.editOverlay(_femaleButtonSelected, {visible: false});
- Overlays.editOverlay(_maleButtonSelected, {visible: true});
- Overlays.editOverlay(_maleButton, {visible: false});
- return;
-
- case _armsFreeButton:
-
- _motion.armsFree = true;
- Overlays.editOverlay(_armsFreeButtonSelected, {visible: true});
- Overlays.editOverlay(_armsFreeButton, {visible: false});
- return;
-
- case _armsFreeButtonSelected:
-
- _motion.armsFree = false;
- _motion.poseFingers();
- Overlays.editOverlay(_armsFreeButtonSelected, {visible: false});
- Overlays.editOverlay(_armsFreeButton, {visible: true});
- return;
- }
- };
-
- Controller.mousePressEvent.connect(mousePressEvent);
-
- // delete overlays on script ending
- Script.scriptEnding.connect(function() {
-
- // delete overlays
- Overlays.deleteOverlay(_controlsBackground);
- Overlays.deleteOverlay(_controlsMinimisedTab);
- for (var i in _buttonOverlays) {
- Overlays.deleteOverlay(_buttonOverlays[i]);
- }
- });
-
- // public method
- return {
-
- // gather references to objects from the walk.js script
- initialise: function(state, motion, walkAssets) {
-
- _state = state;
- _motion = motion;
- _walkAssets = walkAssets;
- }
-
- }; // end public methods (return)
-
-})();
\ No newline at end of file
diff --git a/examples/libraries/walkSettings.js b/examples/libraries/walkSettings.js
new file mode 100644
index 0000000000..ea35a54352
--- /dev/null
+++ b/examples/libraries/walkSettings.js
@@ -0,0 +1,74 @@
+//
+// walkSettings.js
+// version 1.0
+//
+// Created by David Wooldridge, June 2015
+// Copyright © 2015 High Fidelity, Inc.
+//
+// Presents settings for walk.js
+//
+// 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
+//
+
+WalkSettings = function() {
+ var that = {};
+
+ // ui minimised tab
+ var _innerWidth = Window.innerWidth;
+ var visible = false;
+ var _minimisedTab = Overlays.addOverlay("image", {
+ x: _innerWidth - 58, y: Window.innerHeight - 145,
+ width: 50, height: 50,
+ imageURL: pathToAssets + 'overlay-images/ddpa-minimised-ddpa-tab.png',
+ visible: true, alpha: 0.9
+ });
+ function mousePressEvent(event) {
+ if (Overlays.getOverlayAtPoint(event) === _minimisedTab) {
+ visible = !visible;
+ webView.setVisible(visible);
+ }
+ }
+ Controller.mousePressEvent.connect(mousePressEvent);
+ function cleanup() {
+ Overlays.deleteOverlay(_minimisedTab);
+ }
+ Script.update.connect(function() {
+
+ if (_innerWidth !== Window.innerWidth) {
+ _innerWidth = Window.innerWidth;
+ Overlays.editOverlay(_minimisedTab, {x: _innerWidth - 58});
+ }
+ });
+ Script.scriptEnding.connect(cleanup);
+
+ // web window
+ var url = Script.resolvePath('html/walkSettings.html');
+ var webView = new WebWindow('Walk Settings', url, 200, 180, false);
+ webView.setVisible(false);
+
+ webView.eventBridge.webEventReceived.connect(function(data) {
+ data = JSON.parse(data);
+
+ if (data.type == "init") {
+ // send the current settings to the dialog
+ webView.eventBridge.emitScriptEvent(JSON.stringify({
+ type: "update",
+ armsFree: avatar.armsFree,
+ footstepSounds: avatar.makesFootStepSounds,
+ blenderPreRotations: avatar.isBlenderExport
+ }));
+ } else if (data.type == "powerToggle") {
+ motion.isLive = !motion.isLive;
+ } else if (data.type == "update") {
+ // receive settings from the dialog
+ avatar.armsFree = data.armsFree;
+ avatar.makesFootStepSounds = data.footstepSounds;
+ avatar.isBlenderExport = data.blenderPreRotations;
+ }
+ });
+
+ return that;
+};
\ No newline at end of file
diff --git a/examples/walk.js b/examples/walk.js
index f0f42c0b4c..544a98512b 100644
--- a/examples/walk.js
+++ b/examples/walk.js
@@ -1,637 +1,56 @@
//
// walk.js
+// version 1.25
//
-// version 1.12
+// Created by David Wooldridge, June 2015
+// Copyright © 2014 - 2015 High Fidelity, Inc.
//
-// Created by David Wooldridge, Autumn 2014
-//
-// Animates an avatar using procedural animation techniques
+// 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
//
-// constants
-var MALE = 1;
-var FEMALE = 2;
-var MAX_WALK_SPEED = 2.5;
-var TAKE_FLIGHT_SPEED = 4.55;
-var TOP_SPEED = 300;
+// locomotion states
+var STATIC = 1;
+var SURFACE_MOTION = 2;
+var AIR_MOTION = 4;
+
+// directions
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 NONE = 64;
+
+// waveshapes
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)
+// constants
+var MOVE_THRESHOLD = 0.075;
+var MAX_WALK_SPEED = 2.9; // peak, by observation
+var MAX_FT_WHEEL_INCREMENT = 25; // avoid fast walk when landing
+var TOP_SPEED = 300;
+var ACCELERATION_THRESHOLD = 0.2; // detect stop to walking
+var DECELERATION_THRESHOLD = -6; // detect walking to stop
+var FAST_DECELERATION_THRESHOLD = -150; // detect flying to stop
+var BOUNCE_ACCELERATION_THRESHOLD = 25; // used to ignore gravity influence fluctuations after landing
+var GRAVITY_THRESHOLD = 3.0; // height above surface where gravity is in effect
+var OVERCOME_GRAVITY_SPEED = 0.5; // reaction sensitivity to jumping under gravity
+var LANDING_THRESHOLD = 0.35; // metres from a surface below which need to prepare for impact
+var ON_SURFACE_THRESHOLD = 0.1; // height above surface to be considered as on the surface
+var MAX_TRANSITION_RECURSION = 10; // how many nested transitions are permitted
+var TRANSITION_COMPLETE = 1000;
+var HIFI_PUBLIC_BUCKET = "https://hifi-public.s3.amazonaws.com/";
+
+// check for existence of data file object property
function isDefined(value) {
-
try {
if (typeof value != 'undefined') return true;
} catch (e) {
@@ -639,214 +58,445 @@ function isDefined(value) {
}
}
-// helper function for animateAvatar(). calculate joint translations based on animation file settings and frequency * time
-function calculateTranslations(animation, ft, direction) {
+// path to animations, reach-poses, reachPoses, transitions, overlay images and reference files
+var pathToAssets = HIFI_PUBLIC_BUCKET + "procedural-animator/assets/";
- var jointName = "Hips";
- var joint = animation.joints[jointName];
- var jointTranslations = {x:0, y:0, z:0};
+Script.include([
+ "./libraries/walkFilters.js",
+ "./libraries/walkApi.js",
+ pathToAssets + "walkAssets.js"
+]);
- // gather modifiers and multipliers
- modifiers = new JointModifiers(joint, direction);
+// construct Avatar and Motion
+var avatar = new Avatar();
+var motion = new Motion();
- // calculate translations. Use synthesis filters where specified by the animation file.
+// create settings dialog
+Script.include("./libraries/walkSettings.js");
+var walkSettings = WalkSettings();
- // sway (oscillation on the x-axis)
- if(animation.filters.hasOwnProperty(jointName) &&
- 'swayFilter' in animation.filters[jointName]) {
+// create and initialise Transition
+var nullTransition = new Transition();
+motion.currentTransition = nullTransition;
- jointTranslations.x = joint.sway *
- animation.filters[jointName].swayFilter.
- calculate(filter.degToRad(ft + joint.swayPhase)) +
- joint.swayOffset;
+// motion smoothing / damping filters
+var FLY_BLEND_DAMPING = 50;
+var leanPitchSmoothingFilter = filter.createButterworthFilter();
+var leanRollSmoothingFilter = filter.createButterworthFilter();
+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);
- } else {
+// Main loop
+Script.update.connect(function(deltaTime) {
- jointTranslations.x = joint.sway *
- Math.sin(filter.degToRad(ft + joint.swayPhase)) +
- joint.swayOffset;
+ if (motion.isLive) {
+
+ // assess current locomotion state
+ motion.assess(deltaTime);
+
+ // decide which animation should be playing
+ selectAnimation();
+
+ // turn the frequency time wheels. determine stride length
+ determineStride();
+
+ // update the progress of any live transitions
+ updateTransitions();
+
+ // apply translation and rotations
+ renderMotion();
+
+ // record this frame's parameters
+ motion.saveHistory();
}
+});
- // 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);
+// 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 (isDefined(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;
- // 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;
+ // reset default first footstep
+ if (nextAnimation === avatar.selectedWalkBlend && lastTransition === nullTransition) {
+ avatar.nextStep = RIGHT;
}
-
- return jointTranslations;
}
-// helper function for animateAvatar(). calculate joint rotations based on animation file settings and frequency * time
-function calculateRotations(jointName, animation, ft, direction) {
+// select / blend the appropriate animation for the current state of motion
+function selectAnimation() {
+ var playTransitionReachPoses = true;
- var joint = animation.joints[jointName];
- var jointRotations = {x:0, y:0, z:0};
+ // 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);
+ }
+ if (motion.state !== STATIC) {
+ motion.state = STATIC;
+ }
+ avatar.selectedWalkBlend.lastDirection = NONE;
+ break;
+ }
- // gather modifiers and multipliers for this joint
- modifiers = new JointModifiers(joint, direction);
+ 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;
- // calculate rotations. Use synthesis filters where specified by the animation file.
+ 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;
- // calculate pitch
- if(animation.filters.hasOwnProperty(jointName) &&
- 'pitchFilter' in animation.filters[jointName]) {
+ case LEFT:
+ animationOperations.deepCopy(avatar.selectedSideStepLeft, avatar.selectedWalkBlend);
+ avatar.selectedWalkBlend.lastDirection = LEFT;
+ avatar.calibration.strideLength = avatar.selectedSideStepLeft.calibration.strideLength;
+ break
- 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);
+ 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);
+ }
+ if (motion.state !== SURFACE_MOTION) {
+ 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);
+ }
+ if (motion.state !== AIR_MOTION) {
+ 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 determineStride() {
+ 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 {
-
- 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);
+ // turn the frequency time wheel by the amount specified for this animation
+ wheelAdvance = filter.radToDeg(avatar.currentAnimation.calibration.frequency * motion.deltaTime);
}
- // 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;
+ 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);
}
-
- // 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;
+
+ // avoid unnaturally fast walking when landing at speed - simulates skimming / skidding
+ if (Math.abs(wheelAdvance) > MAX_FT_WHEEL_INCREMENT) {
+ wheelAdvance = 0;
}
- return jointRotations;
+
+ // 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)
+ if (avatar.currentAnimation === avatar.selectedWalkBlend &&
+ (Vec3.length(motion.velocity) / MAX_WALK_SPEED > 0.97)) {
+
+ var strideMaxAt = avatar.currentAnimation.calibration.strideMaxAt;
+ var 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;
+ }
+ }
+ }
+ // update the Transition progress
+ 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
+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 motion.calibration.PITCH_MAX * leanProgress;
+}
+
+// helper function for renderMotion(). calculate the angle at which to bank into corners whilst turning
+function getLeanRoll() {
+ var leanRollProgress = 0;
+ var linearContribution = 0;
+
+ if (Vec3.length(motion.velocity) > 0) {
+ linearContribution = (Math.log(Vec3.length(motion.velocity) / TOP_SPEED) + 8) / 8;
+ }
+ var angularContribution = Math.abs(motion.yawDelta) / motion.calibration.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 motion.calibration.ROLL_MAX * leanRollProgress;
}
// animate the avatar using sine waves, geometric waveforms and harmonic generators
-function animateAvatar(deltaTime, speed) {
+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};
- // 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);
-
+ // 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 = calculateTranslations(motion.currentAnimation,
- motion.frequencyTimeWheelPos,
- motion.direction);
+ hipsTranslations = animationOperations.calculateTranslations(avatar.currentAnimation,
+ motion.frequencyTimeWheelPos,
+ motion.direction);
}
-
- // factor in any acceleration driven leaning
- hipsTranslations.z += motion.calibration.hipsToFeet * Math.sin(filter.degToRad(leanPitch));
-
+ // 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);
- // 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));
+ // 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) {
+ var ftWheelPosition = motion.frequencyTimeWheelPos;
+ if (motion.currentTransition !== nullTransition &&
+ motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) {
+ ftWheelPosition = motion.currentTransition.lastFrequencyTimeWheelPos;
+ }
+ if (avatar.nextStep === LEFT && ftWheelPosition > 270) {
+ avatar.makeFootStepSound();
+ } else if (avatar.nextStep === RIGHT && (ftWheelPosition < 270 && ftWheelPosition > 90)) {
+ 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
+ if (avatar.armsFree && (joint.IKChain === "LeftArm" || joint.IKChain === "RightArm")) {
+ continue;
+ }
+ if (avatar.headFree && joint.IKChain === "Head") {
+ continue;
+ }
+
+ // if there's a live transition, blend 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.setJointData(jointName, Quat.fromVec3Degrees(jointRotations));
+ }
+}
\ No newline at end of file