Added network resource download support to AnimNodeLoader.

This commit is contained in:
Anthony J. Thibault 2015-08-24 19:00:12 -07:00
parent bde75e9e51
commit 7a2ca047cb
5 changed files with 125 additions and 84 deletions

View file

@ -32,11 +32,11 @@ static TypeInfo typeInfoArray[AnimNode::NumTypes] = {
{ AnimNode::OverlayType, "overlay" } { AnimNode::OverlayType, "overlay" }
}; };
typedef AnimNode::Pointer (*NodeLoaderFunc)(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl); typedef AnimNode::Pointer (*NodeLoaderFunc)(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl); static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QString& 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 QString& jsonUrl); static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl);
static NodeLoaderFunc nodeLoaderFuncs[AnimNode::NumTypes] = { static NodeLoaderFunc nodeLoaderFuncs[AnimNode::NumTypes] = {
loadClipNode, loadClipNode,
@ -49,7 +49,7 @@ static NodeLoaderFunc nodeLoaderFuncs[AnimNode::NumTypes] = {
if (!NAME##_VAL.isString()) { \ if (!NAME##_VAL.isString()) { \
qCCritical(animation) << "AnimNodeLoader, error reading string" \ qCCritical(animation) << "AnimNodeLoader, error reading string" \
<< #NAME << ", id =" << ID \ << #NAME << ", id =" << ID \
<< ", url =" << URL; \ << ", url =" << URL.toDisplayString(); \
return nullptr; \ return nullptr; \
} \ } \
QString NAME = NAME##_VAL.toString() QString NAME = NAME##_VAL.toString()
@ -59,7 +59,7 @@ static NodeLoaderFunc nodeLoaderFuncs[AnimNode::NumTypes] = {
if (!NAME##_VAL.isBool()) { \ if (!NAME##_VAL.isBool()) { \
qCCritical(animation) << "AnimNodeLoader, error reading bool" \ qCCritical(animation) << "AnimNodeLoader, error reading bool" \
<< #NAME << ", id =" << ID \ << #NAME << ", id =" << ID \
<< ", url =" << URL; \ << ", url =" << URL.toDisplayString(); \
return nullptr; \ return nullptr; \
} \ } \
bool NAME = NAME##_VAL.toBool() bool NAME = NAME##_VAL.toBool()
@ -69,7 +69,7 @@ static NodeLoaderFunc nodeLoaderFuncs[AnimNode::NumTypes] = {
if (!NAME##_VAL.isDouble()) { \ if (!NAME##_VAL.isDouble()) { \
qCCritical(animation) << "AnimNodeLoader, error reading double" \ qCCritical(animation) << "AnimNodeLoader, error reading double" \
<< #NAME << "id =" << ID \ << #NAME << "id =" << ID \
<< ", url =" << URL; \ << ", url =" << URL.toDisplayString(); \
return nullptr; \ return nullptr; \
} \ } \
float NAME = (float)NAME##_VAL.toDouble() float NAME = (float)NAME##_VAL.toDouble()
@ -83,29 +83,29 @@ static AnimNode::Type stringToEnum(const QString& str) {
return AnimNode::NumTypes; return AnimNode::NumTypes;
} }
static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QString& jsonUrl) { static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QUrl& jsonUrl) {
auto idVal = jsonObj.value("id"); auto idVal = jsonObj.value("id");
if (!idVal.isString()) { if (!idVal.isString()) {
qCCritical(animation) << "AnimNodeLoader, bad string \"id\", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad string \"id\", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
QString id = idVal.toString(); QString id = idVal.toString();
auto typeVal = jsonObj.value("type"); auto typeVal = jsonObj.value("type");
if (!typeVal.isString()) { if (!typeVal.isString()) {
qCCritical(animation) << "AnimNodeLoader, bad object \"type\", id =" << id << ", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad object \"type\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
QString typeStr = typeVal.toString(); QString typeStr = typeVal.toString();
AnimNode::Type type = stringToEnum(typeStr); AnimNode::Type type = stringToEnum(typeStr);
if (type == AnimNode::NumTypes) { if (type == AnimNode::NumTypes) {
qCCritical(animation) << "AnimNodeLoader, unknown node type" << typeStr << ", id =" << id << ", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, unknown node type" << typeStr << ", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
auto dataValue = jsonObj.value("data"); auto dataValue = jsonObj.value("data");
if (!dataValue.isObject()) { if (!dataValue.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad string \"data\", id =" << id << ", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad string \"data\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
auto dataObj = dataValue.toObject(); auto dataObj = dataValue.toObject();
@ -115,13 +115,13 @@ static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QString& jso
auto childrenValue = jsonObj.value("children"); auto childrenValue = jsonObj.value("children");
if (!childrenValue.isArray()) { if (!childrenValue.isArray()) {
qCCritical(animation) << "AnimNodeLoader, bad array \"children\", id =" << id << ", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad array \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
auto childrenAry = childrenValue.toArray(); auto childrenAry = childrenValue.toArray();
for (const auto& childValue : childrenAry) { for (const auto& childValue : childrenAry) {
if (!childValue.isObject()) { if (!childValue.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad object in \"children\", id =" << id << ", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
node->addChild(loadNode(childValue.toObject(), jsonUrl)); node->addChild(loadNode(childValue.toObject(), jsonUrl));
@ -129,7 +129,7 @@ static AnimNode::Pointer loadNode(const QJsonObject& jsonObj, const QString& jso
return node; return node;
} }
static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl) { static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
READ_STRING(url, jsonObj, id, jsonUrl); READ_STRING(url, jsonObj, id, jsonUrl);
READ_FLOAT(startFrame, jsonObj, id, jsonUrl); READ_FLOAT(startFrame, jsonObj, id, jsonUrl);
@ -140,7 +140,7 @@ static AnimNode::Pointer loadClipNode(const QJsonObject& jsonObj, const QString&
return std::make_shared<AnimClip>(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag); return std::make_shared<AnimClip>(id.toStdString(), url.toStdString(), startFrame, endFrame, timeScale, loopFlag);
} }
static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl) { static AnimNode::Pointer loadBlendLinearNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
READ_FLOAT(alpha, jsonObj, id, jsonUrl); READ_FLOAT(alpha, jsonObj, id, jsonUrl);
@ -169,40 +169,35 @@ static AnimOverlay::BoneSet stringToBoneSetEnum(const QString& str) {
return AnimOverlay::NumBoneSets; return AnimOverlay::NumBoneSets;
} }
static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QString& jsonUrl) { static AnimNode::Pointer loadOverlayNode(const QJsonObject& jsonObj, const QString& id, const QUrl& jsonUrl) {
READ_STRING(boneSet, jsonObj, id, jsonUrl); READ_STRING(boneSet, jsonObj, id, jsonUrl);
auto boneSetEnum = stringToBoneSetEnum(boneSet); auto boneSetEnum = stringToBoneSetEnum(boneSet);
if (boneSetEnum == AnimOverlay::NumBoneSets) { if (boneSetEnum == AnimOverlay::NumBoneSets) {
qCCritical(animation) << "AnimNodeLoader, unknown bone set =" << boneSet << ", defaulting to \"fullBody\", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, unknown bone set =" << boneSet << ", defaulting to \"fullBody\", url =" << jsonUrl.toDisplayString();
boneSetEnum = AnimOverlay::FullBodyBoneSet; boneSetEnum = AnimOverlay::FullBodyBoneSet;
} }
return std::make_shared<AnimBlendLinear>(id.toStdString(), boneSetEnum); return std::make_shared<AnimBlendLinear>(id.toStdString(), boneSetEnum);
} }
AnimNodeLoader::AnimNodeLoader() { 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 std::string& filename) const { AnimNode::Pointer AnimNodeLoader::load(const QByteArray& contents, const QUrl& jsonUrl) {
// 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 // convert string into a json doc
QJsonParseError error; QJsonParseError error;
auto doc = QJsonDocument::fromJson(contents.toUtf8(), &error); auto doc = QJsonDocument::fromJson(contents, &error);
if (error.error != QJsonParseError::NoError) { if (error.error != QJsonParseError::NoError) {
qCCritical(animation) << "AnimNodeLoader, failed to parse json, error =" << error.errorString() << ", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, failed to parse json, error =" << error.errorString() << ", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
QJsonObject obj = doc.object(); QJsonObject obj = doc.object();
@ -210,23 +205,32 @@ AnimNode::Pointer AnimNodeLoader::load(const std::string& filename) const {
// version // version
QJsonValue versionVal = obj.value("version"); QJsonValue versionVal = obj.value("version");
if (!versionVal.isString()) { if (!versionVal.isString()) {
qCCritical(animation) << "AnimNodeLoader, bad string \"version\", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad string \"version\", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
QString version = versionVal.toString(); QString version = versionVal.toString();
// check version // check version
if (version != "1.0") { if (version != "1.0") {
qCCritical(animation) << "AnimNodeLoader, bad version number" << version << "expected \"1.0\", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad version number" << version << "expected \"1.0\", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
// root // root
QJsonValue rootVal = obj.value("root"); QJsonValue rootVal = obj.value("root");
if (!rootVal.isObject()) { if (!rootVal.isObject()) {
qCCritical(animation) << "AnimNodeLoader, bad object \"root\", url =" << jsonUrl; qCCritical(animation) << "AnimNodeLoader, bad object \"root\", url =" << jsonUrl.toDisplayString();
return nullptr; return nullptr;
} }
return loadNode(rootVal.toObject(), jsonUrl); return loadNode(rootVal.toObject(), jsonUrl);
} }
void AnimNodeLoader::onRequestDone(QNetworkReply& request) {
auto node = load(request.readAll(), _url);
emit success(node);
}
void AnimNodeLoader::onRequestError(QNetworkReply::NetworkError netError) {
emit error((int)netError, "Resource download error");
}

View file

@ -12,13 +12,36 @@
#include <memory> #include <memory>
class AnimNode; #include <QString.h>
#include <QUrl.h>
#include <QNetworkReply.h>
#include "AnimNode.h"
class Resource;
class AnimNodeLoader : public QObject {
Q_OBJECT
class AnimNodeLoader {
public: public:
AnimNodeLoader(); AnimNodeLoader(const QUrl& url);
// TODO: load from url
std::shared_ptr<AnimNode> load(const std::string& filename) const; 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 // no copies
AnimNodeLoader(const AnimNodeLoader&) = delete; AnimNodeLoader(const AnimNodeLoader&) = delete;

View file

@ -30,7 +30,7 @@ void AnimClipTests::cleanupTestCase() {
void AnimClipTests::testAccessors() { void AnimClipTests::testAccessors() {
std::string id = "my anim clip"; std::string id = "my anim clip";
std::string url = "foo"; std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx";
float startFrame = 2.0f; float startFrame = 2.0f;
float endFrame = 20.0f; float endFrame = 20.0f;
float timeScale = 1.1f; float timeScale = 1.1f;
@ -47,7 +47,7 @@ void AnimClipTests::testAccessors() {
QVERIFY(clip.getTimeScale() == timeScale); QVERIFY(clip.getTimeScale() == timeScale);
QVERIFY(clip.getLoopFlag() == loopFlag); QVERIFY(clip.getLoopFlag() == loopFlag);
std::string url2 = "bar"; std::string url2 = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_walk.fbx";
float startFrame2 = 22.0f; float startFrame2 = 22.0f;
float endFrame2 = 100.0f; float endFrame2 = 100.0f;
float timeScale2 = 1.2f; float timeScale2 = 1.2f;
@ -73,7 +73,7 @@ static float framesToSec(float secs) {
void AnimClipTests::testEvaulate() { void AnimClipTests::testEvaulate() {
std::string id = "my clip node"; std::string id = "my clip node";
std::string url = "foo"; std::string url = "https://hifi-public.s3.amazonaws.com/ozan/support/FightClubBotTest1/Animations/standard_idle.fbx";
float startFrame = 2.0f; float startFrame = 2.0f;
float endFrame = 22.0f; float endFrame = 22.0f;
float timeScale = 1.0f; float timeScale = 1.0f;
@ -95,43 +95,60 @@ void AnimClipTests::testEvaulate() {
} }
void AnimClipTests::testLoader() { void AnimClipTests::testLoader() {
AnimNodeLoader loader; auto url = QUrl("https://gist.githubusercontent.com/hyperlogic/857129fe04567cbe670f/raw/8ba57a8f0a76f88b39a11f77f8d9df04af9cec95/test.json");
AnimNodeLoader loader(url);
#ifdef Q_OS_WIN const int timeout = 1000;
auto node = loader.load("../../../tests/animation/src/data/test.json"); QEventLoop loop;
#else QTimer timer;
auto node = loader.load("../../../../tests/animation/src/data/test.json"); timer.setInterval(timeout);
#endif timer.setSingleShot(true);
QVERIFY((bool)node); bool done = false;
QVERIFY(node->getID() == "blend"); connect(&loader, &AnimNodeLoader::success, [&](AnimNode::Pointer node) {
QVERIFY(node->getType() == AnimNode::BlendLinearType); QVERIFY((bool)node);
QVERIFY(node->getID() == "blend");
QVERIFY(node->getType() == AnimNode::BlendLinearType);
auto blend = std::static_pointer_cast<AnimBlendLinear>(node); QVERIFY((bool)node);
QVERIFY(blend->getAlpha() == 0.5f); QVERIFY(node->getID() == "blend");
QVERIFY(node->getType() == AnimNode::BlendLinearType);
QVERIFY(node->getChildCount() == 3); auto blend = std::static_pointer_cast<AnimBlendLinear>(node);
QVERIFY(blend->getAlpha() == 0.5f);
std::shared_ptr<AnimNode> nodes[3] = { node->getChild(0), node->getChild(1), node->getChild(2) }; QVERIFY(node->getChildCount() == 3);
QVERIFY(nodes[0]->getID() == "test01"); std::shared_ptr<AnimNode> nodes[3] = { node->getChild(0), node->getChild(1), node->getChild(2) };
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<AnimClip>(nodes[0]); QVERIFY(nodes[0]->getID() == "test01");
QVERIFY(test01->getURL() == "test01.fbx"); QVERIFY(nodes[0]->getChildCount() == 0);
QVERIFY(test01->getStartFrame() == 1.0f); QVERIFY(nodes[1]->getID() == "test02");
QVERIFY(test01->getEndFrame() == 20.0f); QVERIFY(nodes[1]->getChildCount() == 0);
QVERIFY(test01->getTimeScale() == 1.0f); QVERIFY(nodes[2]->getID() == "test03");
QVERIFY(test01->getLoopFlag() == false); QVERIFY(nodes[2]->getChildCount() == 0);
auto test02 = std::static_pointer_cast<AnimClip>(nodes[1]); auto test01 = std::static_pointer_cast<AnimClip>(nodes[0]);
QVERIFY(test02->getURL() == "test02.fbx"); QVERIFY(test01->getURL() == "test01.fbx");
QVERIFY(test02->getStartFrame() == 2.0f); QVERIFY(test01->getStartFrame() == 1.0f);
QVERIFY(test02->getEndFrame() == 21.0f); QVERIFY(test01->getEndFrame() == 20.0f);
QVERIFY(test02->getTimeScale() == 0.9f); QVERIFY(test01->getTimeScale() == 1.0f);
QVERIFY(test02->getLoopFlag() == true); QVERIFY(test01->getLoopFlag() == false);
auto test02 = std::static_pointer_cast<AnimClip>(nodes[1]);
QVERIFY(test02->getURL() == "test02.fbx");
QVERIFY(test02->getStartFrame() == 2.0f);
QVERIFY(test02->getEndFrame() == 21.0f);
QVERIFY(test02->getTimeScale() == 0.9f);
QVERIFY(test02->getLoopFlag() == true);
done = true;
});
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(done);
} }

View file

@ -13,10 +13,6 @@
#include <QtTest/QtTest> #include <QtTest/QtTest>
#include <glm/glm.hpp> #include <glm/glm.hpp>
inline float getErrorDifference(float a, float b) {
return fabs(a - b);
}
class AnimClipTests : public QObject { class AnimClipTests : public QObject {
Q_OBJECT Q_OBJECT
private slots: private slots:

View file

@ -78,24 +78,25 @@ void RigTests::initTestCase() {
#ifdef FROM_FILE #ifdef FROM_FILE
QFile file(FROM_FILE); QFile file(FROM_FILE);
QCOMPARE(file.open(QIODevice::ReadOnly), true); QCOMPARE(file.open(QIODevice::ReadOnly), true);
FBXGeometry geometry = readFBX(file.readAll(), QVariantHash()); FBXGeometry* geometry = readFBX(file.readAll(), QVariantHash());
#else #else
QUrl fbxUrl("https://s3.amazonaws.com/hifi-public/models/skeletons/Zack/Zack.fbx"); 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 QNetworkReply* reply = OBJReader().request(fbxUrl, false); // Just a convenience hack for synchronoud http request
auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); auto fbxHttpCode = !reply->isFinished() ? -1 : reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QCOMPARE(fbxHttpCode, 200); QCOMPARE(fbxHttpCode, 200);
FBXGeometry geometry = readFBX(reply->readAll(), QVariantHash()); FBXGeometry* geometry = readFBX(reply->readAll(), QVariantHash());
#endif #endif
QVERIFY((bool)geometry);
QVector<JointState> jointStates; QVector<JointState> jointStates;
for (int i = 0; i < geometry.joints.size(); ++i) { for (int i = 0; i < geometry->joints.size(); ++i) {
JointState state(geometry.joints[i]); JointState state(geometry->joints[i]);
jointStates.append(state); jointStates.append(state);
} }
_rig = std::make_shared<AvatarRig>(); _rig = std::make_shared<AvatarRig>();
_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? _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); reportAll(_rig);
} }