diff --git a/examples/html/walkSettings.html b/examples/html/walkSettings.html index 6c0eaff2cc..cc9e8793f7 100644 --- a/examples/html/walkSettings.html +++ b/examples/html/walkSettings.html @@ -1,6 +1,6 @@ <html> <head> - <link rel="stylesheet" type="text/css" href="style.css"> + <link rel="stylesheet" type="text/css" href="walkStyle.css"> <script> function emitUpdate() { @@ -14,6 +14,7 @@ function loaded() { // assign form elements to vars + var powerOn = true; elPower = document.getElementById("power"); elArmsFree = document.getElementById("arms-free"); elFootstepSounds = document.getElementById("footstep-sounds"); @@ -25,13 +26,13 @@ if (data.type == "update") { if (data.armsFree !== undefined) { - elArmsFree.checked = data.armsFree == true; + elArmsFree.checked = data.armsFree; } if (data.footstepSounds !== undefined) { - elFootstepSounds.checked = data.footstepSounds == true; + elFootstepSounds.checked = data.footstepSounds; } if (data.blenderPreRotations !== undefined) { - elBlenderPreRotations.checked = data.blenderPreRotations == true; + elBlenderPreRotations.checked = data.blenderPreRotations; } } }); @@ -45,6 +46,12 @@ EventBridge.emitWebEvent(JSON.stringify({ type: "powerToggle" })); + powerOn = !powerOn; + if (powerOn) { + elPower.value = "Turn Animation Off"; + } else { + elPower.value = "Turn Animation On"; + } }); // request initial values EventBridge.emitWebEvent(JSON.stringify({ type: 'init' })); @@ -52,27 +59,27 @@ </script> </head> <body onload='loaded();'> - <div class="grid-section"> + <div> - <div id="entity-list-header"> - <input type="button" id="power" value="Power" style="margin-left:68px; margin-top:10px"></button> + <div id="walk-settings-header"> + <input type="button" id="power" value="Turn Animation Off" style="margin-left:30px; margin-top:10px"></button> </div> - <div class="property-section"> + <div class="settings-section"> <label>Arms free</label> <span> <input type='checkbox' id="arms-free"> </span> </div> - <div class="property-section"> + <div class="settings-section"> <label>Footstep sounds</label> <span> <input type='checkbox' id="footstep-sounds"> </span> </div> - <div class="property-section"> + <div class="settings-section"> <label>Blender pre-rotations</label> <span> <input type='checkbox' id="bender-pre-rotations"> diff --git a/examples/libraries/walkApi.js b/examples/libraries/walkApi.js index a3a73a59a0..3144b9df45 100644 --- a/examples/libraries/walkApi.js +++ b/examples/libraries/walkApi.js @@ -2,10 +2,10 @@ // walkApi.js // version 1.3 // -// Created by David Wooldridge, June 2015 -// Copyright © 2014 - 2015 High Fidelity, Inc. +// Created by David Wooldridge, Autumn 2014 +// Copyright © 2015 High Fidelity, Inc. // -// Exposes API for use by walk.js version 1.2+. +// Exposes API for use by walk.js version 1.2+. // // Editing tools for animation data files available here: https://github.com/DaveDubUK/walkTools // @@ -13,6 +13,44 @@ // See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html // +// locomotion states +const STATIC = 1; +const SURFACE_MOTION = 2; +const AIR_MOTION = 4; + +// directions +const UP = 1; +const DOWN = 2; +const LEFT = 4; +const RIGHT = 8; +const FORWARDS = 16; +const BACKWARDS = 32; +const NONE = 64; + +// waveshapes +const SAWTOOTH = 1; +const TRIANGLE = 2; +const SQUARE = 4; + +// constants used by walk.js and walkApi.js +const MAX_WALK_SPEED = 2.9; // peak, by observation +const MAX_FT_WHEEL_INCREMENT = 25; // avoid fast walk when landing +const TOP_SPEED = 300; +const ON_SURFACE_THRESHOLD = 0.1; // height above surface to be considered as on the surface +const TRANSITION_COMPLETE = 1000; +const HIFI_PUBLIC_BUCKET = "https://hifi-public.s3.amazonaws.com/"; + +// constants used by walkApi.js +const MOVE_THRESHOLD = 0.075; +const ACCELERATION_THRESHOLD = 0.2; // detect stop to walking +const DECELERATION_THRESHOLD = -6; // detect walking to stop +const FAST_DECELERATION_THRESHOLD = -150; // detect flying to stop +const BOUNCE_ACCELERATION_THRESHOLD = 25; // used to ignore gravity influence fluctuations after landing +const GRAVITY_THRESHOLD = 3.0; // height above surface where gravity is in effect +const OVERCOME_GRAVITY_SPEED = 0.5; // reaction sensitivity to jumping under gravity +const LANDING_THRESHOLD = 0.35; // metres from a surface below which need to prepare for impact +const MAX_TRANSITION_RECURSION = 10; // how many nested transitions are permitted + 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() { @@ -20,8 +58,13 @@ Avatar = function() { var numberOfButtons = Controller.getNumberOfButtons(); var numberOfTriggers = Controller.getNumberOfTriggers(); var numberOfSpatialControls = Controller.getNumberOfSpatialControls(); + const HYDRA_BUTTONS = 12; + const HYDRA_TRIGGERS = 2; + const HYDRA_CONTROLLERS_PER_TRIGGER = 2; var controllersPerTrigger = numberOfSpatialControls / numberOfTriggers; - if (numberOfButtons == 12 && numberOfTriggers == 2 && controllersPerTrigger == 2) { + if (numberOfButtons == HYDRA_BUTTONS && + numberOfTriggers == HYDRA_TRIGGERS && + controllersPerTrigger == HYDRA_CONTROLLERS_PER_TRIGGER) { print('walk.js info: Razer Hydra detected. Setting arms free (not controlled by script)'); return true; } else { @@ -32,7 +75,7 @@ Avatar = function() { // settings 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.makesFootStepSounds = false; this.isBlenderExport = false; // temporary fix this.animationSet = undefined; // currently just one animation set this.setAnimationSet = function(animationSet) { @@ -45,8 +88,6 @@ Avatar = function() { 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"); @@ -59,17 +100,16 @@ Avatar = function() { } this.setAnimationSet('standardMale'); - this.startTime = new Date().getTime(); - // calibration this.calibration = { - hipsToFeet: 1.011, + hipsToFeet: 1, 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; + const MAX_ATTEMPTS = 3; + var attempts = MAX_ATTEMPTS; var extraAttempts = 0; do { for (joint in walkAssets.animationReference.joints) { @@ -81,8 +121,8 @@ Avatar = function() { } } this.calibration.hipsToFeet = MyAvatar.getJointPosition("Hips").y - MyAvatar.getJointPosition("RightToeBase").y; - - // maybe measuring before Blender pre-rotations have been applied? + + // maybe measuring before Blender pre-rotations have been applied? if (this.calibration.hipsToFeet < 0 && this.isBlenderExport) { this.calibration.hipsToFeet *= -1; } @@ -95,7 +135,7 @@ Avatar = function() { // just in case if (this.calibration.hipsToFeet <= 0 || isNaN(this.calibration.hipsToFeet)) { - this.calibration.hipsToFeet = 1.0; + this.calibration.hipsToFeet = 1; 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 { @@ -125,22 +165,34 @@ Avatar = function() { // footsteps this.nextStep = RIGHT; // the first step is right, because the waveforms say so + + this.leftAudioInjector = null; + this.rightAudioInjector = null; 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; + const SPEED_THRESHOLD = 0.4; + const VOLUME_ATTENUATION = 0.8; + const 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); + if (this.rightAudioInjector === null) { + this.rightAudioInjector = Audio.playSound(walkAssets.footsteps[0], options); + } else { + //this.rightAudioInjector.setOptions(options); + this.rightAudioInjector.restart(); + } this.nextStep = LEFT; } else if (this.nextStep === LEFT) { - Audio.playSound(walkAssets.footsteps[1], options); + if (this.leftAudioInjector === null) { + this.leftAudioInjector = Audio.playSound(walkAssets.footsteps[1], options); + } else { + //this.leftAudioInjector.setOptions(options); + this.leftAudioInjector.restart(); + } this.nextStep = RIGHT; } } @@ -189,12 +241,12 @@ Motion = function() { 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); - this.deltaTimeFilter = filter.createAveragingFilter(YAW_SMOOTHING); - this.yawDeltaAccelerationFilter = filter.createAveragingFilter(YAW_SMOOTHING); + this.yawFilter = filter.createAveragingFilter(YAW_SMOOTHING); //createButterworthFilter(); // + this.deltaTimeFilter = filter.createAveragingFilter(YAW_SMOOTHING); //createButterworthFilter(); // + this.yawDeltaAccelerationFilter = filter.createAveragingFilter(YAW_SMOOTHING); //createButterworthFilter(); // // assess locomotion state this.assess = function(deltaTime) { @@ -298,16 +350,16 @@ Motion = function() { var surfaceMotion = isOnSurface && this.isMoving; var acceleratingAndAirborne = this.isAccelerating && !isOnSurface; var goingTooFastToWalk = !this.isDecelerating && this.isFlyingSpeed; - var movingDirectlyUpOrDown = (this.direction === UP || this.direction === DOWN) + 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 || + var staticToAirMotion = this.isMoving && (acceleratingAndAirborne || goingTooFastToWalk || (movingDirectlyUpOrDown && !isOnSurface)); var staticToSurfaceMotion = surfaceMotion && !motion.isComingToHalt && !movingDirectlyUpOrDown && - !this.isDecelerating && lateralVelocity > MOVE_THRESHOLD; + !this.isDecelerating && lateralVelocity > MOVE_THRESHOLD; if (staticToAirMotion) { this.nextState = AIR_MOTION; } else if (staticToSurfaceMotion) { @@ -318,8 +370,8 @@ Motion = function() { break; case SURFACE_MOTION: - var surfaceMotionToStatic = !this.isMoving || - (this.isDecelerating && motion.lastDirection !== DOWN && surfaceMotion && + 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) || @@ -339,7 +391,8 @@ Motion = function() { case AIR_MOTION: var airMotionToSurfaceMotion = (surfaceMotion || aboutToLand) && !movingDirectlyUpOrDown; - var airMotionToStatic = !this.isMoving && this.direction === this.lastDirection; + var airMotionToStatic = !this.isMoving && this.direction === this.lastDirection; //|| + //this.isDeceleratingFast || isOnSurface; if (airMotionToSurfaceMotion){ this.nextState = SURFACE_MOTION; } else if (airMotionToStatic) { @@ -352,10 +405,12 @@ Motion = function() { } // frequency time wheel (foot / ground speed matching) + const DEFAULT_HIPS_TO_FEET = 1; this.frequencyTimeWheelPos = 0; - this.frequencyTimeWheelRadius = 0.5; + this.frequencyTimeWheelRadius = DEFAULT_HIPS_TO_FEET / 2; this.recentFrequencyTimeIncrements = []; - for (var i = 0; i < 8; i++) { + const FT_WHEEL_HISTORY_LENGTH = 8; + for (var i = 0; i < FT_WHEEL_HISTORY_LENGTH; i++) { this.recentFrequencyTimeIncrements.push(0); } this.averageFrequencyTimeIncrement = 0; @@ -370,8 +425,9 @@ Motion = function() { } this.averageFrequencyTimeIncrement /= this.recentFrequencyTimeIncrements.length; this.frequencyTimeWheelPos += angle; - if (this.frequencyTimeWheelPos >= 360) { - this.frequencyTimeWheelPos = this.frequencyTimeWheelPos % 360; + const FULL_CIRCLE = 360; + if (this.frequencyTimeWheelPos >= FULL_CIRCLE) { + this.frequencyTimeWheelPos = this.frequencyTimeWheelPos % FULL_CIRCLE; } } @@ -428,7 +484,7 @@ animationOperations = (function() { } else { jointTranslations.z = joint.thrust * Math.sin (filter.degToRad(modifiers.thrustFrequencyMultiplier * ft + joint.thrustPhase)) + joint.thrustOffset; - } + } return jointTranslations; }, @@ -516,7 +572,7 @@ animationOperations = (function() { // harmonics targetAnimation.harmonics = {}; - if (isDefined(sourceAnimation.harmonics)) { + if (sourceAnimation.harmonics) { targetAnimation.harmonics = JSON.parse(JSON.stringify(sourceAnimation.harmonics)); } @@ -524,26 +580,26 @@ animationOperations = (function() { targetAnimation.filters = {}; for (i in sourceAnimation.filters) { // are any filters specified for this joint? - if (isDefined(sourceAnimation.filters[i])) { + if (sourceAnimation.filters[i]) { targetAnimation.filters[i] = sourceAnimation.filters[i]; // wave shapers - if (isDefined(sourceAnimation.filters[i].pitchFilter)) { + if (sourceAnimation.filters[i].pitchFilter) { targetAnimation.filters[i].pitchFilter = sourceAnimation.filters[i].pitchFilter; } - if (isDefined(sourceAnimation.filters[i].yawFilter)) { + if (sourceAnimation.filters[i].yawFilter) { targetAnimation.filters[i].yawFilter = sourceAnimation.filters[i].yawFilter; } - if (isDefined(sourceAnimation.filters[i].rollFilter)) { + if (sourceAnimation.filters[i].rollFilter) { targetAnimation.filters[i].rollFilter = sourceAnimation.filters[i].rollFilter; } // LP filters - if (isDefined(sourceAnimation.filters[i].swayLPFilter)) { + if (sourceAnimation.filters[i].swayLPFilter) { targetAnimation.filters[i].swayLPFilter = sourceAnimation.filters[i].swayLPFilter; } - if (isDefined(sourceAnimation.filters[i].bobLPFilter)) { + if (sourceAnimation.filters[i].bobLPFilter) { targetAnimation.filters[i].bobLPFilter = sourceAnimation.filters[i].bobLPFilter; } - if (isDefined(sourceAnimation.filters[i].thrustLPFilter)) { + if (sourceAnimation.filters[i].thrustLPFilter) { targetAnimation.filters[i].thrustLPFilter = sourceAnimation.filters[i].thrustLPFilter; } } @@ -618,6 +674,11 @@ TransitionParameters = function() { this.reachPoseNames = []; } +const QUARTER_CYCLE = 90; +const HALF_CYCLE = 180; +const THREE_QUARTER_CYCLE = 270; +const FULL_CYCLE = 360; + // constructor for animation Transition Transition = function(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses) { @@ -645,7 +706,7 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti // set parameters for the transition this.parameters = new TransitionParameters(); this.liveReachPoses = []; - if (walkAssets && isDefined(lastAnimation) && isDefined(nextAnimation)) { + if (walkAssets && lastAnimation && 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 @@ -661,11 +722,10 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti // 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; + + const FULL_CYCLE_THRESHOLD = 320; + const HALF_CYCLE_THRESHOLD = 140; + const CYCLE_COMMIT_THRESHOLD = 5; // 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) { @@ -691,7 +751,8 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti // 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 + const TWO_THIRDS = 0.6667; + this.lastFrequencyTimeIncrement *= TWO_THIRDS; // 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, @@ -719,7 +780,7 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti 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 @@ -727,20 +788,20 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti var wheelAdvance = undefined; if (this.lastAnimation === avatar.selectedWalkBlend && - this.nextAnimation === avatar.selectedIdle) { + this.nextAnimation === avatar.selectedIdle) { if (this.degreesRemaining <= 0) { // stop continued motion wheelAdvance = 0; if (motion.isComingToHalt) { - if (this.lastFrequencyTimeWheelPos < 90) { + if (this.lastFrequencyTimeWheelPos < QUARTER_CYCLE) { this.lastFrequencyTimeWheelPos = 0; } else { - this.lastFrequencyTimeWheelPos = 180; + this.lastFrequencyTimeWheelPos = HALF_CYCLE; } } } else { wheelAdvance = this.lastFrequencyTimeIncrement; - var distanceToTravel = avatar.calibration.strideLength * wheelAdvance / 180; + var distanceToTravel = avatar.calibration.strideLength * wheelAdvance / HALF_CYCLE; if (this.degreesRemaining <= 0) { distanceToTravel = 0; this.degreesRemaining = 0; @@ -754,8 +815,8 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti // advance the ft wheel this.lastFrequencyTimeWheelPos += wheelAdvance; - if (this.lastFrequencyTimeWheelPos >= 360) { - this.lastFrequencyTimeWheelPos = this.lastFrequencyTimeWheelPos % 360; + if (this.lastFrequencyTimeWheelPos >= FULL_CYCLE) { + this.lastFrequencyTimeWheelPos = this.lastFrequencyTimeWheelPos % FULL_CYCLE; } // advance ft wheel for the nested (previous) Transition @@ -768,9 +829,11 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti }; this.updateProgress = function() { - var elapasedTime = (new Date().getTime() - this.startTime) / 1000; + const MILLISECONDS_CONVERT = 1000; + const ACCURACY_INCREASER = 1000; + var elapasedTime = (new Date().getTime() - this.startTime) / MILLISECONDS_CONVERT; this.progress = elapasedTime / this.parameters.duration; - this.progress = Math.round(this.progress * 1000) / 1000; + this.progress = Math.round(this.progress * ACCURACY_INCREASER) / ACCURACY_INCREASER; // updated nested transition/s if (this.lastTransition !== nullTransition) { @@ -792,6 +855,7 @@ Transition = function(nextAnimation, lastAnimation, lastTransition, playTransiti // 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; }; @@ -896,23 +960,23 @@ FrequencyMultipliers = function(joint, direction) { this.bobFrequencyMultiplier = 1; this.thrustFrequencyMultiplier = 1; - if (isDefined(joint)) { - if (isDefined(joint.pitchFrequencyMultiplier)) { + if (joint) { + if (joint.pitchFrequencyMultiplier) { this.pitchFrequencyMultiplier = joint.pitchFrequencyMultiplier; } - if (isDefined(joint.yawFrequencyMultiplier)) { + if (joint.yawFrequencyMultiplier) { this.yawFrequencyMultiplier = joint.yawFrequencyMultiplier; } - if (isDefined(joint.rollFrequencyMultiplier)) { + if (joint.rollFrequencyMultiplier) { this.rollFrequencyMultiplier = joint.rollFrequencyMultiplier; } - if (isDefined(joint.swayFrequencyMultiplier)) { + if (joint.swayFrequencyMultiplier) { this.swayFrequencyMultiplier = joint.swayFrequencyMultiplier; } - if (isDefined(joint.bobFrequencyMultiplier)) { + if (joint.bobFrequencyMultiplier) { this.bobFrequencyMultiplier = joint.bobFrequencyMultiplier; } - if (isDefined(joint.thrustFrequencyMultiplier)) { + if (joint.thrustFrequencyMultiplier) { this.thrustFrequencyMultiplier = joint.thrustFrequencyMultiplier; } } diff --git a/examples/libraries/walkFilters.js b/examples/libraries/walkFilters.js index 7bf05d2c9f..1734c81414 100644 --- a/examples/libraries/walkFilters.js +++ b/examples/libraries/walkFilters.js @@ -2,8 +2,8 @@ // walkFilters.js // version 1.1 // -// Created by David Wooldridge, June 2015 -// Copyright © 2014 - 2015 High Fidelity, Inc. +// Created by David Wooldridge, Autumn 2014 +// Copyright © 2015 High Fidelity, Inc. // // Provides a variety of filters for use by the walk.js script v1.2+ // @@ -118,6 +118,7 @@ WaveSynth = function(waveShape, numHarmonics, smoothing) { HarmonicsFilter = function(magnitudes, phaseAngles) { this.magnitudes = magnitudes; this.phaseAngles = phaseAngles; + this.calculate = function(twoPiFT) { var harmonics = 0; var numHarmonics = magnitudes.length; @@ -130,6 +131,9 @@ HarmonicsFilter = function(magnitudes, phaseAngles) { // the main filter object literal filter = (function() { + + const HALF_CYCLE = 180; + // Bezier private variables var _C1 = {x:0, y:0}; var _C4 = {x:1, y:1}; @@ -144,17 +148,17 @@ filter = (function() { // helper methods degToRad: function(degrees) { - var convertedValue = degrees * Math.PI / 180; + var convertedValue = degrees * Math.PI / HALF_CYCLE; return convertedValue; }, radToDeg: function(radians) { - var convertedValue = radians * 180 / Math.PI; + var convertedValue = radians * HALF_CYCLE / 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); @@ -180,7 +184,7 @@ filter = (function() { }, // the following filters do not need separate instances, as they hold no previous values - + // 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/ diff --git a/examples/libraries/walkSettings.js b/examples/libraries/walkSettings.js index ea35a54352..6175d8b9fe 100644 --- a/examples/libraries/walkSettings.js +++ b/examples/libraries/walkSettings.js @@ -1,8 +1,8 @@ // // walkSettings.js -// version 1.0 +// version 0.1 // -// Created by David Wooldridge, June 2015 +// Created by David Wooldridge, Summer 2015 // Copyright © 2015 High Fidelity, Inc. // // Presents settings for walk.js @@ -14,52 +14,83 @@ // WalkSettings = function() { - var that = {}; - - // ui minimised tab + var _visible = false; var _innerWidth = Window.innerWidth; - var visible = false; - var _minimisedTab = Overlays.addOverlay("image", { - x: _innerWidth - 58, y: Window.innerHeight - 145, - width: 50, height: 50, + const MARGIN_RIGHT = 58; + const MARGIN_TOP = 145; + const ICON_SIZE = 50; + const ICON_ALPHA = 0.9; + + var minimisedTab = Overlays.addOverlay("image", { + x: _innerWidth - MARGIN_RIGHT, y: Window.innerHeight - MARGIN_TOP, + width: ICON_SIZE, height: ICON_SIZE, imageURL: pathToAssets + 'overlay-images/ddpa-minimised-ddpa-tab.png', - visible: true, alpha: 0.9 + visible: true, alpha: ICON_ALPHA }); + function mousePressEvent(event) { - if (Overlays.getOverlayAtPoint(event) === _minimisedTab) { - visible = !visible; - webView.setVisible(visible); + 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.update.connect(function(deltaTime) { + if (window.innerWidth !== _innerWidth) { + _innerWidth = window.innerWidth; + Overlays.EditOverlay(minimisedTab, {x: _innerWidth - MARGIN_RIGHT}); } }); + + function cleanup() { + Overlays.deleteOverlay(minimisedTab); + } Script.scriptEnding.connect(cleanup); + + var _control = false; + var _shift = false; + function keyPressEvent(event) { + if (event.text === "CONTROL") { + _control = true; + } + if (event.text === "SHIFT") { + _shift = true; + } + if (_shift && (event.text === 'o' || event.text === 'O')) { + _visible = !_visible; + _webView.setVisible(_visible); + } + } + function keyReleaseEvent(event) { + if (event.text === "CONTROL") { + _control = false; + } + if (event.text === "SHIFT") { + _shift = false; + } + } + Controller.keyPressEvent.connect(keyPressEvent); + Controller.keyReleaseEvent.connect(keyReleaseEvent); // web window - var url = Script.resolvePath('html/walkSettings.html'); - var webView = new WebWindow('Walk Settings', url, 200, 180, false); - webView.setVisible(false); + var _url = Script.resolvePath('html/walkSettings.html'); + const PANEL_WIDTH = 200; + const PANEL_HEIGHT = 180; + var _webView = new WebWindow('Walk Settings', _url, PANEL_WIDTH, PANEL_HEIGHT, false); + _webView.setVisible(false); - webView.eventBridge.webEventReceived.connect(function(data) { + _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 - })); + _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") { @@ -69,6 +100,6 @@ WalkSettings = function() { avatar.isBlenderExport = data.blenderPreRotations; } }); +}; - return that; -}; \ No newline at end of file +walkSettings = WalkSettings(); \ No newline at end of file diff --git a/examples/walk.js b/examples/walk.js index 70acbf551a..0b57eb2ece 100644 --- a/examples/walk.js +++ b/examples/walk.js @@ -2,11 +2,11 @@ // walk.js // version 1.25 // -// Created by David Wooldridge, June 2015 -// Copyright � 2014 - 2015 High Fidelity, Inc. +// Created by David Wooldridge, May 2015 +// Copyright © 2015 High Fidelity, Inc. // // Animates an avatar using procedural animation techniques. -// +// // Editing tools for animation data files available here: https://github.com/DaveDubUK/walkTools // // Distributed under the Apache License, Version 2.0. @@ -33,30 +33,12 @@ var TRIANGLE = 2; var SQUARE = 4; // 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) { - return false; - } -} +const MAX_WALK_SPEED = 2.9; // peak, by observation +const MAX_FT_WHEEL_INCREMENT = 25; // avoid fast walk when landing +const TOP_SPEED = 300; +const ON_SURFACE_THRESHOLD = 0.1; // height above surface to be considered as on the surface +const TRANSITION_COMPLETE = 1000; +const HIFI_PUBLIC_BUCKET = "https://hifi-public.s3.amazonaws.com/"; // path to animations, reach-poses, reachPoses, transitions, overlay images and reference files var pathToAssets = HIFI_PUBLIC_BUCKET + "procedural-animator/assets/"; @@ -67,13 +49,12 @@ Script.include([ pathToAssets + "walkAssets.js" ]); -// construct Avatar and Motion +// construct Avatar and Motion var avatar = new Avatar(); var motion = new Motion(); // create settings dialog Script.include("./libraries/walkSettings.js"); -var walkSettings = WalkSettings(); // create and initialise Transition var nullTransition = new Transition(); @@ -92,14 +73,14 @@ var flyBackwardFilter = filter.createAveragingFilter(FLY_BLEND_DAMPING); Script.update.connect(function(deltaTime) { 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 + // turn the frequency time wheels and determine stride length determineStride(); // update the progress of any live transitions @@ -109,7 +90,7 @@ Script.update.connect(function(deltaTime) { renderMotion(); // record this frame's parameters - motion.saveHistory(); + motion.saveHistory(); } }); @@ -117,29 +98,29 @@ Script.update.connect(function(deltaTime) { 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)) { + if (lastAnimation.lastDirection) { switch(lastAnimation.lastDirection) { - + case FORWARDS: lastAnimation = avatar.selectedWalk; break; - - case BACKWARDS: + + case BACKWARDS: lastAnimation = avatar.selectedWalkBackwards; break; - + case LEFT: lastAnimation = avatar.selectedSideStepLeft; break; - - case RIGHT: + + case RIGHT: lastAnimation = avatar.selectedSideStepRight; - break; + break; } } - + motion.currentTransition = new Transition(nextAnimation, lastAnimation, lastTransition, playTransitionReachPoses); avatar.currentAnimation = nextAnimation; @@ -156,29 +137,27 @@ function selectAnimation() { // select appropriate animation. create transitions where appropriate switch (motion.nextState) { case STATIC: { - if (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD && - avatar.currentAnimation !== avatar.selectedIdle) { + if (avatar.distanceFromSurface < ON_SURFACE_THRESHOLD && + avatar.currentAnimation !== avatar.selectedIdle) { setTransition(avatar.selectedIdle, playTransitionReachPoses); - } else if (!(avatar.distanceFromSurface < ON_SURFACE_THRESHOLD) && + } else if (!(avatar.distanceFromSurface < ON_SURFACE_THRESHOLD) && avatar.currentAnimation !== avatar.selectedHover) { setTransition(avatar.selectedHover, playTransitionReachPoses); } - if (motion.state !== STATIC) { - motion.state = STATIC; - } + motion.state = STATIC; avatar.selectedWalkBlend.lastDirection = NONE; break; } case SURFACE_MOTION: { // walk transition reach poses are currently only specified for starting to walk forwards - playTransitionReachPoses = (motion.direction === FORWARDS); + 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); + animationOperations.deepCopy(avatar.selectedWalk, avatar.selectedWalkBlend); avatar.calibration.strideLength = avatar.selectedWalk.calibration.strideLength; } avatar.selectedWalkBlend.lastDirection = FORWARDS; @@ -186,7 +165,7 @@ function selectAnimation() { case BACKWARDS: if (avatar.selectedWalkBlend.lastDirection !== BACKWARDS) { - animationOperations.deepCopy(avatar.selectedWalkBackwards, avatar.selectedWalkBlend); + animationOperations.deepCopy(avatar.selectedWalkBackwards, avatar.selectedWalkBlend); avatar.calibration.strideLength = avatar.selectedWalkBackwards.calibration.strideLength; } avatar.selectedWalkBlend.lastDirection = BACKWARDS; @@ -203,7 +182,7 @@ function selectAnimation() { 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); @@ -211,13 +190,11 @@ function selectAnimation() { 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; - } + motion.state = SURFACE_MOTION; break; } @@ -229,18 +206,18 @@ function selectAnimation() { 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); + backwardComponent = flyBackwardFilter.process(backwardComponent); // normalise directional components var normaliser = upComponent + downComponent + forwardComponent + backwardComponent; @@ -248,7 +225,7 @@ function selectAnimation() { downComponent = downComponent / normaliser; forwardComponent = forwardComponent / normaliser; backwardComponent = backwardComponent / normaliser; - + // blend animations proportionally if (upComponent > 0) { animationOperations.blendAnimation(avatar.selectedFlyUp, @@ -274,9 +251,7 @@ function selectAnimation() { if (avatar.currentAnimation !== avatar.selectedFlyBlend) { setTransition(avatar.selectedFlyBlend, playTransitionReachPoses); } - if (motion.state !== AIR_MOTION) { - motion.state = AIR_MOTION; - } + motion.state = AIR_MOTION; avatar.selectedWalkBlend.lastDirection = NONE; break; } @@ -299,7 +274,7 @@ function determineStride() { wheelAdvance = filter.radToDeg(motion.deltaTime * ftWheelAngularVelocity); } else { // turn the frequency time wheel by the amount specified for this animation - wheelAdvance = filter.radToDeg(avatar.currentAnimation.calibration.frequency * motion.deltaTime); + wheelAdvance = filter.radToDeg(avatar.currentAnimation.calibration.frequency * motion.deltaTime); } if (motion.currentTransition !== nullTransition) { @@ -316,25 +291,25 @@ function determineStride() { } motion.currentTransition.advancePreviousFrequencyTimeWheel(motion.deltaTime); } - + // avoid unnaturally fast walking when landing at speed - simulates skimming / skidding if (Math.abs(wheelAdvance) > MAX_FT_WHEEL_INCREMENT) { wheelAdvance = 0; } - + // advance the walk wheel the appropriate amount motion.advanceFrequencyTimeWheel(wheelAdvance); // walking? then see if it's a good time to measure the stride length (needs to be at least 97% of max walking speed) - var JUST_UNDER_ONE = 0.97; - if (avatar.currentAnimation === avatar.selectedWalkBlend && - (Vec3.length(motion.velocity) / MAX_WALK_SPEED > JUST_UNDER_ONE)) { - + const ALMOST_ONE = 0.97; + if (avatar.currentAnimation === avatar.selectedWalkBlend && + (Vec3.length(motion.velocity) / MAX_WALK_SPEED > ALMOST_ONE)) { + var strideMaxAt = avatar.currentAnimation.calibration.strideMaxAt; - var tolerance = 1.0; + const TOLERANCE = 1.0; - if (motion.frequencyTimeWheelPos < (strideMaxAt + tolerance) && - motion.frequencyTimeWheelPos > (strideMaxAt - tolerance) && + if (motion.frequencyTimeWheelPos < (strideMaxAt + TOLERANCE) && + motion.frequencyTimeWheelPos > (strideMaxAt - TOLERANCE) && motion.currentTransition === nullTransition) { // measure and save stride length var footRPos = MyAvatar.getJointPosition("RightFoot"); @@ -363,6 +338,7 @@ function updateTransitions() { } } } + // update the Transition progress if (motion.currentTransition.updateProgress() === TRANSITION_COMPLETE) { motion.currentTransition = nullTransition; } @@ -372,7 +348,7 @@ function updateTransitions() { // 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) { @@ -387,9 +363,10 @@ function getLeanPitch() { function getLeanRoll() { var leanRollProgress = 0; var linearContribution = 0; - + const LOG_SCALER = 8; + if (Vec3.length(motion.velocity) > 0) { - linearContribution = (Math.log(Vec3.length(motion.velocity) / TOP_SPEED) + 8) / 8; + linearContribution = (Math.log(Vec3.length(motion.velocity) / TOP_SPEED) + LOG_SCALER) / LOG_SCALER; } var angularContribution = Math.abs(motion.yawDelta) / motion.calibration.DELTA_YAW_MAX; leanRollProgress = linearContribution; @@ -398,7 +375,7 @@ function getLeanRoll() { 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; @@ -432,6 +409,7 @@ function renderMotion() { // 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; @@ -444,7 +422,7 @@ function renderMotion() { // 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) { @@ -452,14 +430,17 @@ function renderMotion() { } } if (producingFootstepSounds) { + const QUARTER_CYCLE = 90; + const THREE_QUARTER_CYCLE = 270; var ftWheelPosition = motion.frequencyTimeWheelPos; - if (motion.currentTransition !== nullTransition && + + if (motion.currentTransition !== nullTransition && motion.currentTransition.lastAnimation === avatar.selectedWalkBlend) { ftWheelPosition = motion.currentTransition.lastFrequencyTimeWheelPos; } - if (avatar.nextStep === LEFT && ftWheelPosition > 270) { + if (avatar.nextStep === LEFT && ftWheelPosition > THREE_QUARTER_CYCLE) { avatar.makeFootStepSound(); - } else if (avatar.nextStep === RIGHT && (ftWheelPosition < 270 && ftWheelPosition > 90)) { + } else if (avatar.nextStep === RIGHT && (ftWheelPosition < THREE_QUARTER_CYCLE && ftWheelPosition > QUARTER_CYCLE)) { avatar.makeFootStepSound(); } } @@ -469,15 +450,15 @@ function renderMotion() { var joint = walkAssets.animationReference.joints[jointName]; var jointRotations = undefined; - // ignore arms / head rotations if avatar options are selected + // ignore arms / head rotations if options are selected in the settings if (avatar.armsFree && (joint.IKChain === "LeftArm" || joint.IKChain === "RightArm")) { continue; - } + } if (avatar.headFree && joint.IKChain === "Head") { continue; } - // if there's a live transition, blend rotations with the last animation's rotations + // if there's a live transition, blend the rotations with the last animation's rotations if (motion.currentTransition !== nullTransition) { jointRotations = motion.currentTransition.blendRotations(jointName, motion.frequencyTimeWheelPos, @@ -488,12 +469,13 @@ function renderMotion() { 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)); }