Added AnimNodeLoader which loads AnimClip nodes from json

* added tests and sample json file
This commit is contained in:
Anthony J. Thibault 2015-07-31 15:17:47 -07:00
parent 35196a0059
commit 343b2ccf9d
8 changed files with 334 additions and 7 deletions

View file

@ -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),

View file

@ -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

View file

@ -10,13 +10,51 @@
#ifndef hifi_AnimNode_h
#define hifi_AnimNode_h
#include <string>
#include <vector>
#include <memory>
#include <cassert>
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<AnimNode> child) { _children.push_back(child); }
void removeChild(std::shared_ptr<AnimNode> child) {
auto iter = std::find(_children.begin(), _children.end(), child);
if (iter != _children.end()) {
_children.erase(iter);
}
}
const std::shared_ptr<AnimNode>& 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<std::shared_ptr<AnimNode>> _children;
// no copies
AnimNode(const AnimNode&) = delete;
AnimNode& operator=(const AnimNode&) = delete;
};
#endif // hifi_AnimNode_h

View file

@ -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 <qjsondocument.h>
#include <qjsonobject.h>
#include <qjsonarray.h>
#include <qfile.h>
#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<AnimNode> (*NodeLoaderFunc)(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl);
static std::shared_ptr<AnimNode> 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<AnimNode> 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<AnimNode> 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<AnimClip>(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag);
}
std::shared_ptr<AnimNode> 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);
}

View file

@ -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 <memory>
class AnimNode;
class AnimNodeLoader {
public:
// TODO: load from url
std::shared_ptr<AnimNode> load(const std::string& filename) const;
};
#endif // hifi_AnimNodeLoader

View file

@ -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<AnimClip>(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<AnimNode> 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);
}

View file

@ -22,6 +22,7 @@ class AnimClipTests : public QObject {
private slots:
void testAccessors();
void testEvaulate();
void testLoader();
};
#endif // hifi_TransformTests_h

View file

@ -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": []
}
]
}
}