From 9b629a73266a94d0d457089a82411415f4e84ab6 Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Thu, 24 Jul 2014 16:55:23 -0700 Subject: [PATCH 01/11] Added slerp and squad to the Quat scripting interface --- libraries/script-engine/src/Quat.cpp | 10 ++++++++++ libraries/script-engine/src/Quat.h | 2 ++ 2 files changed, 12 insertions(+) diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 4acc60e7b4..8308536f97 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -66,6 +66,16 @@ glm::quat Quat::mix(const glm::quat& q1, const glm::quat& q2, float alpha) { return safeMix(q1, q2, alpha); } +/// Spherical Linear Interpolation +glm::quat Quat::slerp(const glm::quat& q1, const glm::quat& q2, float alpha) { + return glm::slerp(q1, q2, alpha); +} + +// Spherical Quadratic Interpolation +glm::quat Quat::squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h) { + return glm::squad(q1, q2, s1, s2, h); +} + void Quat::print(const QString& lable, const glm::quat& q) { qDebug() << qPrintable(lable) << q.x << "," << q.y << "," << q.z << "," << q.w; } diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index c97ccf9a1e..190c823118 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -36,6 +36,8 @@ public slots: glm::vec3 safeEulerAngles(const glm::quat& orientation); // degrees glm::quat angleAxis(float angle, const glm::vec3& v); // degrees glm::quat mix(const glm::quat& q1, const glm::quat& q2, float alpha); + glm::quat slerp(const glm::quat& q1, const glm::quat& q2, float alpha); + glm::quat squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h); void print(const QString& lable, const glm::quat& q); }; From 5c47e9013c696b9baf13fb9f5289ab8c75f4bf69 Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Mon, 28 Jul 2014 18:36:25 -0700 Subject: [PATCH 02/11] Working procedural walk animation with two keyframes --- examples/dancer.js | 561 +++++++++++++++++++++++++++ libraries/script-engine/src/Quat.cpp | 4 + libraries/script-engine/src/Quat.h | 1 + 3 files changed, 566 insertions(+) create mode 100644 examples/dancer.js diff --git a/examples/dancer.js b/examples/dancer.js new file mode 100644 index 0000000000..e7a75f7596 --- /dev/null +++ b/examples/dancer.js @@ -0,0 +1,561 @@ +// +// dancer.js +// hifi +// +// Created by Stephen Birarda on 2/20/14. +// Modified by Philip on 3/3/14 +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// This is an example script that demonstrates an NPC avatar. +// +// + +function getRandomFloat(min, max) { +return Math.random() * (max - min) + min; +} + +function getRandomInt (min, max) { +return Math.floor(Math.random() * (max - min + 1)) + min; +} + +function printVector(string, vector) { +print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); +} + +var CHANCE_OF_MOVING = 0.01; +var CHANCE_OF_SOUND = 0; +var CHANCE_OF_HEAD_TURNING = 0.05; +var CHANCE_OF_BIG_MOVE = 0.1; +var CHANCE_OF_WAVING = 0.009; + +var isMoving = true; +var isTurningHead = false; +var isPlay +ingAudio = false; +var isWaving = false; +var waveFrequency = 0.0; +var waveAmplitude = 0.0; + +var X_MIN = 5.50; +var X_MAX = 5.60; +var Z_MIN = 5.00; +var Z_MAX = 5.10; +var Y_PELVIS = 1.0; +var MAX_PELVIS_DELTA = 2.5; + +var AVATAR_PELVIS_HEIGHT = 0.75; + +var MOVE_RANGE_SMALL = 1.0; +var TURN_RANGE = 70.0; +var STOP_TOLERANCE = 0.05; +var MOVE_RATE = 0.05; +var TURN_RATE = 0.15; +var PITCH_RATE = 0.20; +var PITCH_RANGE = 30.0; + +var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; +var targetPosition = { x: 0, y: 0, z: 0 }; +var targetDirection = { x: 0, y: 0, z: 0, w: 0 }; +var currentDirection = { x: 0, y: 0, z: 0, w: 0 }; +var targetHeadPitch = 0.0; + +var cumulativeTime = 0.0; + +var basePelvisHeight = 0.0; +var pelvisOscillatorPosition = 0.0; +var pelvisOscillatorVelocity = 0.0; + +function clamp(val, min, max){ + return Math.max(min, Math.min(max, val)) +} + +// pick an integer between 1 and 100 that is not 28 for the face model for this bot +botNumber = 28; + +while (botNumber == 28) { + botNumber = getRandomInt(1, 100); +} + +if (botNumber <= 20) { + newFaceFilePrefix = "ron"; + newBodyFilePrefix = "defaultAvatar_body" +} else { + if (botNumber <= 40) { + newFaceFilePrefix = "superhero"; + } else if (botNumber <= 60) { + newFaceFilePrefix = "amber"; + } else if (botNumber <= 80) { + newFaceFilePrefix = "ron"; + } else { + newFaceFilePrefix = "angie"; + } + + newBodyFilePrefix = "bot" + botNumber; +} + + newFaceFilePrefix = "ron"; + newBodyFilePrefix = "bot" + 63; + +// set the face model fst using the bot number +// there is no need to change the body model - we're using the default +Avatar.faceModelURL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/meshes/" + newFaceFilePrefix + ".fst"; +Avatar.skeletonModelURL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/meshes/" + newBodyFilePrefix + "_a.fst"; +Avatar.billboardURL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/meshes/billboards/bot" + botNumber + ".png"; + +Agent.isAvatar = true; +Agent.isListeningToAudioStream = true; + +// change the avatar's position to the random one +Avatar.position = firstPosition; +basePelvisHeight = firstPosition.y; +printVector("New dancer, position = ", Avatar.position); + +function loadSounds() { + var sound_filenames = ["AB1.raw", "Anchorman2.raw", "B1.raw", "B1.raw", "Bale1.raw", "Bandcamp.raw", + "Big1.raw", "Big2.raw", "Brian1.raw", "Buster1.raw", "CES1.raw", "CES2.raw", "CES3.raw", "CES4.raw", + "Carrie1.raw", "Carrie3.raw", "Charlotte1.raw", "EN1.raw", "EN2.raw", "EN3.raw", "Eugene1.raw", "Francesco1.raw", + "Italian1.raw", "Japanese1.raw", "Leigh1.raw", "Lucille1.raw", "Lucille2.raw", "MeanGirls.raw", "Murray2.raw", + "Nigel1.raw", "PennyLane.raw", "Pitt1.raw", "Ricardo.raw", "SN.raw", "Sake1.raw", "Samantha1.raw", "Samantha2.raw", + "Spicoli1.raw", "Supernatural.raw", "Swearengen1.raw", "TheDude.raw", "Tony.raw", "Triumph1.raw", "Uma1.raw", + "Walken1.raw", "Walken2.raw", "Z1.raw", "Z2.raw" + ]; + + var SOUND_BASE_URL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Cocktail+Party+Snippets/Raws/"; + + for (var i = 0; i < sound_filenames.length; i++) { + sounds.push(new Sound(SOUND_BASE_URL + sound_filenames[i])); + } +} + +var sounds = []; +loadSounds(); + +function loadAnimations() { + + var animation_filenames = []; + var ANIMATION_BASE_URL = "http://highfidelity-dev.s3.amazonaws.com/animations/"; + + if (botNumber < 20) { + animation_filenames = ["robot/wave_hip_hop_dance.fbx", "robot/robot_hip_hop_dance.fbx"]; + } else if (botNumber <= 40) { + animation_filenames = ["superhero/house_dancing_2.fbx", "superhero/house_dancing_3.fbx", "superhero/house_dancing_4.fbx"]; + } else if (botNumber <= 60) { + animation_filenames = ["amber/house_dancing.fbx"] + } else if (botNumber <= 80) { + animation_filenames = ["ron/hip_hop_dancing.fbx", "ron/gangnam_style.fbx"]; + } else { + animation_filenames = ["angie/hip_hop_dancing_6.fbx"]; + } + + for (var i = 0; i < animation_filenames.length; i++) { + animations.push(AnimationCache.getAnimation(ANIMATION_BASE_URL + animation_filenames[i])); + } +} + +var animations = []; +loadAnimations(); + +function playRandomSound() { + if (!Agent.isPlayingAvatarSound) { + var whichSound = Math.floor((Math.random() * sounds.length) % sounds.length); + Agent.playAvatarSound(sounds[whichSound]); + } +} + +function stopWaving() { + isWaving = false; + Avatar.clearJointData(SHOULDER_JOINT_NUMBER); + Avatar.clearJointData(ELBOW_JOINT_NUMBER); + Avatar.clearJointData(JOINT_SPINE); +} + +//Animation KeyFrame constructor. rightJoints and leftJoints must be the same size +function WalkKeyFrame(rightJoints, leftJoints, singleJoints) { + this.rotations = []; + + for (var i = 0; i < rightJoints.length; i++) { + this.rotations[this.rotations.length] = rightJoints[i]; + this.rotations[this.rotations.length] = leftJoints[i]; + } + for (var i = 0; i < singleJoints.length; i++) { + this.rotations[this.rotations.length] = singleJoints[i]; + } +} + +//Procedural walk animation using two keyframes +//We use a separate array for front and back joints +var frontKeyFrames = []; +var backKeyFrames = []; +//for non mirrored joints such as the spine +var singleKeyFrames = []; +//Pitch, yaw, and roll for the joints +var frontAngles = []; +var backAngles = []; +//for non mirrored joints such as the spine +var singleAngles = []; + + + +//Actual joint mappings +var SHOULDER_JOINT_NUMBER = 15; +var ELBOW_JOINT_NUMBER = 16; +var JOINT_R_HIP = 1; +var JOINT_R_KNEE = 2; +var JOINT_L_HIP = 6; +var JOINT_L_KNEE = 7; +var JOINT_R_ARM = 15; +var JOINT_R_FOREARM = 16; +var JOINT_L_ARM = 39; +var JOINT_L_FOREARM = 40; +var JOINT_SPINE = 11; + +// ******************************* Animation Is Defined Below ************************************* + +var NUM_FRAMES = 2; +for (var i = 0; i < NUM_FRAMES; i++) { + frontAngles[i] = []; + backAngles[i] = []; + singleAngles[i] = []; + frontKeyFrames[i] = []; + backKeyFrames[i] = []; + singleKeyFrames[i] = []; +} +//Joint order for actual joint mappings, should be interleaved R,L,R,L,...S,S,S for R = right, L = left, S = single +var JOINT_ORDER = [JOINT_R_HIP, JOINT_L_HIP, JOINT_R_KNEE, JOINT_L_KNEE, JOINT_R_ARM, JOINT_L_ARM, JOINT_R_FOREARM, JOINT_L_FOREARM, JOINT_SPINE]; + +//Joint indices for joints that are duplicated, such as arms, It must match JOINT_ORDER +var HIP = 0; +var KNEE = 1; +var ARM = 2; +var FOREARM = 3; +//Joint indices for single joints +var SPINE = 0; + +//Symmetry multipliers for dthe left half [pitch, roll, yaw]. -1 means reflect, 1 means no reflect +var SYMMETRY = []; +SYMMETRY[HIP] = [1, -1, -1]; +SYMMETRY[KNEE] = [1, -1, -1]; +SYMMETRY[ARM] = [1, -1, -1]; +SYMMETRY[FOREARM] = [1, -1, -1]; + +//We have to store the angles so we can invert yaw and roll when making the animation +//symmetrical + + +//Front refers to leg, not arm. +//Legs Extending +frontAngles[0][HIP] = [30.0, 0.0, 8.0]; +frontAngles[0][KNEE] = [-15.0, 0.0, 0.0]; +frontAngles[0][ARM] = [85.0, -25.0, 0.0]; +frontAngles[0][FOREARM] = [0.0, 0.0, -15.0]; + +backAngles[0][HIP] = [-15, 0.0, 8.0]; +backAngles[0][KNEE] = [-28, 0.0, 0.0]; +backAngles[0][ARM] = [85.0, 20.0, 0.0]; +backAngles[0][FOREARM] = [10.0, 0.0, -25.0]; + +singleAngles[0][SPINE] = [-0.0, 0.0, 0.0]; + +//Legs Passing +frontAngles[1][HIP] = [6.0, 0.0, 8.0]; +frontAngles[1][KNEE] = [-12.0, 0.0, 0.0]; +frontAngles[1][ARM] = [85.0, 0.0, 0.0]; +frontAngles[1][FOREARM] = [0.0, 0.0, -15.0]; + +backAngles[1][HIP] = [10.0, 0.0, 8.0]; +backAngles[1][KNEE] = [-55.0, 0.0, 0.0]; +backAngles[1][ARM] = [85.0, 0.0, 0.0]; +backAngles[1][FOREARM] = [0.0, 0.0, -15.0]; + +singleAngles[1][SPINE] = [0.0, 0.0, 0.0]; + +// ******************************* Animation Is Defined Above ************************************* + +//Actual keyframes for the animation +var walkKeyFrames = []; +//Generate quaternions from the angles +for (var i = 0; i < frontAngles.length; i++) { + for (var j = 0; j < frontAngles[i].length; j++) { + frontKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(frontAngles[i][j][0], frontAngles[i][j][1], frontAngles[i][j][2]); + backKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(SYMMETRY[j][0] * backAngles[i][j][0], SYMMETRY[j][1] * backAngles[i][j][1], SYMMETRY[j][2] * backAngles[i][j][2]); + } +} +for (var i = 0; i < singleAngles.length; i++) { + for (var j = 0; j < singleAngles[i].length; j++) { + singleKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(singleAngles[i][j][0], singleAngles[i][j][1], singleAngles[i][j][2]); + } +} +walkKeyFrames[0] = new WalkKeyFrame(frontKeyFrames[0], backKeyFrames[0], singleKeyFrames[0]); +walkKeyFrames[1] = new WalkKeyFrame(frontKeyFrames[1], backKeyFrames[1], singleKeyFrames[1]); + +//Generate mirrored quaternions for the other half of the body +for (var i = 0; i < frontAngles.length; i++) { + for (var j = 0; j < frontAngles[i].length; j++) { + frontKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(SYMMETRY[j][0] * frontAngles[i][j][0], SYMMETRY[j][1] * frontAngles[i][j][1], SYMMETRY[j][2] * frontAngles[i][j][2]); + backKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(backAngles[i][j][0], backAngles[i][j][1], backAngles[i][j][2]); + } +} +for (var i = 0; i < singleAngles.length; i++) { + for (var j = 0; j < singleAngles[i].length; j++) { + singleKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(-singleAngles[i][j][0], -singleAngles[i][j][1], -singleAngles[i][j][2]); + } +} +walkKeyFrames[2] = new WalkKeyFrame(backKeyFrames[0], frontKeyFrames[0], singleKeyFrames[0]); +walkKeyFrames[3] = new WalkKeyFrame(backKeyFrames[1], frontKeyFrames[1], singleKeyFrames[1]); + +//Hook up pointers to the next keyframe +for (var i = 0; i < walkKeyFrames.length - 1; i++) { + walkKeyFrames[i].nextFrame = walkKeyFrames[i+1]; +} +walkKeyFrames[walkKeyFrames.length-1].nextFrame = walkKeyFrames[0]; + +//Set up the bezier curve control points using technique described at +//https://www.cs.tcd.ie/publications/tech-reports/reports.94/TCD-CS-94-18.pdf +//Set up all C1 +for (var i = 0; i < walkKeyFrames.length; i++) { + walkKeyFrames[i].nextFrame.controlPoints = []; + for (var j = 0; j < walkKeyFrames[i].rotations.length; j++) { + walkKeyFrames[i].nextFrame.controlPoints[j] = []; + var R = Quat.slerp(walkKeyFrames[i].rotations[j], walkKeyFrames[i].nextFrame.rotations[j], 2.0); + var T = Quat.slerp(R, walkKeyFrames[i].nextFrame.nextFrame.rotations[j], 0.5); + walkKeyFrames[i].nextFrame.controlPoints[j][0] = Quat.slerp(walkKeyFrames[i].nextFrame.rotations[j], T, 0.33333); + } +} +//Set up all C2 +for (var i = 0; i < walkKeyFrames.length; i++) { + for (var j = 0; j < walkKeyFrames[i].rotations.length; j++) { + walkKeyFrames[i].controlPoints[j][1] = Quat.slerp(walkKeyFrames[i].nextFrame.rotations[j], walkKeyFrames[i].nextFrame.controlPoints[j][0], -1.0); + } +} +//DeCasteljau evaluation to evaluate the bezier curve +function deCasteljau(k1, k2, c1, c2, f) { + var a = Quat.slerp(k1, c1, f); + var b = Quat.slerp(c1, c2, f); + var c = Quat.slerp(c2, k2, f); + var d = Quat.slerp(a, b, f); + var e = Quat.slerp(b, c, f); + return Quat.slerp(d, e, f); +} + +var currentFrame = 0; + +var walkTime = 0.0; +var walkFrequency = 3.0; + +function keepWalking(deltaTime) { + + walkTime += walkFrequency * deltaTime; + if (walkTime > 1.0) { + walkTime = 0.0; + currentFrame++; + if (currentFrame > 3) { + currentFrame = 0; + } + } + + var frame = walkKeyFrames[currentFrame]; + + for (var i = 0; i < JOINT_ORDER.length; i++) { + Avatar.setJointData(JOINT_ORDER[i], deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], walkTime)); + } +} + +function stopWalking() { + Avatar.clearJointData(JOINT_R_HIP); + Avatar.clearJointData(JOINT_R_KNEE); + Avatar.clearJointData(JOINT_L_HIP); + Avatar.clearJointData(JOINT_L_KNEE); +} + +var trailingAverageLoudness = 0; +var MAX_SAMPLE = 32767; +var DB_METER_BASE = Math.log(MAX_SAMPLE); + +var RAND_RATIO_LAST = getRandomFloat(0.1, 0.3); +var RAND_TRAILING = 1 - RAND_RATIO_LAST; + +function jumpWithLoudness(deltaTime) { + // potentially change pelvis height depending on trailing average loudness + + pelvisOscillatorVelocity += deltaTime * Agent.lastReceivedAudioLoudness * 700.0 ; + + pelvisOscillatorVelocity -= pelvisOscillatorPosition * 0.75; + pelvisOscillatorVelocity *= 0.97; + pelvisOscillatorPosition += deltaTime * pelvisOscillatorVelocity; + Avatar.headPitch = pelvisOscillatorPosition * 60.0; + + var pelvisPosition = Avatar.position; + pelvisPosition.y = (Y_PELVIS - 0.35) + pelvisOscillatorPosition; + + if (pelvisPosition.y < Y_PELVIS) { + pelvisPosition.y = Y_PELVIS; + } else if (pelvisPosition.y > Y_PELVIS + 1.0) { + pelvisPosition.y = Y_PELVIS + 1.0; + } + + Avatar.position = pelvisPosition; +} + +var jointMapping = null; +var frameIndex = 0.0; +var isPlayingDanceAnimation = false; +var randomAnimation = null; +var animationLoops = 1; +var forcedMove = false; + +var FRAME_RATE = 30.0; + +var wasMovingLastFrame = false; +var wasDancing = false; + +function danceAnimation(deltaTime) { + + var flooredFrame = Math.floor(frameIndex); + + if (jointMapping === null || flooredFrame >= randomAnimation.frames.length * animationLoops) { + // we've run our animation for our number of loops, start a new one + frameIndex = 0.0; + jointMapping = null; + randomAnimation = null; + } + + if (isMoving || (!wasMovingLastFrame && frameIndex === 0)) { + if (!isMoving) { + forcedMove = true; + possiblyStopDancing(); + } + + wasMovingLastFrame = true; + handleWalking(); + } else { + if (jointMapping === null) { + // pick a random animation + var whichAnimation = Math.floor((Math.random() * animations.length) % animations.length); + randomAnimation = animations[whichAnimation]; + + var avatarJointNames = Avatar.jointNames; + var animationJointNames = randomAnimation.jointNames; + if (avatarJointNames === 0 || animationJointNames.length === 0) { + return; + } + jointMapping = new Array(avatarJointNames.length); + for (var i = 0; i < avatarJointNames.length; i++) { + jointMapping[i] = animationJointNames.indexOf(avatarJointNames[i]); + } + } + + frameIndex += deltaTime * FRAME_RATE; + var frames = randomAnimation.frames; + var rotations = frames[flooredFrame % frames.length].rotations; + for (var j = 0; j < jointMapping.length; j++) { + var rotationIndex = jointMapping[j]; + if (rotationIndex != -1) { + Avatar.setJointData(j, rotations[rotationIndex]); + } + } + + wasMovingLastFrame = false; + wasDancing = true; + } +} + +function handleHeadTurn() { + if (!isTurningHead && (Math.random() < CHANCE_OF_HEAD_TURNING)) { + targetHeadPitch = getRandomFloat(-PITCH_RANGE, PITCH_RANGE); + isTurningHead = true; + } else { + Avatar.headPitch = Avatar.headPitch + (targetHeadPitch - Avatar.headPitch) * PITCH_RATE; + if (Math.abs(Avatar.headPitch - targetHeadPitch) < STOP_TOLERANCE) { + isTurningHead = false; + } + } +} + +var currentShoulderQuat = Avatar.getJointRotation(SHOULDER_JOINT_NUMBER); +var targetShoulderQuat = currentShoulderQuat; +var idleShoulderQuat = currentShoulderQuat; +var currentSpineQuat = Avatar.getJointRotation(JOINT_SPINE); +var targetSpineQuat = currentSpineQuat; +var idleSpineQuat = currentSpineQuat; +var currentElbowQuat = Avatar.getJointRotation(ELBOW_JOINT_NUMBER); +var targetElbowQuat = currentElbowQuat; +var idleElbowQuat = currentElbowQuat; + +function handleWalking(deltaTime) { + if (forcedMove || (!isMoving && Math.random() < CHANCE_OF_MOVING)) { + // Set new target location + targetDirection = Quat.multiply(Avatar.orientation, Quat.angleAxis(getRandomFloat(-TURN_RANGE, TURN_RANGE), { x:0, y:1, z:0 })); + var front = Quat.getFront(targetDirection); + + targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, MOVE_RANGE_SMALL))); + + targetPosition.x = clamp(targetPosition.x, X_MIN, X_MAX); + targetPosition.z = clamp(targetPosition.z, Z_MIN, Z_MAX); + targetPosition.y = Y_PELVIS; + + wasMovingLastFrame = true; + isMoving = true; + forcedMove = false; + } else if (isMoving) { + keepWalking(deltaTime); + // Avatar.position = Vec3.sum(Avatar.position, Vec3.multiply(Vec3.subtract(targetPosition, Avatar.position), MOVE_RATE)); + Avatar.orientation = Quat.slerp(Avatar.orientation, targetDirection, TURN_RATE); + var diff = Vec3.subtract(Avatar.position, targetPosition); + diff.y = 0.0; + + wasMovingLastFrame = true; + + if (Vec3.length(diff) < STOP_TOLERANCE) { + isMoving = false; + stopWalking(); + } + } +} + +function handleTalking() { + if (Math.random() < CHANCE_OF_SOUND) { + playRandomSound(); + } +} + +function changePelvisHeight(newHeight) { + var newPosition = Avatar.position; + newPosition.y = newHeight; + Avatar.position = newPosition; +} + +function possiblyStopDancing() { + if (wasDancing) { + for (var j = 0; j < Avatar.jointNames.length; j++) { + Avatar.clearJointData(j); + } + + changePelvisHeight(Y_PELVIS); + } +} + +function updateBehavior(deltaTime) { + cumulativeTime += deltaTime; + + if (AvatarList.containsAvatarWithDisplayName("mrdj")) { + if (wasMovingLastFrame && !wasDancing) { + isMoving = false; + } + + // we have a DJ, shouldn't we be dancing? + jumpWithLoudness(deltaTime); + danceAnimation(deltaTime); + } else { + // make sure we're not dancing anymore + possiblyStopDancing(); + + wasDancing = false; + + // no DJ, let's just chill on the dancefloor - randomly walking and talking + handleHeadTurn(); + handleWalking(deltaTime); + handleTalking(); + } +} + +Script.update.connect(updateBehavior); \ No newline at end of file diff --git a/libraries/script-engine/src/Quat.cpp b/libraries/script-engine/src/Quat.cpp index 8308536f97..66281883f0 100644 --- a/libraries/script-engine/src/Quat.cpp +++ b/libraries/script-engine/src/Quat.cpp @@ -76,6 +76,10 @@ glm::quat Quat::squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& return glm::squad(q1, q2, s1, s2, h); } +float Quat::dot(const glm::quat& q1, const glm::quat& q2) { + return glm::dot(q1, q2); +} + void Quat::print(const QString& lable, const glm::quat& q) { qDebug() << qPrintable(lable) << q.x << "," << q.y << "," << q.z << "," << q.w; } diff --git a/libraries/script-engine/src/Quat.h b/libraries/script-engine/src/Quat.h index 190c823118..faae636f02 100644 --- a/libraries/script-engine/src/Quat.h +++ b/libraries/script-engine/src/Quat.h @@ -38,6 +38,7 @@ public slots: glm::quat mix(const glm::quat& q1, const glm::quat& q2, float alpha); glm::quat slerp(const glm::quat& q1, const glm::quat& q2, float alpha); glm::quat squad(const glm::quat& q1, const glm::quat& q2, const glm::quat& s1, const glm::quat& s2, float h); + float dot(const glm::quat& q1, const glm::quat& q2); void print(const QString& lable, const glm::quat& q); }; From 6be62842e3e1ede19769946c806ff84b7937c617 Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Tue, 29 Jul 2014 15:45:12 -0700 Subject: [PATCH 03/11] Added walkWheel for syncinc animation speed, plus other improvements. --- examples/proceduralAnimation.js | 0 examples/{dancer.js => proceduralBot.js} | 214 +++++++++-------------- 2 files changed, 81 insertions(+), 133 deletions(-) create mode 100644 examples/proceduralAnimation.js rename examples/{dancer.js => proceduralBot.js} (74%) diff --git a/examples/proceduralAnimation.js b/examples/proceduralAnimation.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/dancer.js b/examples/proceduralBot.js similarity index 74% rename from examples/dancer.js rename to examples/proceduralBot.js index e7a75f7596..ce88f596b6 100644 --- a/examples/dancer.js +++ b/examples/proceduralBot.js @@ -1,15 +1,18 @@ // -// dancer.js +// proceduralBot.js // hifi // -// Created by Stephen Birarda on 2/20/14. -// Modified by Philip on 3/3/14 +// Created by Ben Arnold on 7/29/2013 +// // Copyright (c) 2014 HighFidelity, Inc. All rights reserved. // // This is an example script that demonstrates an NPC avatar. // // +//For procedural walk animation +Script.include("proceduralAnimation.js"); + function getRandomFloat(min, max) { return Math.random() * (max - min) + min; } @@ -22,41 +25,41 @@ function printVector(string, vector) { print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); } -var CHANCE_OF_MOVING = 0.01; +var CHANCE_OF_MOVING = 0.1; var CHANCE_OF_SOUND = 0; var CHANCE_OF_HEAD_TURNING = 0.05; var CHANCE_OF_BIG_MOVE = 0.1; var CHANCE_OF_WAVING = 0.009; -var isMoving = true; +var isMoving = false; var isTurningHead = false; -var isPlay -ingAudio = false; +var isPlayingAudio = false; var isWaving = false; var waveFrequency = 0.0; var waveAmplitude = 0.0; -var X_MIN = 5.50; -var X_MAX = 5.60; -var Z_MIN = 5.00; -var Z_MAX = 5.10; +var X_MIN = 0.50; +var X_MAX = 15.60; +var Z_MIN = 0.50; +var Z_MAX = 15.10; var Y_PELVIS = 1.0; var MAX_PELVIS_DELTA = 2.5; var AVATAR_PELVIS_HEIGHT = 0.75; -var MOVE_RANGE_SMALL = 1.0; +var MOVE_RANGE_SMALL = 10.0; var TURN_RANGE = 70.0; var STOP_TOLERANCE = 0.05; var MOVE_RATE = 0.05; -var TURN_RATE = 0.15; -var PITCH_RATE = 0.20; -var PITCH_RANGE = 30.0; +var TURN_RATE = 0.2; +var PITCH_RATE = 0.10; +var PITCH_RANGE = 20.0; -var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; +//var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; +var firstPosition = { x: 0.5, y: Y_PELVIS, z: 0.5 }; var targetPosition = { x: 0, y: 0, z: 0 }; -var targetDirection = { x: 0, y: 0, z: 0, w: 0 }; -var currentDirection = { x: 0, y: 0, z: 0, w: 0 }; +var targetOrientation = { x: 0, y: 0, z: 0, w: 0 }; +var currentOrientation = { x: 0, y: 0, z: 0, w: 0 }; var targetHeadPitch = 0.0; var cumulativeTime = 0.0; @@ -130,30 +133,6 @@ function loadSounds() { var sounds = []; loadSounds(); -function loadAnimations() { - - var animation_filenames = []; - var ANIMATION_BASE_URL = "http://highfidelity-dev.s3.amazonaws.com/animations/"; - - if (botNumber < 20) { - animation_filenames = ["robot/wave_hip_hop_dance.fbx", "robot/robot_hip_hop_dance.fbx"]; - } else if (botNumber <= 40) { - animation_filenames = ["superhero/house_dancing_2.fbx", "superhero/house_dancing_3.fbx", "superhero/house_dancing_4.fbx"]; - } else if (botNumber <= 60) { - animation_filenames = ["amber/house_dancing.fbx"] - } else if (botNumber <= 80) { - animation_filenames = ["ron/hip_hop_dancing.fbx", "ron/gangnam_style.fbx"]; - } else { - animation_filenames = ["angie/hip_hop_dancing_6.fbx"]; - } - - for (var i = 0; i < animation_filenames.length; i++) { - animations.push(AnimationCache.getAnimation(ANIMATION_BASE_URL + animation_filenames[i])); - } -} - -var animations = []; -loadAnimations(); function playRandomSound() { if (!Agent.isPlayingAvatarSound) { @@ -162,13 +141,6 @@ function playRandomSound() { } } -function stopWaving() { - isWaving = false; - Avatar.clearJointData(SHOULDER_JOINT_NUMBER); - Avatar.clearJointData(ELBOW_JOINT_NUMBER); - Avatar.clearJointData(JOINT_SPINE); -} - //Animation KeyFrame constructor. rightJoints and leftJoints must be the same size function WalkKeyFrame(rightJoints, leftJoints, singleJoints) { this.rotations = []; @@ -231,13 +203,6 @@ var FOREARM = 3; //Joint indices for single joints var SPINE = 0; -//Symmetry multipliers for dthe left half [pitch, roll, yaw]. -1 means reflect, 1 means no reflect -var SYMMETRY = []; -SYMMETRY[HIP] = [1, -1, -1]; -SYMMETRY[KNEE] = [1, -1, -1]; -SYMMETRY[ARM] = [1, -1, -1]; -SYMMETRY[FOREARM] = [1, -1, -1]; - //We have to store the angles so we can invert yaw and roll when making the animation //symmetrical @@ -254,7 +219,7 @@ backAngles[0][KNEE] = [-28, 0.0, 0.0]; backAngles[0][ARM] = [85.0, 20.0, 0.0]; backAngles[0][FOREARM] = [10.0, 0.0, -25.0]; -singleAngles[0][SPINE] = [-0.0, 0.0, 0.0]; +singleAngles[0][SPINE] = [0.0, -15.0, 5.0]; //Legs Passing frontAngles[1][HIP] = [6.0, 0.0, 8.0]; @@ -277,7 +242,7 @@ var walkKeyFrames = []; for (var i = 0; i < frontAngles.length; i++) { for (var j = 0; j < frontAngles[i].length; j++) { frontKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(frontAngles[i][j][0], frontAngles[i][j][1], frontAngles[i][j][2]); - backKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(SYMMETRY[j][0] * backAngles[i][j][0], SYMMETRY[j][1] * backAngles[i][j][1], SYMMETRY[j][2] * backAngles[i][j][2]); + backKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(backAngles[i][j][0], -backAngles[i][j][1], -backAngles[i][j][2]); } } for (var i = 0; i < singleAngles.length; i++) { @@ -291,7 +256,7 @@ walkKeyFrames[1] = new WalkKeyFrame(frontKeyFrames[1], backKeyFrames[1], singleK //Generate mirrored quaternions for the other half of the body for (var i = 0; i < frontAngles.length; i++) { for (var j = 0; j < frontAngles[i].length; j++) { - frontKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(SYMMETRY[j][0] * frontAngles[i][j][0], SYMMETRY[j][1] * frontAngles[i][j][1], SYMMETRY[j][2] * frontAngles[i][j][2]); + frontKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(frontAngles[i][j][0], -frontAngles[i][j][1], -frontAngles[i][j][2]); backKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(backAngles[i][j][0], backAngles[i][j][1], backAngles[i][j][2]); } } @@ -340,12 +305,18 @@ function deCasteljau(k1, k2, c1, c2, f) { var currentFrame = 0; var walkTime = 0.0; -var walkFrequency = 3.0; + +var walkWheelRadius = 0.5; +var walkWheelRate = 2.0 * 3.141592 * walkWheelRadius / 8.0; + +var avatarAcceleration = 0.75; +var avatarVelocity = 0.0; +var avatarMaxVelocity = 1.4; function keepWalking(deltaTime) { - walkTime += walkFrequency * deltaTime; - if (walkTime > 1.0) { + walkTime += avatarVelocity * deltaTime; + if (walkTime > walkWheelRate) { walkTime = 0.0; currentFrame++; if (currentFrame > 3) { @@ -355,18 +326,13 @@ function keepWalking(deltaTime) { var frame = walkKeyFrames[currentFrame]; + var interp = walkTime / walkWheelRate; + for (var i = 0; i < JOINT_ORDER.length; i++) { - Avatar.setJointData(JOINT_ORDER[i], deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], walkTime)); + Avatar.setJointData(JOINT_ORDER[i], deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], interp)); } } -function stopWalking() { - Avatar.clearJointData(JOINT_R_HIP); - Avatar.clearJointData(JOINT_R_KNEE); - Avatar.clearJointData(JOINT_L_HIP); - Avatar.clearJointData(JOINT_L_KNEE); -} - var trailingAverageLoudness = 0; var MAX_SAMPLE = 32767; var DB_METER_BASE = Math.log(MAX_SAMPLE); @@ -408,56 +374,6 @@ var FRAME_RATE = 30.0; var wasMovingLastFrame = false; var wasDancing = false; -function danceAnimation(deltaTime) { - - var flooredFrame = Math.floor(frameIndex); - - if (jointMapping === null || flooredFrame >= randomAnimation.frames.length * animationLoops) { - // we've run our animation for our number of loops, start a new one - frameIndex = 0.0; - jointMapping = null; - randomAnimation = null; - } - - if (isMoving || (!wasMovingLastFrame && frameIndex === 0)) { - if (!isMoving) { - forcedMove = true; - possiblyStopDancing(); - } - - wasMovingLastFrame = true; - handleWalking(); - } else { - if (jointMapping === null) { - // pick a random animation - var whichAnimation = Math.floor((Math.random() * animations.length) % animations.length); - randomAnimation = animations[whichAnimation]; - - var avatarJointNames = Avatar.jointNames; - var animationJointNames = randomAnimation.jointNames; - if (avatarJointNames === 0 || animationJointNames.length === 0) { - return; - } - jointMapping = new Array(avatarJointNames.length); - for (var i = 0; i < avatarJointNames.length; i++) { - jointMapping[i] = animationJointNames.indexOf(avatarJointNames[i]); - } - } - - frameIndex += deltaTime * FRAME_RATE; - var frames = randomAnimation.frames; - var rotations = frames[flooredFrame % frames.length].rotations; - for (var j = 0; j < jointMapping.length; j++) { - var rotationIndex = jointMapping[j]; - if (rotationIndex != -1) { - Avatar.setJointData(j, rotations[rotationIndex]); - } - } - - wasMovingLastFrame = false; - wasDancing = true; - } -} function handleHeadTurn() { if (!isTurningHead && (Math.random() < CHANCE_OF_HEAD_TURNING)) { @@ -481,14 +397,32 @@ var currentElbowQuat = Avatar.getJointRotation(ELBOW_JOINT_NUMBER); var targetElbowQuat = currentElbowQuat; var idleElbowQuat = currentElbowQuat; +function stopWalking() { + Avatar.clearJointData(JOINT_R_HIP); + Avatar.clearJointData(JOINT_R_KNEE); + Avatar.clearJointData(JOINT_L_HIP); + Avatar.clearJointData(JOINT_L_KNEE); + avatarVelocity = 0.0; + isMoving = false; +} + +var MAX_ATTEMPTS = 40; function handleWalking(deltaTime) { + if (forcedMove || (!isMoving && Math.random() < CHANCE_OF_MOVING)) { // Set new target location - targetDirection = Quat.multiply(Avatar.orientation, Quat.angleAxis(getRandomFloat(-TURN_RANGE, TURN_RANGE), { x:0, y:1, z:0 })); - var front = Quat.getFront(targetDirection); - - targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, MOVE_RANGE_SMALL))); + //Keep trying new orientations if the desired target location is out of bounds + var attempts = 0; + do { + targetOrientation = Quat.multiply(Avatar.orientation, Quat.angleAxis(getRandomFloat(-TURN_RANGE, TURN_RANGE), { x:0, y:1, z:0 })); + var front = Quat.getFront(targetOrientation); + + targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, MOVE_RANGE_SMALL))); + } + while ((targetPosition.x < X_MIN || targetPosition.x > X_MAX || targetPosition.z < Z_MIN || targetPosition.z > Z_MAX) + && attempts < MAX_ATEMPTS); + targetPosition.x = clamp(targetPosition.x, X_MIN, X_MAX); targetPosition.z = clamp(targetPosition.z, Z_MIN, Z_MAX); targetPosition.y = Y_PELVIS; @@ -498,16 +432,31 @@ function handleWalking(deltaTime) { forcedMove = false; } else if (isMoving) { keepWalking(deltaTime); - // Avatar.position = Vec3.sum(Avatar.position, Vec3.multiply(Vec3.subtract(targetPosition, Avatar.position), MOVE_RATE)); - Avatar.orientation = Quat.slerp(Avatar.orientation, targetDirection, TURN_RATE); - var diff = Vec3.subtract(Avatar.position, targetPosition); - diff.y = 0.0; - wasMovingLastFrame = true; + var targetVector = Vec3.subtract(targetPosition, Avatar.position); + var distance = Vec3.length(targetVector); + if (distance <= avatarVelocity * deltaTime) { + Avatar.position = targetPosition; + stopWalking(); + } else { + var direction = Vec3.normalize(targetVector); + //Figure out if we should be slowing down + var t = avatarVelocity / avatarAcceleration; + var d = (avatarVelocity / 2.0) * t; + if (distance < d) { + avatarVelocity -= avatarAcceleration * deltaTime; + if (avatarVelocity <= 0) { + stopWalking(); + } + } else { + avatarVelocity += avatarAcceleration * deltaTime; + if (avatarVelocity > avatarMaxVelocity) avatarVelocity = avatarMaxVelocity; + } + Avatar.position = Vec3.sum(Avatar.position, Vec3.multiply(direction, avatarVelocity * deltaTime)); + Avatar.orientation = Quat.mix(Avatar.orientation, targetOrientation, TURN_RATE); + + wasMovingLastFrame = true; - if (Vec3.length(diff) < STOP_TOLERANCE) { - isMoving = false; - stopWalking(); } } } @@ -544,7 +493,6 @@ function updateBehavior(deltaTime) { // we have a DJ, shouldn't we be dancing? jumpWithLoudness(deltaTime); - danceAnimation(deltaTime); } else { // make sure we're not dancing anymore possiblyStopDancing(); From 9be9f1417acad39c8ca5d1d5d0be24dc28562347 Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Tue, 29 Jul 2014 16:51:57 -0700 Subject: [PATCH 04/11] Made some API changes for procedural animation --- examples/proceduralAnimation.js | 134 ++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) diff --git a/examples/proceduralAnimation.js b/examples/proceduralAnimation.js index e69de29bb2..7eb0873994 100644 --- a/examples/proceduralAnimation.js +++ b/examples/proceduralAnimation.js @@ -0,0 +1,134 @@ +// +// proceduralAnimation.js +// hifi +// +// Created by Ben Arnold on 7/29/14. +// Copyright (c) 2014 HighFidelity, Inc. All rights reserved. +// +// This is a Procedural Animation API for creating procedural animations in JS. +// To include it in your JS files, simply use the following line at the top: +// Script.include("proceduralAnimation.js"); + +// You can see a usage example in proceduralBot.js + +ProcAnimAPI = function() { + + // generateKeyFrames(rightAngles, leftAngles, middleAngles, numFrames) + // + // Parameters: + // rightAngles - An array of tuples. The angles in degrees for the joints + // on the right side of the body + // leftAngles - An array of tuples. The angles in degrees for the joints + // on the left side of the body + // middleAngles - An array of tuples. The angles in degrees for the joints + // on the left side of the body + // numFrames - The number of frames in the animation, before mirroring. + // for a 4 frame walk animation, simply supply 2 frames + // and generateKeyFrames will return 4 frames. + // + // Return Value: + // Returns an array of KeyFrames. Each KeyFrame has an array of quaternions + // for each of the joints, generated from the input angles. They will be ordered + // R,L,R,L,...M,M,M,M where R ~ rightAngles, L ~ leftAngles, M ~ middlesAngles. + // The size of the returned array will be numFrames * 2 + this.generateKeyframes = function(rightAngles, leftAngles, middleAngles, numFrames) { + + if (rightAngles.length != leftAngles.length) { + print("ERROR: generateKeyFrames(...) rightAngles and leftAngles must have equal length."); + } + + //for mirrored joints, such as the arms or legs + var rightQuats = []; + var leftQuats = []; + //for non mirrored joints such as the spine + var middleQuats = []; + + for (var i = 0; i < numFrames; i++) { + rightQuats[i] = []; + leftQuats[i] = []; + middleQuats[i] = []; + } + + var finalKeyFrames = []; + //Generate quaternions + for (var i = 0; i < rightAngles.length; i++) { + for (var j = 0; j < rightAngles[i].length; j++) { + rightQuats[i][j] = Quat.fromPitchYawRollDegrees(rightAngles[i][j][0], rightAngles[i][j][1], rightAngles[i][j][2]); + leftQuats[i][j] = Quat.fromPitchYawRollDegrees(leftAngles[i][j][0], -leftAngles[i][j][1], -leftAngles[i][j][2]); + } + } + for (var i = 0; i < middleAngles.length; i++) { + for (var j = 0; j < middleAngles[i].length; j++) { + middleQuats[i][j] = Quat.fromPitchYawRollDegrees(middleAngles[i][j][0], middleAngles[i][j][1], middleAngles[i][j][2]); + } + } + finalKeyFrames[0] = new KeyFrame(rightQuats[0], leftQuats[0], middleQuats[0]); + finalKeyFrames[1] = new KeyFrame(rightQuats[1], leftQuats[1], middleQuats[1]); + + //Generate mirrored quaternions for the other half of the animation + for (var i = 0; i < rightAngles.length; i++) { + for (var j = 0; j < rightAngles[i].length; j++) { + rightQuats[i][j] = Quat.fromPitchYawRollDegrees(rightAngles[i][j][0], -rightAngles[i][j][1], -rightAngles[i][j][2]); + leftQuats[i][j] = Quat.fromPitchYawRollDegrees(leftAngles[i][j][0], leftAngles[i][j][1], leftAngles[i][j][2]); + } + } + for (var i = 0; i < middleAngles.length; i++) { + for (var j = 0; j < middleAngles[i].length; j++) { + middleQuats[i][j] = Quat.fromPitchYawRollDegrees(-middleAngles[i][j][0], -middleAngles[i][j][1], -middleAngles[i][j][2]); + } + } + finalKeyFrames[2] = new KeyFrame(leftQuats[0], rightQuats[0], middleQuats[0]); + finalKeyFrames[3] = new KeyFrame(leftQuats[1], rightQuats[1], middleQuats[1]); + + //Hook up pointers to the next keyframe + for (var i = 0; i < finalKeyFrames.length - 1; i++) { + finalKeyFrames[i].nextFrame = finalKeyFrames[i+1]; + } + finalKeyFrames[finalKeyFrames.length-1].nextFrame = finalKeyFrames[0]; + + //Set up the bezier curve control points using technique described at + //https://www.cs.tcd.ie/publications/tech-reports/reports.94/TCD-CS-94-18.pdf + //Set up all C1 + for (var i = 0; i < finalKeyFrames.length; i++) { + finalKeyFrames[i].nextFrame.controlPoints = []; + for (var j = 0; j < finalKeyFrames[i].rotations.length; j++) { + finalKeyFrames[i].nextFrame.controlPoints[j] = []; + var R = Quat.slerp(finalKeyFrames[i].rotations[j], finalKeyFrames[i].nextFrame.rotations[j], 2.0); + var T = Quat.slerp(R, finalKeyFrames[i].nextFrame.nextFrame.rotations[j], 0.5); + finalKeyFrames[i].nextFrame.controlPoints[j][0] = Quat.slerp(finalKeyFrames[i].nextFrame.rotations[j], T, 0.33333); + } + } + //Set up all C2 + for (var i = 0; i < finalKeyFrames.length; i++) { + for (var j = 0; j < finalKeyFrames[i].rotations.length; j++) { + finalKeyFrames[i].controlPoints[j][1] = Quat.slerp(finalKeyFrames[i].nextFrame.rotations[j], finalKeyFrames[i].nextFrame.controlPoints[j][0], -1.0); + } + } + + return finalKeyFrames; + } + + // Animation KeyFrame constructor. rightJoints and leftJoints must be the same size + this.KeyFrame = function(rightJoints, leftJoints, middleJoints) { + this.rotations = []; + + for (var i = 0; i < rightJoints.length; i++) { + this.rotations[this.rotations.length] = rightJoints[i]; + this.rotations[this.rotations.length] = leftJoints[i]; + } + for (var i = 0; i < middleJoints.length; i++) { + this.rotations[this.rotations.length] = middleJoints[i]; + } + } + + // DeCasteljau evaluation to evaluate the bezier curve. + // This is a very natural looking interpolation + this.deCasteljau = function(k1, k2, c1, c2, f) { + var a = Quat.slerp(k1, c1, f); + var b = Quat.slerp(c1, c2, f); + var c = Quat.slerp(c2, k2, f); + var d = Quat.slerp(a, b, f); + var e = Quat.slerp(b, c, f); + return Quat.slerp(d, e, f); + } +} \ No newline at end of file From 1c6834cdd6c93b19d8f676c26d8e3e9a13cbc624 Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Tue, 29 Jul 2014 17:49:33 -0700 Subject: [PATCH 05/11] Improved the API and cleaned up the code --- ...Animation.js => proceduralAnimationAPI.js} | 16 +- examples/proceduralBot.js | 198 ++++-------------- 2 files changed, 44 insertions(+), 170 deletions(-) rename examples/{proceduralAnimation.js => proceduralAnimationAPI.js} (91%) diff --git a/examples/proceduralAnimation.js b/examples/proceduralAnimationAPI.js similarity index 91% rename from examples/proceduralAnimation.js rename to examples/proceduralAnimationAPI.js index 7eb0873994..ae6b5c3d31 100644 --- a/examples/proceduralAnimation.js +++ b/examples/proceduralAnimationAPI.js @@ -10,6 +10,8 @@ // Script.include("proceduralAnimation.js"); // You can see a usage example in proceduralBot.js +// The current implementation is quite simple. If you would like a feature +// to be added or expanded, you can contact Ben at brb555@vols.utk.edu ProcAnimAPI = function() { @@ -62,8 +64,8 @@ ProcAnimAPI = function() { middleQuats[i][j] = Quat.fromPitchYawRollDegrees(middleAngles[i][j][0], middleAngles[i][j][1], middleAngles[i][j][2]); } } - finalKeyFrames[0] = new KeyFrame(rightQuats[0], leftQuats[0], middleQuats[0]); - finalKeyFrames[1] = new KeyFrame(rightQuats[1], leftQuats[1], middleQuats[1]); + finalKeyFrames[0] = new this.KeyFrame(rightQuats[0], leftQuats[0], middleQuats[0]); + finalKeyFrames[1] = new this.KeyFrame(rightQuats[1], leftQuats[1], middleQuats[1]); //Generate mirrored quaternions for the other half of the animation for (var i = 0; i < rightAngles.length; i++) { @@ -77,8 +79,8 @@ ProcAnimAPI = function() { middleQuats[i][j] = Quat.fromPitchYawRollDegrees(-middleAngles[i][j][0], -middleAngles[i][j][1], -middleAngles[i][j][2]); } } - finalKeyFrames[2] = new KeyFrame(leftQuats[0], rightQuats[0], middleQuats[0]); - finalKeyFrames[3] = new KeyFrame(leftQuats[1], rightQuats[1], middleQuats[1]); + finalKeyFrames[2] = new this.KeyFrame(leftQuats[0], rightQuats[0], middleQuats[0]); + finalKeyFrames[3] = new this.KeyFrame(leftQuats[1], rightQuats[1], middleQuats[1]); //Hook up pointers to the next keyframe for (var i = 0; i < finalKeyFrames.length - 1; i++) { @@ -106,7 +108,7 @@ ProcAnimAPI = function() { } return finalKeyFrames; - } + }; // Animation KeyFrame constructor. rightJoints and leftJoints must be the same size this.KeyFrame = function(rightJoints, leftJoints, middleJoints) { @@ -119,7 +121,7 @@ ProcAnimAPI = function() { for (var i = 0; i < middleJoints.length; i++) { this.rotations[this.rotations.length] = middleJoints[i]; } - } + }; // DeCasteljau evaluation to evaluate the bezier curve. // This is a very natural looking interpolation @@ -130,5 +132,5 @@ ProcAnimAPI = function() { var d = Quat.slerp(a, b, f); var e = Quat.slerp(b, c, f); return Quat.slerp(d, e, f); - } + }; } \ No newline at end of file diff --git a/examples/proceduralBot.js b/examples/proceduralBot.js index ce88f596b6..05838da095 100644 --- a/examples/proceduralBot.js +++ b/examples/proceduralBot.js @@ -11,32 +11,30 @@ // //For procedural walk animation -Script.include("proceduralAnimation.js"); +Script.include("http://s3-us-west-1.amazonaws.com/highfidelity-public/scripts/proceduralAnimationAPI.js"); + +var procAnimAPI = new ProcAnimAPI(); function getRandomFloat(min, max) { -return Math.random() * (max - min) + min; + return Math.random() * (max - min) + min; } function getRandomInt (min, max) { -return Math.floor(Math.random() * (max - min + 1)) + min; + return Math.floor(Math.random() * (max - min + 1)) + min; } function printVector(string, vector) { -print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); + print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); } var CHANCE_OF_MOVING = 0.1; var CHANCE_OF_SOUND = 0; var CHANCE_OF_HEAD_TURNING = 0.05; var CHANCE_OF_BIG_MOVE = 0.1; -var CHANCE_OF_WAVING = 0.009; var isMoving = false; var isTurningHead = false; var isPlayingAudio = false; -var isWaving = false; -var waveFrequency = 0.0; -var waveAmplitude = 0.0; var X_MIN = 0.50; var X_MAX = 15.60; @@ -141,32 +139,13 @@ function playRandomSound() { } } -//Animation KeyFrame constructor. rightJoints and leftJoints must be the same size -function WalkKeyFrame(rightJoints, leftJoints, singleJoints) { - this.rotations = []; - - for (var i = 0; i < rightJoints.length; i++) { - this.rotations[this.rotations.length] = rightJoints[i]; - this.rotations[this.rotations.length] = leftJoints[i]; - } - for (var i = 0; i < singleJoints.length; i++) { - this.rotations[this.rotations.length] = singleJoints[i]; - } -} - //Procedural walk animation using two keyframes //We use a separate array for front and back joints -var frontKeyFrames = []; -var backKeyFrames = []; -//for non mirrored joints such as the spine -var singleKeyFrames = []; //Pitch, yaw, and roll for the joints -var frontAngles = []; -var backAngles = []; +var rightAngles = []; +var leftAngles = []; //for non mirrored joints such as the spine -var singleAngles = []; - - +var middleAngles = []; //Actual joint mappings var SHOULDER_JOINT_NUMBER = 15; @@ -185,12 +164,9 @@ var JOINT_SPINE = 11; var NUM_FRAMES = 2; for (var i = 0; i < NUM_FRAMES; i++) { - frontAngles[i] = []; - backAngles[i] = []; - singleAngles[i] = []; - frontKeyFrames[i] = []; - backKeyFrames[i] = []; - singleKeyFrames[i] = []; + rightAngles[i] = []; + leftAngles[i] = []; + middleAngles[i] = []; } //Joint order for actual joint mappings, should be interleaved R,L,R,L,...S,S,S for R = right, L = left, S = single var JOINT_ORDER = [JOINT_R_HIP, JOINT_L_HIP, JOINT_R_KNEE, JOINT_L_KNEE, JOINT_R_ARM, JOINT_L_ARM, JOINT_R_FOREARM, JOINT_L_FOREARM, JOINT_SPINE]; @@ -206,101 +182,37 @@ var SPINE = 0; //We have to store the angles so we can invert yaw and roll when making the animation //symmetrical - //Front refers to leg, not arm. //Legs Extending -frontAngles[0][HIP] = [30.0, 0.0, 8.0]; -frontAngles[0][KNEE] = [-15.0, 0.0, 0.0]; -frontAngles[0][ARM] = [85.0, -25.0, 0.0]; -frontAngles[0][FOREARM] = [0.0, 0.0, -15.0]; +rightAngles[0][HIP] = [30.0, 0.0, 8.0]; +rightAngles[0][KNEE] = [-15.0, 0.0, 0.0]; +rightAngles[0][ARM] = [85.0, -25.0, 0.0]; +rightAngles[0][FOREARM] = [0.0, 0.0, -15.0]; -backAngles[0][HIP] = [-15, 0.0, 8.0]; -backAngles[0][KNEE] = [-28, 0.0, 0.0]; -backAngles[0][ARM] = [85.0, 20.0, 0.0]; -backAngles[0][FOREARM] = [10.0, 0.0, -25.0]; +leftAngles[0][HIP] = [-15, 0.0, 8.0]; +leftAngles[0][KNEE] = [-28, 0.0, 0.0]; +leftAngles[0][ARM] = [85.0, 20.0, 0.0]; +leftAngles[0][FOREARM] = [10.0, 0.0, -25.0]; -singleAngles[0][SPINE] = [0.0, -15.0, 5.0]; +middleAngles[0][SPINE] = [0.0, -15.0, 5.0]; //Legs Passing -frontAngles[1][HIP] = [6.0, 0.0, 8.0]; -frontAngles[1][KNEE] = [-12.0, 0.0, 0.0]; -frontAngles[1][ARM] = [85.0, 0.0, 0.0]; -frontAngles[1][FOREARM] = [0.0, 0.0, -15.0]; +rightAngles[1][HIP] = [6.0, 0.0, 8.0]; +rightAngles[1][KNEE] = [-12.0, 0.0, 0.0]; +rightAngles[1][ARM] = [85.0, 0.0, 0.0]; +rightAngles[1][FOREARM] = [0.0, 0.0, -15.0]; -backAngles[1][HIP] = [10.0, 0.0, 8.0]; -backAngles[1][KNEE] = [-55.0, 0.0, 0.0]; -backAngles[1][ARM] = [85.0, 0.0, 0.0]; -backAngles[1][FOREARM] = [0.0, 0.0, -15.0]; +leftAngles[1][HIP] = [10.0, 0.0, 8.0]; +leftAngles[1][KNEE] = [-55.0, 0.0, 0.0]; +leftAngles[1][ARM] = [85.0, 0.0, 0.0]; +leftAngles[1][FOREARM] = [0.0, 0.0, -15.0]; -singleAngles[1][SPINE] = [0.0, 0.0, 0.0]; +middleAngles[1][SPINE] = [0.0, 0.0, 0.0]; // ******************************* Animation Is Defined Above ************************************* //Actual keyframes for the animation -var walkKeyFrames = []; -//Generate quaternions from the angles -for (var i = 0; i < frontAngles.length; i++) { - for (var j = 0; j < frontAngles[i].length; j++) { - frontKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(frontAngles[i][j][0], frontAngles[i][j][1], frontAngles[i][j][2]); - backKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(backAngles[i][j][0], -backAngles[i][j][1], -backAngles[i][j][2]); - } -} -for (var i = 0; i < singleAngles.length; i++) { - for (var j = 0; j < singleAngles[i].length; j++) { - singleKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(singleAngles[i][j][0], singleAngles[i][j][1], singleAngles[i][j][2]); - } -} -walkKeyFrames[0] = new WalkKeyFrame(frontKeyFrames[0], backKeyFrames[0], singleKeyFrames[0]); -walkKeyFrames[1] = new WalkKeyFrame(frontKeyFrames[1], backKeyFrames[1], singleKeyFrames[1]); - -//Generate mirrored quaternions for the other half of the body -for (var i = 0; i < frontAngles.length; i++) { - for (var j = 0; j < frontAngles[i].length; j++) { - frontKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(frontAngles[i][j][0], -frontAngles[i][j][1], -frontAngles[i][j][2]); - backKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(backAngles[i][j][0], backAngles[i][j][1], backAngles[i][j][2]); - } -} -for (var i = 0; i < singleAngles.length; i++) { - for (var j = 0; j < singleAngles[i].length; j++) { - singleKeyFrames[i][j] = Quat.fromPitchYawRollDegrees(-singleAngles[i][j][0], -singleAngles[i][j][1], -singleAngles[i][j][2]); - } -} -walkKeyFrames[2] = new WalkKeyFrame(backKeyFrames[0], frontKeyFrames[0], singleKeyFrames[0]); -walkKeyFrames[3] = new WalkKeyFrame(backKeyFrames[1], frontKeyFrames[1], singleKeyFrames[1]); - -//Hook up pointers to the next keyframe -for (var i = 0; i < walkKeyFrames.length - 1; i++) { - walkKeyFrames[i].nextFrame = walkKeyFrames[i+1]; -} -walkKeyFrames[walkKeyFrames.length-1].nextFrame = walkKeyFrames[0]; - -//Set up the bezier curve control points using technique described at -//https://www.cs.tcd.ie/publications/tech-reports/reports.94/TCD-CS-94-18.pdf -//Set up all C1 -for (var i = 0; i < walkKeyFrames.length; i++) { - walkKeyFrames[i].nextFrame.controlPoints = []; - for (var j = 0; j < walkKeyFrames[i].rotations.length; j++) { - walkKeyFrames[i].nextFrame.controlPoints[j] = []; - var R = Quat.slerp(walkKeyFrames[i].rotations[j], walkKeyFrames[i].nextFrame.rotations[j], 2.0); - var T = Quat.slerp(R, walkKeyFrames[i].nextFrame.nextFrame.rotations[j], 0.5); - walkKeyFrames[i].nextFrame.controlPoints[j][0] = Quat.slerp(walkKeyFrames[i].nextFrame.rotations[j], T, 0.33333); - } -} -//Set up all C2 -for (var i = 0; i < walkKeyFrames.length; i++) { - for (var j = 0; j < walkKeyFrames[i].rotations.length; j++) { - walkKeyFrames[i].controlPoints[j][1] = Quat.slerp(walkKeyFrames[i].nextFrame.rotations[j], walkKeyFrames[i].nextFrame.controlPoints[j][0], -1.0); - } -} -//DeCasteljau evaluation to evaluate the bezier curve -function deCasteljau(k1, k2, c1, c2, f) { - var a = Quat.slerp(k1, c1, f); - var b = Quat.slerp(c1, c2, f); - var c = Quat.slerp(c2, k2, f); - var d = Quat.slerp(a, b, f); - var e = Quat.slerp(b, c, f); - return Quat.slerp(d, e, f); -} +var walkKeyFrames = procAnimAPI.generateKeyframes(rightAngles, leftAngles, middleAngles, NUM_FRAMES); var currentFrame = 0; @@ -314,7 +226,7 @@ var avatarVelocity = 0.0; var avatarMaxVelocity = 1.4; function keepWalking(deltaTime) { - + walkTime += avatarVelocity * deltaTime; if (walkTime > walkWheelRate) { walkTime = 0.0; @@ -329,17 +241,10 @@ function keepWalking(deltaTime) { var interp = walkTime / walkWheelRate; for (var i = 0; i < JOINT_ORDER.length; i++) { - Avatar.setJointData(JOINT_ORDER[i], deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], interp)); + Avatar.setJointData(JOINT_ORDER[i], procAnimAPI.deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], interp)); } } -var trailingAverageLoudness = 0; -var MAX_SAMPLE = 32767; -var DB_METER_BASE = Math.log(MAX_SAMPLE); - -var RAND_RATIO_LAST = getRandomFloat(0.1, 0.3); -var RAND_TRAILING = 1 - RAND_RATIO_LAST; - function jumpWithLoudness(deltaTime) { // potentially change pelvis height depending on trailing average loudness @@ -362,18 +267,9 @@ function jumpWithLoudness(deltaTime) { Avatar.position = pelvisPosition; } -var jointMapping = null; -var frameIndex = 0.0; -var isPlayingDanceAnimation = false; -var randomAnimation = null; -var animationLoops = 1; var forcedMove = false; -var FRAME_RATE = 30.0; - var wasMovingLastFrame = false; -var wasDancing = false; - function handleHeadTurn() { if (!isTurningHead && (Math.random() < CHANCE_OF_HEAD_TURNING)) { @@ -387,16 +283,6 @@ function handleHeadTurn() { } } -var currentShoulderQuat = Avatar.getJointRotation(SHOULDER_JOINT_NUMBER); -var targetShoulderQuat = currentShoulderQuat; -var idleShoulderQuat = currentShoulderQuat; -var currentSpineQuat = Avatar.getJointRotation(JOINT_SPINE); -var targetSpineQuat = currentSpineQuat; -var idleSpineQuat = currentSpineQuat; -var currentElbowQuat = Avatar.getJointRotation(ELBOW_JOINT_NUMBER); -var targetElbowQuat = currentElbowQuat; -var idleElbowQuat = currentElbowQuat; - function stopWalking() { Avatar.clearJointData(JOINT_R_HIP); Avatar.clearJointData(JOINT_R_KNEE); @@ -421,7 +307,7 @@ function handleWalking(deltaTime) { targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, MOVE_RANGE_SMALL))); } while ((targetPosition.x < X_MIN || targetPosition.x > X_MAX || targetPosition.z < Z_MIN || targetPosition.z > Z_MAX) - && attempts < MAX_ATEMPTS); + && attempts < MAX_ATTEMPTS); targetPosition.x = clamp(targetPosition.x, X_MIN, X_MAX); targetPosition.z = clamp(targetPosition.z, Z_MIN, Z_MAX); @@ -473,31 +359,17 @@ function changePelvisHeight(newHeight) { Avatar.position = newPosition; } -function possiblyStopDancing() { - if (wasDancing) { - for (var j = 0; j < Avatar.jointNames.length; j++) { - Avatar.clearJointData(j); - } - - changePelvisHeight(Y_PELVIS); - } -} - function updateBehavior(deltaTime) { cumulativeTime += deltaTime; if (AvatarList.containsAvatarWithDisplayName("mrdj")) { - if (wasMovingLastFrame && !wasDancing) { + if (wasMovingLastFrame) { isMoving = false; } // we have a DJ, shouldn't we be dancing? jumpWithLoudness(deltaTime); } else { - // make sure we're not dancing anymore - possiblyStopDancing(); - - wasDancing = false; // no DJ, let's just chill on the dancefloor - randomly walking and talking handleHeadTurn(); From 303f4bf4f1f106f704819525a81f3608de876b5b Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Wed, 30 Jul 2014 12:08:07 -0700 Subject: [PATCH 06/11] Improved API, added footstep sounds --- examples/proceduralAnimationAPI.js | 54 +++--- examples/proceduralBot.js | 264 ++++++++++++++++------------- 2 files changed, 176 insertions(+), 142 deletions(-) diff --git a/examples/proceduralAnimationAPI.js b/examples/proceduralAnimationAPI.js index ae6b5c3d31..61397ec180 100644 --- a/examples/proceduralAnimationAPI.js +++ b/examples/proceduralAnimationAPI.js @@ -64,8 +64,10 @@ ProcAnimAPI = function() { middleQuats[i][j] = Quat.fromPitchYawRollDegrees(middleAngles[i][j][0], middleAngles[i][j][1], middleAngles[i][j][2]); } } - finalKeyFrames[0] = new this.KeyFrame(rightQuats[0], leftQuats[0], middleQuats[0]); - finalKeyFrames[1] = new this.KeyFrame(rightQuats[1], leftQuats[1], middleQuats[1]); + + for (var i = 0; i < numFrames; i++) { + finalKeyFrames[i] = new this.KeyFrame(rightQuats[i], leftQuats[i], middleQuats[i]); + } //Generate mirrored quaternions for the other half of the animation for (var i = 0; i < rightAngles.length; i++) { @@ -79,35 +81,41 @@ ProcAnimAPI = function() { middleQuats[i][j] = Quat.fromPitchYawRollDegrees(-middleAngles[i][j][0], -middleAngles[i][j][1], -middleAngles[i][j][2]); } } - finalKeyFrames[2] = new this.KeyFrame(leftQuats[0], rightQuats[0], middleQuats[0]); - finalKeyFrames[3] = new this.KeyFrame(leftQuats[1], rightQuats[1], middleQuats[1]); - - //Hook up pointers to the next keyframe - for (var i = 0; i < finalKeyFrames.length - 1; i++) { - finalKeyFrames[i].nextFrame = finalKeyFrames[i+1]; + for (var i = 0; i < numFrames; i++) { + finalKeyFrames[numFrames + i] = new this.KeyFrame(leftQuats[i], rightQuats[i], middleQuats[i]); } - finalKeyFrames[finalKeyFrames.length-1].nextFrame = finalKeyFrames[0]; + + //Generate control points + this.computeBezierControlPoints(finalKeyFrames); + + return finalKeyFrames; + }; + + //Computes 2 controlPoints to each keyframe to be used in the bezier evaluation. + //Technique is described at: //https://www.cs.tcd.ie/publications/tech-reports/reports.94/TCD-CS-94-18.pdf + this.computeBezierControlPoints = function(keyFrames) { + //Hook up pointers to the next keyframe + for (var i = 0; i < keyFrames.length - 1; i++) { + keyFrames[i].nextFrame = keyFrames[i+1]; + } + keyFrames[keyFrames.length-1].nextFrame = keyFrames[0]; - //Set up the bezier curve control points using technique described at - //https://www.cs.tcd.ie/publications/tech-reports/reports.94/TCD-CS-94-18.pdf //Set up all C1 - for (var i = 0; i < finalKeyFrames.length; i++) { - finalKeyFrames[i].nextFrame.controlPoints = []; - for (var j = 0; j < finalKeyFrames[i].rotations.length; j++) { - finalKeyFrames[i].nextFrame.controlPoints[j] = []; - var R = Quat.slerp(finalKeyFrames[i].rotations[j], finalKeyFrames[i].nextFrame.rotations[j], 2.0); - var T = Quat.slerp(R, finalKeyFrames[i].nextFrame.nextFrame.rotations[j], 0.5); - finalKeyFrames[i].nextFrame.controlPoints[j][0] = Quat.slerp(finalKeyFrames[i].nextFrame.rotations[j], T, 0.33333); + for (var i = 0; i < keyFrames.length; i++) { + keyFrames[i].nextFrame.controlPoints = []; + for (var j = 0; j < keyFrames[i].rotations.length; j++) { + keyFrames[i].nextFrame.controlPoints[j] = []; + var R = Quat.slerp(keyFrames[i].rotations[j], keyFrames[i].nextFrame.rotations[j], 2.0); + var T = Quat.slerp(R, keyFrames[i].nextFrame.nextFrame.rotations[j], 0.5); + keyFrames[i].nextFrame.controlPoints[j][0] = Quat.slerp(keyFrames[i].nextFrame.rotations[j], T, 0.33333); } } //Set up all C2 - for (var i = 0; i < finalKeyFrames.length; i++) { - for (var j = 0; j < finalKeyFrames[i].rotations.length; j++) { - finalKeyFrames[i].controlPoints[j][1] = Quat.slerp(finalKeyFrames[i].nextFrame.rotations[j], finalKeyFrames[i].nextFrame.controlPoints[j][0], -1.0); + for (var i = 0; i < keyFrames.length; i++) { + for (var j = 0; j < keyFrames[i].rotations.length; j++) { + keyFrames[i].controlPoints[j][1] = Quat.slerp(keyFrames[i].nextFrame.rotations[j], keyFrames[i].nextFrame.controlPoints[j][0], -1.0); } } - - return finalKeyFrames; }; // Animation KeyFrame constructor. rightJoints and leftJoints must be the same size diff --git a/examples/proceduralBot.js b/examples/proceduralBot.js index 05838da095..d8218c8341 100644 --- a/examples/proceduralBot.js +++ b/examples/proceduralBot.js @@ -27,7 +27,7 @@ function printVector(string, vector) { print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); } -var CHANCE_OF_MOVING = 0.1; +var CHANCE_OF_MOVING = 0.01; var CHANCE_OF_SOUND = 0; var CHANCE_OF_HEAD_TURNING = 0.05; var CHANCE_OF_BIG_MOVE = 0.1; @@ -60,8 +60,6 @@ var targetOrientation = { x: 0, y: 0, z: 0, w: 0 }; var currentOrientation = { x: 0, y: 0, z: 0, w: 0 }; var targetHeadPitch = 0.0; -var cumulativeTime = 0.0; - var basePelvisHeight = 0.0; var pelvisOscillatorPosition = 0.0; var pelvisOscillatorVelocity = 0.0; @@ -209,11 +207,34 @@ leftAngles[1][FOREARM] = [0.0, 0.0, -15.0]; middleAngles[1][SPINE] = [0.0, 0.0, 0.0]; -// ******************************* Animation Is Defined Above ************************************* - //Actual keyframes for the animation var walkKeyFrames = procAnimAPI.generateKeyframes(rightAngles, leftAngles, middleAngles, NUM_FRAMES); +// ******************************* Animation Is Defined Above ************************************* + +// ********************************** Standing Key Frame ****************************************** +//We don't have to do any mirroring or anything, since this is just a single pose. +var rightQuats = []; +var leftQuats = []; +var middleQuats = []; + +rightQuats[HIP] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 7.0); +rightQuats[KNEE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); +rightQuats[ARM] = Quat.fromPitchYawRollDegrees(85.0, 0.0, 0.0); +rightQuats[FOREARM] = Quat.fromPitchYawRollDegrees(0.0, 0.0, -10.0); + +leftQuats[HIP] = Quat.fromPitchYawRollDegrees(0, 0.0, -7.0); +leftQuats[KNEE] = Quat.fromPitchYawRollDegrees(0, 0.0, 0.0); +leftQuats[ARM] = Quat.fromPitchYawRollDegrees(85.0, 0.0, 0.0); +leftQuats[FOREARM] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 10.0); + +middleQuats[SPINE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); + +var standingKeyFrame = new procAnimAPI.KeyFrame(rightQuats, leftQuats, middleQuats); + +// ************************************************************************************************ + + var currentFrame = 0; var walkTime = 0.0; @@ -225,46 +246,56 @@ var avatarAcceleration = 0.75; var avatarVelocity = 0.0; var avatarMaxVelocity = 1.4; -function keepWalking(deltaTime) { - - walkTime += avatarVelocity * deltaTime; - if (walkTime > walkWheelRate) { - walkTime = 0.0; - currentFrame++; - if (currentFrame > 3) { - currentFrame = 0; - } - } +function handleAnimation(deltaTime) { - var frame = walkKeyFrames[currentFrame]; + if (avatarVelocity == 0.0) { + walkTime = 0.0; + currentFrame = 0; + } else { + walkTime += avatarVelocity * deltaTime; + if (walkTime > walkWheelRate) { + walkTime = 0.0; + currentFrame++; + if (currentFrame > 3) { + currentFrame = 0; + } + } + } + + var frame = walkKeyFrames[currentFrame]; - var interp = walkTime / walkWheelRate; + var walkInterp = walkTime / walkWheelRate; + var animInterp = avatarVelocity / (avatarMaxVelocity / 2.0); + if (animInterp > 1.0) animInterp = 1.0; - for (var i = 0; i < JOINT_ORDER.length; i++) { - Avatar.setJointData(JOINT_ORDER[i], procAnimAPI.deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], interp)); - } + for (var i = 0; i < JOINT_ORDER.length; i++) { + var walkJoint = procAnimAPI.deCasteljau(frame.rotations[i], frame.nextFrame.rotations[i], frame.controlPoints[i][0], frame.controlPoints[i][1], walkInterp); + var standJoint = standingKeyFrame.rotations[i]; + var finalJoint = Quat.mix(standJoint, walkJoint, animInterp); + Avatar.setJointData(JOINT_ORDER[i], finalJoint); + } } function jumpWithLoudness(deltaTime) { - // potentially change pelvis height depending on trailing average loudness - - pelvisOscillatorVelocity += deltaTime * Agent.lastReceivedAudioLoudness * 700.0 ; + // potentially change pelvis height depending on trailing average loudness - pelvisOscillatorVelocity -= pelvisOscillatorPosition * 0.75; - pelvisOscillatorVelocity *= 0.97; - pelvisOscillatorPosition += deltaTime * pelvisOscillatorVelocity; - Avatar.headPitch = pelvisOscillatorPosition * 60.0; + pelvisOscillatorVelocity += deltaTime * Agent.lastReceivedAudioLoudness * 700.0 ; - var pelvisPosition = Avatar.position; - pelvisPosition.y = (Y_PELVIS - 0.35) + pelvisOscillatorPosition; - - if (pelvisPosition.y < Y_PELVIS) { - pelvisPosition.y = Y_PELVIS; - } else if (pelvisPosition.y > Y_PELVIS + 1.0) { - pelvisPosition.y = Y_PELVIS + 1.0; - } - - Avatar.position = pelvisPosition; + pelvisOscillatorVelocity -= pelvisOscillatorPosition * 0.75; + pelvisOscillatorVelocity *= 0.97; + pelvisOscillatorPosition += deltaTime * pelvisOscillatorVelocity; + Avatar.headPitch = pelvisOscillatorPosition * 60.0; + + var pelvisPosition = Avatar.position; + pelvisPosition.y = (Y_PELVIS - 0.35) + pelvisOscillatorPosition; + + if (pelvisPosition.y < Y_PELVIS) { + pelvisPosition.y = Y_PELVIS; + } else if (pelvisPosition.y > Y_PELVIS + 1.0) { + pelvisPosition.y = Y_PELVIS + 1.0; + } + + Avatar.position = pelvisPosition; } var forcedMove = false; @@ -272,110 +303,105 @@ var forcedMove = false; var wasMovingLastFrame = false; function handleHeadTurn() { - if (!isTurningHead && (Math.random() < CHANCE_OF_HEAD_TURNING)) { - targetHeadPitch = getRandomFloat(-PITCH_RANGE, PITCH_RANGE); - isTurningHead = true; - } else { - Avatar.headPitch = Avatar.headPitch + (targetHeadPitch - Avatar.headPitch) * PITCH_RATE; - if (Math.abs(Avatar.headPitch - targetHeadPitch) < STOP_TOLERANCE) { - isTurningHead = false; + if (!isTurningHead && (Math.random() < CHANCE_OF_HEAD_TURNING)) { + targetHeadPitch = getRandomFloat(-PITCH_RANGE, PITCH_RANGE); + isTurningHead = true; + } else { + Avatar.headPitch = Avatar.headPitch + (targetHeadPitch - Avatar.headPitch) * PITCH_RATE; + if (Math.abs(Avatar.headPitch - targetHeadPitch) < STOP_TOLERANCE) { + isTurningHead = false; + } } - } } function stopWalking() { - Avatar.clearJointData(JOINT_R_HIP); - Avatar.clearJointData(JOINT_R_KNEE); - Avatar.clearJointData(JOINT_L_HIP); - Avatar.clearJointData(JOINT_L_KNEE); - avatarVelocity = 0.0; - isMoving = false; + avatarVelocity = 0.0; + isMoving = false; } var MAX_ATTEMPTS = 40; function handleWalking(deltaTime) { - if (forcedMove || (!isMoving && Math.random() < CHANCE_OF_MOVING)) { - // Set new target location - - //Keep trying new orientations if the desired target location is out of bounds - var attempts = 0; - do { - targetOrientation = Quat.multiply(Avatar.orientation, Quat.angleAxis(getRandomFloat(-TURN_RANGE, TURN_RANGE), { x:0, y:1, z:0 })); - var front = Quat.getFront(targetOrientation); - - targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, MOVE_RANGE_SMALL))); - } - while ((targetPosition.x < X_MIN || targetPosition.x > X_MAX || targetPosition.z < Z_MIN || targetPosition.z > Z_MAX) - && attempts < MAX_ATTEMPTS); - - targetPosition.x = clamp(targetPosition.x, X_MIN, X_MAX); - targetPosition.z = clamp(targetPosition.z, Z_MIN, Z_MAX); - targetPosition.y = Y_PELVIS; - - wasMovingLastFrame = true; - isMoving = true; - forcedMove = false; - } else if (isMoving) { - keepWalking(deltaTime); - - var targetVector = Vec3.subtract(targetPosition, Avatar.position); - var distance = Vec3.length(targetVector); - if (distance <= avatarVelocity * deltaTime) { - Avatar.position = targetPosition; - stopWalking(); - } else { - var direction = Vec3.normalize(targetVector); - //Figure out if we should be slowing down - var t = avatarVelocity / avatarAcceleration; - var d = (avatarVelocity / 2.0) * t; - if (distance < d) { - avatarVelocity -= avatarAcceleration * deltaTime; - if (avatarVelocity <= 0) { - stopWalking(); - } - } else { - avatarVelocity += avatarAcceleration * deltaTime; - if (avatarVelocity > avatarMaxVelocity) avatarVelocity = avatarMaxVelocity; + if (forcedMove || (!isMoving && Math.random() < CHANCE_OF_MOVING)) { + // Set new target location + + //Keep trying new orientations if the desired target location is out of bounds + var attempts = 0; + do { + targetOrientation = Quat.multiply(Avatar.orientation, Quat.angleAxis(getRandomFloat(-TURN_RANGE, TURN_RANGE), { x:0, y:1, z:0 })); + var front = Quat.getFront(targetOrientation); + + targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, MOVE_RANGE_SMALL))); + } + while ((targetPosition.x < X_MIN || targetPosition.x > X_MAX || targetPosition.z < Z_MIN || targetPosition.z > Z_MAX) + && attempts < MAX_ATTEMPTS); + + targetPosition.x = clamp(targetPosition.x, X_MIN, X_MAX); + targetPosition.z = clamp(targetPosition.z, Z_MIN, Z_MAX); + targetPosition.y = Y_PELVIS; + + wasMovingLastFrame = true; + isMoving = true; + forcedMove = false; + } else if (isMoving) { + + var targetVector = Vec3.subtract(targetPosition, Avatar.position); + var distance = Vec3.length(targetVector); + if (distance <= avatarVelocity * deltaTime) { + Avatar.position = targetPosition; + stopWalking(); + } else { + var direction = Vec3.normalize(targetVector); + //Figure out if we should be slowing down + var t = avatarVelocity / avatarAcceleration; + var d = (avatarVelocity / 2.0) * t; + if (distance < d) { + avatarVelocity -= avatarAcceleration * deltaTime; + if (avatarVelocity <= 0) { + stopWalking(); + } + } else { + avatarVelocity += avatarAcceleration * deltaTime; + if (avatarVelocity > avatarMaxVelocity) avatarVelocity = avatarMaxVelocity; + } + Avatar.position = Vec3.sum(Avatar.position, Vec3.multiply(direction, avatarVelocity * deltaTime)); + Avatar.orientation = Quat.mix(Avatar.orientation, targetOrientation, TURN_RATE); + + wasMovingLastFrame = true; + } - Avatar.position = Vec3.sum(Avatar.position, Vec3.multiply(direction, avatarVelocity * deltaTime)); - Avatar.orientation = Quat.mix(Avatar.orientation, targetOrientation, TURN_RATE); - - wasMovingLastFrame = true; - } - } } function handleTalking() { - if (Math.random() < CHANCE_OF_SOUND) { - playRandomSound(); - } + if (Math.random() < CHANCE_OF_SOUND) { + playRandomSound(); + } } function changePelvisHeight(newHeight) { - var newPosition = Avatar.position; - newPosition.y = newHeight; - Avatar.position = newPosition; + var newPosition = Avatar.position; + newPosition.y = newHeight; + Avatar.position = newPosition; } function updateBehavior(deltaTime) { - cumulativeTime += deltaTime; - if (AvatarList.containsAvatarWithDisplayName("mrdj")) { - if (wasMovingLastFrame) { - isMoving = false; + if (AvatarList.containsAvatarWithDisplayName("mrdj")) { + if (wasMovingLastFrame) { + isMoving = false; + } + + // we have a DJ, shouldn't we be dancing? + jumpWithLoudness(deltaTime); + } else { + + // no DJ, let's just chill on the dancefloor - randomly walking and talking + handleHeadTurn(); + handleAnimation(deltaTime); + handleWalking(deltaTime); + handleTalking(); } - - // we have a DJ, shouldn't we be dancing? - jumpWithLoudness(deltaTime); - } else { - - // no DJ, let's just chill on the dancefloor - randomly walking and talking - handleHeadTurn(); - handleWalking(deltaTime); - handleTalking(); - } } Script.update.connect(updateBehavior); \ No newline at end of file From 055a97fc2d634db805a5730c22ae783a71fe8cef Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Wed, 30 Jul 2014 14:18:38 -0700 Subject: [PATCH 07/11] Added facial animation to the procedural bot --- examples/proceduralBot.js | 288 ++++++++++++++++++++++++++++++++++---- 1 file changed, 263 insertions(+), 25 deletions(-) diff --git a/examples/proceduralBot.js b/examples/proceduralBot.js index d8218c8341..f02ca934dc 100644 --- a/examples/proceduralBot.js +++ b/examples/proceduralBot.js @@ -27,10 +27,10 @@ function printVector(string, vector) { print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); } -var CHANCE_OF_MOVING = 0.01; -var CHANCE_OF_SOUND = 0; +var CHANCE_OF_MOVING = 0.00; +var CHANCE_OF_SOUND = 0.005; var CHANCE_OF_HEAD_TURNING = 0.05; -var CHANCE_OF_BIG_MOVE = 0.1; +var CHANCE_OF_BIG_MOVE = 1.0; var isMoving = false; var isTurningHead = false; @@ -45,12 +45,13 @@ var MAX_PELVIS_DELTA = 2.5; var AVATAR_PELVIS_HEIGHT = 0.75; -var MOVE_RANGE_SMALL = 10.0; +var MOVE_RANGE_SMALL = 3.0; +var MOVE_RANGE_BIG = 10.0; var TURN_RANGE = 70.0; var STOP_TOLERANCE = 0.05; var MOVE_RATE = 0.05; var TURN_RATE = 0.2; -var PITCH_RATE = 0.10; +var PITCH_RATE = 0.05; var PITCH_RANGE = 20.0; //var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; @@ -110,33 +111,254 @@ basePelvisHeight = firstPosition.y; printVector("New dancer, position = ", Avatar.position); function loadSounds() { - var sound_filenames = ["AB1.raw", "Anchorman2.raw", "B1.raw", "B1.raw", "Bale1.raw", "Bandcamp.raw", + var sound_filenames = ["AB1.raw", "Anchorman2.raw", "B1.raw", "B1.raw", "Bale1.raw", "Bandcamp.raw", "Big1.raw", "Big2.raw", "Brian1.raw", "Buster1.raw", "CES1.raw", "CES2.raw", "CES3.raw", "CES4.raw", "Carrie1.raw", "Carrie3.raw", "Charlotte1.raw", "EN1.raw", "EN2.raw", "EN3.raw", "Eugene1.raw", "Francesco1.raw", "Italian1.raw", "Japanese1.raw", "Leigh1.raw", "Lucille1.raw", "Lucille2.raw", "MeanGirls.raw", "Murray2.raw", "Nigel1.raw", "PennyLane.raw", "Pitt1.raw", "Ricardo.raw", "SN.raw", "Sake1.raw", "Samantha1.raw", "Samantha2.raw", "Spicoli1.raw", "Supernatural.raw", "Swearengen1.raw", "TheDude.raw", "Tony.raw", "Triumph1.raw", "Uma1.raw", "Walken1.raw", "Walken2.raw", "Z1.raw", "Z2.raw" - ]; - - var SOUND_BASE_URL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Cocktail+Party+Snippets/Raws/"; - - for (var i = 0; i < sound_filenames.length; i++) { - sounds.push(new Sound(SOUND_BASE_URL + sound_filenames[i])); - } + ]; + + var footstep_filenames = ["FootstepW2Left-12db.wav", "FootstepW2Right-12db.wav", "FootstepW3Left-12db.wav", "FootstepW3Right-12db.wav", + "FootstepW5Left-12db.wav", "FootstepW5Right-12db.wav"]; + + var SOUND_BASE_URL = "https://s3-us-west-1.amazonaws.com/highfidelity-public/sounds/Cocktail+Party+Snippets/Raws/"; + + var FOOTSTEP_BASE_URL = "http://highfidelity-public.s3-us-west-1.amazonaws.com/sounds/Footsteps/"; + + for (var i = 0; i < sound_filenames.length; i++) { + sounds.push(new Sound(SOUND_BASE_URL + sound_filenames[i])); + } + + for (var i = 0; i < footstep_filenames.length; i++) { + footstepSounds.push(new Sound(FOOTSTEP_BASE_URL + footstep_filenames[i])); + } } var sounds = []; +var footstepSounds = []; loadSounds(); function playRandomSound() { - if (!Agent.isPlayingAvatarSound) { - var whichSound = Math.floor((Math.random() * sounds.length) % sounds.length); - Agent.playAvatarSound(sounds[whichSound]); - } + if (!Agent.isPlayingAvatarSound) { + var whichSound = Math.floor((Math.random() * sounds.length)); + Agent.playAvatarSound(sounds[whichSound]); + } } +function playRandomFootstepSound() { + + var whichSound = Math.floor((Math.random() * footstepSounds.length)); + var options = new AudioInjectionOptions(); + options.position = Avatar.position; + options.volume = 1.0; + Audio.playSound(footstepSounds[whichSound], options); + +} + +// ************************************ Facial Animation ********************************** +var allBlendShapes = []; +var targetBlendCoefficient = []; +var currentBlendCoefficient = []; + +//Blendshape constructor +function addBlendshapeToPose(pose, shapeIndex, val) { + var index = pose.blendShapes.length; + pose.blendShapes[index] = {shapeIndex: shapeIndex, val: val }; +} +//The mood of the avatar, determines face. 0 = happy, 1 = angry, 2 = sad. +var avatarMood = 0; +var currentExpression = -1; +//Face pose constructor +var happyPoses = []; + +happyPoses[0] = {blendShapes: []}; +addBlendshapeToPose(happyPoses[0], 28, 0.7); //MouthSmile_L +addBlendshapeToPose(happyPoses[0], 29, 0.7); //MouthSmile_R + +happyPoses[1] = {blendShapes: []}; +addBlendshapeToPose(happyPoses[1], 28, 1.0); //MouthSmile_L +addBlendshapeToPose(happyPoses[1], 29, 1.0); //MouthSmile_R +addBlendshapeToPose(happyPoses[1], 21, 0.2); //JawOpen + +happyPoses[2] = {blendShapes: []}; +addBlendshapeToPose(happyPoses[2], 28, 1.0); //MouthSmile_L +addBlendshapeToPose(happyPoses[2], 29, 1.0); //MouthSmile_R +addBlendshapeToPose(happyPoses[2], 21, 0.5); //JawOpen +addBlendshapeToPose(happyPoses[2], 46, 1.0); //CheekSquint_L +addBlendshapeToPose(happyPoses[2], 47, 1.0); //CheekSquint_R +addBlendshapeToPose(happyPoses[2], 17, 1.0); //BrowsU_L +addBlendshapeToPose(happyPoses[2], 18, 1.0); //BrowsU_R + +var angryPoses = []; + +angryPoses[0] = {blendShapes: []}; +addBlendshapeToPose(angryPoses[0], 26, 0.6); //MouthFrown_L +addBlendshapeToPose(angryPoses[0], 27, 0.6); //MouthFrown_R +addBlendshapeToPose(angryPoses[0], 14, 0.6); //BrowsD_L +addBlendshapeToPose(angryPoses[0], 15, 0.6); //BrowsD_R + +angryPoses[1] = {blendShapes: []}; +addBlendshapeToPose(angryPoses[1], 26, 0.9); //MouthFrown_L +addBlendshapeToPose(angryPoses[1], 27, 0.9); //MouthFrown_R +addBlendshapeToPose(angryPoses[1], 14, 0.9); //BrowsD_L +addBlendshapeToPose(angryPoses[1], 15, 0.9); //BrowsD_R + +angryPoses[2] = {blendShapes: []}; +addBlendshapeToPose(angryPoses[2], 26, 1.0); //MouthFrown_L +addBlendshapeToPose(angryPoses[2], 27, 1.0); //MouthFrown_R +addBlendshapeToPose(angryPoses[2], 14, 1.0); //BrowsD_L +addBlendshapeToPose(angryPoses[2], 15, 1.0); //BrowsD_R +addBlendshapeToPose(angryPoses[2], 21, 0.5); //JawOpen +addBlendshapeToPose(angryPoses[2], 46, 1.0); //CheekSquint_L +addBlendshapeToPose(angryPoses[2], 47, 1.0); //CheekSquint_R + +var sadPoses = []; + +sadPoses[0] = {blendShapes: []}; +addBlendshapeToPose(sadPoses[0], 26, 0.6); //MouthFrown_L +addBlendshapeToPose(sadPoses[0], 27, 0.6); //MouthFrown_R +addBlendshapeToPose(sadPoses[0], 16, 0.2); //BrowsU_C +addBlendshapeToPose(sadPoses[0], 2, 0.6); //EyeSquint_L +addBlendshapeToPose(sadPoses[0], 3, 0.6); //EyeSquint_R + +sadPoses[1] = {blendShapes: []}; +addBlendshapeToPose(sadPoses[1], 26, 0.9); //MouthFrown_L +addBlendshapeToPose(sadPoses[1], 27, 0.9); //MouthFrown_R +addBlendshapeToPose(sadPoses[1], 16, 0.6); //BrowsU_C +addBlendshapeToPose(sadPoses[1], 2, 0.9); //EyeSquint_L +addBlendshapeToPose(sadPoses[1], 3, 0.9); //EyeSquint_R + +sadPoses[2] = {blendShapes: []}; +addBlendshapeToPose(sadPoses[2], 26, 1.0); //MouthFrown_L +addBlendshapeToPose(sadPoses[2], 27, 1.0); //MouthFrown_R +addBlendshapeToPose(sadPoses[2], 16, 0.1); //BrowsU_C +addBlendshapeToPose(sadPoses[2], 2, 1.0); //EyeSquint_L +addBlendshapeToPose(sadPoses[2], 3, 1.0); //EyeSquint_R +addBlendshapeToPose(sadPoses[2], 21, 0.3); //JawOpen + +var facePoses = []; +facePoses[0] = happyPoses; +facePoses[1] = angryPoses; +facePoses[2] = sadPoses; + + +function addBlendShape(s) { + allBlendShapes[allBlendShapes.length] = s; +} + +//It is imperative that the following blendshapes are all present and are in the correct order +addBlendShape("EyeBlink_L"); //0 +addBlendShape("EyeBlink_R"); //1 +addBlendShape("EyeSquint_L"); //2 +addBlendShape("EyeSquint_R"); //3 +addBlendShape("EyeDown_L"); //4 +addBlendShape("EyeDown_R"); //5 +addBlendShape("EyeIn_L"); //6 +addBlendShape("EyeIn_R"); //7 +addBlendShape("EyeOpen_L"); //8 +addBlendShape("EyeOpen_R"); //9 +addBlendShape("EyeOut_L"); //10 +addBlendShape("EyeOut_R"); //11 +addBlendShape("EyeUp_L"); //12 +addBlendShape("EyeUp_R"); //13 +addBlendShape("BrowsD_L"); //14 +addBlendShape("BrowsD_R"); //15 +addBlendShape("BrowsU_C"); //16 +addBlendShape("BrowsU_L"); //17 +addBlendShape("BrowsU_R"); //18 +addBlendShape("JawFwd"); //19 +addBlendShape("JawLeft"); //20 +addBlendShape("JawOpen"); //21 +addBlendShape("JawChew"); //22 +addBlendShape("JawRight"); //23 +addBlendShape("MouthLeft"); //24 +addBlendShape("MouthRight"); //25 +addBlendShape("MouthFrown_L"); //26 +addBlendShape("MouthFrown_R"); //27 +addBlendShape("MouthSmile_L"); //28 +addBlendShape("MouthSmile_R"); //29 +addBlendShape("MouthDimple_L"); //30 +addBlendShape("MouthDimple_R"); //31 +addBlendShape("LipsStretch_L"); //32 +addBlendShape("LipsStretch_R"); //33 +addBlendShape("LipsUpperClose"); //34 +addBlendShape("LipsLowerClose"); //35 +addBlendShape("LipsUpperUp"); //36 +addBlendShape("LipsLowerDown"); //37 +addBlendShape("LipsUpperOpen"); //38 +addBlendShape("LipsLowerOpen"); //39 +addBlendShape("LipsFunnel"); //40 +addBlendShape("LipsPucker"); //41 +addBlendShape("ChinLowerRaise"); //42 +addBlendShape("ChinUpperRaise"); //43 +addBlendShape("Sneer"); //44 +addBlendShape("Puff"); //45 +addBlendShape("CheekSquint_L"); //46 +addBlendShape("CheekSquint_R"); //47 + +for (var i = 0; i < allBlendShapes.length; i++) { + targetBlendCoefficient[i] = 0; + currentBlendCoefficient[i] = 0; +} + +function setRandomExpression() { + + //Clear all expression data for current expression + if (currentExpression != -1) { + var expression = facePoses[avatarMood][currentExpression]; + for (var i = 0; i < expression.blendShapes.length; i++) { + targetBlendCoefficient[expression.blendShapes[i].shapeIndex] = 0.0; + } + } + //Get a new current expression + currentExpression = Math.floor(Math.random() * facePoses[avatarMood].length); + var expression = facePoses[avatarMood][currentExpression]; + for (var i = 0; i < expression.blendShapes.length; i++) { + targetBlendCoefficient[expression.blendShapes[i].shapeIndex] = expression.blendShapes[i].val; + } +} + +var expressionChangeSpeed = 0.1; +function updateBlendShapes(deltaTime) { + + for (var i = 0; i < allBlendShapes.length; i++) { + currentBlendCoefficient[i] += (targetBlendCoefficient[i] - currentBlendCoefficient[i]) * expressionChangeSpeed; + Avatar.setBlendshape(allBlendShapes[i], currentBlendCoefficient[i]); + } +} + +var BLINK_SPEED = 0.15; +var CHANCE_TO_BLINK = 0.0025; +var MAX_BLINK = 0.85; +var blink = 0.0; +var isBlinking = false; +function updateBlinking(deltaTime) { + if (isBlinking == false) { + if (Math.random() < CHANCE_TO_BLINK) { + isBlinking = true; + } else { + blink -= BLINK_SPEED; + if (blink < 0.0) blink = 0.0; + } + } else { + blink += BLINK_SPEED; + if (blink > MAX_BLINK) { + blink = MAX_BLINK; + isBlinking = false; + } + } + + currentBlendCoefficient[0] = blink; + currentBlendCoefficient[1] = blink; + targetBlendCoefficient[0] = blink; + targetBlendCoefficient[1] = blink; +} + +// ************************************************************************************* + //Procedural walk animation using two keyframes //We use a separate array for front and back joints //Pitch, yaw, and roll for the joints @@ -248,24 +470,33 @@ var avatarMaxVelocity = 1.4; function handleAnimation(deltaTime) { + updateBlinking(deltaTime); + updateBlendShapes(deltaTime); + + if (Math.random() < 0.01) { + setRandomExpression(); + } + if (avatarVelocity == 0.0) { walkTime = 0.0; currentFrame = 0; } else { - walkTime += avatarVelocity * deltaTime; - if (walkTime > walkWheelRate) { - walkTime = 0.0; - currentFrame++; - if (currentFrame > 3) { + walkTime += avatarVelocity * deltaTime; + if (walkTime > walkWheelRate) { + walkTime = 0.0; + currentFrame++; + if (currentFrame % 2 == 1) { + playRandomFootstepSound(); + } + if (currentFrame > 3) { currentFrame = 0; } } } - var frame = walkKeyFrames[currentFrame]; var walkInterp = walkTime / walkWheelRate; - var animInterp = avatarVelocity / (avatarMaxVelocity / 2.0); + var animInterp = avatarVelocity / (avatarMaxVelocity / 1.3); if (animInterp > 1.0) animInterp = 1.0; for (var i = 0; i < JOINT_ORDER.length; i++) { @@ -325,13 +556,20 @@ function handleWalking(deltaTime) { if (forcedMove || (!isMoving && Math.random() < CHANCE_OF_MOVING)) { // Set new target location + var moveRange; + if (Math.random() < CHANCE_OF_BIG_MOVE) { + moveRange = MOVE_RANGE_BIG; + } else { + moveRange = MOVE_RANGE_SMALL; + } + //Keep trying new orientations if the desired target location is out of bounds var attempts = 0; do { targetOrientation = Quat.multiply(Avatar.orientation, Quat.angleAxis(getRandomFloat(-TURN_RANGE, TURN_RANGE), { x:0, y:1, z:0 })); var front = Quat.getFront(targetOrientation); - targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, MOVE_RANGE_SMALL))); + targetPosition = Vec3.sum(Avatar.position, Vec3.multiply(front, getRandomFloat(0.0, moveRange))); } while ((targetPosition.x < X_MIN || targetPosition.x > X_MAX || targetPosition.z < Z_MIN || targetPosition.z > Z_MAX) && attempts < MAX_ATTEMPTS); From 66c1aba7f7de493965a2bf23a236f89533f726ef Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Wed, 30 Jul 2014 15:23:03 -0700 Subject: [PATCH 08/11] Animation improvements, exposed roll/yaw head control. --- examples/proceduralBot.js | 78 ++++++++++++++++++++++-------- libraries/avatars/src/AvatarData.h | 10 +++- 2 files changed, 66 insertions(+), 22 deletions(-) diff --git a/examples/proceduralBot.js b/examples/proceduralBot.js index f02ca934dc..9b10b28243 100644 --- a/examples/proceduralBot.js +++ b/examples/proceduralBot.js @@ -27,9 +27,9 @@ function printVector(string, vector) { print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); } -var CHANCE_OF_MOVING = 0.00; -var CHANCE_OF_SOUND = 0.005; -var CHANCE_OF_HEAD_TURNING = 0.05; +var CHANCE_OF_MOVING = 0.000; +var CHANCE_OF_SOUND = 0;//0.005; +var CHANCE_OF_HEAD_TURNING = 0.01; var CHANCE_OF_BIG_MOVE = 1.0; var isMoving = false; @@ -51,8 +51,9 @@ var TURN_RANGE = 70.0; var STOP_TOLERANCE = 0.05; var MOVE_RATE = 0.05; var TURN_RATE = 0.2; -var PITCH_RATE = 0.05; -var PITCH_RANGE = 20.0; +var HEAD_TURN_RATE = 0.05; +var PITCH_RANGE = 15.0; +var YAW_RANGE = 35.0; //var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; var firstPosition = { x: 0.5, y: Y_PELVIS, z: 0.5 }; @@ -60,6 +61,7 @@ var targetPosition = { x: 0, y: 0, z: 0 }; var targetOrientation = { x: 0, y: 0, z: 0, w: 0 }; var currentOrientation = { x: 0, y: 0, z: 0, w: 0 }; var targetHeadPitch = 0.0; +var targetHeadYaw = 0.0; var basePelvisHeight = 0.0; var pelvisOscillatorPosition = 0.0; @@ -93,8 +95,8 @@ if (botNumber <= 20) { newBodyFilePrefix = "bot" + botNumber; } - newFaceFilePrefix = "ron"; - newBodyFilePrefix = "bot" + 63; +// newFaceFilePrefix = "ron"; +// newBodyFilePrefix = "bot" + 63; // set the face model fst using the bot number // there is no need to change the body model - we're using the default @@ -379,6 +381,10 @@ var JOINT_R_FOREARM = 16; var JOINT_L_ARM = 39; var JOINT_L_FOREARM = 40; var JOINT_SPINE = 11; +var JOINT_R_FOOT = 3; +var JOINT_L_FOOT = 8; +var JOINT_R_TOE = 4; +var JOINT_L_TOE = 9; // ******************************* Animation Is Defined Below ************************************* @@ -389,15 +395,29 @@ for (var i = 0; i < NUM_FRAMES; i++) { middleAngles[i] = []; } //Joint order for actual joint mappings, should be interleaved R,L,R,L,...S,S,S for R = right, L = left, S = single -var JOINT_ORDER = [JOINT_R_HIP, JOINT_L_HIP, JOINT_R_KNEE, JOINT_L_KNEE, JOINT_R_ARM, JOINT_L_ARM, JOINT_R_FOREARM, JOINT_L_FOREARM, JOINT_SPINE]; - -//Joint indices for joints that are duplicated, such as arms, It must match JOINT_ORDER +var JOINT_ORDER = []; +//*** right / left joints *** var HIP = 0; +JOINT_ORDER.push(JOINT_R_HIP); +JOINT_ORDER.push(JOINT_L_HIP); var KNEE = 1; +JOINT_ORDER.push(JOINT_R_KNEE); +JOINT_ORDER.push(JOINT_L_KNEE); var ARM = 2; +JOINT_ORDER.push(JOINT_R_ARM); +JOINT_ORDER.push(JOINT_L_ARM); var FOREARM = 3; -//Joint indices for single joints +JOINT_ORDER.push(JOINT_R_FOREARM); +JOINT_ORDER.push(JOINT_L_FOREARM); +var FOOT = 4; +JOINT_ORDER.push(JOINT_R_FOOT); +JOINT_ORDER.push(JOINT_L_FOOT); +var TOE = 5; +JOINT_ORDER.push(JOINT_R_TOE); +JOINT_ORDER.push(JOINT_L_TOE); +//*** middle joints *** var SPINE = 0; +JOINT_ORDER.push(JOINT_SPINE); //We have to store the angles so we can invert yaw and roll when making the animation //symmetrical @@ -408,11 +428,15 @@ rightAngles[0][HIP] = [30.0, 0.0, 8.0]; rightAngles[0][KNEE] = [-15.0, 0.0, 0.0]; rightAngles[0][ARM] = [85.0, -25.0, 0.0]; rightAngles[0][FOREARM] = [0.0, 0.0, -15.0]; +rightAngles[0][FOOT] = [0.0, 0.0, 0.0]; +rightAngles[0][TOE] = [0.0, 0.0, 0.0]; leftAngles[0][HIP] = [-15, 0.0, 8.0]; -leftAngles[0][KNEE] = [-28, 0.0, 0.0]; +leftAngles[0][KNEE] = [-26, 0.0, 0.0]; leftAngles[0][ARM] = [85.0, 20.0, 0.0]; leftAngles[0][FOREARM] = [10.0, 0.0, -25.0]; +leftAngles[0][FOOT] = [-13.0, 0.0, 0.0]; +leftAngles[0][TOE] = [34.0, 0.0, 0.0]; middleAngles[0][SPINE] = [0.0, -15.0, 5.0]; @@ -421,11 +445,15 @@ rightAngles[1][HIP] = [6.0, 0.0, 8.0]; rightAngles[1][KNEE] = [-12.0, 0.0, 0.0]; rightAngles[1][ARM] = [85.0, 0.0, 0.0]; rightAngles[1][FOREARM] = [0.0, 0.0, -15.0]; +rightAngles[1][FOOT] = [6.0, -8.0, 0.0]; +rightAngles[1][TOE] = [0.0, 0.0, 0.0]; leftAngles[1][HIP] = [10.0, 0.0, 8.0]; -leftAngles[1][KNEE] = [-55.0, 0.0, 0.0]; +leftAngles[1][KNEE] = [-60.0, 0.0, 0.0]; leftAngles[1][ARM] = [85.0, 0.0, 0.0]; leftAngles[1][FOREARM] = [0.0, 0.0, -15.0]; +leftAngles[1][FOOT] = [0.0, 0.0, 0.0]; +leftAngles[1][TOE] = [0.0, 0.0, 0.0]; middleAngles[1][SPINE] = [0.0, 0.0, 0.0]; @@ -444,11 +472,15 @@ rightQuats[HIP] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 7.0); rightQuats[KNEE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); rightQuats[ARM] = Quat.fromPitchYawRollDegrees(85.0, 0.0, 0.0); rightQuats[FOREARM] = Quat.fromPitchYawRollDegrees(0.0, 0.0, -10.0); +rightQuats[FOOT] = Quat.fromPitchYawRollDegrees(0.0, -8.0, 0.0); +rightQuats[TOE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); leftQuats[HIP] = Quat.fromPitchYawRollDegrees(0, 0.0, -7.0); leftQuats[KNEE] = Quat.fromPitchYawRollDegrees(0, 0.0, 0.0); leftQuats[ARM] = Quat.fromPitchYawRollDegrees(85.0, 0.0, 0.0); leftQuats[FOREARM] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 10.0); +leftQuats[FOOT] = Quat.fromPitchYawRollDegrees(0.0, 8.0, 0.0); +leftQuats[TOE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); middleQuats[SPINE] = Quat.fromPitchYawRollDegrees(0.0, 0.0, 0.0); @@ -477,12 +509,12 @@ function handleAnimation(deltaTime) { setRandomExpression(); } - if (avatarVelocity == 0.0) { - walkTime = 0.0; - currentFrame = 0; - } else { - walkTime += avatarVelocity * deltaTime; - if (walkTime > walkWheelRate) { + if (avatarVelocity == 0.0) { + walkTime = 0.0; + currentFrame = 0; + } else { + walkTime += avatarVelocity * deltaTime; + if (walkTime > walkWheelRate) { walkTime = 0.0; currentFrame++; if (currentFrame % 2 == 1) { @@ -493,6 +525,7 @@ function handleAnimation(deltaTime) { } } } + var frame = walkKeyFrames[currentFrame]; var walkInterp = walkTime / walkWheelRate; @@ -536,10 +569,13 @@ var wasMovingLastFrame = false; function handleHeadTurn() { if (!isTurningHead && (Math.random() < CHANCE_OF_HEAD_TURNING)) { targetHeadPitch = getRandomFloat(-PITCH_RANGE, PITCH_RANGE); + targetHeadYaw = getRandomFloat(-YAW_RANGE, YAW_RANGE); isTurningHead = true; } else { - Avatar.headPitch = Avatar.headPitch + (targetHeadPitch - Avatar.headPitch) * PITCH_RATE; - if (Math.abs(Avatar.headPitch - targetHeadPitch) < STOP_TOLERANCE) { + Avatar.headPitch = Avatar.headPitch + (targetHeadPitch - Avatar.headPitch) * HEAD_TURN_RATE; + Avatar.headYaw = Avatar.headYaw + (targetHeadYaw - Avatar.headYaw) * HEAD_TURN_RATE; + if (Math.abs(Avatar.headPitch - targetHeadPitch) < STOP_TOLERANCE && + Math.abs(Avatar.headYaw - targetHeadYaw) < STOP_TOLERANCE) { isTurningHead = false; } } diff --git a/libraries/avatars/src/AvatarData.h b/libraries/avatars/src/AvatarData.h index 008aecc817..8533b8b0e8 100755 --- a/libraries/avatars/src/AvatarData.h +++ b/libraries/avatars/src/AvatarData.h @@ -120,6 +120,8 @@ class AvatarData : public QObject { Q_PROPERTY(glm::quat orientation READ getOrientation WRITE setOrientation) Q_PROPERTY(glm::quat headOrientation READ getHeadOrientation WRITE setHeadOrientation) Q_PROPERTY(float headPitch READ getHeadPitch WRITE setHeadPitch) + Q_PROPERTY(float headYaw READ getHeadYaw WRITE setHeadYaw) + Q_PROPERTY(float headRoll READ getHeadRoll WRITE setHeadRoll) Q_PROPERTY(float audioLoudness READ getAudioLoudness WRITE setAudioLoudness) Q_PROPERTY(float audioAverageLoudness READ getAudioAverageLoudness WRITE setAudioAverageLoudness) @@ -171,7 +173,13 @@ public: // access to Head().set/getMousePitch (degrees) float getHeadPitch() const { return _headData->getBasePitch(); } - void setHeadPitch(float value) { _headData->setBasePitch(value); }; + void setHeadPitch(float value) { _headData->setBasePitch(value); } + + float getHeadYaw() const { return _headData->getBaseYaw(); } + void setHeadYaw(float value) { _headData->setBaseYaw(value); } + + float getHeadRoll() const { return _headData->getBaseRoll(); } + void setHeadRoll(float value) { _headData->setBaseRoll(value); } // access to Head().set/getAverageLoudness float getAudioLoudness() const { return _headData->getAudioLoudness(); } From 8bb5c2b84c1b04070adf280d95ae5273ae8cc9b3 Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Wed, 30 Jul 2014 16:59:15 -0700 Subject: [PATCH 09/11] Small improvements to script, renamed to bot_procedural --- .../{proceduralBot.js => bot_procedural.js.} | 39 ++++++------------- 1 file changed, 11 insertions(+), 28 deletions(-) rename examples/{proceduralBot.js => bot_procedural.js.} (96%) diff --git a/examples/proceduralBot.js b/examples/bot_procedural.js. similarity index 96% rename from examples/proceduralBot.js rename to examples/bot_procedural.js. index 9b10b28243..17e54007cc 100644 --- a/examples/proceduralBot.js +++ b/examples/bot_procedural.js. @@ -27,8 +27,8 @@ function printVector(string, vector) { print(string + " " + vector.x + ", " + vector.y + ", " + vector.z); } -var CHANCE_OF_MOVING = 0.000; -var CHANCE_OF_SOUND = 0;//0.005; +var CHANCE_OF_MOVING = 0.005; +var CHANCE_OF_SOUND = 0.005; var CHANCE_OF_HEAD_TURNING = 0.01; var CHANCE_OF_BIG_MOVE = 1.0; @@ -40,11 +40,11 @@ var X_MIN = 0.50; var X_MAX = 15.60; var Z_MIN = 0.50; var Z_MAX = 15.10; -var Y_PELVIS = 1.0; +var Y_FEET = 0.0; +var AVATAR_PELVIS_HEIGHT = 0.84; +var Y_PELVIS = Y_FEET + AVATAR_PELVIS_HEIGHT; var MAX_PELVIS_DELTA = 2.5; -var AVATAR_PELVIS_HEIGHT = 0.75; - var MOVE_RANGE_SMALL = 3.0; var MOVE_RANGE_BIG = 10.0; var TURN_RANGE = 70.0; @@ -71,32 +71,15 @@ function clamp(val, min, max){ return Math.max(min, Math.min(max, val)) } -// pick an integer between 1 and 100 that is not 28 for the face model for this bot -botNumber = 28; +//Array of all valid bot numbers +var validBotNumbers = []; -while (botNumber == 28) { - botNumber = getRandomInt(1, 100); -} +// right now we only use bot 63, since many other bots have messed up skeletons and LOD issues +var botNumber = 63;//getRandomInt(0, 99); -if (botNumber <= 20) { - newFaceFilePrefix = "ron"; - newBodyFilePrefix = "defaultAvatar_body" -} else { - if (botNumber <= 40) { - newFaceFilePrefix = "superhero"; - } else if (botNumber <= 60) { - newFaceFilePrefix = "amber"; - } else if (botNumber <= 80) { - newFaceFilePrefix = "ron"; - } else { - newFaceFilePrefix = "angie"; - } +var newFaceFilePrefix = "ron"; - newBodyFilePrefix = "bot" + botNumber; -} - -// newFaceFilePrefix = "ron"; -// newBodyFilePrefix = "bot" + 63; +var newBodyFilePrefix = "bot" + botNumber; // set the face model fst using the bot number // there is no need to change the body model - we're using the default From b6c0b4fd218543a7356a6d4c49ba6aa2a2b81477 Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Wed, 30 Jul 2014 17:39:24 -0700 Subject: [PATCH 10/11] Random mood in procedural bot --- examples/bot_procedural.js. | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/examples/bot_procedural.js. b/examples/bot_procedural.js. index 17e54007cc..00c2829924 100644 --- a/examples/bot_procedural.js. +++ b/examples/bot_procedural.js. @@ -1,5 +1,5 @@ // -// proceduralBot.js +// bot_procedural.js // hifi // // Created by Ben Arnold on 7/29/2013 @@ -154,7 +154,18 @@ function addBlendshapeToPose(pose, shapeIndex, val) { pose.blendShapes[index] = {shapeIndex: shapeIndex, val: val }; } //The mood of the avatar, determines face. 0 = happy, 1 = angry, 2 = sad. -var avatarMood = 0; + +//Randomly pick avatar mood. 80% happy, 10% mad 10% sad +var randMood = Math.floor(Math.random() * 11); +var avatarMood; +if (randMood == 0) { + avatarMood = 1; +} else if (randMood == 2) { + avatarMood = 2; +} else { + avatarMood = 0; +} + var currentExpression = -1; //Face pose constructor var happyPoses = []; From a65f81c71aa26cfeb0f49a6721cf47d0707d09ed Mon Sep 17 00:00:00 2001 From: barnold1953 Date: Wed, 30 Jul 2014 18:25:58 -0700 Subject: [PATCH 11/11] Random start position for bot --- examples/bot_procedural.js. | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/bot_procedural.js. b/examples/bot_procedural.js. index 00c2829924..265b887e0a 100644 --- a/examples/bot_procedural.js. +++ b/examples/bot_procedural.js. @@ -55,8 +55,7 @@ var HEAD_TURN_RATE = 0.05; var PITCH_RANGE = 15.0; var YAW_RANGE = 35.0; -//var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; -var firstPosition = { x: 0.5, y: Y_PELVIS, z: 0.5 }; +var firstPosition = { x: getRandomFloat(X_MIN, X_MAX), y: Y_PELVIS, z: getRandomFloat(Z_MIN, Z_MAX) }; var targetPosition = { x: 0, y: 0, z: 0 }; var targetOrientation = { x: 0, y: 0, z: 0, w: 0 }; var currentOrientation = { x: 0, y: 0, z: 0, w: 0 };