Move skeleton loading code to job

Also removes last couple of messages updating backend to frontend.

Change-Id: I65056c7cf5ff06efab9c9a205f843ed882f9c0be
Reviewed-by: Paul Lemire <paul.lemire@kdab.com>
This commit is contained in:
Mike Krus 2019-10-03 11:10:40 +01:00
parent 06567dc188
commit e504957c84
5 changed files with 240 additions and 283 deletions

View File

@ -36,24 +36,19 @@
#include "skeleton_p.h"
#include <Qt3DCore/qjoint.h>
#include <Qt3DCore/qpropertyupdatedchange.h>
#include <QCoreApplication>
#include <QFile>
#include <QFileInfo>
#include <Qt3DCore/qjoint.h>
#include <Qt3DRender/private/abstractrenderer_p.h>
#include <Qt3DRender/private/gltfskeletonloader_p.h>
#include <Qt3DRender/private/managers_p.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DRender/private/renderlogging_p.h>
#include <Qt3DRender/private/qurlhelper_p.h>
#include <Qt3DCore/private/qskeletoncreatedchange_p.h>
#include <Qt3DCore/private/qskeleton_p.h>
#include <Qt3DCore/private/qskeletonloader_p.h>
#include <Qt3DCore/private/qmath3d_p.h>
#include <Qt3DCore/private/qabstractnodefactory_p.h>
QT_BEGIN_NAMESPACE
@ -146,192 +141,8 @@ void Skeleton::syncFromFrontEnd(const QNode *frontEnd, bool firstTime)
void Skeleton::setStatus(QSkeletonLoader::Status status)
{
if (status != m_status) {
if (status != m_status)
m_status = status;
Qt3DCore::QPropertyUpdatedChangePtr e = Qt3DCore::QPropertyUpdatedChangePtr::create(peerId());
e->setDeliveryFlags(Qt3DCore::QSceneChange::DeliverToAll);
e->setPropertyName("status");
e->setValue(QVariant::fromValue(m_status));
notifyObservers(e);
}
}
void Skeleton::loadSkeleton()
{
qCDebug(Jobs) << Q_FUNC_INFO << m_source;
clearData();
// Load the data
switch (m_dataType) {
case File:
loadSkeletonFromUrl();
break;
case Data:
loadSkeletonFromData();
break;
default:
Q_UNREACHABLE();
}
// If using a loader inform the frontend of the status change.
// Don't bother if asked to create frontend joints though. When
// the backend gets notified of those joints we'll update the
// status at that point.
if (m_dataType == File && !m_createJoints) {
if (jointCount() == 0)
setStatus(QSkeletonLoader::Error);
else
setStatus(QSkeletonLoader::Ready);
}
qCDebug(Jobs) << "Loaded skeleton data:" << *this;
}
void Skeleton::loadSkeletonFromUrl()
{
// TODO: Handle remote files
QString filePath = Qt3DRender::QUrlHelper::urlToLocalFileOrQrc(m_source);
QFileInfo info(filePath);
if (!info.exists()) {
qWarning() << "Could not open skeleton file:" << filePath;
setStatus(Qt3DCore::QSkeletonLoader::Error);
return;
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Could not open skeleton file:" << filePath;
setStatus(Qt3DCore::QSkeletonLoader::Error);
return;
}
// TODO: Make plugin based for more file type support. For now gltf or native
const QString ext = info.suffix();
if (ext == QLatin1String("gltf")) {
GLTFSkeletonLoader loader;
loader.load(&file);
m_skeletonData = loader.createSkeleton(m_name);
// If the user has requested it, create the frontend nodes for the joints
// and send them to the (soon to be owning) QSkeletonLoader.
if (m_createJoints) {
std::unique_ptr<QJoint> rootJoint(createFrontendJoints(m_skeletonData));
if (!rootJoint) {
qWarning() << "Failed to create frontend joints";
setStatus(Qt3DCore::QSkeletonLoader::Error);
return;
}
// Move the QJoint tree to the main thread and notify the
// corresponding QSkeletonLoader
const auto appThread = QCoreApplication::instance()->thread();
rootJoint->moveToThread(appThread);
auto e = QJointChangePtr::create(peerId());
e->setDeliveryFlags(Qt3DCore::QSceneChange::Nodes);
e->setPropertyName("rootJoint");
e->data = std::move(rootJoint);
notifyObservers(e);
// Clear the skeleton data. It will be recreated from the
// frontend joints. A little bit inefficient but ensures
// that joints created this way and via QSkeleton go through
// the same code path.
m_skeletonData = SkeletonData();
}
} else if (ext == QLatin1String("json")) {
// TODO: Support native skeleton type
} else {
qWarning() << "Unknown skeleton file type:" << ext;
setStatus(Qt3DCore::QSkeletonLoader::Error);
return;
}
m_skinningPalette.resize(m_skeletonData.joints.size());
}
void Skeleton::loadSkeletonFromData()
{
// Recurse down through the joint hierarchy and process it into
// the vector of joints used within SkeletonData. The recursion
// ensures that a parent always appears before its children in
// the vector of JointInfo objects.
//
// In addition, we set up a mapping from the joint ids to the
// index of the corresponding JointInfo object in the vector.
// This will allow us to easily update entries in the vector of
// JointInfos when a Joint node marks itself as dirty.
const int rootParentIndex = -1;
processJointHierarchy(m_rootJointId, rootParentIndex, m_skeletonData);
m_skinningPalette.resize(m_skeletonData.joints.size());
}
Qt3DCore::QJoint *Skeleton::createFrontendJoints(const SkeletonData &skeletonData) const
{
if (skeletonData.joints.isEmpty())
return nullptr;
// Create frontend joints from the joint info objects
QVector<QJoint *> frontendJoints;
const int jointCount = skeletonData.joints.size();
frontendJoints.reserve(jointCount);
for (int i = 0; i < jointCount; ++i) {
const QMatrix4x4 &inverseBindMatrix = skeletonData.joints[i].inverseBindPose;
const QString &jointName = skeletonData.jointNames[i];
const Qt3DCore::Sqt &localPose = skeletonData.localPoses[i];
frontendJoints.push_back(createFrontendJoint(jointName, localPose, inverseBindMatrix));
}
// Now go through and resolve the parent for each joint
for (int i = 0; i < frontendJoints.size(); ++i) {
const auto parentIndex = skeletonData.joints[i].parentIndex;
if (parentIndex == -1)
continue;
// It's not enough to just set up the QObject parent-child relationship.
// We need to explicitly add the child to the parent's list of joints so
// that information is then propagated to the backend.
frontendJoints[parentIndex]->addChildJoint(frontendJoints[i]);
}
return frontendJoints[0];
}
Qt3DCore::QJoint *Skeleton::createFrontendJoint(const QString &jointName,
const Qt3DCore::Sqt &localPose,
const QMatrix4x4 &inverseBindMatrix) const
{
auto joint = QAbstractNodeFactory::createNode<QJoint>("QJoint");
joint->setTranslation(localPose.translation);
joint->setRotation(localPose.rotation);
joint->setScale(localPose.scale);
joint->setInverseBindMatrix(inverseBindMatrix);
joint->setName(jointName);
return joint;
}
void Skeleton::processJointHierarchy(Qt3DCore::QNodeId jointId,
int parentJointIndex,
SkeletonData &skeletonData)
{
// Lookup the joint, create a JointInfo, and add an entry to the index map
Joint *joint = m_renderer->nodeManagers()->jointManager()->lookupResource(jointId);
Q_ASSERT(joint);
joint->setOwningSkeleton(m_skeletonHandle);
const JointInfo jointInfo(joint, parentJointIndex);
skeletonData.joints.push_back(jointInfo);
skeletonData.localPoses.push_back(joint->localPose());
skeletonData.jointNames.push_back(joint->name());
const int jointIndex = skeletonData.joints.size() - 1;
const HJoint jointHandle = m_jointManager->lookupHandle(jointId);
skeletonData.jointIndices.insert(jointHandle, jointIndex);
// Recurse to the children
const auto childIds = joint->childJointIds();
for (const auto childJointId : childIds)
processJointHierarchy(childJointId, jointIndex, skeletonData);
}
void Skeleton::clearData()
@ -343,6 +154,12 @@ void Skeleton::clearData()
m_skeletonData.jointIndices.clear();
}
void Skeleton::setSkeletonData(const SkeletonData &data)
{
m_skeletonData = data;
m_skinningPalette.resize(m_skeletonData.joints.size());
}
// Called from UpdateSkinningPaletteJob
void Skeleton::setLocalPose(HJoint jointHandle, const Qt3DCore::Sqt &localPose)
{

View File

@ -75,6 +75,12 @@ class SkeletonManager;
class Q_AUTOTEST_EXPORT Skeleton : public BackendNode
{
public:
enum SkeletonDataType {
Unknown,
File,
Data
};
Skeleton();
void setSkeletonManager(SkeletonManager *skeletonManager) { m_skeletonManager = skeletonManager; }
@ -89,6 +95,8 @@ public:
Qt3DCore::QSkeletonLoader::Status status() const { return m_status; }
QUrl source() const { return m_source; }
SkeletonDataType dataType() const { return m_dataType; }
bool createJoints() const { return m_createJoints; }
void setName(const QString &name) { m_name = name; }
QString name() const { return m_name; }
@ -101,34 +109,21 @@ public:
Qt3DCore::QNodeId rootJointId() const { return m_rootJointId; }
// Called from jobs
void loadSkeleton();
void setLocalPose(HJoint jointHandle, const Qt3DCore::Sqt &localPose);
QVector<QMatrix4x4> calculateSkinningMatrixPalette();
void clearData();
void setSkeletonData(const SkeletonData &data);
const SkeletonData &skeletonData() const { return m_skeletonData; }
SkeletonData skeletonData() { return m_skeletonData; }
// Allow unit tests to set the data type
#if !defined(QT_BUILD_INTERNAL)
private:
#endif
enum SkeletonDataType {
Unknown,
File,
Data
};
#if defined(QT_BUILD_INTERNAL)
public:
void setDataType(SkeletonDataType dataType) { m_dataType = dataType; }
#endif
private:
void loadSkeletonFromUrl();
void loadSkeletonFromData();
Qt3DCore::QJoint *createFrontendJoints(const SkeletonData &skeletonData) const;
Qt3DCore::QJoint *createFrontendJoint(const QString &jointName,
const Qt3DCore::Sqt &localPose,
const QMatrix4x4 &inverseBindMatrix) const;
void processJointHierarchy(Qt3DCore::QNodeId jointId, int parentJointIndex, SkeletonData &skeletonData);
void clearData();
QVector<QMatrix4x4> m_skinningPalette;
// QSkeletonLoader Properties

View File

@ -35,12 +35,18 @@
****************************************************************************/
#include "loadskeletonjob_p.h"
#include <Qt3DCore/qjoint.h>
#include <Qt3DCore/qabstractskeleton.h>
#include <Qt3DCore/private/qaspectmanager_p.h>
#include <Qt3DCore/qskeletonloader.h>
#include <Qt3DCore/private/qabstractskeleton_p.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DCore/private/qabstractnodefactory_p.h>
#include <Qt3DCore/private/qaspectmanager_p.h>
#include <Qt3DCore/private/qskeletonloader_p.h>
#include <Qt3DRender/private/managers_p.h>
#include <Qt3DRender/private/nodemanagers_p.h>
#include <Qt3DRender/private/job_common_p.h>
#include <Qt3DRender/private/qurlhelper_p.h>
#include <Qt3DRender/private/gltfskeletonloader_p.h>
QT_BEGIN_NAMESPACE
@ -50,12 +56,13 @@ namespace Render {
class LoadSkeletonJobPrivate : public Qt3DCore::QAspectJobPrivate
{
public:
LoadSkeletonJobPrivate() : m_backendSkeleton(nullptr) { }
LoadSkeletonJobPrivate() : m_backendSkeleton(nullptr), m_loadedRootJoint(nullptr) { }
~LoadSkeletonJobPrivate() override { }
void postFrame(Qt3DCore::QAspectManager *manager) override;
Skeleton *m_backendSkeleton;
Qt3DCore::QJoint* m_loadedRootJoint;
};
LoadSkeletonJob::LoadSkeletonJob(const HSkeleton &handle)
@ -66,10 +73,6 @@ LoadSkeletonJob::LoadSkeletonJob(const HSkeleton &handle)
SET_JOB_RUN_STAT_TYPE(this, JobTypes::LoadSkeleton, 0)
}
LoadSkeletonJob::~LoadSkeletonJob()
{
}
void LoadSkeletonJob::run()
{
Q_D(LoadSkeletonJob);
@ -78,10 +81,191 @@ void LoadSkeletonJob::run()
Skeleton *skeleton = m_nodeManagers->skeletonManager()->data(m_handle);
if (skeleton != nullptr) {
d->m_backendSkeleton = skeleton;
skeleton->loadSkeleton();
loadSkeleton(skeleton);
}
}
void LoadSkeletonJob::loadSkeleton(Skeleton *skeleton)
{
qCDebug(Jobs) << Q_FUNC_INFO << skeleton->source();
skeleton->clearData();
// Load the data
switch (skeleton->dataType()) {
case Skeleton::File:
loadSkeletonFromUrl(skeleton);
break;
case Skeleton::Data:
loadSkeletonFromData(skeleton);
break;
default:
Q_UNREACHABLE();
}
// If using a loader inform the frontend of the status change.
// Don't bother if asked to create frontend joints though. When
// the backend gets notified of those joints we'll update the
// status at that point.
if (skeleton->dataType() == Skeleton::File && !skeleton->createJoints()) {
if (skeleton->jointCount() == 0)
skeleton->setStatus(Qt3DCore::QSkeletonLoader::Error);
else
skeleton->setStatus(Qt3DCore::QSkeletonLoader::Ready);
}
qCDebug(Jobs) << "Loaded skeleton data:" << *skeleton;
}
void LoadSkeletonJob::loadSkeletonFromUrl(Skeleton *skeleton)
{
Q_D(LoadSkeletonJob);
using namespace Qt3DCore;
// TODO: Handle remote files
QString filePath = Qt3DRender::QUrlHelper::urlToLocalFileOrQrc(skeleton->source());
QFileInfo info(filePath);
if (!info.exists()) {
qWarning() << "Could not open skeleton file:" << filePath;
skeleton->setStatus(Qt3DCore::QSkeletonLoader::Error);
return;
}
QFile file(filePath);
if (!file.open(QIODevice::ReadOnly)) {
qWarning() << "Could not open skeleton file:" << filePath;
skeleton->setStatus(QSkeletonLoader::Error);
return;
}
// TODO: Make plugin based for more file type support. For now gltf or native
const QString ext = info.suffix();
SkeletonData skeletonData;
if (ext == QLatin1String("gltf")) {
GLTFSkeletonLoader loader;
loader.load(&file);
skeletonData = loader.createSkeleton(skeleton->name());
// If the user has requested it, create the frontend nodes for the joints
// and send them to the (soon to be owning) QSkeletonLoader.
if (skeleton->createJoints()) {
QJoint *rootJoint = createFrontendJoints(skeletonData);
if (!rootJoint) {
qWarning() << "Failed to create frontend joints";
skeleton->setStatus(QSkeletonLoader::Error);
return;
}
// Move the QJoint tree to the main thread and notify the
// corresponding QSkeletonLoader
const auto appThread = QCoreApplication::instance()->thread();
rootJoint->moveToThread(appThread);
d->m_loadedRootJoint = rootJoint;
// Clear the skeleton data. It will be recreated from the
// frontend joints. A little bit inefficient but ensures
// that joints created this way and via QSkeleton go through
// the same code path.
skeletonData = SkeletonData();
}
} else if (ext == QLatin1String("json")) {
// TODO: Support native skeleton type
} else {
qWarning() << "Unknown skeleton file type:" << ext;
skeleton->setStatus(QSkeletonLoader::Error);
return;
}
skeleton->setSkeletonData(skeletonData);
}
void LoadSkeletonJob::loadSkeletonFromData(Skeleton *skeleton)
{
// Recurse down through the joint hierarchy and process it into
// the vector of joints used within SkeletonData. The recursion
// ensures that a parent always appears before its children in
// the vector of JointInfo objects.
//
// In addition, we set up a mapping from the joint ids to the
// index of the corresponding JointInfo object in the vector.
// This will allow us to easily update entries in the vector of
// JointInfos when a Joint node marks itself as dirty.
const int rootParentIndex = -1;
auto skeletonData = skeleton->skeletonData();
processJointHierarchy(skeleton->rootJointId(), rootParentIndex, skeletonData);
skeleton->setSkeletonData(skeletonData);
}
Qt3DCore::QJoint *LoadSkeletonJob::createFrontendJoints(const SkeletonData &skeletonData) const
{
if (skeletonData.joints.isEmpty())
return nullptr;
// Create frontend joints from the joint info objects
QVector<Qt3DCore::QJoint *> frontendJoints;
const int jointCount = skeletonData.joints.size();
frontendJoints.reserve(jointCount);
for (int i = 0; i < jointCount; ++i) {
const QMatrix4x4 &inverseBindMatrix = skeletonData.joints[i].inverseBindPose;
const QString &jointName = skeletonData.jointNames[i];
const Qt3DCore::Sqt &localPose = skeletonData.localPoses[i];
frontendJoints.push_back(createFrontendJoint(jointName, localPose, inverseBindMatrix));
}
// Now go through and resolve the parent for each joint
for (int i = 0; i < frontendJoints.size(); ++i) {
const auto parentIndex = skeletonData.joints[i].parentIndex;
if (parentIndex == -1)
continue;
// It's not enough to just set up the QObject parent-child relationship.
// We need to explicitly add the child to the parent's list of joints so
// that information is then propagated to the backend.
frontendJoints[parentIndex]->addChildJoint(frontendJoints[i]);
}
return frontendJoints[0];
}
Qt3DCore::QJoint *LoadSkeletonJob::createFrontendJoint(const QString &jointName,
const Qt3DCore::Sqt &localPose,
const QMatrix4x4 &inverseBindMatrix) const
{
auto joint = Qt3DCore::QAbstractNodeFactory::createNode<Qt3DCore::QJoint>("QJoint");
joint->setTranslation(localPose.translation);
joint->setRotation(localPose.rotation);
joint->setScale(localPose.scale);
joint->setInverseBindMatrix(inverseBindMatrix);
joint->setName(jointName);
return joint;
}
void LoadSkeletonJob::processJointHierarchy(Qt3DCore::QNodeId jointId,
int parentJointIndex,
SkeletonData &skeletonData)
{
// Lookup the joint, create a JointInfo, and add an entry to the index map
Joint *joint = m_nodeManagers->jointManager()->lookupResource(jointId);
Q_ASSERT(joint);
joint->setOwningSkeleton(m_handle);
const JointInfo jointInfo(joint, parentJointIndex);
skeletonData.joints.push_back(jointInfo);
skeletonData.localPoses.push_back(joint->localPose());
skeletonData.jointNames.push_back(joint->name());
const int jointIndex = skeletonData.joints.size() - 1;
const HJoint jointHandle = m_nodeManagers->jointManager()->lookupHandle(jointId);
skeletonData.jointIndices.insert(jointHandle, jointIndex);
// Recurse to the children
const auto childIds = joint->childJointIds();
for (const auto &childJointId : childIds)
processJointHierarchy(childJointId, jointIndex, skeletonData);
}
void LoadSkeletonJobPrivate::postFrame(Qt3DCore::QAspectManager *manager)
{
if (!m_backendSkeleton)
@ -97,6 +281,17 @@ void LoadSkeletonJobPrivate::postFrame(Qt3DCore::QAspectManager *manager)
dnode->m_jointNames = m_backendSkeleton->jointNames();
dnode->m_localPoses = m_backendSkeleton->localPoses();
dnode->update();
QSkeletonLoader *loaderNode = qobject_cast<QSkeletonLoader *>(node);
if (loaderNode) {
QSkeletonLoaderPrivate *dloaderNode = static_cast<QSkeletonLoaderPrivate *>(QSkeletonLoaderPrivate::get(loaderNode));
dloaderNode->setStatus(m_backendSkeleton->status());
if (m_loadedRootJoint) {
dloaderNode->m_rootJoint = m_loadedRootJoint;
m_loadedRootJoint = nullptr;
}
}
}
} // namespace Render

View File

@ -51,11 +51,15 @@
#include <Qt3DCore/qaspectjob.h>
#include <QtCore/qsharedpointer.h>
#include <Qt3DRender/private/skeletondata_p.h>
#include <Qt3DRender/private/handle_types_p.h>
QT_BEGIN_NAMESPACE
namespace Qt3DCore {
class QJoint;
}
namespace Qt3DRender {
namespace Render {
@ -66,12 +70,22 @@ class LoadSkeletonJob : public Qt3DCore::QAspectJob
{
public:
explicit LoadSkeletonJob(const HSkeleton &handle);
~LoadSkeletonJob();
void setNodeManagers(NodeManagers *nodeManagers) { m_nodeManagers = nodeManagers; }
protected:
void run() override;
void loadSkeleton(Skeleton *skeleton);
void loadSkeletonFromUrl(Skeleton *skeleton);
void loadSkeletonFromData(Skeleton *skeleton);
Qt3DCore::QJoint *createFrontendJoints(const SkeletonData &skeletonData) const;
Qt3DCore::QJoint *createFrontendJoint(const QString &jointName,
const Qt3DCore::Sqt &localPose,
const QMatrix4x4 &inverseBindMatrix) const;
void processJointHierarchy(Qt3DCore::QNodeId jointId,
int parentJointIndex,
SkeletonData &skeletonData);
HSkeleton m_handle;
NodeManagers *m_nodeManagers;

View File

@ -220,30 +220,6 @@ private Q_SLOTS:
QTest::newRow("inverseBind") << m << localPose << name << joint;
}
void checkCreateFrontendJoint()
{
// GIVEN
Skeleton backendSkeleton;
QFETCH(QMatrix4x4, inverseBindMatrix);
QFETCH(Qt3DCore::Sqt, localPose);
QFETCH(QString, jointName);
QFETCH(QJoint *, expectedJoint);
// WHEN
const QJoint *actualJoint = backendSkeleton.createFrontendJoint(jointName, localPose, inverseBindMatrix);
// THEN
QCOMPARE(actualJoint->scale(), expectedJoint->scale());
QCOMPARE(actualJoint->rotation(), expectedJoint->rotation());
QCOMPARE(actualJoint->translation(), expectedJoint->translation());
QCOMPARE(actualJoint->inverseBindMatrix(), expectedJoint->inverseBindMatrix());
QCOMPARE(actualJoint->name(), expectedJoint->name());
// Cleanup
delete actualJoint;
delete expectedJoint;
}
void checkCreateFrontendJoints_data()
{
QTest::addColumn<SkeletonData>("skeletonData");
@ -307,46 +283,6 @@ private Q_SLOTS:
QTest::newRow("deep") << skeletonData << rootJoint;
}
void checkCreateFrontendJoints()
{
// GIVEN
Skeleton backendSkeleton;
QFETCH(SkeletonData, skeletonData);
QFETCH(QJoint *, expectedRootJoint);
// WHEN
QJoint *actualRootJoint = backendSkeleton.createFrontendJoints(skeletonData);
// THEN
if (skeletonData.joints.isEmpty()) {
QVERIFY(actualRootJoint == expectedRootJoint); // nullptr
return;
}
// Linearise the tree of joints and check them against the skeletonData
QVector<QJoint *> joints = linearizeTree(actualRootJoint);
QCOMPARE(joints.size(), skeletonData.joints.size());
for (int i = 0; i < joints.size(); ++i) {
// Check the translations match
QCOMPARE(joints[i]->translation(), skeletonData.localPoses[i].translation);
}
// Now we know the order of Joints match. Check the parents match too
for (int i = 0; i < joints.size(); ++i) {
// Get parent index from joint info
const int parentIndex = skeletonData.joints[i].parentIndex;
if (parentIndex == -1) {
QVERIFY(joints[i]->parent() == nullptr);
} else {
QCOMPARE(joints[i]->parent(), joints[parentIndex]);
}
}
// Cleanup
delete actualRootJoint;
delete expectedRootJoint;
}
};
QTEST_APPLESS_MAIN(tst_Skeleton)