From 61198a658c3a43da82e2bb0264b958cb5bd87e0d Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 4 Aug 2015 17:04:15 -0700 Subject: [PATCH 1/5] Smooth fading of animations in and out. Also, turn on the secret rig animations from Javascript with MyAvatar.setEnableRigAnimations(true). (persists) --- interface/src/avatar/MyAvatar.cpp | 6 + interface/src/avatar/MyAvatar.h | 1 + libraries/animation/src/AnimationHandle.cpp | 10 +- libraries/animation/src/AnimationHandle.h | 12 +- libraries/animation/src/Rig.cpp | 161 +++++++++++++++----- libraries/animation/src/Rig.h | 7 +- 6 files changed, 148 insertions(+), 49 deletions(-) diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index 0c01ce0249..b30fa7096e 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -625,6 +625,12 @@ float loadSetting(QSettings& settings, const char* name, float defaultValue) { return value; } +void MyAvatar::setEnableRigAnimations(bool isEnabled) { + Settings settings; + settings.setValue("enableRig", isEnabled); + _rig->setEnableRig(isEnabled); +} + void MyAvatar::loadData() { Settings settings; settings.beginGroup("Avatar"); diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 129a05f93b..7eea803d39 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -72,6 +72,7 @@ public: Q_INVOKABLE AnimationDetails getAnimationDetailsByRole(const QString& role); Q_INVOKABLE AnimationDetails getAnimationDetails(const QString& url); void clearJointAnimationPriorities(); + Q_INVOKABLE void setEnableRigAnimations(bool isEnabled); // get/set avatar data void saveData(); diff --git a/libraries/animation/src/AnimationHandle.cpp b/libraries/animation/src/AnimationHandle.cpp index 605fb25f1c..ebf0e29b97 100644 --- a/libraries/animation/src/AnimationHandle.cpp +++ b/libraries/animation/src/AnimationHandle.cpp @@ -49,7 +49,7 @@ void AnimationHandle::setMaskedJoints(const QStringList& maskedJoints) { _jointMappings.clear(); } -void AnimationHandle::setRunning(bool running) { +void AnimationHandle::setRunning(bool running, bool doRestoreJoints) { if (running && isRunning()) { // if we're already running, this is the same as a restart setFrameIndex(getFirstFrame()); @@ -62,7 +62,9 @@ void AnimationHandle::setRunning(bool running) { } } else { _rig->removeRunningAnimation(getAnimationHandlePointer()); - restoreJoints(); + if (doRestoreJoints) { + restoreJoints(); + } replaceMatchingPriorities(0.0f); } emit runningChanged(isRunning()); @@ -71,7 +73,9 @@ void AnimationHandle::setRunning(bool running) { AnimationHandle::AnimationHandle(RigPointer rig) : QObject(rig.get()), _rig(rig), - _priority(1.0f) + _priority(1.0f), + _fade(0.0f), + _fadePerSecond(0.0f) { } diff --git a/libraries/animation/src/AnimationHandle.h b/libraries/animation/src/AnimationHandle.h index 42e564944e..0d941da388 100644 --- a/libraries/animation/src/AnimationHandle.h +++ b/libraries/animation/src/AnimationHandle.h @@ -64,6 +64,10 @@ public: void setPriority(float priority); float getPriority() const { return _priority; } void setMix(float mix) { _mix = mix; } + void setFade(float fade) { _fade = fade; } + float getFade() { return _fade; } + void setFadePerSecond(float fadePerSecond) { _fadePerSecond = fadePerSecond; } + float getFadePerSecond() { return _fadePerSecond; } void setMaskedJoints(const QStringList& maskedJoints); const QStringList& getMaskedJoints() const { return _maskedJoints; } @@ -87,7 +91,7 @@ public: void setLastFrame(float lastFrame) { _animationLoop.setLastFrame(lastFrame); } float getLastFrame() const { return _animationLoop.getLastFrame(); } - void setRunning(bool running); + void setRunning(bool running, bool restoreJoints = true); bool isRunning() const { return _animationLoop.isRunning(); } void setFrameIndex(float frameIndex) { _animationLoop.setFrameIndex(frameIndex); } @@ -111,7 +115,7 @@ signals: public slots: void start() { setRunning(true); } - void stop() { setRunning(false); } + void stop() { setRunning(false); _fadePerSecond = _fade = 0.0f; } private: @@ -120,7 +124,9 @@ private: QString _role; QUrl _url; float _priority; - float _mix; + float _mix; // How much of this animation to blend against what is already there. 1.0 sets to just this animation. + float _fade; // How far are we into full strength. 0.0 uses none of this animation, 1.0 (the max) is as much as possible. + float _fadePerSecond; // How fast should _fade change? +1.0 means _fade is increasing to 1.0 in 1 second. Negative is fading out. QStringList _maskedJoints; QVector _jointMappings; diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 8406b61d12..2ca961b65b 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -77,8 +77,8 @@ void Rig::startAnimation(const QString& url, float fps, float priority, handle->start(); } -void Rig::addAnimationByRole(const QString& role, const QString& url, float fps, float priority, - bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints, bool startAutomatically) { +AnimationHandlePointer Rig::addAnimationByRole(const QString& role, const QString& url, float fps, float priority, + bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints, bool startAutomatically) { // check for a configured animation for the role //qCDebug(animation) << "addAnimationByRole" << role << url << fps << priority << loop << hold << firstFrame << lastFrame << maskedJoints << startAutomatically; foreach (const AnimationHandlePointer& candidate, _animationHandles) { @@ -86,12 +86,38 @@ void Rig::addAnimationByRole(const QString& role, const QString& url, float fps, if (startAutomatically) { candidate->start(); } - return; + return candidate; } } AnimationHandlePointer handle = createAnimationHandle(); + QString standard = ""; + if (url.isEmpty()) { // Default animations for fight club + const QString& base = "https://hifi-public.s3.amazonaws.com/ozan/"; + if (role == "walk") { + standard = base + "support/FightClubBotTest1/Animations/standard_walk.fbx"; + lastFrame = 60; + } else if (role == "leftTurn") { + standard = base + "support/FightClubBotTest1/Animations/left_turn_noHipRotation.fbx"; + lastFrame = 29; + } else if (role == "rightTurn") { + standard = base + "support/FightClubBotTest1/Animations/right_turn_noHipRotation.fbx"; + lastFrame = 31; + } else if (role == "leftStrafe") { + standard = base + "animations/fightclub_bot_anims/side_step_left_inPlace.fbx"; + lastFrame = 31; + } else if (role == "rightStrafe") { + standard = base + "animations/fightclub_bot_anims/side_step_right_inPlace.fbx"; + lastFrame = 31; + } else if (role == "idle") { + standard = base + "support/FightClubBotTest1/Animations/standard_idle.fbx"; + fps = 25.0f; + } + if (!standard.isEmpty()) { + loop = true; + } + } handle->setRole(role); - handle->setURL(url); + handle->setURL(url.isEmpty() ? standard : url); handle->setFPS(fps); handle->setPriority(priority); handle->setLoop(loop); @@ -102,16 +128,18 @@ void Rig::addAnimationByRole(const QString& role, const QString& url, float fps, if (startAutomatically) { handle->start(); } + return handle; } void Rig::startAnimationByRole(const QString& role, const QString& url, float fps, float priority, bool loop, bool hold, float firstFrame, float lastFrame, const QStringList& maskedJoints) { - addAnimationByRole(role, url, fps, priority, loop, hold, firstFrame, lastFrame, maskedJoints, true); + AnimationHandlePointer handle = addAnimationByRole(role, url, fps, priority, loop, hold, firstFrame, lastFrame, maskedJoints, true); + handle->setFadePerSecond(1.0f); // For now. Could be individualized later. } void Rig::stopAnimationByRole(const QString& role) { foreach (const AnimationHandlePointer& handle, getRunningAnimations()) { if (handle->getRole() == role) { - handle->stop(); + handle->setFadePerSecond(-1.0f); // For now. Could be individualized later. } } } @@ -135,6 +163,14 @@ void Rig::addRunningAnimation(AnimationHandlePointer animationHandle) { bool Rig::isRunningAnimation(AnimationHandlePointer animationHandle) { return _runningAnimations.contains(animationHandle); } +bool Rig::isRunningRole(const QString& role) { //obviously, there are more efficient ways to do this + for (auto animation : _runningAnimations) { + if ((animation->getRole() == role) && (animation->getFadePerSecond() >= 0.0f)) { // Don't count those being faded out + return true; + } + } + return false; +} void Rig::deleteAnimations() { for (auto animation : _animationHandles) { @@ -356,47 +392,92 @@ glm::mat4 Rig::getJointVisibleTransform(int jointIndex) const { } void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { - if (_enableRig) { - glm::vec3 front = worldRotation * IDENTITY_FRONT; - float forwardSpeed = glm::dot(worldVelocity, front); - float rotationalSpeed = glm::angle(front, _lastFront) / deltaTime; - bool isWalking = std::abs(forwardSpeed) > 0.01f; - bool isTurning = std::abs(rotationalSpeed) > 0.5f; - - // Crude, until we have blending: - isTurning = isTurning && !isWalking; // Only one of walk/turn, walk wins. - isTurning = false; // FIXME - bool isIdle = !isWalking && !isTurning; - auto singleRole = [](bool walking, bool turning, bool idling) { - return walking ? "walk" : (turning ? "turn" : (idling ? "idle" : "")); - }; - QString toStop = singleRole(_isWalking && !isWalking, _isTurning && !isTurning, _isIdle && !isIdle); - if (!toStop.isEmpty()) { - //qCDebug(animation) << "isTurning" << isTurning << "fronts" << front << _lastFront << glm::angle(front, _lastFront) << rotationalSpeed; - stopAnimationByRole(toStop); - } - QString newRole = singleRole(isWalking && !_isWalking, isTurning && !_isTurning, isIdle && !_isIdle); - if (!newRole.isEmpty()) { - startAnimationByRole(newRole); - qCDebug(animation) << deltaTime << ":" << worldVelocity << "." << front << "=> " << forwardSpeed << newRole; - } - - _lastPosition = worldPosition; - _lastFront = front; - _isWalking = isWalking; - _isTurning = isTurning; - _isIdle = isIdle; + if (!_enableRig) { + return; } + bool isMoving = false; + glm::vec3 front = worldRotation * IDENTITY_FRONT; + float forwardSpeed = glm::dot(worldVelocity, front); + float rightLateralSpeed = glm::dot(worldVelocity, worldRotation * IDENTITY_RIGHT); + float rightTurningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; + auto updateRole = [&](const QString& role, bool isOn) { + isMoving = isMoving || isOn; + if (isOn) { + if (!isRunningRole(role)) { + qCDebug(animation) << "Rig STARTING" << role; + startAnimationByRole(role); + } + } else { + if (isRunningRole(role)) { + qCDebug(animation) << "Rig stopping" << role; + stopAnimationByRole(role); + } + } + }; + updateRole("walk", std::abs(forwardSpeed) > 0.01f); + bool isTurning = std::abs(rightTurningSpeed) > 0.5f; + updateRole("rightTurn", isTurning && (rightTurningSpeed > 0)); + updateRole("leftTurn", isTurning && (rightTurningSpeed < 0)); + bool isStrafing = std::abs(rightLateralSpeed) > 0.01f; + updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f)); + updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f)); + updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus. + _lastFront = front; + _lastPosition = worldPosition; } void Rig::updateAnimations(float deltaTime, glm::mat4 parentTransform) { - int nAnimationsSoFar = 0; + + // First normalize the fades so that they sum to 1.0. + // update the fade data in each animation (not normalized as they are an independent propert of animation) foreach (const AnimationHandlePointer& handle, _runningAnimations) { - handle->setMix(1.0f / ++nAnimationsSoFar); - handle->setPriority(1.0); + float fadePerSecond = handle->getFadePerSecond(); + float fade = handle->getFade(); + if (fadePerSecond != 0.0f) { + fade += fadePerSecond * deltaTime; + if ((0.0f >= fade) || (fade >= 1.0f)) { + fade = glm::clamp(fade, 0.0f, 1.0f); + handle->setFadePerSecond(0.0f); + } + handle->setFade(fade); + if (fade <= 0.0f) { // stop any finished animations now + handle->setRunning(false, false); // but do not restore joints as it causes a flicker + } + } + } + // collect the remaining fade data + float fadeSum = 0.0f; + float normalizedFades[_runningAnimations.count()]; + int animationIndex = 0; + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + float fade = handle->getFade(); + normalizedFades[animationIndex++] = fade; + fadeSum += fade; + } + // normalize our copy of the fade data + for (int i = 0; i < _runningAnimations.count(); i++) { + normalizedFades[i] /= fadeSum; + } + // set the mix based on the normalized fade data + animationIndex = 0; + fadeSum = 0.0f; + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + handle->setPriority(1.0f); + float normalizedFade = normalizedFades[animationIndex++]; + // simulate() will blend each animation result into the result so far, based on the pairwise mix at at each step. + // i.e., slerp the 'mix' distance from the result so far towards this iteration's animation result. + // The formula here for mix is based on the idea that, at each step: + // fadeSum is to normalizedFade, as (1 - mix) is to mix + // i.e., fadeSum/normalizedFade = (1 - mix)/mix + // Then we solve for mix. + // Sanity check: For the first animation, fadeSum = 0, and the mix will always be 1. + // Sanity check: For equal blending, the formula is equivalent to mix = 1 / nAnimationsSoFar++ + float mix = 1.0f / ((fadeSum / normalizedFade) + 1.0f); + fadeSum += normalizedFade; + handle->setMix(mix); handle->simulate(deltaTime); } - + for (int i = 0; i < _jointStates.size(); i++) { updateJointState(i, parentTransform); } diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 52db16826a..c704a20537 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -76,6 +76,7 @@ public: bool removeRunningAnimation(AnimationHandlePointer animationHandle); void addRunningAnimation(AnimationHandlePointer animationHandle); bool isRunningAnimation(AnimationHandlePointer animationHandle); + bool isRunningRole(const QString& role); const QList& getRunningAnimations() const { return _runningAnimations; } void deleteAnimations(); const QList& getAnimationHandles() const { return _animationHandles; } @@ -86,9 +87,9 @@ public: float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList()); void stopAnimationByRole(const QString& role); - void addAnimationByRole(const QString& role, const QString& url = QString(), float fps = 30.0f, - float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, - float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList(), bool startAutomatically = false); + AnimationHandlePointer addAnimationByRole(const QString& role, const QString& url = QString(), float fps = 30.0f, + float priority = 1.0f, bool loop = false, bool hold = false, float firstFrame = 0.0f, + float lastFrame = FLT_MAX, const QStringList& maskedJoints = QStringList(), bool startAutomatically = false); float initJointStates(QVector states, glm::mat4 parentTransform); bool jointStatesEmpty() { return _jointStates.isEmpty(); }; From a1f86cc7b8d7de6b158a731d6d53e2cad37aa774 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Tue, 4 Aug 2015 19:49:12 -0700 Subject: [PATCH 2/5] Make MSVC happy. --- libraries/animation/src/Rig.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index fb365fd349..c187121827 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -450,7 +450,7 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 parentTransform) { } // collect the remaining fade data float fadeSum = 0.0f; - float normalizedFades[_runningAnimations.count()]; + std::vector normalizedFades(_runningAnimations.count()); int animationIndex = 0; foreach (const AnimationHandlePointer& handle, _runningAnimations) { float fade = handle->getFade(); From 653e46fdd3b8ce99a50e8620aa65fc6618e66085 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 5 Aug 2015 10:51:26 -0700 Subject: [PATCH 3/5] const methods. --- libraries/animation/src/AnimationHandle.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/animation/src/AnimationHandle.h b/libraries/animation/src/AnimationHandle.h index 0d941da388..65f7423aa5 100644 --- a/libraries/animation/src/AnimationHandle.h +++ b/libraries/animation/src/AnimationHandle.h @@ -65,9 +65,9 @@ public: float getPriority() const { return _priority; } void setMix(float mix) { _mix = mix; } void setFade(float fade) { _fade = fade; } - float getFade() { return _fade; } + float getFade() const { return _fade; } void setFadePerSecond(float fadePerSecond) { _fadePerSecond = fadePerSecond; } - float getFadePerSecond() { return _fadePerSecond; } + float getFadePerSecond() const { return _fadePerSecond; } void setMaskedJoints(const QStringList& maskedJoints); const QStringList& getMaskedJoints() const { return _maskedJoints; } From f51b5be167c3744b060bf4d9a8211b2045c15102 Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 5 Aug 2015 11:00:40 -0700 Subject: [PATCH 4/5] Simplify fade normalization, eliminating a loop. --- libraries/animation/src/Rig.cpp | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index c01e60abd9..43b2892cbf 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -464,35 +464,25 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 parentTransform) { } } } - // collect the remaining fade data - float fadeSum = 0.0f; - std::vector normalizedFades(_runningAnimations.count()); - int animationIndex = 0; + // sum the remaining fade data + float fadeTotal = 0.0f; foreach (const AnimationHandlePointer& handle, _runningAnimations) { - float fade = handle->getFade(); - normalizedFades[animationIndex++] = fade; - fadeSum += fade; + fadeTotal += handle->getFade(); } - // normalize our copy of the fade data - for (int i = 0; i < _runningAnimations.count(); i++) { - normalizedFades[i] /= fadeSum; - } - // set the mix based on the normalized fade data - animationIndex = 0; - fadeSum = 0.0f; + float fadeSumSoFar = 0.0f; foreach (const AnimationHandlePointer& handle, _runningAnimations) { handle->setPriority(1.0f); - float normalizedFade = normalizedFades[animationIndex++]; + float normalizedFade = handle->getFade() / fadeTotal; // simulate() will blend each animation result into the result so far, based on the pairwise mix at at each step. // i.e., slerp the 'mix' distance from the result so far towards this iteration's animation result. // The formula here for mix is based on the idea that, at each step: // fadeSum is to normalizedFade, as (1 - mix) is to mix - // i.e., fadeSum/normalizedFade = (1 - mix)/mix + // i.e., fadeSumSoFar/normalizedFade = (1 - mix)/mix // Then we solve for mix. // Sanity check: For the first animation, fadeSum = 0, and the mix will always be 1. // Sanity check: For equal blending, the formula is equivalent to mix = 1 / nAnimationsSoFar++ - float mix = 1.0f / ((fadeSum / normalizedFade) + 1.0f); - fadeSum += normalizedFade; + float mix = 1.0f / ((fadeSumSoFar / normalizedFade) + 1.0f); + fadeSumSoFar += normalizedFade; handle->setMix(mix); handle->simulate(deltaTime); } From bb974edf7f5d74d2bb8a418e869f727fffb8b0ec Mon Sep 17 00:00:00 2001 From: Howard Stearns Date: Wed, 5 Aug 2015 11:09:04 -0700 Subject: [PATCH 5/5] Add assert on mix range. --- libraries/animation/src/Rig.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 43b2892cbf..ec34e90b5c 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -482,6 +482,7 @@ void Rig::updateAnimations(float deltaTime, glm::mat4 parentTransform) { // Sanity check: For the first animation, fadeSum = 0, and the mix will always be 1. // Sanity check: For equal blending, the formula is equivalent to mix = 1 / nAnimationsSoFar++ float mix = 1.0f / ((fadeSumSoFar / normalizedFade) + 1.0f); + assert((0.0f <= mix) && (mix <= 1.0f)); fadeSumSoFar += normalizedFade; handle->setMix(mix); handle->simulate(deltaTime);