diff --git a/interface/src/Application.cpp b/interface/src/Application.cpp index 4084c563bd..7321be5ac1 100644 --- a/interface/src/Application.cpp +++ b/interface/src/Application.cpp @@ -150,10 +150,10 @@ #include "ui/Stats.h" #include "ui/AddressBarDialog.h" #include "ui/UpdateDialog.h" - #include "ui/overlays/Cube3DOverlay.h" #include "PluginContainerProxy.h" +#include "AnimDebugDraw.h" // ON WIndows PC, NVidia Optimus laptop, we want to enable NVIDIA GPU // FIXME seems to be broken. @@ -2889,6 +2889,11 @@ void Application::update(float deltaTime) { loadViewFrustum(_myCamera, _viewFrustum); } + // Update animation debug draw renderer + { + AnimDebugDraw::getInstance().update(); + } + quint64 now = usecTimestampNow(); // Update my voxel servers with my current voxel query... diff --git a/interface/src/Menu.cpp b/interface/src/Menu.cpp index ddb3716666..48b6758474 100644 --- a/interface/src/Menu.cpp +++ b/interface/src/Menu.cpp @@ -434,6 +434,14 @@ Menu::Menu() { addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::FixGaze, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableRigAnimations, 0, false, avatar, SLOT(setEnableRigAnimations(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::EnableAnimGraph, 0, false, + avatar, SLOT(setEnableAnimGraph(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawBindPose, 0, false, + avatar, SLOT(setEnableDebugDrawBindPose(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::AnimDebugDrawAnimPose, 0, false, + avatar, SLOT(setEnableDebugDrawAnimPose(bool))); + addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::MeshVisible, 0, true, + avatar, SLOT(setEnableMeshVisible(bool))); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::DisableEyelidAdjustment, 0, false); addCheckableActionToQMenuAndActionHash(avatarDebugMenu, MenuOption::Connexion, diff --git a/interface/src/Menu.h b/interface/src/Menu.h index 92f790a595..b98775e3c6 100644 --- a/interface/src/Menu.h +++ b/interface/src/Menu.h @@ -132,6 +132,8 @@ namespace MenuOption { const QString AddRemoveFriends = "Add/Remove Friends..."; const QString AddressBar = "Show Address Bar"; const QString Animations = "Animations..."; + const QString AnimDebugDrawAnimPose = "Debug Draw Animation"; + const QString AnimDebugDrawBindPose = "Debug Draw Bind Pose"; const QString Antialiasing = "Antialiasing"; const QString Atmosphere = "Atmosphere"; const QString Attachments = "Attachments..."; @@ -186,6 +188,7 @@ namespace MenuOption { const QString EchoServerAudio = "Echo Server Audio"; const QString EditEntitiesHelp = "Edit Entities Help..."; const QString Enable3DTVMode = "Enable 3DTV Mode"; + const QString EnableAnimGraph = "Enable Anim Graph"; const QString EnableCharacterController = "Enable avatar collisions"; const QString EnableRigAnimations = "Enable Rig Animations"; const QString ExpandMyAvatarSimulateTiming = "Expand /myAvatar/simulation"; @@ -215,6 +218,7 @@ namespace MenuOption { const QString Log = "Log"; const QString LogExtraTimings = "Log Extra Timing Details"; const QString LowVelocityFilter = "Low Velocity Filter"; + const QString MeshVisible = "Draw Mesh"; const QString Mirror = "Mirror"; const QString MuteAudio = "Mute Microphone"; const QString MuteEnvironment = "Mute Environment"; diff --git a/interface/src/avatar/MyAvatar.cpp b/interface/src/avatar/MyAvatar.cpp index dc9c2500c0..de7e2ba8fb 100644 --- a/interface/src/avatar/MyAvatar.cpp +++ b/interface/src/avatar/MyAvatar.cpp @@ -33,6 +33,7 @@ #include #include #include +#include #include "devices/Faceshift.h" @@ -711,6 +712,38 @@ void MyAvatar::setEnableRigAnimations(bool isEnabled) { } } +void MyAvatar::setEnableAnimGraph(bool isEnabled) { + _rig->setEnableAnimGraph(isEnabled); + if (isEnabled) { + if (_skeletonModel.readyToAddToScene()) { + initAnimGraph(); + } + } else { + destroyAnimGraph(); + } +} + +void MyAvatar::setEnableDebugDrawBindPose(bool isEnabled) { + _enableDebugDrawBindPose = isEnabled; + + if (!isEnabled) { + AnimDebugDraw::getInstance().removeSkeleton("myAvatar"); + } +} + +void MyAvatar::setEnableDebugDrawAnimPose(bool isEnabled) { + _enableDebugDrawAnimPose = isEnabled; + + if (!isEnabled) { + AnimDebugDraw::getInstance().removeAnimNode("myAvatar"); + } +} + +void MyAvatar::setEnableMeshVisible(bool isEnabled) { + render::ScenePointer scene = Application::getInstance()->getMain3DScene(); + _skeletonModel.setVisibleInScene(isEnabled, scene); +} + void MyAvatar::loadData() { Settings settings; settings.beginGroup("Avatar"); @@ -1205,6 +1238,20 @@ void MyAvatar::initHeadBones() { } } +void MyAvatar::initAnimGraph() { + // https://gist.github.com/hyperlogic/7d6a0892a7319c69e2b9 + // python2 -m SimpleHTTPServer& + //auto graphUrl = QUrl("http://localhost:8000/avatar.json"); + auto graphUrl = QUrl("https://gist.githubusercontent.com/hyperlogic/7d6a0892a7319c69e2b9/raw/e2cb37aee601b6fba31d60eac3f6ae3ef72d4a66/avatar.json"); + _skeletonModel.initAnimGraph(graphUrl, _skeletonModel.getGeometry()->getFBXGeometry()); +} + +void MyAvatar::destroyAnimGraph() { + _rig->destroyAnimGraph(); + AnimDebugDraw::getInstance().removeSkeleton("myAvatar"); + AnimDebugDraw::getInstance().removeAnimNode("myAvatar"); +} + void MyAvatar::preRender(RenderArgs* renderArgs) { render::ScenePointer scene = Application::getInstance()->getMain3DScene(); @@ -1213,6 +1260,28 @@ void MyAvatar::preRender(RenderArgs* renderArgs) { if (_skeletonModel.initWhenReady(scene)) { initHeadBones(); _skeletonModel.setCauterizeBoneSet(_headBoneSet); + initAnimGraph(); + } + + if (_enableDebugDrawBindPose || _enableDebugDrawAnimPose) { + + AnimSkeleton::ConstPointer animSkeleton = _rig->getAnimSkeleton(); + AnimNode::ConstPointer animNode = _rig->getAnimNode(); + + // bones space is rotated + glm::quat rotY180 = glm::angleAxis((float)M_PI, glm::vec3(0.0f, 1.0f, 0.0f)); + AnimPose xform(glm::vec3(1), rotY180 * getOrientation(), getPosition()); + + if (animSkeleton && _enableDebugDrawBindPose) { + glm::vec4 gray(0.2f, 0.2f, 0.2f, 0.2f); + AnimDebugDraw::getInstance().addSkeleton("myAvatar", animSkeleton, xform, gray); + } + + // This only works for when new anim system is enabled, at the moment. + if (animNode && animSkeleton && _enableDebugDrawAnimPose && _rig->getEnableAnimGraph()) { + glm::vec4 cyan(0.1f, 0.6f, 0.6f, 1.0f); + AnimDebugDraw::getInstance().addAnimNode("myAvatar", animNode, xform, cyan); + } } if (shouldDrawHead != _prevShouldDrawHead) { diff --git a/interface/src/avatar/MyAvatar.h b/interface/src/avatar/MyAvatar.h index 34e045076d..fadb5d669c 100644 --- a/interface/src/avatar/MyAvatar.h +++ b/interface/src/avatar/MyAvatar.h @@ -19,6 +19,7 @@ #include "Avatar.h" class ModelItemID; +class AnimNode; enum eyeContactTarget { LEFT_EYE, @@ -189,7 +190,12 @@ public slots: void loadLastRecording(); virtual void rebuildSkeletonBody(); + void setEnableRigAnimations(bool isEnabled); + void setEnableAnimGraph(bool isEnabled); + void setEnableDebugDrawBindPose(bool isEnabled); + void setEnableDebugDrawAnimPose(bool isEnabled); + void setEnableMeshVisible(bool isEnabled); signals: void transformChanged(); @@ -283,6 +289,8 @@ private: void updateCollisionSound(const glm::vec3& penetration, float deltaTime, float frequency); void maybeUpdateBillboard(); void initHeadBones(); + void initAnimGraph(); + void destroyAnimGraph(); // Avatar Preferences QUrl _fullAvatarURLFromPreferences; @@ -310,6 +318,9 @@ private: std::unordered_set _headBoneSet; RigPointer _rig; bool _prevShouldDrawHead; + + bool _enableDebugDrawBindPose = false; + bool _enableDebugDrawAnimPose = false; }; #endif // hifi_MyAvatar_h diff --git a/interface/src/avatar/SkeletonModel.cpp b/interface/src/avatar/SkeletonModel.cpp index 181816ed5f..5ffd0f8dec 100644 --- a/interface/src/avatar/SkeletonModel.cpp +++ b/interface/src/avatar/SkeletonModel.cpp @@ -606,3 +606,7 @@ bool SkeletonModel::hasSkeleton() { void SkeletonModel::onInvalidate() { } + +void SkeletonModel::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) { + _rig->initAnimGraph(url, fbxGeometry); +} diff --git a/interface/src/avatar/SkeletonModel.h b/interface/src/avatar/SkeletonModel.h index 75ad728d46..389b1e7d36 100644 --- a/interface/src/avatar/SkeletonModel.h +++ b/interface/src/avatar/SkeletonModel.h @@ -105,6 +105,8 @@ public: virtual void onInvalidate() override; + void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry); + signals: void skeletonLoaded(); diff --git a/libraries/animation/src/AnimBlendLinear.cpp b/libraries/animation/src/AnimBlendLinear.cpp new file mode 100644 index 0000000000..63c66a2b9d --- /dev/null +++ b/libraries/animation/src/AnimBlendLinear.cpp @@ -0,0 +1,62 @@ +// +// AnimBlendLinear.cpp +// +// Created by Anthony J. Thibault on 9/2/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 "AnimBlendLinear.h" +#include "GLMHelpers.h" +#include "AnimationLogging.h" +#include "AnimUtil.h" + +AnimBlendLinear::AnimBlendLinear(const std::string& id, float alpha) : + AnimNode(AnimNode::Type::BlendLinear, id), + _alpha(alpha) { + +} + +AnimBlendLinear::~AnimBlendLinear() { + +} + +const AnimPoseVec& AnimBlendLinear::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + _alpha = animVars.lookup(_alphaVar, _alpha); + + if (_children.size() == 0) { + for (auto&& pose : _poses) { + pose = AnimPose::identity; + } + } 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]); + } + } + } + return _poses; +} + +// for AnimDebugDraw rendering +const AnimPoseVec& AnimBlendLinear::getPosesInternal() const { + return _poses; +} diff --git a/libraries/animation/src/AnimBlendLinear.h b/libraries/animation/src/AnimBlendLinear.h new file mode 100644 index 0000000000..3a09245575 --- /dev/null +++ b/libraries/animation/src/AnimBlendLinear.h @@ -0,0 +1,52 @@ +// +// AnimBlendLinear.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimBlendLinear_h +#define hifi_AnimBlendLinear_h + +#include "AnimNode.h" + +// Linear blend between two AnimNodes. +// the amount of blending is determined by the alpha parameter. +// If the number of children is 2, then the alpha parameters should be between +// 0 and 1. The first animation will have a (1 - alpha) factor, and the second +// will have factor of alpha. +// This node supports more then 2 children. In this case the alpha should be +// 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. + +class AnimBlendLinear : public AnimNode { +public: + friend class AnimTests; + + AnimBlendLinear(const std::string& id, float alpha); + virtual ~AnimBlendLinear() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + +protected: + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimPoseVec _poses; + + float _alpha; + + std::string _alphaVar; + + // no copies + AnimBlendLinear(const AnimBlendLinear&) = delete; + AnimBlendLinear& operator=(const AnimBlendLinear&) = delete; +}; + +#endif // hifi_AnimBlendLinear_h diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp new file mode 100644 index 0000000000..fdc5fc504a --- /dev/null +++ b/libraries/animation/src/AnimClip.cpp @@ -0,0 +1,161 @@ +// +// AnimClip.cpp +// +// Created by Anthony J. Thibault on 9/2/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 "GLMHelpers.h" +#include "AnimClip.h" +#include "AnimationLogging.h" +#include "AnimUtil.h" + +AnimClip::AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag) : + AnimNode(AnimNode::Type::Clip, id), + _startFrame(startFrame), + _endFrame(endFrame), + _timeScale(timeScale), + _loopFlag(loopFlag), + _frame(startFrame) +{ + loadURL(url); +} + +AnimClip::~AnimClip() { + +} + +const AnimPoseVec& AnimClip::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + // lookup parameters from animVars, using current instance variables as defaults. + _startFrame = animVars.lookup(_startFrameVar, _startFrame); + _endFrame = animVars.lookup(_endFrameVar, _endFrame); + _timeScale = animVars.lookup(_timeScaleVar, _timeScale); + _loopFlag = animVars.lookup(_loopFlagVar, _loopFlag); + _frame = accumulateTime(animVars.lookup(_frameVar, _frame), dt, triggersOut); + + // poll network anim to see if it's finished loading yet. + if (_networkAnim && _networkAnim->isLoaded() && _skeleton) { + // loading is complete, copy animation frames from network animation, then throw it away. + copyFromNetworkAnim(); + _networkAnim.reset(); + } + + 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; + } + + // 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. + prevIndex = std::min(std::max(0, prevIndex), frameCount - 1); + nextIndex = std::min(std::max(0, nextIndex), frameCount - 1); + + const AnimPoseVec& prevFrame = _anim[prevIndex]; + const AnimPoseVec& nextFrame = _anim[nextIndex]; + float alpha = glm::fract(_frame); + + ::blend(_poses.size(), &prevFrame[0], &nextFrame[0], alpha, &_poses[0]); + } + + return _poses; +} + +void AnimClip::loadURL(const std::string& url) { + auto animCache = DependencyManager::get(); + _networkAnim = animCache->getAnimation(QString::fromStdString(url)); + _url = url; +} + +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; +} + +void AnimClip::copyFromNetworkAnim() { + assert(_networkAnim && _networkAnim->isLoaded() && _skeleton); + _anim.clear(); + + // build a mapping from animation joint indices to skeleton joint indices. + // by matching joints with the same name. + const FBXGeometry& geom = _networkAnim->getGeometry(); + const QVector& animJoints = geom.joints; + std::vector jointMap; + const int animJointCount = animJoints.count(); + jointMap.reserve(animJointCount); + for (int i = 0; i < animJointCount; i++) { + int skeletonJoint = _skeleton->nameToJointIndex(animJoints.at(i).name); + if (skeletonJoint == -1) { + qCWarning(animation) << "animation contains joint =" << animJoints.at(i).name << " which is not in the skeleton, url =" << _url.c_str(); + } + jointMap.push_back(skeletonJoint); + } + + const int frameCount = geom.animationFrames.size(); + const int skeletonJointCount = _skeleton->getNumJoints(); + _anim.resize(frameCount); + for (int i = 0; i < frameCount; i++) { + + // init all joints in animation to bind pose + _anim[i].reserve(skeletonJointCount); + for (int j = 0; j < skeletonJointCount; j++) { + _anim[i].push_back(_skeleton->getRelativeBindPose(j)); + } + + // init over all joint animations + for (int j = 0; j < animJointCount; j++) { + int k = jointMap[j]; + if (k >= 0 && k < skeletonJointCount) { + // currently FBX animations only have rotation. + _anim[i][k].rot = _skeleton->getRelativeBindPose(k).rot * geom.animationFrames[i].rotations[j]; + } + } + } + _poses.resize(skeletonJointCount); +} + + +const AnimPoseVec& AnimClip::getPosesInternal() const { + return _poses; +} diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h new file mode 100644 index 0000000000..1b9649cc3e --- /dev/null +++ b/libraries/animation/src/AnimClip.h @@ -0,0 +1,74 @@ +// +// AnimClip.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimClip_h +#define hifi_AnimClip_h + +#include +#include "AnimationCache.h" +#include "AnimNode.h" + +// Playback a single animation timeline. +// url determines the location of the fbx file to use within this clip. +// startFrame and endFrame are in frames 1/30th of a second. +// timescale can be used to speed-up or slow-down the animation. +// loop flag, when true, will loop the animation as it reaches the end frame. + +class AnimClip : public AnimNode { +public: + friend class AnimTests; + + AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag); + virtual ~AnimClip() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setStartFrameVar(const std::string& startFrameVar) { _startFrameVar = startFrameVar; } + void setEndFrameVar(const std::string& endFrameVar) { _endFrameVar = endFrameVar; } + void setTimeScaleVar(const std::string& timeScaleVar) { _timeScaleVar = timeScaleVar; } + void setLoopFlagVar(const std::string& loopFlagVar) { _loopFlagVar = loopFlagVar; } + void setFrameVar(const std::string& frameVar) { _frameVar = frameVar; } + +protected: + void loadURL(const std::string& url); + + virtual void setCurrentFrameInternal(float frame) override; + + float accumulateTime(float frame, float dt, Triggers& triggersOut) const; + void copyFromNetworkAnim(); + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimationPointer _networkAnim; + AnimPoseVec _poses; + + // _anim[frame][joint] + std::vector _anim; + + std::string _url; + float _startFrame; + float _endFrame; + float _timeScale; + bool _loopFlag; + float _frame; + + std::string _startFrameVar; + std::string _endFrameVar; + std::string _timeScaleVar; + std::string _loopFlagVar; + std::string _frameVar; + + // no copies + AnimClip(const AnimClip&) = delete; + AnimClip& operator=(const AnimClip&) = delete; +}; + +#endif // hifi_AnimClip_h diff --git a/libraries/animation/src/AnimNode.h b/libraries/animation/src/AnimNode.h new file mode 100644 index 0000000000..4675ae358f --- /dev/null +++ b/libraries/animation/src/AnimNode.h @@ -0,0 +1,115 @@ +// +// AnimNode.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimNode_h +#define hifi_AnimNode_h + +#include +#include +#include +#include +#include +#include + +#include "AnimSkeleton.h" +#include "AnimVariant.h" + +class QJsonObject; + +// Base class for all elements in the animation blend tree. +// It provides the following categories of functions: +// +// * id getter, id is used to identify a node, useful for debugging and node searching. +// * type getter, helpful for determining the derived type of this node. +// * hierarchy accessors, for adding, removing and iterating over child AnimNodes +// * skeleton accessors, the skeleton is from the model whose bones we are going to manipulate +// * evaluate method, perform actual joint manipulations here and return result by reference. +// Also, append any triggers that are detected during evaluation. + +class AnimNode { +public: + enum class Type { + Clip = 0, + BlendLinear, + Overlay, + StateMachine, + NumTypes + }; + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + using Triggers = std::vector; + + friend class AnimDebugDraw; + friend void buildChildMap(std::map& map, Pointer node); + friend class AnimStateMachine; + + AnimNode(Type type, const std::string& id) : _type(type), _id(id) {} + virtual ~AnimNode() {} + + const std::string& getID() const { return _id; } + Type getType() const { return _type; } + + // hierarchy accessors + void addChild(Pointer child) { _children.push_back(child); } + void removeChild(Pointer child) { + auto iter = std::find(_children.begin(), _children.end(), child); + if (iter != _children.end()) { + _children.erase(iter); + } + } + Pointer getChild(int i) const { + assert(i >= 0 && i < (int)_children.size()); + return _children[i]; + } + int getChildCount() const { return (int)_children.size(); } + + // pair this AnimNode graph with a skeleton. + void setSkeleton(const AnimSkeleton::Pointer skeleton) { + setSkeletonInternal(skeleton); + for (auto&& child : _children) { + child->setSkeleton(skeleton); + } + } + + AnimSkeleton::ConstPointer getSkeleton() const { return _skeleton; } + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) = 0; + virtual const AnimPoseVec& overlay(const AnimVariantMap& animVars, float dt, Triggers& triggersOut, const AnimPoseVec& underPoses) { + return evaluate(animVars, dt, triggersOut); + } + +protected: + + void setCurrentFrame(float frame) { + setCurrentFrameInternal(frame); + for (auto&& child : _children) { + child->setCurrentFrameInternal(frame); + } + } + + virtual void setCurrentFrameInternal(float frame) {} + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { + _skeleton = skeleton; + } + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const = 0; + + Type _type; + std::string _id; + std::vector _children; + AnimSkeleton::ConstPointer _skeleton; + + // no copies + AnimNode(const AnimNode&) = delete; + AnimNode& operator=(const AnimNode&) = delete; +}; + +#endif // hifi_AnimNode_h diff --git a/libraries/animation/src/AnimNodeLoader.cpp b/libraries/animation/src/AnimNodeLoader.cpp new file mode 100644 index 0000000000..5f0260db6d --- /dev/null +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -0,0 +1,434 @@ +// +// AnimNodeLoader.cpp +// +// Created by Anthony J. Thibault on 9/2/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 +#include +#include +#include + +#include "AnimNode.h" +#include "AnimClip.h" +#include "AnimBlendLinear.h" +#include "AnimationLogging.h" +#include "AnimOverlay.h" +#include "AnimNodeLoader.h" +#include "AnimStateMachine.h" + +using NodeLoaderFunc = AnimNode::Pointer (*)(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +using NodeProcessFunc = bool (*)(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); + +// factory functions +static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); +static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); + +// called after children have been loaded +static bool processClipNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } +static bool processBlendLinearNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } +static bool processOverlayNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { return true; } +bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl); + +static const char* animNodeTypeToString(AnimNode::Type type) { + switch (type) { + case AnimNode::Type::Clip: return "clip"; + case AnimNode::Type::BlendLinear: return "blendLinear"; + case AnimNode::Type::Overlay: return "overlay"; + case AnimNode::Type::StateMachine: return "stateMachine"; + case AnimNode::Type::NumTypes: return nullptr; + }; + return nullptr; +} + +static NodeLoaderFunc animNodeTypeToLoaderFunc(AnimNode::Type type) { + switch (type) { + case AnimNode::Type::Clip: return loadClipNode; + case AnimNode::Type::BlendLinear: return loadBlendLinearNode; + case AnimNode::Type::Overlay: return loadOverlayNode; + case AnimNode::Type::StateMachine: return loadStateMachineNode; + case AnimNode::Type::NumTypes: return nullptr; + }; + return nullptr; +} + +static NodeProcessFunc animNodeTypeToProcessFunc(AnimNode::Type type) { + switch (type) { + case AnimNode::Type::Clip: return processClipNode; + case AnimNode::Type::BlendLinear: return processBlendLinearNode; + case AnimNode::Type::Overlay: return processOverlayNode; + case AnimNode::Type::StateMachine: return processStateMachineNode; + case AnimNode::Type::NumTypes: return nullptr; + }; + return nullptr; +} + +#define READ_STRING(NAME, JSON_OBJ, ID, URL) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + if (!NAME##_VAL.isString()) { \ + qCCritical(animation) << "AnimNodeLoader, error reading string" \ + << #NAME << ", id =" << ID \ + << ", url =" << URL.toDisplayString(); \ + return nullptr; \ + } \ + QString NAME = NAME##_VAL.toString() + +#define READ_OPTIONAL_STRING(NAME, JSON_OBJ) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + QString NAME; \ + if (NAME##_VAL.isString()) { \ + NAME = NAME##_VAL.toString(); \ + } + +#define READ_BOOL(NAME, JSON_OBJ, ID, URL) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + if (!NAME##_VAL.isBool()) { \ + qCCritical(animation) << "AnimNodeLoader, error reading bool" \ + << #NAME << ", id =" << ID \ + << ", url =" << URL.toDisplayString(); \ + return nullptr; \ + } \ + bool NAME = NAME##_VAL.toBool() + +#define READ_FLOAT(NAME, JSON_OBJ, ID, URL) \ + auto NAME##_VAL = JSON_OBJ.value(#NAME); \ + if (!NAME##_VAL.isDouble()) { \ + qCCritical(animation) << "AnimNodeLoader, error reading double" \ + << #NAME << "id =" << ID \ + << ", url =" << URL.toDisplayString(); \ + return nullptr; \ + } \ + float NAME = (float)NAME##_VAL.toDouble() + +static AnimNode::Type stringToEnum(const QString& str) { + // O(n), move to map when number of types becomes large. + const int NUM_TYPES = static_cast(AnimNode::Type::NumTypes); + for (int i = 0; i < NUM_TYPES; i++ ) { + AnimNode::Type type = static_cast(i); + if (str == animNodeTypeToString(type)) { + return type; + } + } + return AnimNode::Type::NumTypes; +} + +static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUrl) { + auto idVal = jsonObj.value("id"); + if (!idVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"id\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QString id = idVal.toString(); + + auto typeVal = jsonObj.value("type"); + if (!typeVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad object \"type\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QString typeStr = typeVal.toString(); + AnimNode::Type type = stringToEnum(typeStr); + if (type == AnimNode::Type::NumTypes) { + qCCritical(animation) << "AnimNodeLoader, unknown node type" << typeStr << ", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto dataValue = jsonObj.value("data"); + if (!dataValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"data\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto dataObj = dataValue.toObject(); + + assert((int)type >= 0 && type < AnimNode::Type::NumTypes); + auto node = (animNodeTypeToLoaderFunc(type))(dataObj, id, jsonUrl); + + auto childrenValue = jsonObj.value("children"); + if (!childrenValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto childrenArray = childrenValue.toArray(); + for (const auto& childValue : childrenArray) { + if (!childValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + node->addChild(loadNode(childValue.toObject(), jsonUrl)); + } + + if ((animNodeTypeToProcessFunc(type))(node, dataObj, id, jsonUrl)) { + return node; + } else { + return nullptr; + } +} + +static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + + READ_STRING(url, jsonObj, id, jsonUrl); + READ_FLOAT(startFrame, jsonObj, id, jsonUrl); + READ_FLOAT(endFrame, jsonObj, id, jsonUrl); + READ_FLOAT(timeScale, jsonObj, id, jsonUrl); + READ_BOOL(loopFlag, jsonObj, id, jsonUrl); + + READ_OPTIONAL_STRING(startFrameVar, jsonObj); + READ_OPTIONAL_STRING(endFrameVar, jsonObj); + READ_OPTIONAL_STRING(timeScaleVar, jsonObj); + READ_OPTIONAL_STRING(loopFlagVar, jsonObj); + + auto node = std::make_shared(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag); + + if (!startFrameVar.isEmpty()) { + node->setStartFrameVar(startFrameVar.toStdString()); + } + if (!endFrameVar.isEmpty()) { + node->setEndFrameVar(endFrameVar.toStdString()); + } + if (!timeScaleVar.isEmpty()) { + node->setTimeScaleVar(timeScaleVar.toStdString()); + } + if (!loopFlagVar.isEmpty()) { + node->setLoopFlagVar(loopFlagVar.toStdString()); + } + + return node; +} + +static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + + READ_FLOAT(alpha, jsonObj, id, jsonUrl); + + READ_OPTIONAL_STRING(alphaVar, jsonObj); + + auto node = std::make_shared(id.toStdString(), alpha); + + if (!alphaVar.isEmpty()) { + node->setAlphaVar(alphaVar.toStdString()); + } + + return node; +} + +static const char* boneSetStrings[AnimOverlay::NumBoneSets] = { + "fullBody", + "upperBody", + "lowerBody", + "rightArm", + "leftArm", + "aboveTheHead", + "belowTheHead", + "headOnly", + "spineOnly", + "empty" +}; + +static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) { + for (int i = 0; i < (int)AnimOverlay::NumBoneSets; i++) { + if (str == boneSetStrings[i]) { + return (AnimOverlay::BoneSet)i; + } + } + return AnimOverlay::NumBoneSets; +} + +static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + + READ_STRING(boneSet, jsonObj, id, jsonUrl); + READ_FLOAT(alpha, jsonObj, id, jsonUrl); + + auto boneSetEnum = stringToBoneSetEnum(boneSet); + if (boneSetEnum == AnimOverlay::NumBoneSets) { + qCCritical(animation) << "AnimNodeLoader, unknown bone set =" << boneSet << ", defaulting to \"fullBody\", url =" << jsonUrl.toDisplayString(); + boneSetEnum = AnimOverlay::FullBodyBoneSet; + } + + READ_OPTIONAL_STRING(boneSetVar, jsonObj); + READ_OPTIONAL_STRING(alphaVar, jsonObj); + + auto node = std::make_shared(id.toStdString(), boneSetEnum, alpha); + + if (!boneSetVar.isEmpty()) { + node->setBoneSetVar(boneSetVar.toStdString()); + } + if (!alphaVar.isEmpty()) { + node->setAlphaVar(alphaVar.toStdString()); + } + + return node; +} + +static AnimNode::Pointer loadStateMachineNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) { + auto node = std::make_shared(id.toStdString()); + return node; +} + +void buildChildMap(std::map& map, AnimNode::Pointer node) { + for ( auto child : node->_children ) { + map.insert(std::pair(child->_id, child)); + } +} + +bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl) { + auto smNode = std::static_pointer_cast(node); + assert(smNode); + + READ_STRING(currentState, jsonObj, nodeId, jsonUrl); + + auto statesValue = jsonObj.value("states"); + if (!statesValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"states\" in stateMachine node, id =" << nodeId << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + // build a map for all children by name. + std::map childMap; + buildChildMap(childMap, node); + + // first pass parse all the states and build up the state and transition map. + using StringPair = std::pair; + using TransitionMap = std::multimap; + TransitionMap transitionMap; + + using StateMap = std::map; + StateMap stateMap; + + auto statesArray = statesValue.toArray(); + for (const auto& stateValue : statesArray) { + if (!stateValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad state object in \"states\", id =" << nodeId << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto stateObj = stateValue.toObject(); + + READ_STRING(id, stateObj, nodeId, jsonUrl); + READ_FLOAT(interpTarget, stateObj, nodeId, jsonUrl); + READ_FLOAT(interpDuration, stateObj, nodeId, jsonUrl); + + READ_OPTIONAL_STRING(interpTargetVar, stateObj); + READ_OPTIONAL_STRING(interpDurationVar, stateObj); + + auto stdId = id.toStdString(); + + auto iter = childMap.find(stdId); + if (iter == childMap.end()) { + qCCritical(animation) << "AnimNodeLoader, could not find stateMachine child (state) with nodeId =" << nodeId << "stateId =" << id << "url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto statePtr = std::make_shared(stdId, iter->second, interpTarget, interpDuration); + assert(statePtr); + + if (!interpTargetVar.isEmpty()) { + statePtr->setInterpTargetVar(interpTargetVar.toStdString()); + } + if (!interpDurationVar.isEmpty()) { + statePtr->setInterpDurationVar(interpDurationVar.toStdString()); + } + + smNode->addState(statePtr); + stateMap.insert(StateMap::value_type(statePtr->getID(), statePtr)); + + auto transitionsValue = stateObj.value("transitions"); + if (!transitionsValue.isArray()) { + qCritical(animation) << "AnimNodeLoader, bad array \"transitions\" in stateMachine node, stateId =" << id << "nodeId =" << nodeId << "url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + auto transitionsArray = transitionsValue.toArray(); + for (const auto& transitionValue : transitionsArray) { + if (!transitionValue.isObject()) { + qCritical(animation) << "AnimNodeLoader, bad transition object in \"transtions\", stateId =" << id << "nodeId =" << nodeId << "url =" << jsonUrl.toDisplayString(); + return nullptr; + } + auto transitionObj = transitionValue.toObject(); + + READ_STRING(var, transitionObj, nodeId, jsonUrl); + READ_STRING(state, transitionObj, nodeId, jsonUrl); + + transitionMap.insert(TransitionMap::value_type(statePtr, StringPair(var.toStdString(), state.toStdString()))); + } + } + + // second pass: now iterate thru all transitions and add them to the appropriate states. + for (auto& transition : transitionMap) { + AnimStateMachine::State::Pointer srcState = transition.first; + auto iter = stateMap.find(transition.second.second); + if (iter != stateMap.end()) { + srcState->addTransition(AnimStateMachine::State::Transition(transition.second.first, iter->second)); + } else { + qCCritical(animation) << "AnimNodeLoader, bad state machine transtion from srcState =" << srcState->_id.c_str() << "dstState =" << transition.second.second.c_str() << "nodeId =" << nodeId << "url = " << jsonUrl.toDisplayString(); + return nullptr; + } + } + + auto iter = stateMap.find(currentState.toStdString()); + if (iter == stateMap.end()) { + qCCritical(animation) << "AnimNodeLoader, bad currentState =" << currentState << "could not find child node" << "id =" << nodeId << "url = " << jsonUrl.toDisplayString(); + } + smNode->setCurrentState(iter->second); + + return true; +} + +AnimNodeLoader::AnimNodeLoader(const QUrl& url) : + _url(url), + _resource(nullptr) { + + _resource = new Resource(url); + connect(_resource, SIGNAL(loaded(QNetworkReply&)), SLOT(onRequestDone(QNetworkReply&))); + connect(_resource, SIGNAL(failed(QNetworkReply::NetworkError)), SLOT(onRequestError(QNetworkReply::NetworkError))); +} + +AnimNode::Pointer AnimNodeLoader::load(const QByteArray& contents, const QUrl& jsonUrl) { + + // convert string into a json doc + QJsonParseError error; + auto doc = QJsonDocument::fromJson(contents, &error); + if (error.error != QJsonParseError::NoError) { + qCCritical(animation) << "AnimNodeLoader, failed to parse json, error =" << error.errorString() << ", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QJsonObject obj = doc.object(); + + // version + QJsonValue versionVal = obj.value("version"); + if (!versionVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"version\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + QString version = versionVal.toString(); + + // check version + if (version != "1.0") { + qCCritical(animation) << "AnimNodeLoader, bad version number" << version << "expected \"1.0\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + // root + QJsonValue rootVal = obj.value("root"); + if (!rootVal.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad object \"root\", url =" << jsonUrl.toDisplayString(); + return nullptr; + } + + return loadNode(rootVal.toObject(), jsonUrl); +} + +void AnimNodeLoader::onRequestDone(QNetworkReply& request) { + auto node = load(request.readAll(), _url); + if (node) { + emit success(node); + } else { + emit error(0, "json parse error"); + } +} + +void AnimNodeLoader::onRequestError(QNetworkReply::NetworkError netError) { + emit error((int)netError, "Resource download error"); +} diff --git a/libraries/animation/src/AnimNodeLoader.h b/libraries/animation/src/AnimNodeLoader.h new file mode 100644 index 0000000000..71b5552879 --- /dev/null +++ b/libraries/animation/src/AnimNodeLoader.h @@ -0,0 +1,52 @@ +// +// AnimNodeLoader.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimNodeLoader_h +#define hifi_AnimNodeLoader_h + +#include + +#include +#include +#include + +#include "AnimNode.h" + +class Resource; + +class AnimNodeLoader : public QObject { + Q_OBJECT + +public: + AnimNodeLoader(const QUrl& url); + +signals: + void success(AnimNode::Pointer node); + void error(int error, QString str); + +protected: + // synchronous + static AnimNode::Pointer load(const QByteArray& contents, const QUrl& jsonUrl); + +protected slots: + void onRequestDone(QNetworkReply& request); + void onRequestError(QNetworkReply::NetworkError error); + +protected: + QUrl _url; + Resource* _resource; +private: + + // no copies + AnimNodeLoader(const AnimNodeLoader&) = delete; + AnimNodeLoader& operator=(const AnimNodeLoader&) = delete; +}; + +#endif // hifi_AnimNodeLoader diff --git a/libraries/animation/src/AnimOverlay.cpp b/libraries/animation/src/AnimOverlay.cpp new file mode 100644 index 0000000000..52026f7711 --- /dev/null +++ b/libraries/animation/src/AnimOverlay.cpp @@ -0,0 +1,181 @@ +// +// AnimOverlay.cpp +// +// Created by Anthony J. Thibault on 9/2/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 "AnimOverlay.h" +#include "AnimUtil.h" +#include + +AnimOverlay::AnimOverlay(const std::string& id, BoneSet boneSet, float alpha) : + AnimNode(AnimNode::Type::Overlay, id), _boneSet(boneSet), _alpha(alpha) { +} + +AnimOverlay::~AnimOverlay() { + +} + +void AnimOverlay::buildBoneSet(BoneSet boneSet) { + assert(_skeleton); + switch (boneSet) { + case FullBodyBoneSet: buildFullBodyBoneSet(); break; + case UpperBodyBoneSet: buildUpperBodyBoneSet(); break; + case LowerBodyBoneSet: buildLowerBodyBoneSet(); break; + case RightArmBoneSet: buildRightArmBoneSet(); break; + case LeftArmBoneSet: buildLeftArmBoneSet(); break; + case AboveTheHeadBoneSet: buildAboveTheHeadBoneSet(); break; + case BelowTheHeadBoneSet: buildBelowTheHeadBoneSet(); break; + case HeadOnlyBoneSet: buildHeadOnlyBoneSet(); break; + case SpineOnlyBoneSet: buildSpineOnlyBoneSet(); break; + default: + case EmptyBoneSet: buildEmptyBoneSet(); break; + } +} + +const AnimPoseVec& AnimOverlay::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + // lookup parameters from animVars, using current instance variables as defaults. + // NOTE: switching bonesets can be an expensive operation, let's try to avoid it. + auto prevBoneSet = _boneSet; + _boneSet = (BoneSet)animVars.lookup(_boneSetVar, (int)_boneSet); + if (_boneSet != prevBoneSet && _skeleton) { + buildBoneSet(_boneSet); + } + _alpha = animVars.lookup(_alphaVar, _alpha); + + if (_children.size() >= 2) { + auto underPoses = _children[1]->evaluate(animVars, dt, triggersOut); + auto overPoses = _children[0]->overlay(animVars, dt, triggersOut, underPoses); + + if (underPoses.size() > 0 && underPoses.size() == overPoses.size()) { + _poses.resize(underPoses.size()); + assert(_boneSetVec.size() == _poses.size()); + + for (size_t i = 0; i < _poses.size(); i++) { + float alpha = _boneSetVec[i] * _alpha; + ::blend(1, &underPoses[i], &overPoses[i], alpha, &_poses[i]); + } + } + } + return _poses; +} + +template +void for_each_child_joint(AnimSkeleton::ConstPointer skeleton, int startJoint, Func f) { + std::queue q; + q.push(startJoint); + while(q.size() > 0) { + int jointIndex = q.front(); + for (int i = 0; i < skeleton->getNumJoints(); i++) { + if (jointIndex == skeleton->getParentIndex(i)) { + f(i); + q.push(i); + } + } + q.pop(); + } +} + +void AnimOverlay::buildFullBodyBoneSet() { + assert(_skeleton); + _boneSetVec.resize(_skeleton->getNumJoints()); + for (int i = 0; i < _skeleton->getNumJoints(); i++) { + _boneSetVec[i] = 1.0f; + } +} + +void AnimOverlay::buildUpperBodyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int spineJoint = _skeleton->nameToJointIndex("Spine"); + for_each_child_joint(_skeleton, spineJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildLowerBodyBoneSet() { + assert(_skeleton); + buildFullBodyBoneSet(); + int hipsJoint = _skeleton->nameToJointIndex("Hips"); + int spineJoint = _skeleton->nameToJointIndex("Spine"); + _boneSetVec.resize(_skeleton->getNumJoints()); + for_each_child_joint(_skeleton, spineJoint, [&](int i) { + _boneSetVec[i] = 0.0f; + }); + _boneSetVec[hipsJoint] = 0.0f; +} + +void AnimOverlay::buildRightArmBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int rightShoulderJoint = _skeleton->nameToJointIndex("RightShoulder"); + for_each_child_joint(_skeleton, rightShoulderJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildLeftArmBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int leftShoulderJoint = _skeleton->nameToJointIndex("LeftShoulder"); + for_each_child_joint(_skeleton, leftShoulderJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildAboveTheHeadBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("Head"); + for_each_child_joint(_skeleton, headJoint, [&](int i) { + _boneSetVec[i] = 1.0f; + }); +} + +void AnimOverlay::buildBelowTheHeadBoneSet() { + assert(_skeleton); + buildFullBodyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("Head"); + for_each_child_joint(_skeleton, headJoint, [&](int i) { + _boneSetVec[i] = 0.0f; + }); +} + +void AnimOverlay::buildHeadOnlyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int headJoint = _skeleton->nameToJointIndex("Head"); + _boneSetVec[headJoint] = 1.0f; +} + +void AnimOverlay::buildSpineOnlyBoneSet() { + assert(_skeleton); + buildEmptyBoneSet(); + int spineJoint = _skeleton->nameToJointIndex("Spine"); + _boneSetVec[spineJoint] = 1.0f; +} + +void AnimOverlay::buildEmptyBoneSet() { + assert(_skeleton); + _boneSetVec.resize(_skeleton->getNumJoints()); + for (int i = 0; i < _skeleton->getNumJoints(); i++) { + _boneSetVec[i] = 0.0f; + } +} + +// for AnimDebugDraw rendering +const AnimPoseVec& AnimOverlay::getPosesInternal() const { + return _poses; +} + +void AnimOverlay::setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) { + _skeleton = skeleton; + + // we have to re-build the bone set when the skeleton changes. + buildBoneSet(_boneSet); +} diff --git a/libraries/animation/src/AnimOverlay.h b/libraries/animation/src/AnimOverlay.h new file mode 100644 index 0000000000..de563cc403 --- /dev/null +++ b/libraries/animation/src/AnimOverlay.h @@ -0,0 +1,80 @@ +// +// AnimOverlay.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimOverlay_h +#define hifi_AnimOverlay_h + +#include "AnimNode.h" + +// Overlay the AnimPoses from one AnimNode on top of another AnimNode. +// child[0] is overlayed on top of child[1]. The boneset is used +// to control blending on a per-bone bases. +// alpha gives the ability to fade in and fade out overlays. +// alpha of 0, will have no overlay, final pose will be 100% from child[1]. +// alpha of 1, will be a full overlay. + +class AnimOverlay : public AnimNode { +public: + friend class AnimTests; + + enum BoneSet { + FullBodyBoneSet = 0, + UpperBodyBoneSet, + LowerBodyBoneSet, + RightArmBoneSet, + LeftArmBoneSet, + AboveTheHeadBoneSet, + BelowTheHeadBoneSet, + HeadOnlyBoneSet, + SpineOnlyBoneSet, + EmptyBoneSet, + NumBoneSets, + }; + + AnimOverlay(const std::string& id, BoneSet boneSet, float alpha); + virtual ~AnimOverlay() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setBoneSetVar(const std::string& boneSetVar) { _boneSetVar = boneSetVar; } + void setAlphaVar(const std::string& alphaVar) { _alphaVar = alphaVar; } + + protected: + void buildBoneSet(BoneSet boneSet); + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + virtual void setSkeletonInternal(AnimSkeleton::ConstPointer skeleton) override; + + AnimPoseVec _poses; + BoneSet _boneSet; + float _alpha; + std::vector _boneSetVec; + + std::string _boneSetVar; + std::string _alphaVar; + + void buildFullBodyBoneSet(); + void buildUpperBodyBoneSet(); + void buildLowerBodyBoneSet(); + void buildRightArmBoneSet(); + void buildLeftArmBoneSet(); + void buildAboveTheHeadBoneSet(); + void buildBelowTheHeadBoneSet(); + void buildHeadOnlyBoneSet(); + void buildSpineOnlyBoneSet(); + void buildEmptyBoneSet(); + + // no copies + AnimOverlay(const AnimOverlay&) = delete; + AnimOverlay& operator=(const AnimOverlay&) = delete; +}; + +#endif // hifi_AnimOverlay_h diff --git a/libraries/animation/src/AnimSkeleton.cpp b/libraries/animation/src/AnimSkeleton.cpp new file mode 100644 index 0000000000..3f11607f26 --- /dev/null +++ b/libraries/animation/src/AnimSkeleton.cpp @@ -0,0 +1,176 @@ +// +// AnimSkeleton.cpp +// +// Created by Anthony J. Thibault on 9/2/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 "AnimSkeleton.h" +#include "AnimationLogging.h" +#include "GLMHelpers.h" +#include +#include + +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 = extractRotation(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 std::vector& joints, const AnimPose& geometryOffset) { + _joints = joints; + + // build a cache of bind poses + _absoluteBindPoses.reserve(joints.size()); + _relativeBindPoses.reserve(joints.size()); + + // iterate over FBXJoints and extract the bind pose information. + for (size_t i = 0; i < joints.size(); i++) { + if (_joints[i].bindTransformFoundInCluster) { + // Use the FBXJoint::bindTransform, which is absolute model coordinates + // i.e. not relative to it's parent. + AnimPose absoluteBindPose(_joints[i].bindTransform); + _absoluteBindPoses.push_back(absoluteBindPose); + int parentIndex = getParentIndex(i); + if (parentIndex >= 0) { + AnimPose inverseParentAbsoluteBindPose = _absoluteBindPoses[parentIndex].inverse(); + _relativeBindPoses.push_back(inverseParentAbsoluteBindPose * absoluteBindPose); + } else { + _relativeBindPoses.push_back(absoluteBindPose); + } + } else { + // use FBXJoint's local transform, instead + glm::mat4 rotTransform = glm::mat4_cast(_joints[i].preRotation * _joints[i].rotation * _joints[i].postRotation); + glm::mat4 relBindMat = glm::translate(_joints[i].translation) * _joints[i].preTransform * rotTransform * _joints[i].postTransform; + AnimPose relBindPose(relBindMat); + _relativeBindPoses.push_back(relBindPose); + + int parentIndex = getParentIndex(i); + if (parentIndex >= 0) { + _absoluteBindPoses.push_back(_absoluteBindPoses[parentIndex] * relBindPose); + } else { + _absoluteBindPoses.push_back(relBindPose); + } + } + } + + // now we want to normalize scale from geometryOffset to all poses. + // This will ensure our bone translations will be in meters, even if the model was authored with some other unit of mesure. + for (auto& absPose : _absoluteBindPoses) { + absPose.trans = (geometryOffset * absPose).trans; + absPose.scale = vec3(1, 1, 1); + } + + // re-compute relative poses based on the modified absolute poses. + for (size_t i = 0; i < _relativeBindPoses.size(); i++) { + int parentIndex = getParentIndex(i); + if (parentIndex >= 0) { + _relativeBindPoses[i] = _absoluteBindPoses[parentIndex].inverse() * _absoluteBindPoses[i]; + } else { + _relativeBindPoses[i] = _absoluteBindPoses[i]; + } + } +} + +int AnimSkeleton::nameToJointIndex(const QString& jointName) const { + for (size_t i = 0; i < _joints.size(); i++) { + if (_joints[i].name == jointName) { + return i; + } + } + return -1; +} + +int AnimSkeleton::getNumJoints() const { + return _joints.size(); +} + +AnimPose AnimSkeleton::getAbsoluteBindPose(int jointIndex) const { + return _absoluteBindPoses[jointIndex]; +} + +AnimPose AnimSkeleton::getRelativeBindPose(int jointIndex) const { + return _relativeBindPoses[jointIndex]; +} + +int AnimSkeleton::getParentIndex(int jointIndex) const { + return _joints[jointIndex].parentIndex; +} + +const QString& AnimSkeleton::getJointName(int jointIndex) const { + return _joints[jointIndex].name; +} + +#ifndef NDEBUG +void AnimSkeleton::dump() const { + qCDebug(animation) << "["; + for (int i = 0; i < getNumJoints(); i++) { + qCDebug(animation) << " {"; + qCDebug(animation) << " name =" << getJointName(i); + qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i); + qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i); + if (getParentIndex(i) >= 0) { + qCDebug(animation) << " parent =" << getJointName(getParentIndex(i)); + } + qCDebug(animation) << " },"; + } + qCDebug(animation) << "]"; +} + +void AnimSkeleton::dump(const AnimPoseVec& poses) const { + qCDebug(animation) << "["; + for (int i = 0; i < getNumJoints(); i++) { + qCDebug(animation) << " {"; + qCDebug(animation) << " name =" << getJointName(i); + qCDebug(animation) << " absBindPose =" << getAbsoluteBindPose(i); + qCDebug(animation) << " relBindPose =" << getRelativeBindPose(i); + qCDebug(animation) << " pose =" << poses[i]; + if (getParentIndex(i) >= 0) { + qCDebug(animation) << " parent =" << getJointName(getParentIndex(i)); + } + qCDebug(animation) << " },"; + } + qCDebug(animation) << "]"; +} +#endif diff --git a/libraries/animation/src/AnimSkeleton.h b/libraries/animation/src/AnimSkeleton.h new file mode 100644 index 0000000000..c0c5036cc7 --- /dev/null +++ b/libraries/animation/src/AnimSkeleton.h @@ -0,0 +1,78 @@ +// +// AnimSkeleton.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimSkeleton +#define hifi_AnimSkeleton + +#include + +#include "FBXReader.h" + +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; + +class AnimSkeleton { +public: + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + + AnimSkeleton(const std::vector& joints, const AnimPose& geometryOffset); + int nameToJointIndex(const QString& jointName) const; + const QString& getJointName(int jointIndex) const; + int getNumJoints() const; + + // absolute pose, not relative to parent + AnimPose getAbsoluteBindPose(int jointIndex) const; + + // relative to parent pose + AnimPose getRelativeBindPose(int jointIndex) const; + + int getParentIndex(int jointIndex) const; + +#ifndef NDEBUG + void dump() const; + void dump(const AnimPoseVec& poses) const; +#endif + +protected: + std::vector _joints; + AnimPoseVec _absoluteBindPoses; + AnimPoseVec _relativeBindPoses; + + // no copies + AnimSkeleton(const AnimSkeleton&) = delete; + AnimSkeleton& operator=(const AnimSkeleton&) = delete; +}; + +#endif diff --git a/libraries/animation/src/AnimStateMachine.cpp b/libraries/animation/src/AnimStateMachine.cpp new file mode 100644 index 0000000000..5de379dd33 --- /dev/null +++ b/libraries/animation/src/AnimStateMachine.cpp @@ -0,0 +1,114 @@ +// +// AnimStateMachine.cpp +// +// Created by Anthony J. Thibault on 9/2/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 "AnimStateMachine.h" +#include "AnimUtil.h" +#include "AnimationLogging.h" + +AnimStateMachine::AnimStateMachine(const std::string& id) : + AnimNode(AnimNode::Type::StateMachine, id) { + +} + +AnimStateMachine::~AnimStateMachine() { + +} + +const AnimPoseVec& AnimStateMachine::evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) { + + std::string desiredStateID = animVars.lookup(_currentStateVar, _currentState->getID()); + if (_currentState->getID() != desiredStateID) { + // switch states + bool foundState = false; + for (auto& state : _states) { + if (state->getID() == desiredStateID) { + switchState(animVars, state); + foundState = true; + break; + } + } + if (!foundState) { + qCCritical(animation) << "AnimStateMachine could not find state =" << desiredStateID.c_str() << ", referenced by _currentStateVar =" << _currentStateVar.c_str(); + } + } + + // evaluate currentState transitions + auto desiredState = evaluateTransitions(animVars); + if (desiredState != _currentState) { + switchState(animVars, desiredState); + } + + assert(_currentState); + auto currentStateNode = _currentState->getNode(); + assert(currentStateNode); + + if (_duringInterp) { + _alpha += _alphaVel * dt; + if (_alpha < 1.0f) { + if (_poses.size() > 0) { + ::blend(_poses.size(), &_prevPoses[0], &_nextPoses[0], _alpha, &_poses[0]); + } + } else { + _duringInterp = false; + _prevPoses.clear(); + _nextPoses.clear(); + } + } + if (!_duringInterp) { + _poses = currentStateNode->evaluate(animVars, dt, triggersOut); + } + return _poses; +} + +void AnimStateMachine::setCurrentState(State::Pointer state) { + _currentState = state; +} + +void AnimStateMachine::addState(State::Pointer state) { + _states.push_back(state); +} + +void AnimStateMachine::switchState(const AnimVariantMap& animVars, State::Pointer desiredState) { + + qCDebug(animation) << "AnimStateMachine::switchState:" << _currentState->getID().c_str() << "->" << desiredState->getID().c_str(); + + const float FRAMES_PER_SECOND = 30.0f; + + auto prevStateNode = _currentState->getNode(); + auto nextStateNode = desiredState->getNode(); + + _duringInterp = true; + _alpha = 0.0f; + float duration = std::max(0.001f, animVars.lookup(desiredState->_interpDurationVar, desiredState->_interpDuration)); + _alphaVel = FRAMES_PER_SECOND / duration; + _prevPoses = _poses; + nextStateNode->setCurrentFrame(desiredState->_interpTarget); + + // because dt is 0, we should not encounter any triggers + const float dt = 0.0f; + Triggers triggers; + _nextPoses = nextStateNode->evaluate(animVars, dt, triggers); + + _currentState = desiredState; +} + +AnimStateMachine::State::Pointer AnimStateMachine::evaluateTransitions(const AnimVariantMap& animVars) const { + assert(_currentState); + for (auto& transition : _currentState->_transitions) { + if (animVars.lookup(transition._var, false)) { + return transition._state; + } + } + return _currentState; +} + +const AnimPoseVec& AnimStateMachine::getPosesInternal() const { + return _poses; +} diff --git a/libraries/animation/src/AnimStateMachine.h b/libraries/animation/src/AnimStateMachine.h new file mode 100644 index 0000000000..f2d941a568 --- /dev/null +++ b/libraries/animation/src/AnimStateMachine.h @@ -0,0 +1,134 @@ +// +// AnimStateMachine.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimStateMachine_h +#define hifi_AnimStateMachine_h + +#include +#include +#include "AnimNode.h" + +// State Machine for transitioning between children AnimNodes +// +// This is mechinisim for playing animations and smoothly interpolating/fading +// between them. A StateMachine has a set of States, which typically reference +// child AnimNodes. Each State has a list of Transitions, which are evaluated +// to determine when we should switch to a new State. Parameters for the smooth +// interpolation/fading are read from the State that you are transitioning to. +// +// The currentState can be set directly via the setCurrentStateVar() and will override +// any State transitions. +// +// Each State has two parameters that can be changed via AnimVars, +// * interpTarget - (frames) The destination frame of the interpolation. i.e. the first frame of the animation that will +// visible after interpolation is complete. +// * interpDuration - (frames) The total length of time it will take to interp between the current pose and the +// interpTarget frame. + +class AnimStateMachine : public AnimNode { +public: + friend class AnimNodeLoader; + friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl); + +protected: + class State { + public: + friend AnimStateMachine; + friend bool processStateMachineNode(AnimNode::Pointer node, const QJsonObject& jsonObj, const QString& nodeId, const QUrl& jsonUrl); + + using Pointer = std::shared_ptr; + using ConstPointer = std::shared_ptr; + + class Transition { + public: + friend AnimStateMachine; + Transition(const std::string& var, State::Pointer state) : _var(var), _state(state) {} + protected: + std::string _var; + State::Pointer _state; + }; + + State(const std::string& id, AnimNode::Pointer node, float interpTarget, float interpDuration) : + _id(id), + _node(node), + _interpTarget(interpTarget), + _interpDuration(interpDuration) {} + + void setInterpTargetVar(const std::string& interpTargetVar) { _interpTargetVar = interpTargetVar; } + void setInterpDurationVar(const std::string& interpDurationVar) { _interpDurationVar = interpDurationVar; } + + AnimNode::Pointer getNode() const { return _node; } + const std::string& getID() const { return _id; } + + protected: + + void setInterpTarget(float interpTarget) { _interpTarget = interpTarget; } + void setInterpDuration(float interpDuration) { _interpDuration = interpDuration; } + + void addTransition(const Transition& transition) { _transitions.push_back(transition); } + + std::string _id; + AnimNode::Pointer _node; + float _interpTarget; // frames + float _interpDuration; // frames + + std::string _interpTargetVar; + std::string _interpDurationVar; + + std::vector _transitions; + + private: + // no copies + State(const State&) = delete; + State& operator=(const State&) = delete; + }; + +public: + + AnimStateMachine(const std::string& id); + virtual ~AnimStateMachine() override; + + virtual const AnimPoseVec& evaluate(const AnimVariantMap& animVars, float dt, Triggers& triggersOut) override; + + void setCurrentStateVar(std::string& currentStateVar) { _currentStateVar = currentStateVar; } + +protected: + + void setCurrentState(State::Pointer state); + + void addState(State::Pointer state); + + void switchState(const AnimVariantMap& animVars, State::Pointer desiredState); + State::Pointer evaluateTransitions(const AnimVariantMap& animVars) const; + + // for AnimDebugDraw rendering + virtual const AnimPoseVec& getPosesInternal() const override; + + AnimPoseVec _poses; + + // interpolation state + bool _duringInterp = false; + float _alphaVel = 0.0f; + float _alpha = 0.0f; + AnimPoseVec _prevPoses; + AnimPoseVec _nextPoses; + + State::Pointer _currentState; + std::vector _states; + + std::string _currentStateVar; + +private: + // no copies + AnimStateMachine(const AnimStateMachine&) = delete; + AnimStateMachine& operator=(const AnimStateMachine&) = delete; +}; + +#endif // hifi_AnimStateMachine_h diff --git a/libraries/animation/src/AnimUtil.cpp b/libraries/animation/src/AnimUtil.cpp new file mode 100644 index 0000000000..81b294e66c --- /dev/null +++ b/libraries/animation/src/AnimUtil.cpp @@ -0,0 +1,22 @@ +// +// AnimUtil.cpp +// +// Created by Anthony J. Thibault on 9/2/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 "AnimUtil.h" +#include "GLMHelpers.h" + +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]; + const AnimPose& bPose = b[i]; + result[i].scale = lerp(aPose.scale, bPose.scale, alpha); + result[i].rot = glm::normalize(glm::lerp(aPose.rot, bPose.rot, alpha)); + result[i].trans = lerp(aPose.trans, bPose.trans, alpha); + } +} diff --git a/libraries/animation/src/AnimUtil.h b/libraries/animation/src/AnimUtil.h new file mode 100644 index 0000000000..23c02b6183 --- /dev/null +++ b/libraries/animation/src/AnimUtil.h @@ -0,0 +1,24 @@ +// +// AnimUtil.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimUtil_h +#define hifi_AnimUtil_h + +#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); + +#endif + + diff --git a/libraries/animation/src/AnimVariant.h b/libraries/animation/src/AnimVariant.h new file mode 100644 index 0000000000..1d720ba565 --- /dev/null +++ b/libraries/animation/src/AnimVariant.h @@ -0,0 +1,161 @@ +// +// AnimVariant.h +// +// Created by Anthony J. Thibault on 9/2/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_AnimVariant_h +#define hifi_AnimVariant_h + +#include +#include +#include +#include +#include + +class AnimVariant { +public: + enum class Type { + Bool = 0, + Int, + Float, + Vec3, + Quat, + Mat4, + String, + NumTypes + }; + + AnimVariant() : _type(Type::Bool) { memset(&_val, 0, sizeof(_val)); } + AnimVariant(bool value) : _type(Type::Bool) { _val.boolVal = value; } + AnimVariant(int value) : _type(Type::Int) { _val.intVal = value; } + AnimVariant(float value) : _type(Type::Float) { _val.floats[0] = value; } + AnimVariant(const glm::vec3& value) : _type(Type::Vec3) { *reinterpret_cast(&_val) = value; } + AnimVariant(const glm::quat& value) : _type(Type::Quat) { *reinterpret_cast(&_val) = value; } + AnimVariant(const glm::mat4& value) : _type(Type::Mat4) { *reinterpret_cast(&_val) = value; } + AnimVariant(const std::string& value) : _type(Type::String) { _stringVal = value; } + + bool isBool() const { return _type == Type::Bool; } + bool isInt() const { return _type == Type::Int; } + bool isFloat() const { return _type == Type::Float; } + bool isVec3() const { return _type == Type::Vec3; } + bool isQuat() const { return _type == Type::Quat; } + bool isMat4() const { return _type == Type::Mat4; } + bool isString() const { return _type == Type::String; } + + void setBool(bool value) { assert(_type == Type::Bool); _val.boolVal = value; } + void setInt(int value) { assert(_type == Type::Int); _val.intVal = value; } + void setFloat(float value) { assert(_type == Type::Float); _val.floats[0] = value; } + void setVec3(const glm::vec3& value) { assert(_type == Type::Vec3); *reinterpret_cast(&_val) = value; } + void setQuat(const glm::quat& value) { assert(_type == Type::Quat); *reinterpret_cast(&_val) = value; } + void setMat4(const glm::mat4& value) { assert(_type == Type::Mat4); *reinterpret_cast(&_val) = value; } + void setString(const std::string& value) { assert(_type == Type::String); _stringVal = value; } + + bool getBool() const { assert(_type == Type::Bool); return _val.boolVal; } + int getInt() const { assert(_type == Type::Int); return _val.intVal; } + float getFloat() const { assert(_type == Type::Float); return _val.floats[0]; } + const glm::vec3& getVec3() const { assert(_type == Type::Vec3); return *reinterpret_cast(&_val); } + const glm::quat& getQuat() const { assert(_type == Type::Quat); return *reinterpret_cast(&_val); } + const glm::mat4& getMat4() const { assert(_type == Type::Mat4); return *reinterpret_cast(&_val); } + const std::string& getString() const { assert(_type == Type::String); return _stringVal; } + +protected: + Type _type; + std::string _stringVal; + union { + bool boolVal; + int intVal; + float floats[16]; + } _val; +}; + +class AnimVariantMap { +public: + + bool lookup(const std::string& key, bool defaultValue) const { + // check triggers first, then map + if (key.empty()) { + return defaultValue; + } else if (_triggers.find(key) != _triggers.end()) { + return true; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getBool() : defaultValue; + } + } + + int lookup(const std::string& key, int defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getInt() : defaultValue; + } + } + + float lookup(const std::string& key, float defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getFloat() : defaultValue; + } + } + + const glm::vec3& lookup(const std::string& key, const glm::vec3& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getVec3() : defaultValue; + } + } + + const glm::quat& lookup(const std::string& key, const glm::quat& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getQuat() : defaultValue; + } + } + + const glm::mat4& lookup(const std::string& key, const glm::mat4& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getMat4() : defaultValue; + } + } + + const std::string& lookup(const std::string& key, const std::string& defaultValue) const { + if (key.empty()) { + return defaultValue; + } else { + auto iter = _map.find(key); + return iter != _map.end() ? iter->second.getString() : defaultValue; + } + } + + void set(const std::string& key, bool value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, int value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, float value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const glm::vec3& value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const glm::quat& value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const glm::mat4& value) { _map[key] = AnimVariant(value); } + void set(const std::string& key, const std::string& value) { _map[key] = AnimVariant(value); } + + void setTrigger(const std::string& key) { _triggers.insert(key); } + void clearTriggers() { _triggers.clear(); } + +protected: + std::map _map; + std::set _triggers; +}; + +#endif // hifi_AnimVariant_h diff --git a/libraries/animation/src/Rig.cpp b/libraries/animation/src/Rig.cpp index a14491c911..f2ea922ab7 100644 --- a/libraries/animation/src/Rig.cpp +++ b/libraries/animation/src/Rig.cpp @@ -16,6 +16,9 @@ #include "AnimationHandle.h" #include "AnimationLogging.h" +#include "AnimSkeleton.h" + +#include "Rig.h" void Rig::HeadParameters::dump() const { qCDebug(animation, "HeadParameters ="); @@ -186,6 +189,12 @@ void Rig::deleteAnimations() { _animationHandles.clear(); } +void Rig::destroyAnimGraph() { + _animSkeleton = nullptr; + _animLoader = nullptr; + _animNode = nullptr; +} + void Rig::initJointStates(QVector states, glm::mat4 rootTransform, int rootJointIndex, int leftHandJointIndex, @@ -407,106 +416,223 @@ 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) { - return; - } - bool isMoving = false; + glm::vec3 front = worldRotation * IDENTITY_FRONT; - glm::vec3 right = worldRotation * IDENTITY_RIGHT; - const float PERCEPTIBLE_DELTA = 0.001f; - const float PERCEPTIBLE_SPEED = 0.1f; + // It can be more accurate/smooth to use velocity rather than position, // but some modes (e.g., hmd standing) update position without updating velocity. // It's very hard to debug hmd standing. (Look down at yourself, or have a second person observe. HMD third person is a bit undefined...) // So, let's create our own workingVelocity from the worldPosition... glm::vec3 positionDelta = worldPosition - _lastPosition; glm::vec3 workingVelocity = positionDelta / deltaTime; - // But for smoothest (non-hmd standing) results, go ahead and use velocity: + #if !WANT_DEBUG - // Note: Separately, we've arranged for starting/stopping animations by role (as we've done here) to pick up where they've left off when fading, - // so that you wouldn't notice the start/stop if it happens fast enough (e.g., one frame). But the print below would still be noisy. + // But for smoothest (non-hmd standing) results, go ahead and use velocity: if (!positionDelta.x && !positionDelta.y && !positionDelta.z) { workingVelocity = worldVelocity; } #endif - - float forwardSpeed = glm::dot(workingVelocity, front); - float rightLateralSpeed = glm::dot(workingVelocity, right); - float rightTurningDelta = glm::orientedAngle(front, _lastFront, IDENTITY_UP); - float rightTurningSpeed = rightTurningDelta / deltaTime; - bool isTurning = (std::abs(rightTurningDelta) > PERCEPTIBLE_DELTA) && (std::abs(rightTurningSpeed) > PERCEPTIBLE_SPEED); - bool isStrafing = std::abs(rightLateralSpeed) > PERCEPTIBLE_SPEED; - auto updateRole = [&](const QString& role, bool isOn) { - isMoving = isMoving || isOn; - if (isOn) { - if (!isRunningRole(role)) { - qCDebug(animation) << "Rig STARTING" << role; - startAnimationByRole(role); - } + + if (_enableAnimGraph) { + + glm::vec3 localVel = glm::inverse(worldRotation) * workingVelocity; + float forwardSpeed = glm::dot(localVel, IDENTITY_FRONT); + float lateralSpeed = glm::dot(localVel, IDENTITY_RIGHT); + float turningSpeed = glm::orientedAngle(front, _lastFront, IDENTITY_UP) / deltaTime; + + // sine wave LFO var for testing. + static float t = 0.0f; + _animVars.set("sine", static_cast(0.5 * sin(t) + 0.5)); + + // default anim vars to notMoving and notTurning + _animVars.set("isMovingForward", false); + _animVars.set("isMovingBackward", false); + _animVars.set("isMovingLeft", false); + _animVars.set("isMovingRight", false); + _animVars.set("isNotMoving", true); + _animVars.set("isTurningLeft", false); + _animVars.set("isTurningRight", false); + _animVars.set("isNotTurning", true); + + const float ANIM_WALK_SPEED = 1.4f; // m/s + _animVars.set("walkTimeScale", glm::clamp(0.5f, 2.0f, glm::length(localVel) / ANIM_WALK_SPEED)); + + const float MOVE_ENTER_SPEED_THRESHOLD = 0.2f; // m/sec + const float MOVE_EXIT_SPEED_THRESHOLD = 0.07f; // m/sec + const float TURN_ENTER_SPEED_THRESHOLD = 0.5f; // rad/sec + const float TURN_EXIT_SPEED_THRESHOLD = 0.2f; // rad/sec + + float moveThresh; + if (_state != RigRole::Move) { + moveThresh = MOVE_ENTER_SPEED_THRESHOLD; } else { - if (isRunningRole(role)) { - qCDebug(animation) << "Rig stopping" << role; - stopAnimationByRole(role); + moveThresh = MOVE_EXIT_SPEED_THRESHOLD; + } + + float turnThresh; + if (_state != RigRole::Turn) { + turnThresh = TURN_ENTER_SPEED_THRESHOLD; + } else { + turnThresh = TURN_EXIT_SPEED_THRESHOLD; + } + + if (glm::length(localVel) > moveThresh) { + if (fabs(forwardSpeed) > 0.5f * fabs(lateralSpeed)) { + if (forwardSpeed > 0.0f) { + // forward + _animVars.set("isMovingForward", true); + _animVars.set("isNotMoving", false); + + } else { + // backward + _animVars.set("isMovingBackward", true); + _animVars.set("isNotMoving", false); + } + } else { + if (lateralSpeed > 0.0f) { + // right + _animVars.set("isMovingRight", true); + _animVars.set("isNotMoving", false); + } else { + // left + _animVars.set("isMovingLeft", true); + _animVars.set("isNotMoving", false); + } + } + _state = RigRole::Move; + } else { + if (fabs(turningSpeed) > turnThresh) { + if (turningSpeed > 0.0f) { + // turning right + _animVars.set("isTurningRight", true); + _animVars.set("isNotTurning", false); + } else { + // turning left + _animVars.set("isTurningLeft", true); + _animVars.set("isNotTurning", false); + } + _state = RigRole::Turn; + } else { + // idle + _state = RigRole::Idle; } } - }; - updateRole("walk", forwardSpeed > PERCEPTIBLE_SPEED); - updateRole("backup", forwardSpeed < -PERCEPTIBLE_SPEED); - updateRole("rightTurn", isTurning && (rightTurningSpeed > 0.0f)); - updateRole("leftTurn", isTurning && (rightTurningSpeed < 0.0f)); - isStrafing = isStrafing && !isMoving; - updateRole("rightStrafe", isStrafing && (rightLateralSpeed > 0.0f)); - updateRole("leftStrafe", isStrafing && (rightLateralSpeed < 0.0f)); - updateRole("idle", !isMoving); // Must be last, as it makes isMoving bogus. + + t += deltaTime; + } + + if (_enableRig) { + bool isMoving = false; + + glm::vec3 right = worldRotation * IDENTITY_RIGHT; + const float PERCEPTIBLE_DELTA = 0.001f; + const float PERCEPTIBLE_SPEED = 0.1f; + + // Note: Separately, we've arranged for starting/stopping animations by role (as we've done here) to pick up where they've left off when fading, + // so that you wouldn't notice the start/stop if it happens fast enough (e.g., one frame). But the print below would still be noisy. + + float forwardSpeed = glm::dot(workingVelocity, front); + float rightLateralSpeed = glm::dot(workingVelocity, right); + float rightTurningDelta = glm::orientedAngle(front, _lastFront, IDENTITY_UP); + float rightTurningSpeed = rightTurningDelta / deltaTime; + bool isTurning = (std::abs(rightTurningDelta) > PERCEPTIBLE_DELTA) && (std::abs(rightTurningSpeed) > PERCEPTIBLE_SPEED); + bool isStrafing = std::abs(rightLateralSpeed) > PERCEPTIBLE_SPEED; + 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", forwardSpeed > PERCEPTIBLE_SPEED); + updateRole("backup", forwardSpeed < -PERCEPTIBLE_SPEED); + updateRole("rightTurn", isTurning && (rightTurningSpeed > 0.0f)); + updateRole("leftTurn", isTurning && (rightTurningSpeed < 0.0f)); + isStrafing = isStrafing && !isMoving; + 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 rootTransform) { - - // 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) { - 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); + + if (_enableAnimGraph) { + if (!_animNode) { + return; + } + + // evaluate the animation + AnimNode::Triggers triggersOut; + AnimPoseVec poses = _animNode->evaluate(_animVars, deltaTime, triggersOut); + _animVars.clearTriggers(); + for (auto& trigger : triggersOut) { + _animVars.setTrigger(trigger); + } + + // copy poses into jointStates + const float PRIORITY = 1.0f; + for (size_t i = 0; i < poses.size(); i++) { + setJointRotationInConstrainedFrame((int)i, glm::inverse(_animSkeleton->getRelativeBindPose(i).rot) * poses[i].rot, PRIORITY, false); + } + + } else { + + // 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) { + 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 + } } - 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 - } - } + } + // sum the remaining fade data + float fadeTotal = 0.0f; + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + fadeTotal += handle->getFade(); + } + float fadeSumSoFar = 0.0f; + foreach (const AnimationHandlePointer& handle, _runningAnimations) { + handle->setPriority(1.0f); + // if no fadeTotal, everyone's (typically just one running) is starting at zero. In that case, blend equally. + float normalizedFade = (fadeTotal != 0.0f) ? (handle->getFade() / fadeTotal) : (1.0f / _runningAnimations.count()); + assert(normalizedFade != 0.0f); + // 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., 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 / ((fadeSumSoFar / normalizedFade) + 1.0f); + assert((0.0f <= mix) && (mix <= 1.0f)); + fadeSumSoFar += normalizedFade; + handle->setMix(mix); + handle->simulate(deltaTime); + } } - // sum the remaining fade data - float fadeTotal = 0.0f; - foreach (const AnimationHandlePointer& handle, _runningAnimations) { - fadeTotal += handle->getFade(); - } - float fadeSumSoFar = 0.0f; - foreach (const AnimationHandlePointer& handle, _runningAnimations) { - handle->setPriority(1.0f); - // if no fadeTotal, everyone's (typically just one running) is starting at zero. In that case, blend equally. - float normalizedFade = (fadeTotal != 0.0f) ? (handle->getFade() / fadeTotal) : (1.0f / _runningAnimations.count()); - assert(normalizedFade != 0.0f); - // 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., 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 / ((fadeSumSoFar / normalizedFade) + 1.0f); - assert((0.0f <= mix) && (mix <= 1.0f)); - fadeSumSoFar += normalizedFade; - handle->setMix(mix); - handle->simulate(deltaTime); - } - + for (int i = 0; i < _jointStates.size(); i++) { updateJointState(i, rootTransform); } @@ -859,6 +985,7 @@ void Rig::updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& updateEyeJoint(leftEyeIndex, modelTranslation, modelRotation, worldHeadOrientation, lookAtSpot, saccade); updateEyeJoint(rightEyeIndex, modelTranslation, modelRotation, worldHeadOrientation, lookAtSpot, saccade); } + void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm::quat& modelRotation, const glm::quat& worldHeadOrientation, const glm::vec3& lookAtSpot, const glm::vec3& saccade) { if (index >= 0 && _jointStates[index].getParentIndex() >= 0) { auto& state = _jointStates[index]; @@ -877,3 +1004,30 @@ void Rig::updateEyeJoint(int index, const glm::vec3& modelTranslation, const glm state.getDefaultRotation(), DEFAULT_PRIORITY); } } + +void Rig::initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry) { + if (!_enableAnimGraph) { + return; + } + + // convert to std::vector of joints + std::vector joints; + joints.reserve(fbxGeometry.joints.size()); + for (auto& joint : fbxGeometry.joints) { + joints.push_back(joint); + } + + // create skeleton + AnimPose geometryOffset(fbxGeometry.offset); + _animSkeleton = std::make_shared(joints, geometryOffset); + + // load the anim graph + _animLoader.reset(new AnimNodeLoader(url)); + connect(_animLoader.get(), &AnimNodeLoader::success, [this](AnimNode::Pointer nodeIn) { + _animNode = nodeIn; + _animNode->setSkeleton(_animSkeleton); + }); + connect(_animLoader.get(), &AnimNodeLoader::error, [this, url](int error, QString str) { + qCCritical(animation) << "Error loading" << url.toDisplayString() << "code = " << error << "str =" << str; + }); +} diff --git a/libraries/animation/src/Rig.h b/libraries/animation/src/Rig.h index 45fee8aa32..0bf0645b4d 100644 --- a/libraries/animation/src/Rig.h +++ b/libraries/animation/src/Rig.h @@ -40,6 +40,9 @@ #include "JointState.h" // We might want to change this (later) to something that doesn't depend on gpu, fbx and model. -HRS +#include "AnimNode.h" +#include "AnimNodeLoader.h" + class AnimationHandle; typedef std::shared_ptr AnimationHandlePointer; @@ -80,6 +83,7 @@ public: bool isRunningRole(const QString& role); // There can be multiple animations per role, so this is more general than isRunningAnimation. const QList& getRunningAnimations() const { return _runningAnimations; } void deleteAnimations(); + void destroyAnimGraph(); const QList& getAnimationHandles() const { return _animationHandles; } void startAnimation(const QString& url, 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()); @@ -155,6 +159,8 @@ public: virtual void updateJointState(int index, glm::mat4 rootTransform) = 0; void setEnableRig(bool isEnabled) { _enableRig = isEnabled; } + void setEnableAnimGraph(bool isEnabled) { _enableAnimGraph = isEnabled; } + bool getEnableAnimGraph() const { return _enableAnimGraph; } void updateFromHeadParameters(const HeadParameters& params); void updateEyeJoints(int leftEyeIndex, int rightEyeIndex, const glm::vec3& modelTranslation, const glm::quat& modelRotation, @@ -163,6 +169,11 @@ public: virtual void setHandPosition(int jointIndex, const glm::vec3& position, const glm::quat& rotation, float scale, float priority) = 0; + void initAnimGraph(const QUrl& url, const FBXGeometry& fbxGeometry); + + AnimNode::ConstPointer getAnimNode() const { return _animNode; } + AnimSkeleton::ConstPointer getAnimSkeleton() const { return _animSkeleton; } + protected: void updateLeanJoint(int index, float leanSideways, float leanForward, float torsoTwist); @@ -183,9 +194,21 @@ public: QList _animationHandles; QList _runningAnimations; - bool _enableRig; + bool _enableRig = false; + bool _enableAnimGraph = false; glm::vec3 _lastFront; glm::vec3 _lastPosition; + + std::shared_ptr _animNode; + std::shared_ptr _animSkeleton; + std::unique_ptr _animLoader; + AnimVariantMap _animVars; + enum class RigRole { + Idle = 0, + Turn, + Move + }; + RigRole _state = RigRole::Idle; }; #endif /* defined(__hifi__Rig__) */ diff --git a/libraries/fbx/src/FBXReader.cpp b/libraries/fbx/src/FBXReader.cpp index de28aee4c8..35390a8e44 100644 --- a/libraries/fbx/src/FBXReader.cpp +++ b/libraries/fbx/src/FBXReader.cpp @@ -1722,7 +1722,7 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping glm::vec3 rotationOffset; glm::vec3 preRotation, rotation, postRotation; glm::vec3 scale = glm::vec3(1.0f, 1.0f, 1.0f); - glm::vec3 scalePivot, rotationPivot; + glm::vec3 scalePivot, rotationPivot, scaleOffset; bool rotationMinX = false, rotationMinY = false, rotationMinZ = false; bool rotationMaxX = false, rotationMaxY = false, rotationMaxZ = false; glm::vec3 rotationMin, rotationMax; @@ -1771,12 +1771,14 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping } else if (property.properties.at(0) == "Lcl Scaling") { scale = getVec3(property.properties, index); + } else if (property.properties.at(0) == "ScalingOffset") { + scaleOffset = getVec3(property.properties, index); + + // NOTE: these rotation limits are stored in degrees (NOT radians) } else if (property.properties.at(0) == "RotationMin") { rotationMin = getVec3(property.properties, index); - } - // NOTE: these rotation limits are stored in degrees (NOT radians) - else if (property.properties.at(0) == "RotationMax") { + } else if (property.properties.at(0) == "RotationMax") { rotationMax = getVec3(property.properties, index); } else if (property.properties.at(0) == "RotationMinX") { @@ -1843,8 +1845,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping model.preRotation = glm::quat(glm::radians(preRotation)); model.rotation = glm::quat(glm::radians(rotation)); model.postRotation = glm::quat(glm::radians(postRotation)); - model.postTransform = glm::translate(-rotationPivot) * glm::translate(scalePivot) * - glm::scale(scale) * glm::translate(-scalePivot); + model.postTransform = glm::translate(-rotationPivot) * glm::translate(scaleOffset) * + glm::translate(scalePivot) * glm::scale(scale) * glm::translate(-scalePivot); // NOTE: angles from the FBX file are in degrees // so we convert them to radians for the FBXModel class model.rotationMin = glm::radians(glm::vec3(rotationMinX ? rotationMin.x : -180.0f, @@ -2306,7 +2308,9 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping break; } } - + + joint.bindTransformFoundInCluster = false; + geometry.joints.append(joint); geometry.jointIndices.insert(model.name, geometry.joints.size()); @@ -2534,7 +2538,8 @@ FBXGeometry* extractFBXGeometry(const FBXNode& node, const QVariantHash& mapping FBXJoint& joint = geometry.joints[fbxCluster.jointIndex]; joint.inverseBindRotation = glm::inverse(extractRotation(cluster.transformLink)); joint.bindTransform = cluster.transformLink; - + joint.bindTransformFoundInCluster = true; + // update the bind pose extents glm::vec3 bindTranslation = extractTranslation(geometry.offset * joint.bindTransform); geometry.bindExtents.addPoint(bindTranslation); diff --git a/libraries/fbx/src/FBXReader.h b/libraries/fbx/src/FBXReader.h index 471a9c1777..158b5581c6 100644 --- a/libraries/fbx/src/FBXReader.h +++ b/libraries/fbx/src/FBXReader.h @@ -64,12 +64,18 @@ public: int parentIndex; float distanceToParent; float boneRadius; - glm::vec3 translation; - glm::mat4 preTransform; - glm::quat preRotation; - glm::quat rotation; - glm::quat postRotation; - glm::mat4 postTransform; + + // http://download.autodesk.com/us/fbx/20112/FBX_SDK_HELP/SDKRef/a00209.html + + glm::vec3 translation; // T + glm::mat4 preTransform; // Roff * Rp + glm::quat preRotation; // Rpre + glm::quat rotation; // R + glm::quat postRotation; // Rpost + glm::mat4 postTransform; // Rp-1 * Soff * Sp * S * Sp-1 + + // World = ParentWorld * T * (Roff * Rp) * Rpre * R * Rpost * (Rp-1 * Soff * Sp * S * Sp-1) + glm::mat4 transform; glm::vec3 rotationMin; // radians glm::vec3 rotationMax; // radians @@ -78,6 +84,7 @@ public: glm::mat4 bindTransform; QString name; bool isSkeletonJoint; + bool bindTransformFoundInCluster; }; diff --git a/libraries/render-utils/src/AnimDebugDraw.cpp b/libraries/render-utils/src/AnimDebugDraw.cpp new file mode 100644 index 0000000000..1c0f7e0054 --- /dev/null +++ b/libraries/render-utils/src/AnimDebugDraw.cpp @@ -0,0 +1,430 @@ +// +// AnimDebugDraw.cpp +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "animdebugdraw_vert.h" +#include "animdebugdraw_frag.h" +#include +#include "AbstractViewStateInterface.h" +#include "RenderUtilsLogging.h" +#include "GLMHelpers.h" + +#include "AnimDebugDraw.h" + +struct Vertex { + glm::vec3 pos; + uint32_t rgba; +}; + +class AnimDebugDrawData { +public: + typedef render::Payload Payload; + typedef Payload::DataPointer Pointer; + + AnimDebugDrawData() { + + _vertexFormat = std::make_shared(); + _vertexBuffer = std::make_shared(); + _indexBuffer = std::make_shared(); + + _vertexFormat->setAttribute(gpu::Stream::POSITION, 0, gpu::Element::VEC3F_XYZ, 0); + _vertexFormat->setAttribute(gpu::Stream::COLOR, 0, gpu::Element::COLOR_RGBA_32, offsetof(Vertex, rgba)); + } + + void render(RenderArgs* args) { + auto& batch = *args->_batch; + batch.setPipeline(_pipeline); + auto transform = Transform{}; + batch.setModelTransform(transform); + + batch.setInputFormat(_vertexFormat); + batch.setInputBuffer(0, _vertexBuffer, 0, sizeof(Vertex)); + batch.setIndexBuffer(gpu::UINT16, _indexBuffer, 0); + + auto numIndices = _indexBuffer->getSize() / sizeof(uint16_t); + batch.drawIndexed(gpu::LINES, numIndices); + } + + gpu::PipelinePointer _pipeline; + render::ItemID _item; + gpu::Stream::FormatPointer _vertexFormat; + gpu::BufferPointer _vertexBuffer; + gpu::BufferPointer _indexBuffer; +}; + +typedef render::Payload AnimDebugDrawPayload; + +namespace render { + template <> const ItemKey payloadGetKey(const AnimDebugDrawData::Pointer& data) { return ItemKey::Builder::opaqueShape(); } + template <> const Item::Bound payloadGetBound(const AnimDebugDrawData::Pointer& data) { return Item::Bound(); } + template <> void payloadRender(const AnimDebugDrawData::Pointer& data, RenderArgs* args) { + data->render(args); + } +} + +static AnimDebugDraw* instance = nullptr; + +AnimDebugDraw& AnimDebugDraw::getInstance() { + if (!instance) { + instance = new AnimDebugDraw(); + } + return *instance; +} + +static uint32_t toRGBA(uint8_t r, uint8_t g, uint8_t b, uint8_t a) { + return ((uint32_t)r | (uint32_t)g << 8 | (uint32_t)b << 16 | (uint32_t)a << 24); +} + +static uint32_t toRGBA(const glm::vec4& v) { + return toRGBA(static_cast(v.r * 255.0f), + static_cast(v.g * 255.0f), + static_cast(v.b * 255.0f), + static_cast(v.a * 255.0f)); +} + +gpu::PipelinePointer AnimDebugDraw::_pipeline; + +AnimDebugDraw::AnimDebugDraw() : + _itemID(0) { + + auto state = std::make_shared(); + state->setCullMode(gpu::State::CULL_BACK); + state->setDepthTest(true, true, gpu::LESS_EQUAL); + state->setBlendFunction(false, gpu::State::SRC_ALPHA, gpu::State::BLEND_OP_ADD, + gpu::State::INV_SRC_ALPHA, gpu::State::FACTOR_ALPHA, + gpu::State::BLEND_OP_ADD, gpu::State::ONE); + auto vertShader = gpu::ShaderPointer(gpu::Shader::createVertex(std::string(animdebugdraw_vert))); + auto fragShader = gpu::ShaderPointer(gpu::Shader::createPixel(std::string(animdebugdraw_frag))); + auto program = gpu::ShaderPointer(gpu::Shader::createProgram(vertShader, fragShader)); + _pipeline = gpu::PipelinePointer(gpu::Pipeline::create(program, state)); + + _animDebugDrawData = std::make_shared(); + _animDebugDrawPayload = std::make_shared(_animDebugDrawData); + + _animDebugDrawData->_pipeline = _pipeline; + + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + if (scene) { + _itemID = scene->allocateID(); + render::PendingChanges pendingChanges; + pendingChanges.resetItem(_itemID, _animDebugDrawPayload); + scene->enqueuePendingChanges(pendingChanges); + } + + // HACK: add red, green and blue axis at (1,1,1) + _animDebugDrawData->_vertexBuffer->resize(sizeof(Vertex) * 6); + Vertex* data = (Vertex*)_animDebugDrawData->_vertexBuffer->editData(); + + data[0].pos = glm::vec3(1.0, 1.0f, 1.0f); + data[0].rgba = toRGBA(255, 0, 0, 255); + data[1].pos = glm::vec3(2.0, 1.0f, 1.0f); + data[1].rgba = toRGBA(255, 0, 0, 255); + + data[2].pos = glm::vec3(1.0, 1.0f, 1.0f); + data[2].rgba = toRGBA(0, 255, 0, 255); + data[3].pos = glm::vec3(1.0, 2.0f, 1.0f); + data[3].rgba = toRGBA(0, 255, 0, 255); + + data[4].pos = glm::vec3(1.0, 1.0f, 1.0f); + data[4].rgba = toRGBA(0, 0, 255, 255); + data[5].pos = glm::vec3(1.0, 1.0f, 2.0f); + data[5].rgba = toRGBA(0, 0, 255, 255); + + _animDebugDrawData->_indexBuffer->resize(sizeof(uint16_t) * 6); + uint16_t* indices = (uint16_t*)_animDebugDrawData->_indexBuffer->editData(); + for (int i = 0; i < 6; i++) { + indices[i] = i; + } + +} + +AnimDebugDraw::~AnimDebugDraw() { + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + if (scene && _itemID) { + render::PendingChanges pendingChanges; + pendingChanges.removeItem(_itemID); + scene->enqueuePendingChanges(pendingChanges); + } +} + +void AnimDebugDraw::addSkeleton(std::string key, AnimSkeleton::ConstPointer skeleton, const AnimPose& rootPose, const glm::vec4& color) { + _skeletons[key] = SkeletonInfo(skeleton, rootPose, color); +} + +void AnimDebugDraw::removeSkeleton(std::string key) { + _skeletons.erase(key); +} + +void AnimDebugDraw::addAnimNode(std::string key, AnimNode::ConstPointer animNode, const AnimPose& rootPose, const glm::vec4& color) { + _animNodes[key] = AnimNodeInfo(animNode, rootPose, color); +} + +void AnimDebugDraw::removeAnimNode(std::string key) { + _animNodes.erase(key); +} + +static const uint32_t red = toRGBA(255, 0, 0, 255); +static const uint32_t green = toRGBA(0, 255, 0, 255); +static const uint32_t blue = toRGBA(0, 0, 255, 255); + +const int NUM_CIRCLE_SLICES = 24; + +static void addBone(const AnimPose& rootPose, const AnimPose& pose, float radius, Vertex*& v) { + + AnimPose finalPose = rootPose * pose; + glm::vec3 base = rootPose * pose.trans; + + glm::vec3 xRing[NUM_CIRCLE_SLICES + 1]; // one extra for last index. + glm::vec3 yRing[NUM_CIRCLE_SLICES + 1]; + glm::vec3 zRing[NUM_CIRCLE_SLICES + 1]; + const float dTheta = (2.0f * (float)M_PI) / NUM_CIRCLE_SLICES; + for (int i = 0; i < NUM_CIRCLE_SLICES + 1; i++) { + float rCosTheta = radius * cos(dTheta * i); + float rSinTheta = radius * sin(dTheta * i); + xRing[i] = finalPose * glm::vec3(0.0f, rCosTheta, rSinTheta); + yRing[i] = finalPose * glm::vec3(rCosTheta, 0.0f, rSinTheta); + zRing[i] = finalPose * glm::vec3(rCosTheta, rSinTheta, 0.0f); + } + + // x-axis + v->pos = base; + v->rgba = red; + v++; + v->pos = finalPose * glm::vec3(radius * 2.0f, 0.0f, 0.0f); + v->rgba = red; + v++; + + // x-ring + for (int i = 0; i < NUM_CIRCLE_SLICES; i++) { + v->pos = xRing[i]; + v->rgba = red; + v++; + v->pos = xRing[i + 1]; + v->rgba = red; + v++; + } + + // y-axis + v->pos = base; + v->rgba = green; + v++; + v->pos = finalPose * glm::vec3(0.0f, radius * 2.0f, 0.0f); + v->rgba = green; + v++; + + // y-ring + for (int i = 0; i < NUM_CIRCLE_SLICES; i++) { + v->pos = yRing[i]; + v->rgba = green; + v++; + v->pos = yRing[i + 1]; + v->rgba = green; + v++; + } + + // z-axis + v->pos = base; + v->rgba = blue; + v++; + v->pos = finalPose * glm::vec3(0.0f, 0.0f, radius * 2.0f); + v->rgba = blue; + v++; + + // z-ring + for (int i = 0; i < NUM_CIRCLE_SLICES; i++) { + v->pos = zRing[i]; + v->rgba = blue; + v++; + v->pos = zRing[i + 1]; + v->rgba = blue; + v++; + } +} + +static void addLink(const AnimPose& rootPose, const AnimPose& pose, const AnimPose& parentPose, + float radius, const glm::vec4& colorVec, Vertex*& v) { + + uint32_t color = toRGBA(colorVec); + + AnimPose pose0 = rootPose * parentPose; + AnimPose pose1 = rootPose * pose; + + glm::vec3 boneAxisWorld = glm::normalize(pose1.trans - pose0.trans); + glm::vec3 boneAxis0 = glm::normalize(pose0.inverse().xformVector(boneAxisWorld)); + glm::vec3 boneAxis1 = glm::normalize(pose1.inverse().xformVector(boneAxisWorld)); + + glm::vec3 boneBase = pose0 * (boneAxis0 * radius); + glm::vec3 boneTip = pose1 * (boneAxis1 * -radius); + + const int NUM_BASE_CORNERS = 4; + + // make sure there's room between the two bones to draw a nice bone link. + if (glm::dot(boneTip - pose0.trans, boneAxisWorld) > glm::dot(boneBase - pose0.trans, boneAxisWorld)) { + + // there is room, so lets draw a nice bone + + glm::vec3 uAxis, vAxis, wAxis; + generateBasisVectors(boneAxis0, glm::vec3(1, 0, 0), uAxis, vAxis, wAxis); + + glm::vec3 boneBaseCorners[NUM_BASE_CORNERS]; + boneBaseCorners[0] = pose0 * ((uAxis * radius) + (vAxis * radius) + (wAxis * radius)); + boneBaseCorners[1] = pose0 * ((uAxis * radius) - (vAxis * radius) + (wAxis * radius)); + boneBaseCorners[2] = pose0 * ((uAxis * radius) - (vAxis * radius) - (wAxis * radius)); + boneBaseCorners[3] = pose0 * ((uAxis * radius) + (vAxis * radius) - (wAxis * radius)); + + for (int i = 0; i < NUM_BASE_CORNERS; i++) { + v->pos = boneBaseCorners[i]; + v->rgba = color; + v++; + v->pos = boneBaseCorners[(i + 1) % NUM_BASE_CORNERS]; + v->rgba = color; + v++; + } + + for (int i = 0; i < NUM_BASE_CORNERS; i++) { + v->pos = boneBaseCorners[i]; + v->rgba = color; + v++; + v->pos = boneTip; + v->rgba = color; + v++; + } + } else { + // There's no room between the bones to draw the link. + // just draw a line between the two bone centers. + // We add the same line multiple times, so the vertex count is correct. + for (int i = 0; i < NUM_BASE_CORNERS * 2; i++) { + v->pos = pose0.trans; + v->rgba = color; + v++; + v->pos = pose1.trans; + v->rgba = color; + v++; + } + } +} + +void AnimDebugDraw::update() { + + render::ScenePointer scene = AbstractViewStateInterface::instance()->getMain3DScene(); + if (!scene) { + return; + } + + render::PendingChanges pendingChanges; + pendingChanges.updateItem(_itemID, [&](AnimDebugDrawData& data) { + + const size_t VERTICES_PER_BONE = (6 + (NUM_CIRCLE_SLICES * 2) * 3); + const size_t VERTICES_PER_LINK = 8 * 2; + + const float BONE_RADIUS = 0.0075f; + + // figure out how many verts we will need. + int numVerts = 0; + for (auto&& iter : _skeletons) { + AnimSkeleton::ConstPointer& skeleton = std::get<0>(iter.second); + numVerts += skeleton->getNumJoints() * VERTICES_PER_BONE; + for (int i = 0; i < skeleton->getNumJoints(); i++) { + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + numVerts += VERTICES_PER_LINK; + } + } + } + + for (auto&& iter : _animNodes) { + AnimNode::ConstPointer& animNode = std::get<0>(iter.second); + auto poses = animNode->getPosesInternal(); + numVerts += poses.size() * VERTICES_PER_BONE; + auto skeleton = animNode->getSkeleton(); + for (size_t i = 0; i < poses.size(); i++) { + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + numVerts += VERTICES_PER_LINK; + } + } + } + + data._vertexBuffer->resize(sizeof(Vertex) * numVerts); + Vertex* verts = (Vertex*)data._vertexBuffer->editData(); + Vertex* v = verts; + for (auto&& iter : _skeletons) { + AnimSkeleton::ConstPointer& skeleton = std::get<0>(iter.second); + AnimPose rootPose = std::get<1>(iter.second); + int hipsIndex = skeleton->nameToJointIndex("Hips"); + if (hipsIndex >= 0) { + rootPose.trans -= skeleton->getRelativeBindPose(hipsIndex).trans; + } + glm::vec4 color = std::get<2>(iter.second); + + for (int i = 0; i < skeleton->getNumJoints(); i++) { + AnimPose pose = skeleton->getAbsoluteBindPose(i); + + const float radius = BONE_RADIUS / (pose.scale.x * rootPose.scale.x); + + // draw bone + addBone(rootPose, pose, radius, v); + + // draw link to parent + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + assert(parentIndex < skeleton->getNumJoints()); + AnimPose parentPose = skeleton->getAbsoluteBindPose(parentIndex); + addLink(rootPose, pose, parentPose, radius, color, v); + } + } + } + + for (auto&& iter : _animNodes) { + AnimNode::ConstPointer& animNode = std::get<0>(iter.second); + AnimPose rootPose = std::get<1>(iter.second); + if (animNode->_skeleton) { + int hipsIndex = animNode->_skeleton->nameToJointIndex("Hips"); + if (hipsIndex >= 0) { + rootPose.trans -= animNode->_skeleton->getRelativeBindPose(hipsIndex).trans; + } + } + glm::vec4 color = std::get<2>(iter.second); + + auto poses = animNode->getPosesInternal(); + + auto skeleton = animNode->getSkeleton(); + + std::vector absAnimPose; + absAnimPose.resize(skeleton->getNumJoints()); + + for (size_t i = 0; i < poses.size(); i++) { + + auto parentIndex = skeleton->getParentIndex(i); + if (parentIndex >= 0) { + absAnimPose[i] = absAnimPose[parentIndex] * poses[i]; + } else { + absAnimPose[i] = poses[i]; + } + + const float radius = BONE_RADIUS / (absAnimPose[i].scale.x * rootPose.scale.x); + addBone(rootPose, absAnimPose[i], radius, v); + + if (parentIndex >= 0) { + assert((size_t)parentIndex < poses.size()); + // draw line to parent + addLink(rootPose, absAnimPose[i], absAnimPose[parentIndex], radius, color, v); + } + } + } + + assert(numVerts == (v - verts)); + + data._indexBuffer->resize(sizeof(uint16_t) * numVerts); + uint16_t* indices = (uint16_t*)data._indexBuffer->editData(); + for (int i = 0; i < numVerts; i++) { + indices[i] = i; + } + }); + scene->enqueuePendingChanges(pendingChanges); +} diff --git a/libraries/render-utils/src/AnimDebugDraw.h b/libraries/render-utils/src/AnimDebugDraw.h new file mode 100644 index 0000000000..489213b80b --- /dev/null +++ b/libraries/render-utils/src/AnimDebugDraw.h @@ -0,0 +1,56 @@ +// +// AnimDebugDraw.h +// +// Copyright 2015 High Fidelity, Inc. +// +// 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_AnimDebugDraw_h +#define hifi_AnimDebugDraw_h + +#include + +#include "render/Scene.h" +#include "gpu/Pipeline.h" +#include "AnimNode.h" +#include "AnimSkeleton.h" + +class AnimDebugDrawData; +typedef render::Payload AnimDebugDrawPayload; + +class AnimDebugDraw { +public: + static AnimDebugDraw& getInstance(); + + AnimDebugDraw(); + ~AnimDebugDraw(); + + void addSkeleton(std::string key, AnimSkeleton::ConstPointer skeleton, const AnimPose& rootPose, const glm::vec4& color); + void removeSkeleton(std::string key); + + void addAnimNode(std::string key, AnimNode::ConstPointer animNode, const AnimPose& rootPose, const glm::vec4& color); + void removeAnimNode(std::string key); + + void update(); + +protected: + std::shared_ptr _animDebugDrawData; + std::shared_ptr _animDebugDrawPayload; + render::ItemID _itemID; + + static gpu::PipelinePointer _pipeline; + + typedef std::tuple SkeletonInfo; + typedef std::tuple AnimNodeInfo; + + std::unordered_map _skeletons; + std::unordered_map _animNodes; + + // no copies + AnimDebugDraw(const AnimDebugDraw&) = delete; + AnimDebugDraw& operator=(const AnimDebugDraw&) = delete; +}; + +#endif // hifi_AnimDebugDraw diff --git a/libraries/render-utils/src/Model.cpp b/libraries/render-utils/src/Model.cpp index 14b1c58321..3f0516b415 100644 --- a/libraries/render-utils/src/Model.cpp +++ b/libraries/render-utils/src/Model.cpp @@ -1419,6 +1419,7 @@ void Model::deleteGeometry() { _rig->clearJointStates(); _meshStates.clear(); _rig->deleteAnimations(); + _rig->destroyAnimGraph(); _blendedBlendshapeCoefficients.clear(); } diff --git a/libraries/render-utils/src/animdebugdraw.slf b/libraries/render-utils/src/animdebugdraw.slf new file mode 100644 index 0000000000..8a3aca055e --- /dev/null +++ b/libraries/render-utils/src/animdebugdraw.slf @@ -0,0 +1,19 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// unlit untextured fragment shader +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +in vec4 _color; + +out vec4 _fragColor; + +void main(void) { + _fragColor = _color; +} diff --git a/libraries/render-utils/src/animdebugdraw.slv b/libraries/render-utils/src/animdebugdraw.slv new file mode 100644 index 0000000000..f3117714b0 --- /dev/null +++ b/libraries/render-utils/src/animdebugdraw.slv @@ -0,0 +1,26 @@ +<@include gpu/Config.slh@> +<$VERSION_HEADER$> +// Generated on <$_SCRIBE_DATE$> +// +// unlit untextured vertex shader +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +<@include gpu/Inputs.slh@> + +<@include gpu/Transform.slh@> + +<$declareStandardTransform()$> + +out vec4 _color; + +void main(void) { + // pass along the diffuse color + _color = inColor.rgba; + + TransformCamera cam = getTransformCamera(); + TransformObject obj = getTransformObject(); + <$transformModelToClipPos(cam, obj, inPosition, gl_Position)$> +} diff --git a/libraries/shared/src/GLMHelpers.cpp b/libraries/shared/src/GLMHelpers.cpp index 4f2124a343..7d56157e53 100644 --- a/libraries/shared/src/GLMHelpers.cpp +++ b/libraries/shared/src/GLMHelpers.cpp @@ -408,3 +408,22 @@ glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p) { glm::vec4 temp = m * glm::vec4(p, 1.0f); return glm::vec3(temp.x / temp.w, temp.y / temp.w, temp.z / temp.w); } + +glm::vec3 transformVector(const glm::mat4& m, const glm::vec3& v) { + glm::mat3 rot(m); + return glm::inverse(glm::transpose(rot)) * v; +} + +void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& secondaryAxis, + glm::vec3& uAxisOut, glm::vec3& vAxisOut, glm::vec3& wAxisOut) { + + uAxisOut = glm::normalize(primaryAxis); + wAxisOut = glm::cross(uAxisOut, secondaryAxis); + if (glm::length(wAxisOut) > 0.0f) { + wAxisOut = glm::normalize(wAxisOut); + } else { + wAxisOut = glm::normalize(glm::cross(uAxisOut, glm::vec3(0, 1, 0))); + } + vAxisOut = glm::cross(wAxisOut, uAxisOut); +} + diff --git a/libraries/shared/src/GLMHelpers.h b/libraries/shared/src/GLMHelpers.h index a35cdfbfb3..6683088306 100644 --- a/libraries/shared/src/GLMHelpers.h +++ b/libraries/shared/src/GLMHelpers.h @@ -177,9 +177,35 @@ T toNormalizedDeviceScale(const T& value, const T& size) { #define PITCH(euler) euler.x #define ROLL(euler) euler.z +// vec2 lerp - linear interpolate +template +glm::detail::tvec2 lerp(const glm::detail::tvec2& x, const glm::detail::tvec2& y, T a) { + return x * (T(1) - a) + (y * a); +} + +// vec3 lerp - linear interpolate +template +glm::detail::tvec3 lerp(const glm::detail::tvec3& x, const glm::detail::tvec3& y, T a) { + return x * (T(1) - a) + (y * a); +} + +// vec4 lerp - linear interpolate +template +glm::detail::tvec4 lerp(const glm::detail::tvec4& x, const glm::detail::tvec4& y, T a) { + return x * (T(1) - a) + (y * a); +} + glm::mat4 createMatFromQuatAndPos(const glm::quat& q, const glm::vec3& p); glm::quat cancelOutRollAndPitch(const glm::quat& q); glm::mat4 cancelOutRollAndPitch(const glm::mat4& m); glm::vec3 transformPoint(const glm::mat4& m, const glm::vec3& p); +glm::vec3 transformVector(const glm::mat4& m, const glm::vec3& v); + +// Calculate an orthogonal basis from a primary and secondary axis. +// The uAxis, vAxis & wAxis will form an orthognal basis. +// The primary axis will be the uAxis. +// The vAxis will be as close as possible to to the secondary axis. +void generateBasisVectors(const glm::vec3& primaryAxis, const glm::vec3& secondaryAxis, + glm::vec3& uAxisOut, glm::vec3& vAxisOut, glm::vec3& wAxisOut); #endif // hifi_GLMHelpers_h diff --git a/libraries/shared/src/Transform.h b/libraries/shared/src/Transform.h index 3064e18471..699eb8ef0a 100644 --- a/libraries/shared/src/Transform.h +++ b/libraries/shared/src/Transform.h @@ -129,6 +129,7 @@ public: static Transform& inverseMult(Transform& result, const Transform& left, const Transform& right); Vec4 transform(const Vec4& pos) const; + Vec3 transform(const Vec3& pos) const; protected: @@ -504,6 +505,12 @@ inline Transform::Vec4 Transform::transform(const Vec4& pos) const { return m * pos; } +inline Transform::Vec3 Transform::transform(const Vec3& pos) const { + Mat4 m; + getMatrix(m); + Vec4 result = m * Vec4(pos, 1.0f); + return Vec3(result.x / result.w, result.y / result.w, result.z / result.w); +} inline Transform::Mat4& Transform::getCachedMatrix(Transform::Mat4& result) const { updateCache(); diff --git a/tests/animation/CMakeLists.txt b/tests/animation/CMakeLists.txt index 2e9dbc9424..a66e391f69 100644 --- a/tests/animation/CMakeLists.txt +++ b/tests/animation/CMakeLists.txt @@ -1,7 +1,7 @@ # Declare dependencies macro (setup_testcase_dependencies) # link in the shared libraries - link_hifi_libraries(shared animation gpu fbx model) + link_hifi_libraries(shared animation gpu fbx model networking) copy_dlls_beside_windows_executable() endmacro () diff --git a/tests/animation/src/AnimTests.cpp b/tests/animation/src/AnimTests.cpp new file mode 100644 index 0000000000..806560d96f --- /dev/null +++ b/tests/animation/src/AnimTests.cpp @@ -0,0 +1,225 @@ +// +// AnimTests.cpp +// +// Copyright 2015 High Fidelity, Inc. +// +// Distributed under the Apache License, Version 2.0. +// See the accompanying file LICENSE or http://www.apache.org/licenses/LICENSE-2.0.html +// + +#include "AnimTests.h" +#include "AnimNodeLoader.h" +#include "AnimClip.h" +#include "AnimBlendLinear.h" +#include "AnimationLogging.h" +#include "AnimVariant.h" + +#include <../QTestExtensions.h> + +QTEST_MAIN(AnimTests) + +const float EPSILON = 0.001f; + +void AnimTests::initTestCase() { + auto animationCache = DependencyManager::set(); + auto resourceCacheSharedItems = DependencyManager::set(); +} + +void AnimTests::cleanupTestCase() { + DependencyManager::destroy(); +} + +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"; + float startFrame = 2.0f; + float endFrame = 20.0f; + float timeScale = 1.1f; + bool loopFlag = true; + + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + + QVERIFY(clip.getID() == id); + QVERIFY(clip.getType() == AnimNode::Type::Clip); + + QVERIFY(clip._url == url); + QVERIFY(clip._startFrame == startFrame); + QVERIFY(clip._endFrame == endFrame); + QVERIFY(clip._timeScale == timeScale); + QVERIFY(clip._loopFlag == loopFlag); +} + +static float framesToSec(float secs) { + const float FRAMES_PER_SECOND = 30.0f; + return secs / FRAMES_PER_SECOND; +} + +void AnimTests::testClipEvaulate() { + std::string id = "myClipNode"; + std::string 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; + float loopFlag = true; + + auto vars = AnimVariantMap(); + vars.set("FalseVar", false); + + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + + AnimNode::Triggers triggers; + clip.evaluate(vars, framesToSec(10.0f), triggers); + QCOMPARE_WITH_ABS_ERROR(clip._frame, 12.0f, EPSILON); + + // does it loop? + triggers.clear(); + clip.evaluate(vars, framesToSec(11.0f), triggers); + QCOMPARE_WITH_ABS_ERROR(clip._frame, 3.0f, EPSILON); + + // did we receive a loop trigger? + QVERIFY(std::find(triggers.begin(), triggers.end(), "myClipNodeOnLoop") != triggers.end()); + + // does it pause at end? + triggers.clear(); + clip.setLoopFlagVar("FalseVar"); + clip.evaluate(vars, framesToSec(20.0f), triggers); + QCOMPARE_WITH_ABS_ERROR(clip._frame, 22.0f, EPSILON); + + // did we receive a done trigger? + QVERIFY(std::find(triggers.begin(), triggers.end(), "myClipNodeOnDone") != triggers.end()); +} + +void AnimTests::testClipEvaulateWithVars() { + std::string id = "myClipNode"; + std::string 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; + float loopFlag = true; + + float startFrame2 = 22.0f; + float endFrame2 = 100.0f; + float timeScale2 = 1.2f; + bool loopFlag2 = false; + + auto vars = AnimVariantMap(); + vars.set("startFrame2", startFrame2); + vars.set("endFrame2", endFrame2); + vars.set("timeScale2", timeScale2); + vars.set("loopFlag2", loopFlag2); + + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + clip.setStartFrameVar("startFrame2"); + clip.setEndFrameVar("endFrame2"); + clip.setTimeScaleVar("timeScale2"); + clip.setLoopFlagVar("loopFlag2"); + + AnimNode::Triggers triggers; + clip.evaluate(vars, framesToSec(0.1f), triggers); + + // verify that the values from the AnimVariantMap made it into the clipNode's + // internal state + QVERIFY(clip._startFrame == startFrame2); + QVERIFY(clip._endFrame == endFrame2); + QVERIFY(clip._timeScale == timeScale2); + QVERIFY(clip._loopFlag == loopFlag2); +} + +void AnimTests::testLoader() { + auto url = QUrl("https://gist.githubusercontent.com/hyperlogic/857129fe04567cbe670f/raw/8ba57a8f0a76f88b39a11f77f8d9df04af9cec95/test.json"); + AnimNodeLoader loader(url); + + const int timeout = 1000; + QEventLoop loop; + QTimer timer; + timer.setInterval(timeout); + timer.setSingleShot(true); + + AnimNode::Pointer node = nullptr; + connect(&loader, &AnimNodeLoader::success, [&](AnimNode::Pointer nodeIn) { node = nodeIn; }); + + loop.connect(&loader, SIGNAL(success(AnimNode::Pointer)), SLOT(quit())); + loop.connect(&loader, SIGNAL(error(int, QString)), SLOT(quit())); + loop.connect(&timer, SIGNAL(timeout()), SLOT(quit())); + timer.start(); + loop.exec(); + + QVERIFY((bool)node); + + QVERIFY(node->getID() == "blend"); + QVERIFY(node->getType() == AnimNode::Type::BlendLinear); + + QVERIFY((bool)node); + QVERIFY(node->getID() == "blend"); + QVERIFY(node->getType() == AnimNode::Type::BlendLinear); + + auto blend = std::static_pointer_cast(node); + QVERIFY(blend->_alpha == 0.5f); + + QVERIFY(node->getChildCount() == 3); + + std::shared_ptr nodes[3] = { node->getChild(0), node->getChild(1), node->getChild(2) }; + + QVERIFY(nodes[0]->getID() == "test01"); + QVERIFY(nodes[0]->getChildCount() == 0); + QVERIFY(nodes[1]->getID() == "test02"); + QVERIFY(nodes[1]->getChildCount() == 0); + QVERIFY(nodes[2]->getID() == "test03"); + QVERIFY(nodes[2]->getChildCount() == 0); + + auto test01 = std::static_pointer_cast(nodes[0]); + QVERIFY(test01->_url == "test01.fbx"); + QVERIFY(test01->_startFrame == 1.0f); + QVERIFY(test01->_endFrame == 20.0f); + QVERIFY(test01->_timeScale == 1.0f); + QVERIFY(test01->_loopFlag == false); + + auto test02 = std::static_pointer_cast(nodes[1]); + QVERIFY(test02->_url == "test02.fbx"); + QVERIFY(test02->_startFrame == 2.0f); + QVERIFY(test02->_endFrame == 21.0f); + QVERIFY(test02->_timeScale == 0.9f); + QVERIFY(test02->_loopFlag == true); +} + +void AnimTests::testVariant() { + auto defaultVar = AnimVariant(); + auto boolVar = AnimVariant(true); + auto intVar = AnimVariant(1); + auto floatVar = AnimVariant(1.0f); + auto vec3Var = AnimVariant(glm::vec3(1.0f, 2.0f, 3.0f)); + auto quatVar = AnimVariant(glm::quat(1.0f, 2.0f, 3.0f, 4.0f)); + auto mat4Var = AnimVariant(glm::mat4(glm::vec4(1.0f, 2.0f, 3.0f, 4.0f), + glm::vec4(5.0f, 6.0f, 7.0f, 8.0f), + glm::vec4(9.0f, 10.0f, 11.0f, 12.0f), + glm::vec4(13.0f, 14.0f, 15.0f, 16.0f))); + QVERIFY(defaultVar.isBool()); + QVERIFY(defaultVar.getBool() == false); + + QVERIFY(boolVar.isBool()); + QVERIFY(boolVar.getBool() == true); + + QVERIFY(intVar.isInt()); + QVERIFY(intVar.getInt() == 1); + + QVERIFY(floatVar.isFloat()); + QVERIFY(floatVar.getFloat() == 1.0f); + + QVERIFY(vec3Var.isVec3()); + auto v = vec3Var.getVec3(); + QVERIFY(v.x == 1.0f); + QVERIFY(v.y == 2.0f); + QVERIFY(v.z == 3.0f); + + QVERIFY(quatVar.isQuat()); + auto q = quatVar.getQuat(); + QVERIFY(q.w == 1.0f); + QVERIFY(q.x == 2.0f); + QVERIFY(q.y == 3.0f); + QVERIFY(q.z == 4.0f); + + QVERIFY(mat4Var.isMat4()); + auto m = mat4Var.getMat4(); + QVERIFY(m[0].x == 1.0f); + QVERIFY(m[3].w == 16.0f); +} diff --git a/tests/animation/src/AnimTests.h b/tests/animation/src/AnimTests.h new file mode 100644 index 0000000000..e667444657 --- /dev/null +++ b/tests/animation/src/AnimTests.h @@ -0,0 +1,28 @@ +// +// AnimTests.h +// +// Copyright 2015 High Fidelity, Inc. +// +// 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_AnimTests_h +#define hifi_AnimTests_h + +#include +#include + +class AnimTests : public QObject { + Q_OBJECT +private slots: + void initTestCase(); + void cleanupTestCase(); + void testClipInternalState(); + void testClipEvaulate(); + void testClipEvaulateWithVars(); + void testLoader(); + void testVariant(); +}; + +#endif // hifi_AnimTests_h diff --git a/tests/animation/src/RigTests.cpp b/tests/animation/src/RigTests.cpp index b0e0a53ee5..ff457ff804 100644 --- a/tests/animation/src/RigTests.cpp +++ b/tests/animation/src/RigTests.cpp @@ -78,24 +78,25 @@ void RigTests::initTestCase() { #ifdef FROM_FILE QFile file(FROM_FILE); QCOMPARE(file.open(QIODevice::ReadOnly), true); - FBXGeometry geometry = readFBX(file.readAll(), QVariantHash()); + FBXGeometry* geometry = readFBX(file.readAll(), QVariantHash()); #else QUrl fbxUrl("https://s3.amazonaws.com/hifi-public/models/skeletons/Zack/Zack.fbx"); QNetworkReply* reply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); QCOMPARE(fbxHttpCode, 200); - FBXGeometry geometry = readFBX(reply->readAll(), QVariantHash()); + FBXGeometry* geometry = readFBX(reply->readAll(), QVariantHash()); #endif - + QVERIFY((bool)geometry); + QVector jointStates; - for (int i = 0; i < geometry.joints.size(); ++i) { - JointState state(geometry.joints[i]); + for (int i = 0; i < geometry->joints.size(); ++i) { + JointState state(geometry->joints[i]); jointStates.append(state); } _rig = std::make_shared(); _rig->initJointStates(jointStates, glm::mat4(), 0, 41, 40, 39, 17, 16, 15); // FIXME? get by name? do we really want to exclude the shoulder blades? - std::cout << "Rig is ready " << geometry.joints.count() << " joints " << std::endl; + std::cout << "Rig is ready " << geometry->joints.count() << " joints " << std::endl; reportAll(_rig); } diff --git a/tests/animation/src/data/avatar.json b/tests/animation/src/data/avatar.json new file mode 100644 index 0000000000..24967979ea --- /dev/null +++ b/tests/animation/src/data/avatar.json @@ -0,0 +1,191 @@ +{ + "version": "1.0", + "root": { + "id": "root", + "type": "stateMachine", + "data": { + "currentState": "idle", + "states": [ + { + "id": "idle", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkFwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "walkBwd", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "strafeLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotMoving", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isTurningRight", "state": "turnRight" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnRight", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningLeft", "state": "turnLeft" } + ] + }, + { + "id": "turnLeft", + "interpTarget": 6, + "interpDuration": 6, + "transitions": [ + { "var": "isNotTurning", "state": "idle" }, + { "var": "isMovingForward", "state": "walkFwd" }, + { "var": "isMovingBackward", "state": "walkBwd" }, + { "var": "isMovingRight", "state": "strafeRight" }, + { "var": "isMovingLeft", "state": "strafeLeft" }, + { "var": "isTurningRight", "state": "turnRight" } + ] + } + ] + }, + "children": [ + { + "id": "idle", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/idle.fbx", + "startFrame": 0.0, + "endFrame": 90.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "walkFwd", + "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, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "walkBwd", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/walk_bwd.fbx", + "startFrame": 0.0, + "endFrame": 37.0, + "timeScale": 1.0, + "loopFlag": true, + "timeScaleVar": "walkTimeScale" + }, + "children": [] + }, + { + "id": "turnLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_left.fbx", + "startFrame": 0.0, + "endFrame": 28.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "turnRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/turn_right.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeLeft", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_left.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + }, + { + "id": "strafeRight", + "type": "clip", + "data": { + "url": "https://hifi-public.s3.amazonaws.com/ozan/anim/standard_anims/strafe_right.fbx", + "startFrame": 0.0, + "endFrame": 31.0, + "timeScale": 1.0, + "loopFlag": true + }, + "children": [] + } + ] + } +} diff --git a/tests/animation/src/data/test.json b/tests/animation/src/data/test.json new file mode 100644 index 0000000000..765617fa2f --- /dev/null +++ b/tests/animation/src/data/test.json @@ -0,0 +1,48 @@ +{ + "version": "1.0", + "root": { + "id": "blend", + "type": "blendLinear", + "data": { + "alpha": 0.5 + }, + "children": [ + { + "id": "test01", + "type": "clip", + "data": { + "url": "test01.fbx", + "startFrame": 1.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "test02", + "type": "clip", + "data": { + "url": "test02.fbx", + "startFrame": 2.0, + "endFrame": 21.0, + "timeScale": 0.9, + "loopFlag": true + }, + "children": [] + }, + { + "id": "test03", + "type": "clip", + "data": { + "url": "test03.fbx", + "startFrame": 3.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + } +}