diff --git a/libraries/animation/src/AnimClip.cpp b/libraries/animation/src/AnimClip.cpp index 5b0284c08b..a819304d18 100644 --- a/libraries/animation/src/AnimClip.cpp +++ b/libraries/animation/src/AnimClip.cpp @@ -10,7 +10,8 @@ #include "AnimClip.h" #include "AnimationLogging.h" -AnimClip::AnimClip(const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag) : +AnimClip::AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag) : + AnimNode(AnimNode::ClipType, id), _url(url), _startFrame(startFrame), _endFrame(endFrame), diff --git a/libraries/animation/src/AnimClip.h b/libraries/animation/src/AnimClip.h index 60ef8e74f2..db3419332b 100644 --- a/libraries/animation/src/AnimClip.h +++ b/libraries/animation/src/AnimClip.h @@ -18,7 +18,7 @@ class AnimClip : public AnimNode { public: friend class AnimClipTests; - AnimClip(const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag); + AnimClip(const std::string& id, const std::string& url, float startFrame, float endFrame, float timeScale, bool loopFlag); virtual ~AnimClip(); void setURL(const std::string& url); @@ -37,14 +37,21 @@ public: bool getLoopFlag() const { return _loopFlag; } virtual const AnimPose& evaluate(float dt); + protected: AnimationPointer _anim; + std::string _url; float _startFrame; float _endFrame; float _timeScale; - float _frame; bool _loopFlag; + + float _frame; + + // 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 index 8e78012865..af1fd8d5d5 100644 --- a/libraries/animation/src/AnimNode.h +++ b/libraries/animation/src/AnimNode.h @@ -10,13 +10,51 @@ #ifndef hifi_AnimNode_h #define hifi_AnimNode_h +#include +#include +#include +#include + typedef float AnimPose; +class QJsonObject; class AnimNode { public: + enum Type { + ClipType = 0, + NumTypes + }; + + AnimNode(Type type, const std::string& id) : _type(type), _id(id) {} + + const std::string& getID() const { return _id; } + Type getType() const { return _type; } + + void addChild(std::shared_ptr child) { _children.push_back(child); } + void removeChild(std::shared_ptr child) { + auto iter = std::find(_children.begin(), _children.end(), child); + if (iter != _children.end()) { + _children.erase(iter); + } + } + const std::shared_ptr& getChild(int i) const { + assert(i >= 0 && i < (int)_children.size()); + return _children[i]; + } + int getChildCount() const { return (int)_children.size(); } + virtual ~AnimNode() {} virtual const AnimPose& evaluate(float dt) = 0; + +protected: + std::string _id; + Type _type; + std::vector> _children; + + // 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..c2cb75364d --- /dev/null +++ b/libraries/animation/src/AnimNodeLoader.cpp @@ -0,0 +1,173 @@ +// +// AnimNodeLoader.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 +// + +#include +#include +#include +#include + +#include "AnimNode.h" +#include "AnimClip.h" +#include "AnimationLogging.h" +#include "AnimNodeLoader.h" + +struct TypeInfo { + AnimNode::Type type; + const char* str; +}; + +// This will result in a compile error if someone adds a new +// item to the AnimNode::Type enum. This is by design. +static TypeInfo typeInfoArray[AnimNode::NumTypes] = { + { AnimNode::ClipType, "clip" } +}; + +typedef std::shared_ptr (*NodeLoaderFunc)(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl); + +static std::shared_ptr loadClipNode(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl); + +static NodeLoaderFunc nodeLoaderFuncs[AnimNode::NumTypes] = { + loadClipNode +}; + +#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; \ + return nullptr; \ + } \ + QString 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; \ + 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; \ + return nullptr; \ + } \ + float NAME = (float)NAME##_VAL.toDouble() + +static AnimNode::Type stringToEnum(const QString& str) { + for (int i = 0; i < AnimNode::NumTypes; i++ ) { + if (str == typeInfoArray[i].str) { + return typeInfoArray[i].type; + } + } + return AnimNode::NumTypes; +} + +static std::shared_ptr loadNode(const QJsonObject& jsonObj, const QString& jsonUrl) { + auto idVal = jsonObj.value("id"); + if (!idVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"id\", url =" << jsonUrl; + return nullptr; + } + QString id = idVal.toString(); + + auto typeVal = jsonObj.value("type"); + if (!typeVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad object \"type\", id =" << id << ", url =" << jsonUrl; + return nullptr; + } + QString typeStr = typeVal.toString(); + AnimNode::Type type = stringToEnum(typeStr); + if (type == AnimNode::NumTypes) { + qCCritical(animation) << "AnimNodeLoader, unknown node type" << typeStr << ", id =" << id << ", url =" << jsonUrl; + return nullptr; + } + + auto dataValue = jsonObj.value("data"); + if (!dataValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"data\", id =" << id << ", url =" << jsonUrl; + return nullptr; + } + auto dataObj = dataValue.toObject(); + + assert((int)type >= 0 && type < AnimNode::NumTypes); + auto node = nodeLoaderFuncs[type](dataObj, id, jsonUrl); + + auto childrenValue = jsonObj.value("children"); + if (!childrenValue.isArray()) { + qCCritical(animation) << "AnimNodeLoader, bad array \"children\", id =" << id << ", url =" << jsonUrl; + return nullptr; + } + auto childrenAry = childrenValue.toArray(); + for (auto& childValue : childrenAry) { + if (!childValue.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl; + return nullptr; + } + node->addChild(loadNode(childValue.toObject(), jsonUrl)); + } + return node; +} + +static std::shared_ptr loadClipNode(const QJsonObject& jsonObj, const QString& id, const QString& 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); + + return std::make_shared(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag); +} + +std::shared_ptr AnimNodeLoader::load(const std::string& filename) const { + // load entire file into a string. + QString jsonUrl = QString::fromStdString(filename); + QFile file; + file.setFileName(jsonUrl); + if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { + qCCritical(animation) << "AnimNodeLoader, could not open url =" << jsonUrl; + return nullptr; + } + QString contents = file.readAll(); + file.close(); + + // convert string into a json doc + auto doc = QJsonDocument::fromJson(contents.toUtf8()); + QJsonObject obj = doc.object(); + + // version + QJsonValue versionVal = obj.value("version"); + if (!versionVal.isString()) { + qCCritical(animation) << "AnimNodeLoader, bad string \"version\", url =" << jsonUrl; + return nullptr; + } + QString version = versionVal.toString(); + + // check version + if (version != "1.0") { + qCCritical(animation) << "AnimNodeLoader, bad version number" << version << "expected \"1.0\", url =" << jsonUrl; + return nullptr; + } + + // root + QJsonValue rootVal = obj.value("root"); + if (!rootVal.isObject()) { + qCCritical(animation) << "AnimNodeLoader, bad object \"root\", url =" << jsonUrl; + return nullptr; + } + + return loadNode(rootVal.toObject(), jsonUrl); +} diff --git a/libraries/animation/src/AnimNodeLoader.h b/libraries/animation/src/AnimNodeLoader.h new file mode 100644 index 0000000000..941ea32f02 --- /dev/null +++ b/libraries/animation/src/AnimNodeLoader.h @@ -0,0 +1,23 @@ +// +// AnimNodeLoader.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_AnimNodeLoader_h +#define hifi_AnimNodeLoader_h + +#include + +class AnimNode; + +class AnimNodeLoader { +public: + // TODO: load from url + std::shared_ptr load(const std::string& filename) const; +}; + +#endif // hifi_AnimNodeLoader diff --git a/tests/animation/src/AnimClipTests.cpp b/tests/animation/src/AnimClipTests.cpp index 68f92afb2f..bab52f17a3 100644 --- a/tests/animation/src/AnimClipTests.cpp +++ b/tests/animation/src/AnimClipTests.cpp @@ -9,6 +9,7 @@ // #include "AnimClipTests.h" +#include "AnimNodeLoader.h" #include "AnimClip.h" #include "AnimationLogging.h" @@ -19,13 +20,18 @@ QTEST_MAIN(AnimClipTests) const float EPSILON = 0.001f; void AnimClipTests::testAccessors() { + std::string id = "my anim clip"; std::string url = "foo"; float startFrame = 2.0f; float endFrame = 20.0f; float timeScale = 1.1f; - float loopFlag = true; + bool loopFlag = true; + + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); + + QVERIFY(clip.getID() == id); + QVERIFY(clip.getType() == AnimNode::ClipType); - AnimClip clip(url, startFrame, endFrame, timeScale, loopFlag); QVERIFY(clip.getURL() == url); QVERIFY(clip.getStartFrame() == startFrame); QVERIFY(clip.getEndFrame() == endFrame); @@ -36,7 +42,7 @@ void AnimClipTests::testAccessors() { float startFrame2 = 22.0f; float endFrame2 = 100.0f; float timeScale2 = 1.2f; - float loopFlag2 = false; + bool loopFlag2 = false; clip.setURL(url2); clip.setStartFrame(startFrame2); @@ -62,13 +68,14 @@ static float framesToSec(float secs) { } void AnimClipTests::testEvaulate() { + std::string id = "my clip node"; std::string url = "foo"; float startFrame = 2.0f; float endFrame = 22.0f; float timeScale = 1.0f; float loopFlag = true; - AnimClip clip(url, startFrame, endFrame, timeScale, loopFlag); + AnimClip clip(id, url, startFrame, endFrame, timeScale, loopFlag); clip.evaluate(framesToSec(10.0f)); QCOMPARE_WITH_ABS_ERROR(clip._frame, 12.0f, EPSILON); @@ -83,3 +90,28 @@ void AnimClipTests::testEvaulate() { QCOMPARE_WITH_ABS_ERROR(clip._frame, 22.0f, EPSILON); } +void AnimClipTests::testLoader() { + AnimNodeLoader loader; + auto node = loader.load("../../../tests/animation/src/test.json"); + QVERIFY((bool)node); + QVERIFY(node->getID() == "idle"); + QVERIFY(node->getType() == AnimNode::ClipType); + + auto clip = std::static_pointer_cast(node); + + QVERIFY(clip->getURL() == "idle.fbx"); + QVERIFY(clip->getStartFrame() == 0.0f); + QVERIFY(clip->getEndFrame() == 30.0f); + QVERIFY(clip->getTimeScale() == 1.0f); + QVERIFY(clip->getLoopFlag() == true); + + QVERIFY(clip->getChildCount() == 3); + + std::shared_ptr nodes[3] = { clip->getChild(0), clip->getChild(1), clip->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); +} diff --git a/tests/animation/src/AnimClipTests.h b/tests/animation/src/AnimClipTests.h index f49690d400..08f25c324f 100644 --- a/tests/animation/src/AnimClipTests.h +++ b/tests/animation/src/AnimClipTests.h @@ -22,6 +22,7 @@ class AnimClipTests : public QObject { private slots: void testAccessors(); void testEvaulate(); + void testLoader(); }; #endif // hifi_TransformTests_h diff --git a/tests/animation/src/test.json b/tests/animation/src/test.json new file mode 100644 index 0000000000..0bcf91201a --- /dev/null +++ b/tests/animation/src/test.json @@ -0,0 +1,52 @@ +{ + "version": "1.0", + "root": { + "id": "idle", + "type": "clip", + "data": { + "url": "idle.fbx", + "startFrame": 0.0, + "endFrame": 30.0, + "timeScale": 1.0, + "loopFlag": true + }, + "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": 20.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + }, + { + "id": "test03", + "type": "clip", + "data": { + "url": "test03.fbx", + "startFrame": 3.0, + "endFrame": 20.0, + "timeScale": 1.0, + "loopFlag": false + }, + "children": [] + } + ] + } +}