From 3716800b98911235074833134eed22fd8939544f Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Wed, 14 Oct 2015 18:16:13 -0700 Subject: [PATCH 1/9] Moved AnimPose class into it's own set of files --- libraries/animation/src/AnimPose.cpp | 56 ++++++++++++++++++++++++ libraries/animation/src/AnimPose.h | 47 ++++++++++++++++++++ libraries/animation/src/AnimSkeleton.cpp | 44 ------------------- libraries/animation/src/AnimSkeleton.h | 28 +----------- 4 files changed, 104 insertions(+), 71 deletions(-) create mode 100644 libraries/animation/src/AnimPose.cpp create mode 100644 libraries/animation/src/AnimPose.h diff --git a/libraries/animation/src/AnimPose.cpp b/libraries/animation/src/AnimPose.cpp new file mode 100644 index 0000000000..bae34509ae --- /dev/null +++ b/libraries/animation/src/AnimPose.cpp @@ -0,0 +1,56 @@ +// +// AnimPose.cpp +// +// Created by Anthony J. Thibault on 10/14/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimPose.h" +#include "GLMHelpers.h" + +const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), + glm::quat(), + glm::vec3(0.0f)); + +AnimPose::AnimPose(const glm::mat4& mat) { + scale = extractScale(mat); + rot = glm::normalize(glm::quat_cast(mat)); + trans = extractTranslation(mat); +} + +glm::vec3 AnimPose::operator*(const glm::vec3& rhs) const { + return trans + (rot * (scale * rhs)); +} + +glm::vec3 AnimPose::xformPoint(const glm::vec3& rhs) const { + return *this * rhs; +} + +// really slow +glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { + glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f); + glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f); + glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z); + glm::mat3 mat(xAxis, yAxis, zAxis); + glm::mat3 transInvMat = glm::inverse(glm::transpose(mat)); + return transInvMat * rhs; +} + +AnimPose AnimPose::operator*(const AnimPose& rhs) const { + return AnimPose(static_cast(*this) * static_cast(rhs)); +} + +AnimPose AnimPose::inverse() const { + return AnimPose(glm::inverse(static_cast(*this))); +} + +AnimPose::operator glm::mat4() const { + glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f); + glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f); + glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z); + return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), + glm::vec4(zAxis, 0.0f), glm::vec4(trans, 1.0f)); +} diff --git a/libraries/animation/src/AnimPose.h b/libraries/animation/src/AnimPose.h new file mode 100644 index 0000000000..852d84ec1b --- /dev/null +++ b/libraries/animation/src/AnimPose.h @@ -0,0 +1,47 @@ +// +// AnimPose.h +// +// Created by Anthony J. Thibault on 10/14/15. +// Copyright (c) 2015 High Fidelity, Inc. All rights reserved. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#ifndef hifi_AnimPose +#define hifi_AnimPose + +#include +#include +#include +#include +#include + +struct AnimPose { + AnimPose() {} + explicit AnimPose(const glm::mat4& mat); + AnimPose(const glm::vec3& scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : scale(scaleIn), rot(rotIn), trans(transIn) {} + static const AnimPose identity; + + glm::vec3 xformPoint(const glm::vec3& rhs) const; + glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow + + glm::vec3 operator*(const glm::vec3& rhs) const; // same as xformPoint + AnimPose operator*(const AnimPose& rhs) const; + + AnimPose inverse() const; + operator glm::mat4() const; + + glm::vec3 scale; + glm::quat rot; + glm::vec3 trans; +}; + +inline QDebug operator<<(QDebug debug, const AnimPose& pose) { + debug << "AnimPose, trans = (" << pose.trans.x << pose.trans.y << pose.trans.z << "), rot = (" << pose.rot.x << pose.rot.y << pose.rot.z << pose.rot.w << "), scale = (" << pose.scale.x << pose.scale.y << pose.scale.z << ")"; + return debug; +} + +using AnimPoseVec = std::vector; + +#endif diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp index e43a55150c..0db7473c9c 100644 --- a/libraries/animation/src/AnimSkeleton.cpp +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -16,50 +16,6 @@ #include "AnimationLogging.h" -const AnimPose AnimPose::identity = AnimPose(glm::vec3(1.0f), - glm::quat(), - glm::vec3(0.0f)); - -AnimPose::AnimPose(const glm::mat4& mat) { - scale = extractScale(mat); - rot = glm::normalize(glm::quat_cast(mat)); - trans = extractTranslation(mat); -} - -glm::vec3 AnimPose::operator*(const glm::vec3& rhs) const { - return trans + (rot * (scale * rhs)); -} - -glm::vec3 AnimPose::xformPoint(const glm::vec3& rhs) const { - return *this * rhs; -} - -// really slow -glm::vec3 AnimPose::xformVector(const glm::vec3& rhs) const { - glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f); - glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f); - glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z); - glm::mat3 mat(xAxis, yAxis, zAxis); - glm::mat3 transInvMat = glm::inverse(glm::transpose(mat)); - return transInvMat * rhs; -} - -AnimPose AnimPose::operator*(const AnimPose& rhs) const { - return AnimPose(static_cast(*this) * static_cast(rhs)); -} - -AnimPose AnimPose::inverse() const { - return AnimPose(glm::inverse(static_cast(*this))); -} - -AnimPose::operator glm::mat4() const { - glm::vec3 xAxis = rot * glm::vec3(scale.x, 0.0f, 0.0f); - glm::vec3 yAxis = rot * glm::vec3(0.0f, scale.y, 0.0f); - glm::vec3 zAxis = rot * glm::vec3(0.0f, 0.0f, scale.z); - return glm::mat4(glm::vec4(xAxis, 0.0f), glm::vec4(yAxis, 0.0f), - glm::vec4(zAxis, 0.0f), glm::vec4(trans, 1.0f)); -} - AnimSkeleton::AnimSkeleton(const FBXGeometry& fbxGeometry) { // convert to std::vector of joints std::vector joints; diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h index d59719df73..9dda313528 100644 --- a/libraries/animation/src/AnimSkeleton.h +++ b/libraries/animation/src/AnimSkeleton.h @@ -16,33 +16,7 @@ #include #include - -struct AnimPose { - AnimPose() {} - explicit AnimPose(const glm::mat4& mat); - AnimPose(const glm::vec3& scaleIn, const glm::quat& rotIn, const glm::vec3& transIn) : scale(scaleIn), rot(rotIn), trans(transIn) {} - static const AnimPose identity; - - glm::vec3 xformPoint(const glm::vec3& rhs) const; - glm::vec3 xformVector(const glm::vec3& rhs) const; // really slow - - glm::vec3 operator*(const glm::vec3& rhs) const; // same as xformPoint - AnimPose operator*(const AnimPose& rhs) const; - - AnimPose inverse() const; - operator glm::mat4() const; - - glm::vec3 scale; - glm::quat rot; - glm::vec3 trans; -}; - -inline QDebug operator<<(QDebug debug, const AnimPose& pose) { - debug << "AnimPose, trans = (" << pose.trans.x << pose.trans.y << pose.trans.z << "), rot = (" << pose.rot.x << pose.rot.y << pose.rot.z << pose.rot.w << "), scale = (" << pose.scale.x << pose.scale.y << pose.scale.z << ")"; - return debug; -} - -using AnimPoseVec = std::vector; +#include "AnimPose.h" class AnimSkeleton { public: From 2b4788929f6cb7e8c5b5944e69415a0b9d73ac42 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Thu, 15 Oct 2015 17:57:06 -0700 Subject: [PATCH 2/9] AnimBlendLinear: Untested implementation of sync flag. Move accumulateTime into AnimUtil. --- .../defaultAvatar_full/avatar-animation.json | 2 + libraries/animation/src/AnimBlendLinear.cpp | 86 ++++++++++++++++--- libraries/animation/src/AnimBlendLinear.h | 15 +++- libraries/animation/src/AnimClip.cpp | 38 +------- libraries/animation/src/AnimClip.h | 4 +- libraries/animation/src/AnimNode.h | 4 +- libraries/animation/src/AnimNodeLoader.cpp | 3 +- libraries/animation/src/AnimUtil.cpp | 39 +++++++++ libraries/animation/src/AnimUtil.h | 6 +- 9 files changed, 141 insertions(+), 56 deletions(-) diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json index 682e0be1bf..5b042a09b1 100644 --- a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -190,6 +190,7 @@ "type": "blendLinear", "data": { "alpha": 0.0, + "sync": false, "alphaVar": "rightHandGrabBlend" }, "children": [ @@ -339,6 +340,7 @@ "type": "blendLinear", "data": { "alpha": 0.0, + "sync": false, "alphaVar": "leftHandGrabBlend" }, "children": [ diff --git a/libraries/animation/src/AnimBlendLinear.cpp b/libraries/animation/src/AnimBlendLinear.cpp index bc95565f6f..1e65ba2b36 100644 --- a/libraries/animation/src/AnimBlendLinear.cpp +++ b/libraries/animation/src/AnimBlendLinear.cpp @@ -12,10 +12,12 @@ #include "GLMHelpers.h" #include "AnimationLogging.h" #include "AnimUtil.h" +#include "AnimClip.h" -AnimBlendLinear::AnimBlendLinear(const QString& id, float alpha) : +AnimBlendLinear::AnimBlendLinear(const QString& id, float alpha, bool sync) : AnimNode(AnimNode::Type::BlendLinear, id), - _alpha(alpha) { + _alpha(alpha), + _sync(sync) { } @@ -34,24 +36,19 @@ const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, flo } else if (_children.size() == 1) { _poses = _children[0]->evaluate(animVars, dt, triggersOut); } else { + float clampedAlpha = glm::clamp(_alpha, 0.0f, (float)(_children.size() - 1)); size_t prevPoseIndex = glm::floor(clampedAlpha); size_t nextPoseIndex = glm::ceil(clampedAlpha); float alpha = glm::fract(clampedAlpha); - if (prevPoseIndex == nextPoseIndex) { - // this can happen if alpha is on an integer boundary - _poses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); - } else { - // need to eval and blend between two children. - auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); - auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, dt, triggersOut); - if (prevPoses.size() > 0 && prevPoses.size() == nextPoses.size()) { - _poses.resize(prevPoses.size()); - - ::blend(_poses.size(), &prevPoses[0], &nextPoses[0], alpha, &_poses[0]); - } + float prevPoseDeltaTime = dt; + float nextPoseDeltaTime = dt; + if (_sync) { + setSyncFrameAndComputeDeltaTime(dt, prevPoseIndex, nextPoseIndex, &prevPoseDeltaTime, &nextPoseDeltaTime, triggersOut); } + + evaluateAndBlendChildren(animVars, triggersOut, alpha, prevPoseIndex, nextPoseIndex, prevPoseDeltaTime, nextPoseDeltaTime); } return _poses; } @@ -60,3 +57,64 @@ const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, flo const AnimPoseVec& AnimBlendLinear::getPosesInternal() const { return _poses; } + +void AnimBlendLinear::evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, + size_t prevPoseIndex, size_t nextPoseIndex, + float prevPoseDeltaTime, float nextPoseDeltaTime) { + if (prevPoseIndex == nextPoseIndex) { + // this can happen if alpha is on an integer boundary + _poses = _children[prevPoseIndex]->evaluate(animVars, prevPoseDeltaTime, triggersOut); + } else { + // need to eval and blend between two children. + auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, prevPoseDeltaTime, triggersOut); + auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, nextPoseDeltaTime, triggersOut); + + if (prevPoses.size() > 0 && prevPoses.size() == nextPoses.size()) { + _poses.resize(prevPoses.size()); + + ::blend(_poses.size(), &prevPoses[0], &nextPoses[0], alpha, &_poses[0]); + } + } +} + +void AnimBlendLinear::setSyncFrameAndComputeDeltaTime(float dt, size_t prevPoseIndex, size_t nextPoseIndex, + float* prevPoseDeltaTime, float* nextPoseDeltaTime, + Triggers& triggersOut) { + std::vector offsets(_children.size(), 0.0f); + std::vector timeScales(_children.size(), 1.0f); + + float lengthSum = 0.0f; + for (size_t i = 0; i < _children.size(); i++) { + // abort if we find a child that is NOT a clipNode. + if (_children[i]->getType() != AnimNode::Type::Clip) { + // TODO: FIXME: make sync this work for other node types. + *prevPoseDeltaTime = dt; + *nextPoseDeltaTime = dt; + return; + } + auto clipNode = std::dynamic_pointer_cast(_children[i]); + assert(clipNode); + if (clipNode) { + lengthSum += clipNode->getEndFrame() - clipNode->getStartFrame(); + } + } + + float averageLength = lengthSum / (float)_children.size(); + + auto prevClipNode = std::dynamic_pointer_cast(_children[prevPoseIndex]); + float prevTimeScale = (prevClipNode->getEndFrame() - prevClipNode->getStartFrame()) / averageLength; + float prevOffset = prevClipNode->getStartFrame(); + prevClipNode->setCurrentFrame(prevOffset + prevTimeScale / _syncFrame); + + auto nextClipNode = std::dynamic_pointer_cast(_children[nextPoseIndex]); + float nextTimeScale = (nextClipNode->getEndFrame() - nextClipNode->getStartFrame()) / averageLength; + float nextOffset = nextClipNode->getStartFrame(); + nextClipNode->setCurrentFrame(nextOffset + nextTimeScale / _syncFrame); + + const bool LOOP_FLAG = true; + _syncFrame = ::accumulateTime(0.0f, averageLength, _timeScale, _syncFrame, dt, LOOP_FLAG, _id, triggersOut); + + *prevPoseDeltaTime = prevTimeScale; + *nextPoseDeltaTime = nextTimeScale; +} + diff --git a/libraries/animation/src/AnimBlendLinear.h b/libraries/animation/src/AnimBlendLinear.h index 56acd5c2f7..d8b4f52fb6 100644 --- a/libraries/animation/src/AnimBlendLinear.h +++ b/libraries/animation/src/AnimBlendLinear.h @@ -22,12 +22,15 @@ // between 0 and n - 1. This alpha can be used to linearly interpolate between // the closest two children poses. This can be used to sweep through a series // of animation poses. +// +// The sync flag is used to synchronize between child animations of different lengths. +// Typically used to synchronize blending between walk and run cycles. class AnimBlendLinear : public AnimNode { public: friend class AnimTests; - AnimBlendLinear(const QString& id, float alpha); + AnimBlendLinear(const QString& id, float alpha, bool sync); virtual ~AnimBlendLinear() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; @@ -38,9 +41,19 @@ protected: // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override; + void evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, + size_t prevPoseIndex, size_t nextPoseIndex, + float prevPoseDeltaTime, float nextPoseDeltaTime); + void setSyncFrameAndComputeDeltaTime(float dt, size_t prevPoseIndex, size_t nextPoseIndex, + float* prevPoseDeltaTime, float* nextPoseDeltaTime, + Triggers& triggersOut); + AnimPoseVec _poses; float _alpha; + bool _sync; + float _syncFrame = 0.0f; + float _timeScale = 1.0f; // TODO: HOOK THIS UP TO AN ANIMVAR. QString _alphaVar; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 251cb0047a..97f46a1b68 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -35,7 +35,9 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, _endFrame = animVars.lookup(_endFrameVar, _endFrame); _timeScale = animVars.lookup(_timeScaleVar, _timeScale); _loopFlag = animVars.lookup(_loopFlagVar, _loopFlag); - _frame = accumulateTime(animVars.lookup(_frameVar, _frame), dt, triggersOut); + float frame = animVars.lookup(_frameVar, _frame); + + _frame = ::accumulateTime(_startFrame, _endFrame, _timeScale, frame, dt, _loopFlag, _id, triggersOut); // poll network anim to see if it's finished loading yet. if (_networkAnim && _networkAnim->isLoaded() && _skeleton) { @@ -78,39 +80,7 @@ void AnimClip::setCurrentFrameInternal(float frame) { // because dt is 0, we should not encounter any triggers const float dt = 0.0f; Triggers triggers; - _frame = accumulateTime(frame * _timeScale, dt, triggers); -} - -float AnimClip::accumulateTime(float frame, float dt, Triggers& triggersOut) const { - const float startFrame = std::min(_startFrame, _endFrame); - if (startFrame == _endFrame) { - // when startFrame >= endFrame - frame = _endFrame; - } else if (_timeScale > 0.0f) { - // accumulate time, keeping track of loops and end of animation events. - const float FRAMES_PER_SECOND = 30.0f; - float framesRemaining = (dt * _timeScale) * FRAMES_PER_SECOND; - while (framesRemaining > 0.0f) { - float framesTillEnd = _endFrame - _frame; - if (framesRemaining >= framesTillEnd) { - if (_loopFlag) { - // anim loop - triggersOut.push_back(_id + "OnLoop"); - framesRemaining -= framesTillEnd; - frame = startFrame; - } else { - // anim end - triggersOut.push_back(_id + "OnDone"); - frame = _endFrame; - framesRemaining = 0.0f; - } - } else { - frame += framesRemaining; - framesRemaining = 0.0f; - } - } - } - return frame; + _frame = ::accumulateTime(_startFrame, _endFrame, _timeScale, frame * _timeScale, dt, _loopFlag, _id, triggers); } void AnimClip::copyFromNetworkAnim() { diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index 3a76870c98..b7cd2861fa 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -36,12 +36,14 @@ public: void setLoopFlagVar(const QString& loopFlagVar) { _loopFlagVar = loopFlagVar; } void setFrameVar(const QString& frameVar) { _frameVar = frameVar; } + float getStartFrame() const { return _startFrame; } + float getEndFrame() const { return _endFrame; } + protected: void loadURL(const QString& url); virtual void setCurrentFrameInternal(float frame) override; - float accumulateTime(float frame, float dt, Triggers& triggersOut) const; void copyFromNetworkAnim(); // for AnimDebugDraw rendering diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h index 9a21526409..315ee357dd 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -75,10 +75,10 @@ public: return evaluate(animVars, dt, triggersOut); } -protected: - void setCurrentFrame(float frame); +protected: + virtual void setCurrentFrameInternal(float frame) {} virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { _skeleton = skeleton; } diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 147025f1cf..2acbeed734 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -221,10 +221,11 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr); + READ_BOOL(sync, jsonObj, id, jsonUrl, nullptr); READ_OPTIONAL_STRING(alphaVar, jsonObj); - auto node = std::make_shared(id, alpha); + auto node = std::make_shared(id, alpha, sync); if (!alphaVar.isEmpty()) { node->setAlphaVar(alphaVar); diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index 81b294e66c..c8efa85df9 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -11,6 +11,9 @@ #include "AnimUtil.h" #include "GLMHelpers.h" +// TODO: use restrict keyword +// TODO: excellent candidate for simd vectorization. + void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result) { for (size_t i = 0; i < numPoses; i++) { const AnimPose& aPose = a[i]; @@ -20,3 +23,39 @@ void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, A result[i].trans = lerp(aPose.trans, bPose.trans, alpha); } } + +float accumulateTime(float startFrame, float endFrame, float timeScale, float currentFrame, float dt, bool loopFlag, + const QString& id, AnimNode::Triggers& triggersOut) { + + float frame = currentFrame; + const float clampedStartFrame = std::min(startFrame, endFrame); + if (clampedStartFrame == endFrame) { + // when clampedStartFrame >= endFrame + frame = endFrame; + } else if (timeScale > 0.0f) { + // accumulate time, keeping track of loops and end of animation events. + const float FRAMES_PER_SECOND = 30.0f; + float framesRemaining = (dt * timeScale) * FRAMES_PER_SECOND; + while (framesRemaining > 0.0f) { + float framesTillEnd = endFrame - frame; + if (framesRemaining >= framesTillEnd) { + if (loopFlag) { + // anim loop + triggersOut.push_back(id + "OnLoop"); + framesRemaining -= framesTillEnd; + frame = clampedStartFrame; + } else { + // anim end + triggersOut.push_back(id + "OnDone"); + frame = endFrame; + framesRemaining = 0.0f; + } + } else { + frame += framesRemaining; + framesRemaining = 0.0f; + } + } + } + return frame; +} + diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h index 23c02b6183..6d394be882 100644 --- a/libraries/animation/src/AnimUtil.h +++ b/libraries/animation/src/AnimUtil.h @@ -13,12 +13,12 @@ #include "AnimNode.h" -// TODO: use restrict keyword -// TODO: excellent candidate for simd vectorization. - // this is where the magic happens void blend(size_t numPoses, const AnimPose* a, const AnimPose* b, float alpha, AnimPose* result); +float accumulateTime(float startFrame, float endFrame, float timeScale, float currentFrame, float dt, bool loopFlag, + const QString& id, AnimNode::Triggers& triggersOut); + #endif From 9b9bd7fe269ce0eb22390ede7554fd3e70f1981e Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 16 Oct 2015 09:29:59 -0700 Subject: [PATCH 3/9] AnimNodeLoader: Fix for crash condition When a node with children had an error loading, it would lead to a nullptr dereference. --- libraries/animation/src/AnimNodeLoader.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 2acbeed734..4222364629 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -160,6 +160,9 @@ static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUr assert((int)type >= 0 && type < AnimNode::Type::NumTypes); auto node = (animNodeTypeToLoaderFunc(type))(dataObj, id, jsonUrl); + if (!node) { + return nullptr; + } auto childrenValue = jsonObj.value("children"); if (!childrenValue.isArray()) { From 8e7e94c50102e554efb5c7da532690d1bcfd79a9 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 16 Oct 2015 09:31:14 -0700 Subject: [PATCH 4/9] AnimTests: now compile and pass again. --- tests/animation/src/AnimTests.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index a08288000e..200a6c0c3c 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -30,8 +30,8 @@ void AnimTests::cleanupTestCase() { } void AnimTests::testClipInternalState() { - std::string id = "my anim clip"; - std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; + QString id = "my anim clip"; + QString url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; float startFrame = 2.0f; float endFrame = 20.0f; float timeScale = 1.1f; @@ -55,8 +55,8 @@ static float framesToSec(float secs) { } void AnimTests::testClipEvaulate() { - std::string id = "myClipNode"; - std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; + QString id = "myClipNode"; + QString url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; float startFrame = 2.0f; float endFrame = 22.0f; float timeScale = 1.0f; @@ -90,8 +90,8 @@ void AnimTests::testClipEvaulate() { } void AnimTests::testClipEvaulateWithVars() { - std::string id = "myClipNode"; - std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; + QString id = "myClipNode"; + QString url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx"; float startFrame = 2.0f; float endFrame = 22.0f; float timeScale = 1.0f; @@ -126,9 +126,9 @@ void AnimTests::testClipEvaulateWithVars() { } void AnimTests::testLoader() { - auto url = QUrl("https://gist.githubusercontent.com/hyperlogic/857129fe04567cbe670f/raw/8ba57a8f0a76f88b39a11f77f8d9df04af9cec95/test.json"); + auto url = QUrl("https://gist.githubusercontent.com/hyperlogic/857129fe04567cbe670f/raw/0c54500f480fd7314a5aeb147c45a8a707edcc2e/test.json"); // NOTE: This will warn about missing "test01.fbx", "test02.fbx", etc. if the resource loading code doesn't handle relative pathnames! - // However, the test will proceed. + // However, the test will proceed. AnimNodeLoader loader(url); const int timeout = 1000; From 073cec41c41c77546824f2fda5780c8a28c86df5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 16 Oct 2015 12:11:46 -0700 Subject: [PATCH 5/9] AnimClip & accumulateTime smoother looping anims Looping animations should have an extra frame of interpolation between the start and end frames. --- libraries/animation/src/AnimClip.cpp | 11 ++++++----- libraries/animation/src/AnimUtil.cpp | 7 +++++-- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 97f46a1b68..613194164f 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -47,16 +47,17 @@ const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, } if (_anim.size()) { - int frameCount = _anim.size(); - int prevIndex = (int)glm::floor(_frame); - int nextIndex = (int)glm::ceil(_frame); - if (_loopFlag && nextIndex >= frameCount) { - nextIndex = 0; + int nextIndex; + if (_loopFlag && _frame >= _endFrame) { + nextIndex = (int)glm::ceil(_startFrame); + } else { + nextIndex = (int)glm::ceil(_frame); } // It can be quite possible for the user to set _startFrame and _endFrame to // values before or past valid ranges. We clamp the frames here. + int frameCount = _anim.size(); prevIndex = std::min(std::max(0, prevIndex), frameCount - 1); nextIndex = std::min(std::max(0, nextIndex), frameCount - 1); diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp index c8efa85df9..e9e5ea95de 100644 --- a/libraries/animation/src/AnimUtil.cpp +++ b/libraries/animation/src/AnimUtil.cpp @@ -29,8 +29,7 @@ float accumulateTime(float startFrame, float endFrame, float timeScale, float cu float frame = currentFrame; const float clampedStartFrame = std::min(startFrame, endFrame); - if (clampedStartFrame == endFrame) { - // when clampedStartFrame >= endFrame + if (fabsf(clampedStartFrame - endFrame) < 1.0f) { frame = endFrame; } else if (timeScale > 0.0f) { // accumulate time, keeping track of loops and end of animation events. @@ -38,6 +37,10 @@ float accumulateTime(float startFrame, float endFrame, float timeScale, float cu float framesRemaining = (dt * timeScale) * FRAMES_PER_SECOND; while (framesRemaining > 0.0f) { float framesTillEnd = endFrame - frame; + // when looping, add one frame between start and end. + if (loopFlag) { + framesTillEnd += 1.0f; + } if (framesRemaining >= framesTillEnd) { if (loopFlag) { // anim loop From 15f3894001086b2ad856c242079f9d0f52887d96 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 16 Oct 2015 12:13:31 -0700 Subject: [PATCH 6/9] AnimTests: added tests for accumulateTime --- tests/animation/src/AnimTests.cpp | 89 ++++++++++++++++++++++++++++++- tests/animation/src/AnimTests.h | 3 ++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp index 200a6c0c3c..1b5bb4739a 100644 --- a/tests/animation/src/AnimTests.cpp +++ b/tests/animation/src/AnimTests.cpp @@ -13,6 +13,7 @@ #include "AnimBlendLinear.h" #include "AnimationLogging.h" #include "AnimVariant.h" +#include "AnimUtil.h" #include <../QTestExtensions.h> @@ -73,8 +74,8 @@ void AnimTests::testClipEvaulate() { // does it loop? triggers.clear(); - clip.evaluate(vars, framesToSec(11.0f), triggers); - QCOMPARE_WITH_ABS_ERROR(clip._frame, 3.0f, EPSILON); + clip.evaluate(vars, framesToSec(12.0f), triggers); + QCOMPARE_WITH_ABS_ERROR(clip._frame, 3.0f, EPSILON); // Note: frame 3 and not 4, because extra frame between start and end. // did we receive a loop trigger? QVERIFY(std::find(triggers.begin(), triggers.end(), "myClipNodeOnLoop") != triggers.end()); @@ -238,3 +239,87 @@ void AnimTests::testVariant() { QVERIFY(m[1].z == -7.0f); QVERIFY(m[3].w == 16.0f); } + +void AnimTests::testAccumulateTime() { + + float startFrame = 0.0f; + float endFrame = 10.0f; + float timeScale = 1.0f; + testAccumulateTimeWithParameters(startFrame, endFrame, timeScale); + + startFrame = 5.0f; + endFrame = 15.0f; + timeScale = 1.0f; + testAccumulateTimeWithParameters(startFrame, endFrame, timeScale); + + startFrame = 0.0f; + endFrame = 10.0f; + timeScale = 0.5f; + testAccumulateTimeWithParameters(startFrame, endFrame, timeScale); + + startFrame = 5.0f; + endFrame = 15.0f; + timeScale = 2.0f; + testAccumulateTimeWithParameters(startFrame, endFrame, timeScale); +} + +void AnimTests::testAccumulateTimeWithParameters(float startFrame, float endFrame, float timeScale) const { + + float dt = (1.0f / 30.0f) / timeScale; // sec + QString id = "testNode"; + AnimNode::Triggers triggers; + bool loopFlag = false; + + float resultFrame = accumulateTime(startFrame, endFrame, timeScale, startFrame, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == startFrame + 1.0f); + QVERIFY(triggers.empty()); + triggers.clear(); + + resultFrame = accumulateTime(startFrame, endFrame, timeScale, resultFrame, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == startFrame + 2.0f); + QVERIFY(triggers.empty()); + triggers.clear(); + + resultFrame = accumulateTime(startFrame, endFrame, timeScale, resultFrame, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == startFrame + 3.0f); + QVERIFY(triggers.empty()); + triggers.clear(); + + // test onDone trigger and frame clamping. + resultFrame = accumulateTime(startFrame, endFrame, timeScale, endFrame - 1.0f, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == endFrame); + QVERIFY(!triggers.empty() && triggers[0] == "testNodeOnDone"); + triggers.clear(); + + resultFrame = accumulateTime(startFrame, endFrame, timeScale, endFrame - 0.5f, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == endFrame); + QVERIFY(!triggers.empty() && triggers[0] == "testNodeOnDone"); + triggers.clear(); + + // test onLoop trigger and looping frame logic + loopFlag = true; + + // should NOT trigger loop even though we stop at last frame, because there is an extra frame between end and start frames. + resultFrame = accumulateTime(startFrame, endFrame, timeScale, endFrame - 1.0f, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == endFrame); + QVERIFY(triggers.empty()); + triggers.clear(); + + // now we should hit loop trigger + resultFrame = accumulateTime(startFrame, endFrame, timeScale, resultFrame, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == startFrame); + QVERIFY(!triggers.empty() && triggers[0] == "testNodeOnLoop"); + triggers.clear(); + + // should NOT trigger loop, even though we move past the end frame, because of extra frame between end and start. + resultFrame = accumulateTime(startFrame, endFrame, timeScale, endFrame - 0.5f, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == endFrame + 0.5f); + QVERIFY(triggers.empty()); + triggers.clear(); + + // now we should hit loop trigger + resultFrame = accumulateTime(startFrame, endFrame, timeScale, resultFrame, dt, loopFlag, id, triggers); + QVERIFY(resultFrame == startFrame + 0.5f); + QVERIFY(!triggers.empty() && triggers[0] == "testNodeOnLoop"); + triggers.clear(); +} diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h index e667444657..7bd05369c7 100644 --- a/tests/animation/src/AnimTests.h +++ b/tests/animation/src/AnimTests.h @@ -15,6 +15,8 @@ class AnimTests : public QObject { Q_OBJECT +public: + void testAccumulateTimeWithParameters(float startFrame, float endFrame, float timeScale) const; private slots: void initTestCase(); void cleanupTestCase(); @@ -23,6 +25,7 @@ private slots: void testClipEvaulateWithVars(); void testLoader(); void testVariant(); + void testAccumulateTime(); }; #endif // hifi_AnimTests_h From 0c36180e2fd8acefb4c49379ef88bcdf3eac3781 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Fri, 16 Oct 2015 12:17:01 -0700 Subject: [PATCH 7/9] avatar-animation.json: Adjustment to sidestep endFrame, to prevent stalling at end --- .../resources/meshes/defaultAvatar_full/avatar-animation.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json index 5b042a09b1..be1abbc820 100644 --- a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -579,7 +579,7 @@ "data": { "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/side_step_left.fbx", "startFrame": 0.0, - "endFrame": 31.0, + "endFrame": 30.0, "timeScale": 1.0, "loopFlag": true }, @@ -591,7 +591,7 @@ "data": { "url": "http://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/side_step_right.fbx", "startFrame": 0.0, - "endFrame": 31.0, + "endFrame": 30.0, "timeScale": 1.0, "loopFlag": true }, From 11f2d29bf80ce439ac3c697d7cdaffceea03f7f5 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 20 Oct 2015 10:36:37 -0700 Subject: [PATCH 8/9] AnimBlendLinear: bugfixes for sync flag added timeScale --- .../defaultAvatar_full/avatar-animation.json | 37 +++++++++-- libraries/animation/src/AnimBlendLinear.cpp | 61 +++++++++++-------- libraries/animation/src/AnimBlendLinear.h | 17 +++--- libraries/animation/src/AnimClip.cpp | 2 +- libraries/animation/src/AnimClip.h | 3 + libraries/animation/src/AnimNodeLoader.cpp | 8 ++- 6 files changed, 87 insertions(+), 41 deletions(-) diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json index be1abbc820..ea4ef63d16 100644 --- a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -191,6 +191,7 @@ "data": { "alpha": 0.0, "sync": false, + "timeScale": 1.0, "alphaVar": "rightHandGrabBlend" }, "children": [ @@ -341,6 +342,7 @@ "data": { "alpha": 0.0, "sync": false, + "timeScale": 1.0, "alphaVar": "leftHandGrabBlend" }, "children": [ @@ -525,16 +527,39 @@ }, { "id": "walkFwd", - "type": "clip", + "type": "blendLinear", "data": { - "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", - "startFrame": 0.0, - "endFrame": 35.0, + "alpha": 0.0, + "sync": true, "timeScale": 1.0, - "loopFlag": true, "timeScaleVar": "walkTimeScale" }, - "children": [] + "children": [ + { + "id": "walkFwdShort", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_short_fwd.fbx", + "startFrame": 0.0, + "endFrame": 39.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwdNormal", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_fwd.fbx", + "startFrame": 0.0, + "endFrame": 35.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] }, { "id": "walkBwd", diff --git a/libraries/animation/src/AnimBlendLinear.cpp b/libraries/animation/src/AnimBlendLinear.cpp index 1e65ba2b36..1f9026de9d 100644 --- a/libraries/animation/src/AnimBlendLinear.cpp +++ b/libraries/animation/src/AnimBlendLinear.cpp @@ -14,10 +14,11 @@ #include "AnimUtil.h" #include "AnimClip.h" -AnimBlendLinear::AnimBlendLinear(const QString& id, float alpha, bool sync) : +AnimBlendLinear::AnimBlendLinear(const QString& id, float alpha, bool sync, float timeScale) : AnimNode(AnimNode::Type::BlendLinear, id), _alpha(alpha), - _sync(sync) { + _sync(sync), + _timeScale(timeScale) { } @@ -28,6 +29,7 @@ AnimBlendLinear::~AnimBlendLinear() { const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { _alpha = animVars.lookup(_alphaVar, _alpha); + _timeScale = animVars.lookup(_timeScaleVar, _timeScale); if (_children.size() == 0) { for (auto&& pose : _poses) { @@ -42,13 +44,11 @@ const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, flo size_t nextPoseIndex = glm::ceil(clampedAlpha); float alpha = glm::fract(clampedAlpha); - float prevPoseDeltaTime = dt; - float nextPoseDeltaTime = dt; if (_sync) { - setSyncFrameAndComputeDeltaTime(dt, prevPoseIndex, nextPoseIndex, &prevPoseDeltaTime, &nextPoseDeltaTime, triggersOut); + setSyncAndAccumulateTime(dt, prevPoseIndex, nextPoseIndex, triggersOut); } - evaluateAndBlendChildren(animVars, triggersOut, alpha, prevPoseIndex, nextPoseIndex, prevPoseDeltaTime, nextPoseDeltaTime); + evaluateAndBlendChildren(animVars, triggersOut, alpha, prevPoseIndex, nextPoseIndex, dt); } return _poses; } @@ -59,15 +59,14 @@ const AnimPoseVec& AnimBlendLinear::getPosesInternal() const { } void AnimBlendLinear::evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, - size_t prevPoseIndex, size_t nextPoseIndex, - float prevPoseDeltaTime, float nextPoseDeltaTime) { + size_t prevPoseIndex, size_t nextPoseIndex, float dt) { if (prevPoseIndex == nextPoseIndex) { // this can happen if alpha is on an integer boundary - _poses = _children[prevPoseIndex]->evaluate(animVars, prevPoseDeltaTime, triggersOut); + _poses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); } else { // need to eval and blend between two children. - auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, prevPoseDeltaTime, triggersOut); - auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, nextPoseDeltaTime, triggersOut); + auto prevPoses = _children[prevPoseIndex]->evaluate(animVars, dt, triggersOut); + auto nextPoses = _children[nextPoseIndex]->evaluate(animVars, dt, triggersOut); if (prevPoses.size() > 0 && prevPoses.size() == nextPoses.size()) { _poses.resize(prevPoses.size()); @@ -77,9 +76,7 @@ void AnimBlendLinear::evaluateAndBlendChildren(const AnimVariantMap& animVars, T } } -void AnimBlendLinear::setSyncFrameAndComputeDeltaTime(float dt, size_t prevPoseIndex, size_t nextPoseIndex, - float* prevPoseDeltaTime, float* nextPoseDeltaTime, - Triggers& triggersOut) { +void AnimBlendLinear::setSyncAndAccumulateTime(float dt, size_t prevPoseIndex, size_t nextPoseIndex, Triggers& triggersOut) { std::vector offsets(_children.size(), 0.0f); std::vector timeScales(_children.size(), 1.0f); @@ -88,33 +85,45 @@ void AnimBlendLinear::setSyncFrameAndComputeDeltaTime(float dt, size_t prevPoseI // abort if we find a child that is NOT a clipNode. if (_children[i]->getType() != AnimNode::Type::Clip) { // TODO: FIXME: make sync this work for other node types. - *prevPoseDeltaTime = dt; - *nextPoseDeltaTime = dt; return; } auto clipNode = std::dynamic_pointer_cast(_children[i]); assert(clipNode); if (clipNode) { - lengthSum += clipNode->getEndFrame() - clipNode->getStartFrame(); + lengthSum += (clipNode->getEndFrame() - clipNode->getStartFrame()) + 1.0f; } } - float averageLength = lengthSum / (float)_children.size(); + _averageLength = lengthSum / (float)_children.size(); + + float progress = (_syncFrame / _averageLength); auto prevClipNode = std::dynamic_pointer_cast(_children[prevPoseIndex]); - float prevTimeScale = (prevClipNode->getEndFrame() - prevClipNode->getStartFrame()) / averageLength; + float prevLength = (prevClipNode->getEndFrame() - prevClipNode->getStartFrame()) + 1.0f; float prevOffset = prevClipNode->getStartFrame(); - prevClipNode->setCurrentFrame(prevOffset + prevTimeScale / _syncFrame); + float prevFrame = prevOffset + (progress * prevLength); + float prevTimeScale = _timeScale * (_averageLength / prevLength); + prevClipNode->setTimeScale(prevTimeScale); + prevClipNode->setCurrentFrame(prevFrame); auto nextClipNode = std::dynamic_pointer_cast(_children[nextPoseIndex]); - float nextTimeScale = (nextClipNode->getEndFrame() - nextClipNode->getStartFrame()) / averageLength; + float nextLength = (nextClipNode->getEndFrame() - nextClipNode->getStartFrame()) + 1.0f; float nextOffset = nextClipNode->getStartFrame(); - nextClipNode->setCurrentFrame(nextOffset + nextTimeScale / _syncFrame); + float nextFrame = nextOffset + (progress * nextLength); + float nextTimeScale = _timeScale * (_averageLength / nextLength); + nextClipNode->setTimeScale(nextTimeScale); + nextClipNode->setCurrentFrame(nextFrame); + const float START_FRAME = 0.0f; const bool LOOP_FLAG = true; - _syncFrame = ::accumulateTime(0.0f, averageLength, _timeScale, _syncFrame, dt, LOOP_FLAG, _id, triggersOut); - - *prevPoseDeltaTime = prevTimeScale; - *nextPoseDeltaTime = nextTimeScale; + _syncFrame = ::accumulateTime(START_FRAME, _averageLength, _timeScale, _syncFrame, dt, LOOP_FLAG, _id, triggersOut); } +void AnimBlendLinear::setCurrentFrameInternal(float frame) { + // because dt is 0, we should not encounter any triggers + const float dt = 0.0f; + Triggers triggers; + const float START_FRAME = 0.0f; + const bool LOOP_FLAG = true; + _syncFrame = ::accumulateTime(START_FRAME, _averageLength, _timeScale, frame, dt, LOOP_FLAG, _id, triggers); +} diff --git a/libraries/animation/src/AnimBlendLinear.h b/libraries/animation/src/AnimBlendLinear.h index d8b4f52fb6..7def6be91d 100644 --- a/libraries/animation/src/AnimBlendLinear.h +++ b/libraries/animation/src/AnimBlendLinear.h @@ -30,32 +30,35 @@ class AnimBlendLinear : public AnimNode { public: friend class AnimTests; - AnimBlendLinear(const QString& id, float alpha, bool sync); + AnimBlendLinear(const QString& id, float alpha, bool sync, float timeScale); virtual ~AnimBlendLinear() override; virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; void setAlphaVar(const QString& alphaVar) { _alphaVar = alphaVar; } + void setTimeScaleVar(const QString& timeScaleVar) { _timeScaleVar = timeScaleVar; } protected: // for AnimDebugDraw rendering virtual const AnimPoseVec& getPosesInternal() const override; void evaluateAndBlendChildren(const AnimVariantMap& animVars, Triggers& triggersOut, float alpha, - size_t prevPoseIndex, size_t nextPoseIndex, - float prevPoseDeltaTime, float nextPoseDeltaTime); - void setSyncFrameAndComputeDeltaTime(float dt, size_t prevPoseIndex, size_t nextPoseIndex, - float* prevPoseDeltaTime, float* nextPoseDeltaTime, - Triggers& triggersOut); + size_t prevPoseIndex, size_t nextPoseIndex, float dt); + void setSyncAndAccumulateTime(float dt, size_t prevPoseIndex, size_t nextPoseIndex, Triggers& triggersOut); + + virtual void setCurrentFrameInternal(float frame) override; AnimPoseVec _poses; float _alpha; bool _sync; + float _timeScale; + float _syncFrame = 0.0f; - float _timeScale = 1.0f; // TODO: HOOK THIS UP TO AN ANIMVAR. + float _averageLength = 0.0f; // average length of child animations in frames. QString _alphaVar; + QString _timeScaleVar; // no copies AnimBlendLinear(const AnimBlendLinear&) = delete; diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 613194164f..8f50874ed3 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -81,7 +81,7 @@ void AnimClip::setCurrentFrameInternal(float frame) { // because dt is 0, we should not encounter any triggers const float dt = 0.0f; Triggers triggers; - _frame = ::accumulateTime(_startFrame, _endFrame, _timeScale, frame * _timeScale, dt, _loopFlag, _id, triggers); + _frame = ::accumulateTime(_startFrame, _endFrame, _timeScale, frame, dt, _loopFlag, _id, triggers); } void AnimClip::copyFromNetworkAnim() { diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index b7cd2861fa..36867e622b 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -39,6 +39,9 @@ public: float getStartFrame() const { return _startFrame; } float getEndFrame() const { return _endFrame; } + void setTimeScale(float timeScale) { _timeScale = timeScale; } + float getTimeScale() const { return _timeScale; } + protected: void loadURL(const QString& url); diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp index 4222364629..f9ebf2a630 100644 --- a/libraries/animation/src/AnimNodeLoader.cpp +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -225,15 +225,21 @@ static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const Q READ_FLOAT(alpha, jsonObj, id, jsonUrl, nullptr); READ_BOOL(sync, jsonObj, id, jsonUrl, nullptr); + READ_FLOAT(timeScale, jsonObj, id, jsonUrl, nullptr); READ_OPTIONAL_STRING(alphaVar, jsonObj); + READ_OPTIONAL_STRING(timeScaleVar, jsonObj); - auto node = std::make_shared(id, alpha, sync); + auto node = std::make_shared(id, alpha, sync, timeScale); if (!alphaVar.isEmpty()) { node->setAlphaVar(alphaVar); } + if (!timeScaleVar.isEmpty()) { + node->setTimeScaleVar(timeScaleVar); + } + return node; } From 5cd2786c1d4e71a28e47dac550ee2a24b7835d57 Mon Sep 17 00:00:00 2001 From: "Anthony J. Thibault" Date: Tue, 20 Oct 2015 16:37:05 -0700 Subject: [PATCH 9/9] First pass at Rig timeScaling and blending between slow, walk and run. --- .../defaultAvatar_full/avatar-animation.json | 17 ++++++- libraries/animation/src/Rig.cpp | 44 +++++++++++++++++-- libraries/animation/src/Rig.h | 4 ++ libraries/shared/src/GLMHelpers.h | 5 +++ 4 files changed, 65 insertions(+), 5 deletions(-) diff --git a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json index ea4ef63d16..e75e806df6 100644 --- a/interface/resources/meshes/defaultAvatar_full/avatar-animation.json +++ b/interface/resources/meshes/defaultAvatar_full/avatar-animation.json @@ -4,7 +4,7 @@ "id": "ikOverlay", "type": "overlay", "data": { - "alpha": 1.0, + "alpha": 0.0, "boneSet": "fullBody" }, "children": [ @@ -532,7 +532,8 @@ "alpha": 0.0, "sync": true, "timeScale": 1.0, - "timeScaleVar": "walkTimeScale" + "timeScaleVar": "walkTimeScale", + "alphaVar": "walkAlpha" }, "children": [ { @@ -558,6 +559,18 @@ "loopFlag": true }, "children": [] + }, + { + "id": "walkFwdRun", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/run_fwd.fbx", + "startFrame": 0.0, + "endFrame": 21.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] } ] }, diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index 25f28a3f64..82cc19f18c 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -408,6 +408,41 @@ glm::mat4 Rig::getJointTransform(int jointIndex) const { return _jointStates[jointIndex].getTransform(); } +void Rig::calcWalkForwardAlphaAndTimeScale(float speed, float* alphaOut, float* timeScaleOut) { + + // filter speed using a moving average. + _averageForwardSpeed.updateAverage(speed); + speed = _averageForwardSpeed.getAverage(); + + const int NUM_FWD_SPEEDS = 3; + float FWD_SPEEDS[NUM_FWD_SPEEDS] = { 0.3f, 1.4f, 2.7f }; // m/s + + // first calculate alpha by lerping between speeds. + float alpha = 0.0f; + if (speed <= FWD_SPEEDS[0]) { + alpha = 0.0f; + } else if (speed > FWD_SPEEDS[NUM_FWD_SPEEDS - 1]) { + alpha = (float)(NUM_FWD_SPEEDS - 1); + } else { + for (int i = 0; i < NUM_FWD_SPEEDS - 1; i++) { + if (FWD_SPEEDS[i] < speed && speed < FWD_SPEEDS[i + 1]) { + alpha = (float)i + ((speed - FWD_SPEEDS[i]) / (FWD_SPEEDS[i + 1] - FWD_SPEEDS[i])); + break; + } + } + } + + // now keeping the alpha fixed compute the timeScale. + // NOTE: This makes the assumption that the velocity of a linear blend between two animations is also linear. + int prevIndex = glm::floor(alpha); + int nextIndex = glm::ceil(alpha); + float animSpeed = lerp(FWD_SPEEDS[prevIndex], FWD_SPEEDS[nextIndex], (float)glm::fract(alpha)); + float timeScale = glm::clamp(0.5f, 2.0f, speed / animSpeed); + + *alphaOut = alpha; + *timeScaleOut = timeScale; +} + void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPosition, const glm::vec3& worldVelocity, const glm::quat& worldRotation) { glm::vec3 front = worldRotation * IDENTITY_FRONT; @@ -435,10 +470,13 @@ void Rig::computeMotionAnimationState(float deltaTime, const glm::vec3& worldPos // sine wave LFO var for testing. static float t = 0.0f; - _animVars.set("sine", static_cast(0.5 * sin(t) + 0.5)); + _animVars.set("sine", 2.0f * static_cast(0.5 * sin(t) + 0.5)); - const float ANIM_WALK_SPEED = 1.4f; // m/s - _animVars.set("walkTimeScale", glm::clamp(0.5f, 2.0f, glm::length(localVel) / ANIM_WALK_SPEED)); + float walkAlpha, walkTimeScale; + calcWalkForwardAlphaAndTimeScale(glm::length(localVel), &walkAlpha, &walkTimeScale); + + _animVars.set("walkTimeScale", walkTimeScale); + _animVars.set("walkAlpha", walkAlpha); const float MOVE_ENTER_SPEED_THRESHOLD = 0.2f; // m/sec const float MOVE_EXIT_SPEED_THRESHOLD = 0.07f; // m/sec diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 71c27e7213..c6f21deba6 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -42,6 +42,7 @@ #include "AnimNode.h" #include "AnimNodeLoader.h" +#include "SimpleMovingAverage.h" class AnimationHandle; typedef std::shared_ptr AnimationHandlePointer; @@ -206,6 +207,7 @@ public: void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist); void updateNeckJoint(int index, const HeadParameters& params); void updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAt, const glm::vec3& saccade); + void calcWalkForwardAlphaAndTimeScale(float speed, float* alphaOut, float* timeScaleOut); QVector _jointStates; int _rootJointIndex = -1; @@ -240,6 +242,8 @@ public: float _desiredStateAge = 0.0f; float _leftHandOverlayAlpha = 0.0f; float _rightHandOverlayAlpha = 0.0f; + + SimpleMovingAverage _averageForwardSpeed{ 25 }; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index 7bdd3bf2de..9c1bbe23a4 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -183,6 +183,11 @@ T toNormalizedDeviceScale(const T& value, const T& size) { #define PITCH(euler) euler.x #define ROLL(euler) euler.z +// float - linear interpolate +inline float lerp(float x, float y, float a) { + return x * (1.0f - a) + (y * a); +} + // vec2 lerp - linear interpolate template glm::detail::tvec2 lerp(const glm::detail::tvec2& x, const glm::detail::tvec2& y, T a) {